ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

ci(release): auto-repin embedded x402 image pins + release freshness gate - #618

Closed
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images
Closed

ci(release): auto-repin embedded x402 image pins + release freshness gate#618
bussyjd wants to merge 1 commit into
mainfrom
chore/auto-repin-x402-images

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Automates the release-time image repin so it can never be forgotten again. Two mechanisms, belt and suspenders:

flowchart LR
P["push to main / release/**<br/>(or workflow_dispatch)"] --> B["docker-publish-x402<br/>build + push images<br/>:shortsha"]
B --> R["repin-embedded-pins job<br/>rewrite x402.yaml + llm.yaml pins<br/>commit chore(ci): repin … [auto]"]
R --> T["operator tags vX.Y.Z"]
T --> G{"release.yml<br/>verify-image-pins gate"}
G -->|pins fresh| REL["binaries built,<br/>draft release"]
G -->|stale / digest mismatch /<br/>unresolvable pin| BLOCK["release FAILS<br/>with fix instructions"]
Loading
  1. Auto-repin (repin-embedded-pins job in docker-publish-x402.yml): after every successful branch image build, .github/scripts/repin-x402-images.sh rewrites the embedded x402-verifier / serviceoffer-controller / x402-buyer pins to the multi-arch index digests of the images just built, and the job pushes a chore(ci): repin x402 images to <sha> [auto] commit. Branch refs only — never tags.
  2. Release gate (verify-image-pins in release.yml, release now needs it): .github/scripts/verify-x402-pins.sh fails the tag when any source in the three binaries' live import graph (go list -deps, not a hand-maintained path list) changed after the pinned build commit.

This closes the trap that hit v0.10.0-rc14: the train's pins were its own merge base (04bebbc), so the shipped manifests deployed images containing none of the train's verifier/buyer changes until a manual rebuild+repin (2db429b, the rc11 pattern). With this PR that manual step is automatic, and a tag cut without it cannot release.

Gate properties (each adversarially tested)

  • Fail-closed: a go list failure refuses to pass rather than silently degrading to a partial path set (reproduced: with a broken go, the old draft waved stale pins through; now exits 1).
  • Digest↔tag binding: the embedded digest must match what GHCR serves for the pinned tag — a fresh tag with a hand-edited digest fails (the digest, not the tag, is what Kubernetes pulls). VERIFY_X402_PINS_OFFLINE=true skips for air-gapped runs.
  • No self-staleness: the two pin-carrying templates are hunk-filtered — pin lines are ignored, any other edit to them (RBAC, args, env) still counts as stale. _test.go/testdata churn is ignored (doesn't compile into the binaries).
  • Consistency: all three pins must share one build commit; multiple distinct pins for one image fail extraction.
  • Caught the real thing: run against the pre-repin rc14 state, the gate flags agent_render.go, openapi.go, go.mod, … — exactly the trap.

Workflow security

  • Context values (github.ref_name, github.sha) are env-bound in the repin job, never interpolated into script text (branch names may contain shell metacharacters).
  • The push step verifies the diff touches only the two template files before committing; job has contents: write, the new gate job is contents: read.
  • The bump is committed via the GraphQL createCommitOnBranch API: GitHub signs the commit itself (verified, github-actions bot), so the job is compatible with a required_signatures ruleset — a workflow git push can never produce a verified commit. expectedHeadOid is the live remote head with one retry on race; only the two guarded files are ever sent. No recursion: API commits made with GITHUB_TOKEN don't trigger workflows.
  • Path filters extended to approximate the binaries' real import graph (the gate computes the exact one at tag time, so anything the filter misses fails the release instead of shipping stale); release/** pushes now build+repin like main.

Test refactor

The exact-ref equality tests (TestEmbeddedImages_X402ControllerAndBuyerUseFixPins, TestX402VerifierImage_CarriesAgentAuthFix, TestServiceOfferControllerImage_CarriesSecretCreateOnlyFix) became invariant tests, so the bot can bump pins without editing Go files while the guarantees get stronger:

  • pins must be <repo>:<short-sha>@sha256:<digest> (digest discipline already covered by TestEmbeddedImages_NamedImagesAreDigestPinned),
  • all three must share one build commit (TestEmbeddedImages_X402PinsShareOneBuildCommit),
  • the pinned commit must descend from the named fix commits — b39bcaa (Secret-create-only), abfd55a (agent auth), ab71481, 86b8c9f — ancestry-verified via git (TestEmbeddedImages_X402PinsCarryRequiredFixes), skipped gracefully on shallow clones where the release gate covers it. Only main-reachable commits belong in that list (release-branch SHAs stop being ancestors after a squash-merge; documented in the test).

Review

42-finding adversarial review pass (4 lenses × refutation agents): 1 major fixed (fail-open on go list failure), 2 minors fixed (ref_name injection, digest binding), nits fixed (gate job permissions, multi-pin extraction, diff-header anchor that could hide YAML doc-separator deletions, ambiguous-SHA error hint). Branch-protection findings refuted by measurement (no protection/rulesets on main).

Validation

  • shellcheck clean on all three scripts; both workflows parse.
  • Gate: positive (current pins), negative × 3 (rc13 mixed pins → same-tag failure; pre-repin rc14 pins → staleness with the exact culprit files; corrupted digest → registry mismatch), fail-closed (broken go).
  • Repin script: idempotent no-op at current pin; real bump to 2db429b images verified end-to-end (gate + tests pass on bumped state), then restored.
  • Full go test ./... green (34 packages).

Stacks on #616 (branched from the v0.10.0-rc14 tag commit); merge that first.

Rebased onto current main (post-#616 squash) — single verified commit; the earlier stale release-branch history is gone.

@bussyjd
bussyjd requested a review from OisinKyneJune 10, 2026 18:16
…n freshness
Every docker-publish-x402 branch build now lands a pin-bump commit
(repin-embedded-pins job) updating the embedded x402-verifier /
serviceoffer-controller / x402-buyer references to the images just built,
and the release workflow gains a verify-image-pins gate that fails the tag
when any source in the binaries' live import graph (go list -deps) changed
after the pinned build commit. Together they make the rc14 stale-pin trap
— a release whose embedded pins predate its own payment-path changes —
structurally impossible: the bump is automatic, and a tag cut before the
bump lands cannot release.
The bump is committed through the GraphQL createCommitOnBranch API, so
the commit is signed by GitHub itself (verified, github-actions bot) —
compatible with the repo ruleset rejecting unsigned commits, which a
workflow git push could never satisfy. expectedHeadOid is the live
remote head with one retry on race; only the two guarded template files
are ever sent.
The gate is fail-closed (a go-list failure refuses to pass rather than
degrade to a partial path set), binds each embedded digest to what GHCR
serves for the pinned tag (a fresh tag with a hand-edited digest fails),
ignores _test.go/testdata churn, and hunk-filters the two pin-carrying
templates so pin bumps don't self-stale while any other edit to them
still counts. release/** branches get the same build+repin treatment as
main.
The exact-ref pin tests become invariant tests: pins must be digest-
pinned, share one build commit, and descend from the named fix commits
(ancestry-verified via git, skipped on shallow clones) — so the bot can
bump pins without touching Go files while the carries-fix-X guarantees
get stronger.
@bussyjd
bussyjdforce-pushed the chore/auto-repin-x402-images branch from 4b9bb7b to 93d8f62CompareJune 11, 2026 03:43
@OisinKyne
OisinKyne deleted the chore/auto-repin-x402-images branch July 1, 2026 12:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bussyjd@OisinKyne