Docs · Glidepath
ADR-0025: Retire the image-promotion stub check
Status: Accepted (2026-10-09). Built: the check’s onboarding template, Pipeline and releaseGuardrails entry
removed; wait-for-release-guardrails kept and made generic (self-gate); onboarding resync removes a
platform-generated gate file whose template is gone. Not built: image promotion itself
(known-gaps.md #42). Decision 3 superseded by ADR-0029
(2026-10-27): image-promotion is back as a real, required check, started by an event fan-in.
Context
image-promotion was the ninth release gate and the only one that depended on the others. The owner’s
requirement (2026-08) was that it run last, so nothing could promote an image past a failing gate. Its Pipeline
had one real part, wait-for-release-guardrails: a pod polling the GitHub Checks API for the same commit until
every other gate in the registry reported, failing closed if any did not succeed. Then a stub promote step
(governance-gate-stub, ADR-0003: loud, never a silent pass) and a PR comment.
What the Tekton Results archive showed for the 97 runs on real release PRs (Release Record PRs excluded, 2026-09-17 to 2026-10-09):
| Outcome | Runs | Mean wall |
|---|---|---|
| Succeeded | 21 | 312 s |
| Failed because a sibling gate failed | 73 | 197 s |
| Ran to the full 18-minute budget | 3 | 1164 s |
Three facts made the check cost without return:
- It could not block a merge. GitHub Free does not enforce rulesets or branch protection on private
repositories (the API answers
403 Upgrade to GitHub Pro; github-ruleset.md). Every release PR in the archive was merged by hand, with no auto-merge. The only enforcement isdetect-bypass-merge, which runs after the merge and alerts Slack. - Its result carried no information. It was the AND of the sibling checks, which the PR’s check list already shows. Each of the 73 failures added a second “image-promotion failed” comment under the sibling’s own.
- There was nothing to promote. Every cluster, including prod, pulls straight from
ghcr.iowithghcr-pull-secret. No upper registry exists for a copy to land in.
Cost per release PR: one of nine gate PipelineRuns, a pod held for as long as the slowest sibling ran, two GitHub API reads a minute, one extra PR comment, and a 3% chance of a 20-minute hang.
Two other homes were weighed. Keeping it as a PR check is cheap after the 30 s poll interval
(jfillman/glidepath#83) and keeps a visible “last gate” placeholder, but the placeholder states an ordering
nothing enforces. Moving the stub into the post-merge bypass-merge-check Pipeline removes the poller, but it
puts a future real promotion on the wrong side of the merge for Kubernetes targets: ArgoCD on the upper cluster
syncs the moment the PR merges, so a post-merge registry copy races the Rollout into ImagePullBackOff, and a
failed copy leaves a merged release with no image.
Decision
- The
image-promotioncheck is retired. Its onboarding template,image-promotion-checkPipeline andreleaseGuardrailsentry are removed. The gate set says only true things: ADR-0003 asks that a stub never be reported with a real check’s confidence; here no check is reported at all, and release-guardrails.md records that promotion does not exist. wait-for-release-guardrailsstays in the catalog, with aself-gateparameter instead of a hardcoded exclusion. It is the documented “shape B” piece for a gate that genuinely must run last.- Where a real promotion lives is decided by registry topology when it is built:
- A separate upper registry that an ArgoCD-managed upper cluster pulls from: promotion must complete
before merge. Build the fan-in on events, not polling. Every gate’s
debriefalready emitspipelinerun.finishedthrough the broker; a reducer on the release record (ADR-0021) marks each gate as it lands, and the last green one fires promotion. No pod waits, no 18-minute budget. - One registry for every cluster (today): “promotion” is a retag or an attestation, cheap and safe
after merge in
bypass-merge-check, oncedetect-bypass-mergereports no bypass. - Cloud targets (ADR-0020) are unaffected by the choice: Glidepath’s own deploy stage runs after the pin PR merges and can order promote before deploy itself.
- A separate upper registry that an ArgoCD-managed upper cluster pulls from: promotion must complete
before merge. Build the fan-in on events, not polling. Every gate’s
- Onboarding resync prunes a retired gate.
deliver-gitops-repo-filesremoves a.tekton/pull-request-*.yamlwhose template no longer exists and whose first line carries the platform’s own marker (# STUB,# BOILERPLATE, or# The real ... gate). A tenant’s own.tektonfile has no marker and is left alone, which keeps the original “never delete a tenant’s customization” intent.
Consequences
- A release PR carries eight checks instead of nine (seven on a cloud pin PR), posts one fewer comment, and can no longer hang for 20 minutes on a check-run that never appears.
detect-bypass-mergeandhack/apply-guardrail-ruleset.shrender their gate lists from the registry and need no change. The audited ruleset export indocs/admin/reference/predates this ADR and still lists the check.- The catalog Application does not prune, so the live
image-promotion-checkPipeline must be deleted by hand after every gitops repo’s resync PR has merged. Until then the stale.tektonfile still resolves against it, which is the safe order. - Tower’s gate ledger reads the PR’s check runs and shows whatever exists; only comments and a pipeline-name
list referenced the check (
jfillman/tower, cleanup PR). - Reversal is cheap: the template, Pipeline and registry entry are three files in git history, and
wait-for-release-guardrailsis still there to call.