Spec-first development¶
Every component in this repo — all 24 of them, across eight tracks — exists
because a dated spec under docs/development/specs/ said it should, and
described why. This is not a formality bolted on afterwards; it is the
project's actual discipline: no template or pipeline change without a spec
it implements.
Why bother¶
A CI/CD component is a piece of shared infrastructure — a change to
go-test.yml doesn't just affect this repo, it affects every consumer that
pins it. The expensive, error-prone part of a change like that is rarely the
YAML; it's deciding what to build, why, and what the safe default is. A
spec forces that decision to happen explicitly, in writing, before any code
changes — which means:
- The reasoning survives the change. Six months later, "why does
renovate-selfalias its token to a_RUNTIMEvariable instead of usingRENOVATE_TOKENdirectly?" has an answer:2026-06-19-renovate-self-token-self-ref-v0.10.7.mdexplains the self-referencing job/group-variable collision that broke authentication for every consumer on the default token. Without the spec, that's tribal knowledge that evaporates. - Options that were rejected stay visible. A spec records what was
considered and declined, not just what shipped —
2026-05-16-tofu-plan-apply-v0.2.mdD1 explains whytofu-plan/tofu-applyare two components, not one mode-switched component, so nobody re-proposes the merge without rediscovering the reasons it doesn't work. - Reviews check the code against the spec, not just against itself. A merge request that implements a spec can be reviewed by asking "does this match what was decided?" — a much sharper question than "does this look reasonable?"
The lifecycle¶
Every spec carries a status in its YAML frontmatter, and that status is
the single source of truth for where a decision stands:
draft— under discussion. Nothing should be implemented against a draft spec yet.approved— agreed. Safe to implement.rejected— considered and declined, or superseded by a later decision. Rejected specs are kept, not deleted — the value is the durable "we thought about X and chose not to, for these reasons" record, so the question doesn't get re-litigated from scratch by someone who wasn't there the first time.implemented— shipped. Flipped when the change merges or the tag cuts.
The exhaustive, dated index of every spec — its status, a one-line summary, and which component(s) it touches — is the decision log.
The discipline in practice¶
- Draft the spec before touching the implementation. If a template edit is underway with no spec covering it, that's the signal to stop and write (or extend) the spec first.
- Get it to
approvedbefore implementing. - Cite the spec in the merge request. Every MR that implements a decision names the spec (and, where the spec numbers its decisions, the decision letter — "D3") it implements.
- Flip
status: implementedwhen it ships.
Not every change needs this ceremony — a version-pin bump or a typo fix doesn't warrant a spec. The bar is genuinely non-trivial or decision-bearing: if a reasonable person reviewing the change would ask "why was it done this way?", it needed a spec. Twenty-four components and thirty-plus specs in, most of this repo's design decisions are one link away from their reasoning — that's the payoff.
See also¶
- Contributing: the workflow — where the spec step fits alongside writing the template, the self-test, and the reference page.
- Decision log — every spec, indexed.