T20 CD safety nets. Two gaps closed before the first real prod deploy:
1. Promotion gate. promote_dev_to_prod.yml previously fast-forwarded
dev→main unconditionally. It now hard-gates on check-dev-green.sh:
every check-run on the dev HEAD must be completed+passing AND the
Agent CI suite (lint/format/unit/E2E) must be present+success, or the
promotion blocks (fails safe on a missing/renamed check). The promote
run excludes its OWN check-run by run-id (unforgeable), never by the
mutable name "promote", so a colliding red check cannot hide. Fields
are read with a 0x1F separator so an empty conclusion (every
in_progress check) cannot shift columns. ci.yml now also runs on
push:dev so dev HEAD actually carries that signal (a PR check alone
can be admin-merged past).
2. Rollback + last-good. publish-and-deploy.sh advances
releases/last-good/ only after a successful roll (deploy.sh gates on
`systemctl is-active`), and makes releases/latest/ transactional —
reverting to the prior release if the roll fails so a replaced box
never self-deploys a broken release. New rollback.yml + rollback.sh
re-point latest at last-good (or an explicit sha) and re-fire the
deploy; prod is gated by the `prod` Environment approval, same as a
deploy. The shared fire/wait/aggregate-gate logic is factored into
roll-box.sh (used by both forward and backward rolls).
Least-privilege: drop the unused s3:DeleteObject from the app deploy
role — publish/rollback/deploy only Get+Put (S3-to-S3 copy), and the
rollback fallback now depends on immutable release history staying
intact. Lifecycle expiry (not CI) handles old-version cleanup.
Gate logic unit-tested (7 cases + jq round-trip). IAM change +
release-safety control cross-reviewed by GPT-4.1: APPROVE, no blocks.
Claude-Session: https://claude.ai/code/session_01DMhLf4G5V8MStJQyAW95hi