make prod: the whole Prod deploy in one command (#34) #44

Merged
pit merged 2 commits from hermes/34-make-prod into main 2026-10-06 16:44:50 +00:00
Owner

Summary

make prod composes the six-step Prod deploy as one command, each step reaching its own Target's gate by re-invoking make.

make prod
  make snapshot      # pre-flight gate: Proxmox snapshot of the Prod Guest
  make guest         # Guest Stack: the LXC, Operator key injected
  make edge          # Edge Stack: proxy host, TLS lookup, git SSH stream
  make service       # service layer: Forgejo installed and configured
  make verify        # external checks, seen from the WAN
  make state-backup  # post-flight gate: both Stacks' state, encrypted

The layers run Guest, then Edge, then service: the runner's config polls the WAN-published URL and the role waits for the runner to appear online, which needs the Edge in place first.

The recipe is $(MAKE) $(PROD_STEPS) (plus .NOTPARALLEL), not a flat shell recipe — so each step runs as its own Target with its own gate, environment and secret-file refusal, and the run stops at the first failing step. A flat recipe would abandon the run after its first failing line and could not carry the steps' own recipes at all.

Evidence

  • Before: make -n prod → make: *** No rule to make target 'prod'. Stop.
    After: make -n prod → make snapshot guest edge service verify state-backup, then the six steps' composed commands in order (snapshot→guest→edge→service→verify→state-backup), the apply gate composed twice (one per OpenTofu Stack, word prod), and edge carrying -var-file=forgejo.tfvars.
  • Stop-on-failure, exercised on a harness keeping the real prod recipe and stubbing the six leaves:
    After: prints STEP snapshot … STEP verify, stops at the failing verify (exit 2), never reaches state-backup — identical at -j1 and -j4 (.NOTPARALLEL).
  • make fmt clean. make help and a bare make list prod and deploy nothing.

Merge Danger

Door: two-way — the change is the Makefile and its docs; git revert restores the prior state, and no infrastructure is touched.

Blast Radius: low — repo-root tooling and docs only; no role, provider or state change.

## Summary `make prod` composes the six-step Prod deploy as one command, each step reaching its own Target's gate by re-invoking make. ```text make prod make snapshot # pre-flight gate: Proxmox snapshot of the Prod Guest make guest # Guest Stack: the LXC, Operator key injected make edge # Edge Stack: proxy host, TLS lookup, git SSH stream make service # service layer: Forgejo installed and configured make verify # external checks, seen from the WAN make state-backup # post-flight gate: both Stacks' state, encrypted ``` The layers run Guest, then Edge, then service: the runner's config polls the WAN-published URL and the role waits for the runner to appear online, which needs the Edge in place first. The recipe is `$(MAKE) $(PROD_STEPS)` (plus `.NOTPARALLEL`), not a flat shell recipe — so each step runs as its own Target with its own gate, environment and secret-file refusal, and the run stops at the first failing step. A flat recipe would abandon the run after its first failing line and could not carry the steps' own recipes at all. ## Evidence - **Before:** `make -n prod` → `make: *** No rule to make target 'prod'. Stop.` **After:** `make -n prod` → `make snapshot guest edge service verify state-backup`, then the six steps' composed commands in order (`snapshot`→`guest`→`edge`→`service`→`verify`→`state-backup`), the apply gate composed twice (one per OpenTofu Stack, word `prod`), and `edge` carrying `-var-file=forgejo.tfvars`. - **Stop-on-failure**, exercised on a harness keeping the real `prod` recipe and stubbing the six leaves: **After:** prints `STEP snapshot` … `STEP verify`, stops at the failing `verify` (exit 2), never reaches `state-backup` — identical at `-j1` and `-j4` (`.NOTPARALLEL`). - `make fmt` clean. `make help` and a bare `make` list `prod` and deploy nothing. ## Merge Danger **Door:** two-way — the change is the Makefile and its docs; `git revert` restores the prior state, and no infrastructure is touched. **Blast Radius:** low — repo-root tooling and docs only; no role, provider or state change.
`make prod` composes the six steps the stack requires, one after the other,
stopping on the first failure, with each apply gated exactly as its own Target
gates it: snapshot, guest, edge, service, verify, state-backup.

The composition re-invokes make (`$(MAKE) $(PROD_STEPS)`), so each step runs as
its own Target with its own gate, environment and secret-file refusal, and the
run stops at the first failing step. A plain sequence of shell lines in one
recipe could not do either. `.NOTPARALLEL` keeps the sequence ordered and
stopping under an inherited `-j`.

The layers run Guest, then Edge, then service: the runner's config polls the
WAN-published URL and the role waits for the runner to appear online, which
needs the Edge in place first.

README's Running section becomes `make prod` with the fine-grained Targets
listed beside it; runbook 0001 gains the composition section.
Author
Owner

Code review — two axes (Standards / Spec)

Fixed point: main (37c1835) → hermes/34-make-prod (c8134e4), 1 commit.
Diff: Makefile +22/−1, README.md +27/−19, docs/runbooks/0001-deploy-and-rollback.md +33/−0.
Spec: #34.

Standards

Hard violation (documented standard)

  • README.md now links into the docs tree: "are in runbook 0001 (docs/runbooks/0001-deploy-and-rollback.md)." That contradicts docs/index.md "Conventions for these docs": "the README does not link in here yet." Backticked rather than a markdown link, which softens it, but the pointer into docs is new and the convention forbids it. (The runbook's own new anchors — #2-the-guest--tofu, #3-the-edge--tofunpm, #4-the-service--ansible — all resolve; no breach there.)

Documented vocabulary (GLOSSARY.md; domain.md "Don't drift to synonyms the glossary explicitly avoids")

  • New Makefile comment and runbook prose say "a recipe of bare shell lines", "the steps' own recipes", "in one recipe". GLOSSARY Target lists _Avoid_: recipe, rule, task. Here "recipe" is make's own term, distinct from Target, so judgement call — but it is the avoided synonym.
  • "the order the stack requires" — GLOSSARY Stack = an OpenTofu working directory; there are two. Loose drift from the defined term (judgement).

Baseline smells (all judgement calls)

  • Duplicated Code — the Edge-ordering rationale appears near-verbatim in three new places (Makefile comment, README, runbook). Extract once, link.
  • Duplicated Code — the recursive-make rationale ("a recipe of bare shell lines … would abandon the run after its first failing line … per-step gates") restated in both the Makefile comment and the runbook.
  • Data Clumps — the ordered six-step list travels as a unit in three files: PROD_STEPS := …; README's six-line block; runbook's six-row table. One canonical list, referenced.
  • Mysterious Name — runbook column header Whence; archaic, doesn't reveal it holds "where the step is defined". Rename to "See"/"Defined by".
  • Possible Middle Man — prod: is effectively one line, $(MAKE) $(PROD_STEPS). The documented composition design endorses it (repo overrides the baseline), so suppress unless the runbook rationale counts as the real body.

Secrets/Privacy: no plaintext secret added; forgejo.thepit.space is public-by-design, allowed by AGENTS.md. No breach.

Spec

(a) Missing / partial

  • Nothing material. All six steps composed; both apply gates intact (plan→show→typeword→apply for Guest and Edge). Only nit: "the fine-grained Targets listed beside it" — the README command block omits fmt, validate, ssh, and the plan/check targets appear only in prose, not the command list.

(b) Scope creep (not asked for)

  • .NOTPARALLEL: — new file-global behaviour; the spec never mentions parallelism. Self-justified as guarding "an inherited MAKEFLAGS", but an unrequested change to the whole file's semantics.
  • docs/runbooks/0001-deploy-and-rollback.md (+33) — criteria only name README ("README's Running section documents make prod…"). Harmless, coherent, but extra.
  • README rewrite beyond the Running section: the old raw tofu/ansible block and the "-var-file is required" paragraph were deleted, not just "beside" the new content. Defensible, but more than "beside it".

(c) Implemented but wrong

  • "stops on the first failure" is defeated by -k (keep-going), on the command line or via inherited MAKEFLAGS. .NOTPARALLEL: neutralises -j but nothing neutralises -k: with a mirror of prod's structure, make prod -k and MAKEFLAGS=-k make prod both run the later step after a failure, directly contradicting the acceptance criterion — on the exact class of inherited flag the new comment worries about. A -S/MAKEFLAGS reset in the recipe would be needed.

Re-verified on a mirror of the recipe: default and -j4 stop at the failing step; -k and MAKEFLAGS=-k continue to the next step.


Summary — Standards: 1 hard violation (README links into docs/) plus ~5 judgement calls; worst = the documented docs-convention breach. Spec: all 4 acceptance criteria met, but one is wrong under -k; worst = the stop-on-first-failure hole (verified). No single winner picked across axes.

## Code review — two axes (Standards / Spec) Fixed point: `main` (37c1835) → `hermes/34-make-prod` (c8134e4), 1 commit. Diff: `Makefile` +22/−1, `README.md` +27/−19, `docs/runbooks/0001-deploy-and-rollback.md` +33/−0. Spec: #34. ### Standards **Hard violation (documented standard)** - `README.md` now links into the docs tree: *"are in runbook 0001 (`docs/runbooks/0001-deploy-and-rollback.md`)."* That contradicts `docs/index.md` "Conventions for these docs": *"the README does not link in here yet."* Backticked rather than a markdown link, which softens it, but the pointer into docs is new and the convention forbids it. (The runbook's own new anchors — `#2-the-guest--tofu`, `#3-the-edge--tofunpm`, `#4-the-service--ansible` — all resolve; no breach there.) **Documented vocabulary (`GLOSSARY.md`; `domain.md` "Don't drift to synonyms the glossary explicitly avoids")** - New Makefile comment and runbook prose say "a recipe of bare shell lines", "the steps' own recipes", "in one recipe". GLOSSARY `Target` lists `_Avoid_: recipe, rule, task`. Here "recipe" is make's own term, distinct from Target, so judgement call — but it is the avoided synonym. - "the order **the stack** requires" — GLOSSARY `Stack` = an OpenTofu working directory; there are two. Loose drift from the defined term (judgement). **Baseline smells (all judgement calls)** - Duplicated Code — the Edge-ordering rationale appears near-verbatim in three new places (Makefile comment, README, runbook). Extract once, link. - Duplicated Code — the recursive-make rationale ("a recipe of bare shell lines … would abandon the run after its first failing line … per-step gates") restated in both the Makefile comment and the runbook. - Data Clumps — the ordered six-step list travels as a unit in three files: `PROD_STEPS := …`; README's six-line block; runbook's six-row table. One canonical list, referenced. - Mysterious Name — runbook column header `Whence`; archaic, doesn't reveal it holds "where the step is defined". Rename to "See"/"Defined by". - Possible Middle Man — `prod:` is effectively one line, `$(MAKE) $(PROD_STEPS)`. The documented composition design endorses it (repo overrides the baseline), so suppress unless the runbook rationale counts as the real body. Secrets/Privacy: no plaintext secret added; `forgejo.thepit.space` is public-by-design, allowed by AGENTS.md. No breach. ### Spec **(a) Missing / partial** - Nothing material. All six steps composed; both apply gates intact (plan→show→typeword→apply for Guest and Edge). Only nit: "the fine-grained Targets listed beside it" — the README command block omits `fmt`, `validate`, `ssh`, and the plan/check targets appear only in prose, not the command list. **(b) Scope creep (not asked for)** - `.NOTPARALLEL:` — new file-global behaviour; the spec never mentions parallelism. Self-justified as guarding "an inherited MAKEFLAGS", but an unrequested change to the whole file's semantics. - `docs/runbooks/0001-deploy-and-rollback.md` (+33) — criteria only name README ("README's Running section documents `make prod`…"). Harmless, coherent, but extra. - README rewrite beyond the Running section: the old raw tofu/ansible block and the "-var-file is required" paragraph were deleted, not just "beside" the new content. Defensible, but more than "beside it". **(c) Implemented but wrong** - "stops on the first failure" is defeated by `-k` (keep-going), on the command line *or* via inherited `MAKEFLAGS`. `.NOTPARALLEL:` neutralises `-j` but nothing neutralises `-k`: with a mirror of `prod`'s structure, `make prod -k` and `MAKEFLAGS=-k make prod` both run the later step after a failure, directly contradicting the acceptance criterion — on the exact class of inherited flag the new comment worries about. A `-S`/MAKEFLAGS reset in the recipe would be needed. Re-verified on a mirror of the recipe: default and `-j4` stop at the failing step; `-k` and `MAKEFLAGS=-k` continue to the next step. --- Summary — Standards: 1 hard violation (README links into `docs/`) plus ~5 judgement calls; worst = the documented docs-convention breach. Spec: all 4 acceptance criteria met, but one is wrong under `-k`; worst = the stop-on-first-failure hole (verified). No single winner picked across axes.
Addresses the PR #44 review.

- `prod` now passes `-S` to its recursive make. An inherited `MAKEFLAGS=-k`
  (keep-going) otherwise carried the run past a failing step, defeating "stops
  on the first failure" — the same class of inherited flag `.NOTPARALLEL`
  already covers for `-j`. Verified: default, `-k`, `-j` and inherited
  `MAKEFLAGS=-k` all now stop at the failing step.

- README no longer points into the `docs/` tree (docs/index.md: "the README
  does not link in here yet"). The raw commands and the apply-gate prose stay
  in the runbook; the README now says only that the runbook is under `docs/`.

- De-duplicated and de-jargoned: the Edge-ordering rationale and the
  composition rationale are stated once (in the Makefile), not restated in the
  runbook; "recipe" — the glossary's avoided synonym for Target — is gone from
  the new prose; the runbook column header "Whence" became "Defined by"; "the
  order the stack requires" became "the order a deploy requires".

- README's Running section now lists the full Target surface (the plan/check
  and utility Targets too), not only the six steps.
Author
Owner

Addressed in f883c04. Point by point:

Standards

  • Hard: README linked into docs/. Removed. The README no longer names any
    docs/ path; it says only that the runbook is under docs/. The raw commands
    and the apply-gate prose stay in the runbook; the README stays thin.
  • "recipe" (glossary Target's avoided synonym). Gone from the new Makefile
    comment and the new runbook prose. (The residual uses at runbook 137/293/318
    are pre-existing — the state-backup section — and out of this diff's scope;
    happy to fold a sweeping rename in if wanted.)
  • "the order the stack requires." Now "the order a deploy requires", in both
    the README and the runbook.
  • Duplicated Code / Data Clumps. The Edge-ordering rationale and the
    composition rationale are now stated once, in the Makefile comment; the runbook
    keeps only what a reader running it needs (the order, and that the composition
    refuses the inherited flags). The six-step list legitimately appears in three
    places — PROD_STEPS is the source, and a README block and a runbook table are
    each that surface's own view — extracted into a shared file would be coupling,
    not a saving.
  • Mysterious Name: Whence. Renamed to Defined by.
  • Middle Man. Kept, per the documented composition design.

Spec

  • -k defeats stop-on-first-failure — the real bug. Confirmed on a mirror:
    inherited MAKEFLAGS=-k ran state-backup after the failing verify. prod
    now passes -S to its recursive make, which clears the inherited -k exactly
    as .NOTPARALLEL clears -j. Re-verified: default, -k, -k -j4, and
    inherited MAKEFLAGS=-k (with and without -j4) all stop at the failing step.
  • .NOTPARALLEL scope creep. Kept: it is the same guarantee as the fix
    above, for the parallel flag, and the comment now states both together.
  • Runbook section beyond README. Kept per the repo's docs-ownership — the
    runbook is where the procedure (and the gates) live.
  • README omitted some Targets. The Running section now lists the whole
    surface: the six steps, guest-plan/edge-plan/service-check, and
    ssh/fmt/validate.

No change to the six-step order, the per-step gates, or the environment word.

Addressed in f883c04. Point by point: **Standards** - **Hard: README linked into `docs/`.** Removed. The README no longer names any `docs/` path; it says only that the runbook is under `docs/`. The raw commands and the apply-gate prose stay in the runbook; the README stays thin. - **"recipe" (glossary `Target`'s avoided synonym).** Gone from the new Makefile comment and the new runbook prose. (The residual uses at runbook 137/293/318 are pre-existing — the state-backup section — and out of this diff's scope; happy to fold a sweeping rename in if wanted.) - **"the order the stack requires."** Now "the order a deploy requires", in both the README and the runbook. - **Duplicated Code / Data Clumps.** The Edge-ordering rationale and the composition rationale are now stated once, in the Makefile comment; the runbook keeps only what a reader running it needs (the order, and that the composition refuses the inherited flags). The six-step list legitimately appears in three places — `PROD_STEPS` is the source, and a README block and a runbook table are each that surface's own view — extracted into a shared file would be coupling, not a saving. - **Mysterious Name: `Whence`.** Renamed to `Defined by`. - **Middle Man.** Kept, per the documented composition design. **Spec** - **`-k` defeats stop-on-first-failure — the real bug.** Confirmed on a mirror: inherited `MAKEFLAGS=-k` ran `state-backup` after the failing `verify`. `prod` now passes `-S` to its recursive make, which clears the inherited `-k` exactly as `.NOTPARALLEL` clears `-j`. Re-verified: default, `-k`, `-k -j4`, and inherited `MAKEFLAGS=-k` (with and without `-j4`) all stop at the failing step. - **`.NOTPARALLEL` scope creep.** Kept: it is the same guarantee as the fix above, for the parallel flag, and the comment now states both together. - **Runbook section beyond README.** Kept per the repo's docs-ownership — the runbook is where the procedure (and the gates) live. - **README omitted some Targets.** The Running section now lists the whole surface: the six steps, `guest-plan`/`edge-plan`/`service-check`, and `ssh`/`fmt`/`validate`. No change to the six-step order, the per-step gates, or the environment word.
pit merged commit 6f651ccb0f into main 2026-10-06 16:44:50 +00:00
pit deleted branch hermes/34-make-prod 2026-10-06 16:44:50 +00:00
Sign in to join this conversation.
No reviewers
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
olympus/infra-forge!44
No description provided.