ADR-0016: Continuous promotion behind the staging gate; configuration in GitHub Secrets
Status: Accepted Date: 2026-07-30
Context
ADR-0015 introduced the staging tier with a manual promote button, and
its consequences accepted "promotion is a human decision". In practice the
operator wants zero routine manual steps: a merge should reach production
by itself, with the machinery — not a person — deciding whether it's safe
enough to continue. Separately, the VPS env files (.env, .env.stage)
were hand-created over SSH: unversioned, unrepeatable, and easy to drift.
Decision
- Continuous promotion.
deploy.ymlgained adeploy-productionjob that runs only when the staging deploy turned healthy, promoting the same run's exact tag viadeploy.sh promote <tag>. The staging health gate — boot, migrations, config fail-fasts — is the sole automated gate between a merge and production.promote.ymlis demoted to an exception tool (re-promote an explicit tag, or redeploy with fresh Secrets); rollback stays manual by nature. - Configuration lives in GitHub Secrets. Two secrets,
PROD_ENV_FILEandSTAGE_ENV_FILE, hold the full dotenv payloads (templates:deploy/.env.*.example). Every deploy/promote run rewrites/srv/motorph/.env{,.stage}from them (passed viaenvs:— shell-escaped, multiline-safe, never${{ }}-interpolated — written with umask 077). The VPS copies are disposable; changing a value means editing the Secret and re-running Deploy.
Consequences
Positive
- Merge → production with no human in the loop; staging still stops anything that fails to boot or migrate, automatically.
- Configuration is applied by exactly one actor (the pipeline), so the VPS can never drift from the declared config for longer than one run.
- Standing up a replacement VPS is: harden, clone, set six Secrets, run the pipeline.
Negative
- No human staging window. The health gate catches "doesn't boot", not "computes the wrong payroll" — the local test suites are the only behavior gate, and a bad-but-bootable merge is live in minutes. A change that needs soak time on staging requires deliberately holding the merge.
- GitHub Secrets are write-only — the filled payloads must be kept in a password manager, or a lost laptop means reconstructing secrets that can't be read back from anywhere.
- Anyone who can push to
mainor edit workflows can deploy to production and exfiltrate the Secrets (a workflow edit can print them). Mitigations are branch protection and a tight collaborator list — accepted for a single-operator repo. - A Secret edit takes effect only on the next pipeline run — an "applied" config change that skipped the re-run is a documented troubleshooting case, not an error anyone is told about.
References
../../.github/workflows/deploy.yml,../../.github/workflows/promote.yml../../deploy/.env.prod.example,../../deploy/.env.stage.example../deployment/vps-guide.md§5, §8, §10- 0015-staging-promote-pipeline.md — the staging tier itself is unchanged; this ADR amends its promotion model and secrets handling.