Skip to content

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-self alias its token to a _RUNTIME variable instead of using RENOVATE_TOKEN directly?" has an answer: 2026-06-19-renovate-self-token-self-ref-v0.10.7.md explains 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.md D1 explains why tofu-plan/tofu-apply are 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

  1. 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.
  2. Get it to approved before implementing.
  3. 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.
  4. Flip status: implemented when 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