Cut GA over to promoting the RC digest instead of rebuilding - #160
Merged
Conversation
master still ran the pre-redesign promote: SSM-only, auto-fired on a master push, and its own header pointed at push-to-dockerhub to build the image. That is what let today's GA write /image-tag/latest = 20260820-001 while the image build for that version had already failed -- production was promoted onto a tag that the workflow believed did not exist. Bring master onto the build-once-promote model: - manual workflow_dispatch only, so GA is a deliberate gate rather than a side effect of a branch merge; - retag DockerHub :latest to the existing RC digest via buildx imagetools create, for simplerisk-minimal (<V>-php85) and simplerisk (<V>-noble). No rebuild, so the bytes validated in testing are the bytes that ship; - a currency guard per image: refuse to promote a version whose digest is not the one :testing currently points at, so a stale committed Dockerfile version cannot push an old-but-existing release to prod; - mirror the promoted digests into GHCR, cosign-signed, so ghcr <V> and dockerhub <V> are finally the same bytes rather than two builds sharing a name. skip_full_image covers the transition: releases cut before the full-stack RC build landed have no simplerisk/simplerisk RC digest to promote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These are the workflows that failed the 20260820-001 GA. They rebuild from the prod S3 bundle on a master push, but the release PR merges ~17 minutes before the bundle propagation uploads it, so download_and_verify_bundle.sh fail-closed on a bare 403 (S3 returns 403, not 404, for a missing object under an anonymous-list-denied bucket -- it reads as a permissions error). Rebuilding at GA is also what the promote model exists to remove: it shipped bytes to production that were never the bytes validated in testing, and gave GHCR a separate build under the same version tag. Drop the `push: master` trigger and the simplerisk-minimal jobs from both. promote-latest.yml now owns GA for both registries. The jammy/noble jobs stay dispatchable as a transitional escape hatch for releases with no full-stack RC digest; both headers say when they can be deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CI/CD section still said pushes publish to Docker Hub and GHCR, which is now wrong in both halves: nothing publishes on a master push, and GHCR is a mirror of the promoted digest rather than its own build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Cuts
masterover to the build-once-promote model fromdocs/superpowers/specs/2026-07-10-release-image-promotion-design.md(code-development). The design landed on thetestingbranch in July (#147, #148) but was never brought tomaster, so GA still ran the pre-redesign path. That is what failed the 20260820-001 release.What went wrong today. Merging the release PR (#157) at 14:10 fired
push-to-dockerhub.ymlandpush-to-gh-pkgs.yml, which rebuild every image from the prod S3 bundle. The GA bundle was not uploaded until 14:27:37 — 17 minutes later — sodownload_and_verify_bundle.shcorrectly fail-closed. The error reads as a permissions problem but is not: S3 returns 403, not 404, for a missing object under an anonymous-list-denied bucket.Meanwhile
promote-latest.ymlsucceeded on the same push and wrote SSM/image-tag/latest = 20260820-001. It got away with it only because the RC build had already published that bare tag overnight — under master's own stated model that tag was supposed to come from the rebuild that had just failed. Production was promoted onto a tag the workflow believed did not exist. Docker Hub:latest, meanwhile, still points at 20260519-001.Changes
promote-latest.yml— GA becomes a promote, and owns both registries.workflow_dispatchonly. GA is a deliberate gate, not a side effect of a branch merge.:latestto the existing RC digest viabuildx imagetools create(multi-arch preserved) for bothsimplerisk-minimal(<V>-php85) andsimplerisk(<V>-noble). No rebuild, so the bytes validated in testing are the bytes that ship.:testingcurrently points at, so a stale committed Dockerfile version cannot push an old-but-existing release to prod.ghcr <V>anddockerhub <V>were two different images sharing a name.skip_full_imageinput covers the transition — see below.push-to-dockerhub.yml/push-to-gh-pkgs.yml— no longer publish releases.push: mastertrigger removed;simplerisk-minimaljobs removed. The jammy/noble jobs stay dispatchable as a transitional escape hatch, and both headers say when the workflows can be deleted.CLAUDE.md— the CI/CD section claimed pushes publish to both registries, wrong in both halves now.Ordering
Important
Depends on #159 (base
testing). The full-stack image has no RC build today, sosimplerisk/simplerisk:<V>-jammy/-nobledoes not exist for any version yet. #159 adds it.Sequence:
testing.master.promote-latestwithskip_full_image: true— this release predates the full-stack RC build. It promotessimplerisk-minimal(whose RC tags exist, published 03:10–04:23 today), mirrors them to GHCR, and moves:latestoff the stale 20260519-001. If a full-stack image is wanted for this release, dispatchpush-to-dockerhubmanually — the bundle is present and its sha256 now matches the feed, so it will succeed.skip_full_imageis never needed and the two legacy workflows can be deleted.Release Notes
No customer-facing change. Container release plumbing only. Going forward the published
latestimage is byte-identical to the release candidate validated in testing, and the GHCR and Docker Hub images for a given version are the same bytes.Manual Validation Steps
masterpush:push-to-dockerhub.ymlandpush-to-gh-pkgs.ymllistworkflow_dispatchonly;promote-latest.ymllikewise.master→ no image workflow should start.promote-latestwithskip_full_image: true. Expect: minimal RC digest verified, currency guard passes against:testing, Docker Hub:latestretagged, GHCR mirrored + signed, SSM written. Step summary records each digest.docker buildx imagetools inspect simplerisk/simplerisk-minimal:latest --format '{{.Manifest.Digest}}'should equal the<V>-php85digest, and the GHCR copy should match.Type of Change
Testing Results
actionlintclean on all three changed workflows.shellcheck -S warningclean on every embeddedrunblock inpromote-latest.yml.update_workflows.sh's version-pin regex still matches the remaining pins (2 per push workflow, was 4) —make update_versionis unaffected.:latestback to the previous digest is oneimagetools create).CIA Impact Check
MAY negatively impact Confidentiality, Integrity and/or Availability.
Justification for the change
ghcr <V>anddockerhub <V>were independent builds under the same version tag — a real supply-chain ambiguity about what a version identifies.Potential impact on Confidentiality, Integrity and/or Availability
Integrity (net positive, with a new dependency). A single digest now flows testing → prod unchanged, and GHCR is a verified mirror rather than a parallel build. The new dependency is that GA correctness now rests on the RC having been published and on the currency guard; both fail closed and loudly. The cosign signature on the GHCR mirror is generated at promote time over the mirrored digest, preserving the existing signing property.
Availability (deploy-window, one-time). GA promotion becomes a manual dispatch, so a release is not published until someone runs it. This is deliberate — the design calls it a release gate — but it is a behavior change for whoever runs releases, and a forgotten dispatch means
:latestsilently stays on the previous release. Mitigated by the step summary and by:latestremaining valid (just older) until promoted.Confidentiality. No change. No new secrets;
packages: writeand cosign'sid-token: writeare the same scopes the existing GHCR publish path already used.Additional Implementation Steps
mastermerge need updating to add thepromote-latestdispatch step.IMAGE_PROMOTER_LATEST_ROLE_ARNrepo variable is already in use by the existing promote job — no new configuration.push-to-dockerhub.ymlandpush-to-gh-pkgs.yml.