ADR-027: dev follows main through pin PRs
Status: Accepted
Date: 2026-10-09
Amends: ADR-023: Parallel execution of multi-repo plans (pinning stays human, except for dev), ADR-017: The GitOps repo owns every Kubernetes manifest; services ship an image
Amended by: ADR-041 — Staging follows a green dev smoke; prod stays a manual promotion with evidence (decision 4, for staging)
Context
Section titled “Context”dev pinned every service to :latest with imagePullPolicy: Always, and the docs and agents claimed it picked up every new image. It did not: a moving tag gives Argo CD nothing to sync, so a new image only ran after a manual kubectl rollout restart. In end-to-end run 2 (2026-10-09) both merged repos of a feature kept running the old code until the operator restarted them (run log, finding 3). The repo also didn’t show which build dev runs, which /shared:status and rollbacks need. staging and prod don’t have the problem: render.py forbids latest there, and promotions pin immutable tags.
Options
Section titled “Options”- Pin PRs (chosen). On every merge, the service’s CI asks the gitops-app (via
repository_dispatch) to pindevtosha-<7>. The gitops-app opens the PR, and it merges itself once CI passes. - Argo CD Image Updater. It watches GHCR and writes the pin back to git. No token in service repos, but every cluster needs one more component with write access to the gitops repo, and the local cluster too.
- Restart on a new digest. Keep
latestand restart the Deployment on a new digest. The simplest, but the running build isn’t visible in git and can’t be rolled back by a revert.
Decision
Section titled “Decision”-
Service CI (all four deployable templates) gets a
pin-devjob afterdocker-publish, onmainonly. It sendsrepository_dispatchpin-devwith{service, tag: sha-<7>}to every repo in.platform-app.yml, using the secretGITOPS_TOKEN: a fine-grained token with Contents: read and write on the gitops-app repo. Without the file or the token, the job prints a notice and passes. It is not a required check. -
The gitops-app has
scripts/pin.py <env> <service> <tag>(immutable tags only) and.github/workflows/pin-dev.yml:- it pins
devonpin/dev-<service>-<sha7>and opens the PR withGITHUB_TOKEN, closing older pin PRs of the service; - it starts
ci.ymlwithworkflow_dispatch, because PRs made withGITHUB_TOKENtrigger no workflows; - it enables auto-merge (squash).
Branch protection (spec 054, required checks, no review) decides the merge. A red CI leaves the PR open and
devon the old image. - it pins
-
New repos:
/shared:new-service --gitopsturns onallow_auto_mergeand lets Actions create PRs. A linked service, and/shared:new-appfor all its components, prints the one manual step: create the token, thengh secret set GITOPS_TOKEN./shared:update-servicenames the same step when the job first arrives. -
Promotion is unchanged:
stagingandprodmove only through/gitops:promote, one reviewed PR each, never auto-merged. Amended by ADR-041: staging now follows a green dev smoke; prod is unchanged.
Consequences
Section titled “Consequences”devruns what was merged within minutes, and the pin is git history: revert the pin PR to roll back.- The cost: one secret per service repo, a token that expires and has to be renewed, and an auto-merge setting on the gitops repo. Without the token nothing breaks;
devsimply stays where it was, and the CI notice says why. - Untested here: whether check runs started by
workflow_dispatchsatisfy branch protection for a PR opened byGITHUB_TOKENdepends on GitHub, not this repository. The next end-to-end run checks it on ika100/todo (spec 062, verification).