Promote to production #160

Merged
jfraeys merged 7 commits from main into prod 2026-10-14 17:32:47 -04:00
Owner

Promotion: staging passed. Run scripts/release/plan-prod.sh locally before merging

Merging this PR is the approval record; it deploys nothing. Then check out the validated commit and run scripts/release/promote-prod.sh (or scripts/release/deploy-services.sh --tags ... --limit services for an Ansible-only change); both refuse any other commit.

Staging validation: passed on commit 5d15f9742b (this run)
Production drift check: clean

Ansible preview (staging clone, synthetic secrets — approximate, not exact prod values)


PLAY [Deploy the Docker-based services stack] **********************************
skipping: no hosts matched

PLAY RECAP *********************************************************************
### Promotion: staging passed. Run `scripts/release/plan-prod.sh` locally before merging Merging this PR is the approval record; it deploys nothing. Then check out the validated commit and run `scripts/release/promote-prod.sh` (or `scripts/release/deploy-services.sh --tags ... --limit services` for an Ansible-only change); both refuse any other commit. **Staging validation:** passed on commit 5d15f9742bc0584da1858b58a905f908cbe73f3b (this run) **Production drift check:** clean ### Ansible preview (staging clone, synthetic secrets — approximate, not exact prod values) ``` PLAY [Deploy the Docker-based services stack] ********************************** skipping: no hosts matched PLAY RECAP ********************************************************************* ```
scripts/approve-promotion.sh (./infra approve [PR]) checks, before anything is
merged, what the deploy gate will check later: the PR is main -> prod, staging
validated a commit, the PR's head is that commit or differs from it only in files
staging does not exercise (the gate's PROMOTION_STAGED_RE, reused), CI on the head
is green, and scripts/plan-prod.sh does not error. You then type the PR number
("<N> with changes" when the plan found changes); it merges with tea and confirms
on the API that the merge took. Replaces merging in the Forgejo UI, where merges
silently did not take (#129, #133, #134). Needs a terminal; reads no secret.

test-approve-promotion.sh: 15 cases with a fake tea, a fake plan and a throwaway
git repo for the ancestry rule; added to the lint workflow. Runbooks and README
point step 4 at ./infra approve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
fix(promotion): one drift rule for the gate, plan-prod and approve; no false warning
All checks were successful
Lint / lint (pull_request) Successful in 2m16s
Secret scan / scan (pull_request) Successful in 15s
276efcbf46
plan-prod.sh warned "wait for the newer staging run" whenever main had moved past
the validated commit, even when only the vault, docs or scripts outside lib/ had
changed: no staging run ever comes for those, so the warning could never clear
(seen on 2026-10-14 with 022a70a -> 54e508c). The deploy gate already allowed
that case.

promotion_drift <validated> <head> in lib/setup/promotion.sh is now the one rule:
0 = same commit or only files staging does not exercise changed, 1 = deploy-
affecting files changed (listed), 2 = not a descendant. promotion_verify (the
gate), plan-prod.sh and approve-promotion.sh all use it. plan-prod now says OK
in the first case and warns, naming the files, in the others.

test-promotion.sh +5 cases for the rule; checked on the real history:
022a70a -> 54e508c is 0, 54e508c -> main is 1 listing the #152/#157 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
chore(scripts): organize scripts/ by job: release, secrets, checks, ci, dev, migration
All checks were successful
Lint / lint (pull_request) Successful in 2m42s
Secret scan / scan (pull_request) Successful in 37s
0d2667f8eb
scripts/ was flat: production release commands, vault tools, read-only checks, CI
helpers, a dev wrapper and one-off migration wizards side by side (30 scripts).
They now live in release/, secrets/, checks/, ci/, dev/ and migration/; lib/ and
tests/ are unchanged. ./infra keeps the same commands, so daily use does not change.

Every reference is updated: ./infra's dispatch, the workflows and the pre-commit
hook, roles' defaults/docs/alert text, the secrets manifest's rotation fields, the
runbooks, README, tests and the scripts' calls to each other (CHANGELOG.md and
docs/incidents.md keep the paths of their time). Each moved script now finds the
repo root two levels up and lib/ as ../lib. scripts/CLAUDE.md documents the layout.

Checked: bash -n / py_compile on every script; check-downloads, check-secret-hygiene,
39 unit tests, test-promotion, -rotation-lock, -project-key-isolation, -bitwarden-save,
-vault-pr, -tofu-unlock, -infra-cli, -approve-promotion, -container-scan, ansible-lint
and yamllint pass; the moved entry points run from outside the repo (usage paths and
the manifest-only expiry report). test-alert-rules and test-alertmanager-config fail
locally the same way before and after (Docker/promtool on macOS); CI runs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Merge pull request 'feat(infra): ./infra approve merges the promotion PR after the gate's own checks' (#154) from jfraeys/feat/infra-approve into main
All checks were successful
Staging Validation / validate (push) Successful in 12s
Staging Validation / promote (push) Has been skipped
bb781c70e9
Merge pull request 'chore(scripts): organize scripts/ by job: release, secrets, checks, ci, dev, migration' (#156) from jfraeys/chore/scripts-layout into main
Some checks failed
Staging Validation / validate (push) Successful in 12m32s
Staging Validation / promote (push) Successful in 9s
Lint / lint (pull_request) Has started running
Secret scan / scan (pull_request) Has been cancelled
2121fb5dd3
chore(secrets): rotation policy for the unscheduled secrets; make rotate lldap safe
All checks were successful
Lint / lint (pull_request) Successful in 2m7s
Secret scan / scan (pull_request) Successful in 14s
6d101e75b9
Policy (P8, operator decision 2026-10-14), manifest metadata only:
- 18 access credentials get a schedule: 180 days (Postfix sender keys, Zedmail,
  the TF_VAR_* provider tokens, OS_PASSWORD, OVH_APPLICATION_SECRET,
  VAULT_DEPLOY_TOKEN, POSTGRES_PASSWORD, the LDAP service password, deploy and
  SSH keys); OVH_OBJECT_STORAGE_ACCESS_KEY_ID 90, with its secret half.
- 9 are exposure-only, with the reason in `expiry`: secrets that encrypt or sign
  (AUTHELIA_STORAGE_ENCRYPTION_KEY, RESTIC_PASSWORD, AUTHELIA_OIDC_PRIVATE_KEY_PEM,
  LLDAP_KEY_SEED) and identifiers that are not credentials (heartbeat URLs,
  RESTIC_SECONDARY_REPOSITORY).
Expiry report: unknown 44 -> 32 (the rest have a schedule but no recorded date and
get rotated next), ok 18 -> 30.

`./infra rotate lldap` was unsafe in two ways (never run; LLDAP_KEY_SEED has no
rotation date):
- it rotated LLDAP_KEY_SEED, which derives the server key that verifies every
  stored password: everyone would be locked out. It now leaves it alone.
- a rotated LLDAP_ADMIN_PASSWORD never reached LLDAP, which reads LDAP_USER_PASS
  only when it first creates the admin (LLDAP docs), so the vault and LLDAP would
  disagree and the role's bootstrap login would fail. The compose file now sets
  LLDAP_FORCE_LDAP_USER_PASS_RESET=always: the vault is the source of truth.
roles/lldap/CLAUDE.md records both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Merge pull request 'chore(secrets): rotation policy for the unscheduled secrets; make 'rotate lldap' safe' (#159) from jfraeys/chore/secrets-rotation-policy into main
All checks were successful
Staging Validation / validate (push) Successful in 11m46s
Lint / lint (pull_request) Successful in 1m56s
Secret scan / scan (pull_request) Successful in 13s
Staging Validation / promote (push) Successful in 16s
5d15f9742b
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!160
No description provided.