From 5cd2cc43d16697ed5a9ee9ccccb8d28d28c1c3ee Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Mon, 27 Jul 2026 19:55:06 +0200 Subject: [PATCH 1/3] docs(governance): stop asking contributors to declare their tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An AI is a tool. A pull request is judged on whether it satisfies the requirement it cites, passes the gates, and is code somebody can maintain — none of which is a fact about what produced it. A disclosure rule also implies the code needs a different kind of scrutiny because of its provenance, which is the wrong signal to send about the scrutiny everything else gets. And it was unenforceable: obeyed by the careful, ignored by everyone else, leaving a field reviewers could not trust either way. Review is there for a reason. The remaining rules on the page are the ones that were never about disclosure — you are the author, the judgment rules bind identically, no invented citations, no invented facts, no secrets — and they are renumbered to close the gap. Signed-off-by: Wessel Verheij --- 50-governance/README.md | 5 ++--- 50-governance/ai-contributors.md | 38 +++++++++++++++----------------- 50-governance/contributing.md | 4 ++-- 50-governance/overrides.md | 2 +- 70-operations/maintainers.toml | 2 +- 90-appendix/open-questions.md | 2 +- 90-appendix/provenance.md | 2 +- AGENTS.md | 2 +- 8 files changed, 27 insertions(+), 30 deletions(-) diff --git a/50-governance/README.md b/50-governance/README.md index 3ba6c44..1e4f1bb 100644 --- a/50-governance/README.md +++ b/50-governance/README.md @@ -46,9 +46,8 @@ from. | **GOV-R21** | A specification change that alters a released version's locked goals MUST be reviewed as a goals change. | | **GOV-R22** | A pull request MUST receive a sticky comment linking each cited identifier to its defining file. | | **GOV-R23** | An issue that describes a behaviour the specification does not cover MUST become a specification change before implementation. | -| **GOV-R24** | AI-assisted contributions MUST meet the same standards as human ones and MUST be disclosed. | -| **GOV-R25** | The project lead holds override authority; every override MUST be recorded ([overrides.md](overrides.md)). | -| **GOV-R26** | Where a decision is genuinely unmade, the document MUST say so under an explicit open-question heading rather than inventing an answer. | +| **GOV-R24** | The project lead holds override authority; every override MUST be recorded ([overrides.md](overrides.md)). | +| **GOV-R25** | Where a decision is genuinely unmade, the document MUST say so under an explicit open-question heading rather than inventing an answer. | ## Where to ask diff --git a/50-governance/ai-contributors.md b/50-governance/ai-contributors.md index 3dbe4c6..a73492e 100644 --- a/50-governance/ai-contributors.md +++ b/50-governance/ai-contributors.md @@ -13,22 +13,20 @@ get a lower bar. It also does not get a **higher** one. A pull request is judged on whether it satisfies the requirement it cites, passes the gates, and is code somebody can -maintain. How it was written is a disclosure, not a verdict. +maintain. How it was written is not a property of the change. -## The rules - -### 1. Disclose it +**There is nothing to disclose.** An AI is a tool, like an IDE, a linter, or a +search engine, and this project does not ask which of those you used either. A +disclosure requirement would imply the code needs a different kind of scrutiny +because of its provenance — but the scrutiny every contribution gets is already +the right amount, and review is there for a reason. Requiring a declaration +would also be unenforceable, which is the worst property a rule can have: it +would be obeyed by the careful and ignored by everyone else, leaving reviewers +with a signal that means nothing. -A contribution written with significant AI assistance says so -([GOV-R24](README.md#the-gov-r-namespace)). A line in the pull-request body is -enough. - -This is not about suspicion. It is about a reviewer knowing what kind of review -to do — the failure modes of AI-written code are different from the failure -modes of human-written code, and a reviewer who knows which they are looking at -reviews better. +## The rules -### 2. You are the author +### 1. You are the author The sign-off is yours ([dco.md](dco.md)). You are certifying that you have the right to submit this under the project's licence and that you understand the @@ -37,7 +35,7 @@ contribution. **An AI cannot sign off.** If you cannot explain what a change does and why, you cannot certify it, and you should not open the pull request. -### 3. The judgment rules bind identically +### 2. The judgment rules bind identically The comment policy's judgment rules — prefer self-documenting code, comment only genuine complexity, architecture goes in documentation, nothing is deferred in a @@ -49,7 +47,7 @@ next line does, which is exactly what the policy forbids and what the mechanical test catches. Strip it before opening the pull request rather than after CI tells you. -### 4. No invented requirements +### 3. No invented requirements The gate requires citing an identifier that already exists ([canonical-spec.md](canonical-spec.md)). An identifier that does not exist fails @@ -59,28 +57,28 @@ and fails review. Do not let a model choose the citation. Choose it yourself, from the requirement the change actually satisfies. -### 5. No invented facts in documentation +### 4. No invented facts in documentation Every requirement in this specification is traceable to something real. Where a source is silent, the honest answer is an **explicit open question**, not a -confident-sounding invention ([GOV-R26](README.md#the-gov-r-namespace)). +confident-sounding invention ([GOV-R25](README.md#the-gov-r-namespace)). Generated documentation is prone to filling gaps smoothly. A smoothly-filled gap in a specification is worse than a visible hole, because nobody knows to check it. -### 6. Verify against the code, not the plan +### 5. Verify against the code, not the plan A generated change describes what it intended to do. Whether it did is a different question, answered by reading the diff and running the tests. -### 7. Security-relevant code gets extra scrutiny +### 6. Security-relevant code gets extra scrutiny Cryptography, authentication, the sync transport, the encryption boundary, and the outbound surface are reviewed line by line regardless of provenance. The [security](../40-quality/security.md) page names them. -### 8. No secrets, ever +### 7. No secrets, ever Never paste credentials, tokens, a real database, or real financial data into a tool. The product's whole premise is that this data does not leave the user's diff --git a/50-governance/contributing.md b/50-governance/contributing.md index dc58850..085c4cc 100644 --- a/50-governance/contributing.md +++ b/50-governance/contributing.md @@ -116,8 +116,8 @@ not merge. ## AI-assisted contributions -Welcome, held to the same standard, and **disclosed**. See -[ai-contributors.md](ai-contributors.md). +Welcome, held to the same standard as every other contribution, and nothing +to declare. See [ai-contributors.md](ai-contributors.md). ## Code of conduct diff --git a/50-governance/overrides.md b/50-governance/overrides.md index fa88166..44dd9ff 100644 --- a/50-governance/overrides.md +++ b/50-governance/overrides.md @@ -10,7 +10,7 @@ This page says who may break a rule, when, and what they owe afterwards. ## Who -**The project lead** ([GOV-R25](README.md#the-gov-r-namespace)), identified in +**The project lead** ([GOV-R24](README.md#the-gov-r-namespace)), identified in the maintainer registry by the lead flag ([70-operations/maintainers.toml](../70-operations/maintainers.toml)). diff --git a/70-operations/maintainers.toml b/70-operations/maintainers.toml index 7fbc5aa..9eef6f2 100644 --- a/70-operations/maintainers.toml +++ b/70-operations/maintainers.toml @@ -10,7 +10,7 @@ [[maintainer]] handle = "lessevv" name = "Wessel Verheij" -lead = true # project lead; holds the override authority (GOV-R25) +lead = true # project lead; holds the override authority (GOV-R24) scope = ["*"] domains = ["*"] diff --git a/90-appendix/open-questions.md b/90-appendix/open-questions.md index 81eb0aa..35ff1d7 100644 --- a/90-appendix/open-questions.md +++ b/90-appendix/open-questions.md @@ -6,7 +6,7 @@ Every genuinely unresolved question in this specification, in one place. They are recorded here **and** in the document each belongs to. A question smoothed over reads better and is worse: nobody knows to check it -([GOV-R26](../50-governance/README.md#the-gov-r-namespace)). +([GOV-R25](../50-governance/README.md#the-gov-r-namespace)). ## Product and release diff --git a/90-appendix/provenance.md b/90-appendix/provenance.md index 2b140fc..b527c0a 100644 --- a/90-appendix/provenance.md +++ b/90-appendix/provenance.md @@ -85,7 +85,7 @@ Recorded rather than silently resolved: Every unresolved question found in the sources, or created by this specification's own scope, is recorded under an explicit heading in the document it belongs to — never smoothed over -([GOV-R26](../50-governance/README.md#the-gov-r-namespace)). +([GOV-R25](../50-governance/README.md#the-gov-r-namespace)). They are indexed in [open-questions.md](open-questions.md). diff --git a/AGENTS.md b/AGENTS.md index 3617818..3d3200c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ never true.** - Every requirement traces to something real in the product repository — its code, its documentation, its planning corpus, or its history. - Where a source is silent or two sources disagree, say so under an explicit - **open question** heading (`GOV-R26`) and add it to + **open question** heading (`GOV-R25`) and add it to [90-appendix/open-questions.md](90-appendix/open-questions.md). - Where something is not yet satisfied, mark the requirement *(Open)* and say so plainly. Several already are. From 3832d6cee110b66363b2d5508451fb928dd051c1 Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Mon, 27 Jul 2026 20:14:24 +0200 Subject: [PATCH 2/3] docs(ci): reference the shared workflows by major-version tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0012 said first-party reusables are called on @main. The org never did it: this repo and the website pinned them to commits, deliberately, while the product repo's callers used @main. Three repos, two behaviours, and a rule contradicted in the tree — which teaches contributors the specification describes intentions rather than facts. Both positions were right about something. @main is right that a shared fix should propagate in one merge rather than needing a bump in every consumer. Pinning is right that @main lets a change to this repo's default branch reach every sibling's merge gate immediately, with no mechanism for a breaking one. A moving major tag keeps both. v1 moves forward as fixes merge, so propagation is unchanged; a breaking change to a calling interface cuts v2, so consumers migrate one at a time instead of failing at once. Every move is also an immutable v1.x.y tag, so what v1 pointed at on a given day stays recoverable. This is not the mutable-tag risk ADR-0012 warned about, and the new record says why rather than assuming it: there the tag mover is a third party, here it is this protected repository. Anyone who can move v1 can already push to main, which @main would have executed anyway. ADR-0021 records it and supersedes ADR-0012's second rule; the first, full-SHA pinning for third-party actions, is untouched. REPO-R7 and the two quality pages that restated the old rule are brought in line, and cross-repo-ci.md now states what breaks a workflow contract and how to move the tag. Signed-off-by: Wessel Verheij --- .github/workflows/awaiting-maintainer.yml | 2 +- .github/workflows/hygiene.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/spec-check.yml | 2 +- .github/workflows/spec-references.yml | 2 +- .github/workflows/triage.yml | 2 +- 00-overview/decisions/0012-action-pinning.md | 8 +- .../0021-reusable-workflow-version-tags.md | 124 ++++++++++++++++++ 30-repos/README.md | 2 +- 40-quality/ci-cd.md | 2 +- 40-quality/tooling.md | 2 +- 50-governance/cross-repo-ci.md | 50 ++++++- 12 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 00-overview/decisions/0021-reusable-workflow-version-tags.md diff --git a/.github/workflows/awaiting-maintainer.yml b/.github/workflows/awaiting-maintainer.yml index cee3353..c26c176 100644 --- a/.github/workflows/awaiting-maintainer.yml +++ b/.github/workflows/awaiting-maintainer.yml @@ -64,7 +64,7 @@ jobs: notify: needs: toggle if: needs.toggle.outputs.flagged == 'true' - uses: beatrax-app/spec/.github/workflows/discord-notify.yml@c834e5e03d5ece45462cfe2f5896451abf5b55ca # main + uses: beatrax-app/spec/.github/workflows/discord-notify.yml@v1 with: title: "🔔 Awaiting review · ${{ github.event.repository.name }} #${{ needs.toggle.outputs.pr }}" body: "Passed CI with no review — a maintainer needs to take a look." diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml index bf7cfba..937db70 100644 --- a/.github/workflows/hygiene.yml +++ b/.github/workflows/hygiene.yml @@ -1,5 +1,5 @@ # Reusable hygiene checks: workflow lint, spelling, links, markdown. -# Called by each repo: uses: beatrax-app/spec/.github/workflows/hygiene.yml@main +# Called by each repo: uses: beatrax-app/spec/.github/workflows/hygiene.yml@v1 # One definition, no drift (Q-R20). name: hygiene diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 35dd068..1635dc0 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -11,4 +11,4 @@ jobs: permissions: contents: read issues: write - uses: beatrax-app/spec/.github/workflows/label-sync.yml@c834e5e03d5ece45462cfe2f5896451abf5b55ca # main + uses: beatrax-app/spec/.github/workflows/label-sync.yml@v1 diff --git a/.github/workflows/spec-check.yml b/.github/workflows/spec-check.yml index 2796738..e82d1ca 100644 --- a/.github/workflows/spec-check.yml +++ b/.github/workflows/spec-check.yml @@ -2,7 +2,7 @@ # # jobs: # spec-check: -# uses: beatrax-app/spec/.github/workflows/spec-check.yml@main +# uses: beatrax-app/spec/.github/workflows/spec-check.yml@v1 # # Defined once here so the rule has exactly one implementation and cannot drift # between repos. See spec 50-governance/cross-repo-ci.md. diff --git a/.github/workflows/spec-references.yml b/.github/workflows/spec-references.yml index c0f348f..ef9df54 100644 --- a/.github/workflows/spec-references.yml +++ b/.github/workflows/spec-references.yml @@ -11,7 +11,7 @@ # permissions: # contents: read # pull-requests: write -# uses: beatrax-app/spec/.github/workflows/spec-references.yml@main +# uses: beatrax-app/spec/.github/workflows/spec-references.yml@v1 # # `workflow_call` only. This repo is the source of citations rather than a # consumer of them, so it does not self-trigger. diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index e3d0174..e86243d 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -28,7 +28,7 @@ jobs: if [ -n "$owners" ]; then gh issue edit "$NUM" --add-assignee "$owners" || true; fi notify: needs: triage - uses: beatrax-app/spec/.github/workflows/discord-notify.yml@c834e5e03d5ece45462cfe2f5896451abf5b55ca # main + uses: beatrax-app/spec/.github/workflows/discord-notify.yml@v1 with: title: "📥 Needs triage · ${{ github.event.repository.name }} #${{ github.event.issue.number }}" body: ${{ github.event.issue.title }} diff --git a/00-overview/decisions/0012-action-pinning.md b/00-overview/decisions/0012-action-pinning.md index 940c3cb..64a24c2 100644 --- a/00-overview/decisions/0012-action-pinning.md +++ b/00-overview/decisions/0012-action-pinning.md @@ -1,8 +1,14 @@ # ADR-0012: Third-party actions SHA-pinned; first-party reusable workflows on `@main` -**Status:** Accepted +**Status:** Accepted; second rule superseded by [ADR-0021](0021-reusable-workflow-version-tags.md) **Date:** 2026-07-27 +> **The second rule below no longer holds.** First-party reusable workflows are +> referenced by moving major-version tag, not `@main` +> ([ADR-0021](0021-reusable-workflow-version-tags.md)). The first rule — +> full-SHA pinning for third-party actions — stands unchanged, and is the +> reason this record is still load-bearing. + ## Context Every workflow in the org executes with a token that can write to the diff --git a/00-overview/decisions/0021-reusable-workflow-version-tags.md b/00-overview/decisions/0021-reusable-workflow-version-tags.md new file mode 100644 index 0000000..eeab54d --- /dev/null +++ b/00-overview/decisions/0021-reusable-workflow-version-tags.md @@ -0,0 +1,124 @@ +# ADR-0021: First-party reusable workflows are referenced by major version tag + +**Status:** Accepted +**Date:** 2026-07-27 +**Supersedes:** the second rule of +[ADR-0012](0012-action-pinning.md) — first-party reusable workflows on `@main`. +Its first rule, full-SHA pinning for third-party actions, stands unchanged. + +## Context + +ADR-0012 settled two rules. The first — third-party actions pinned to a full +commit SHA — is not in question and is not touched here. + +The second said first-party reusable workflows, the ones this repository defines +and its siblings call, are referenced as `@main`. The reasoning was that they are +org-owned code gated by the org's own review, so pinning buys no security and +costs real coordination. + +In practice the organisation did not do this. The website repository and this +repository's own internal call both pinned to commits, deliberately, in a change +whose message was "a tag or branch reference can be repointed without the calling +workflow changing, which is the whole reason the third-party actions are pinned". +The product repository's new callers used `@main`, per the ADR as written. + +So the org had three repositories doing two different things, and a rule nobody +had followed since it was written. That is worse than either option: a rule that +is contradicted in the tree teaches contributors the specification describes +intentions rather than facts. + +Both positions have a real point, and the disagreement is genuine: + +- `@main` is right that a shared fix should propagate in one merge. Requiring a + bump in every consumer before a DCO-check fix takes effect is exactly how + shared workflows decay back into per-repo copies. +- The pinning change is right that `@main` means an unreviewed-by-the-consumer + change to this repository's default branch reaches every sibling's pipeline + immediately, including the pipelines that gate merges. + +## Decision + +**First-party reusable workflows are referenced by a moving major-version tag.** + +```yaml +uses: beatrax-app/spec/.github/workflows/dco.yml@v1 +``` + +Three rules make that work: + +1. **`v1` moves.** A fix or an additive change to a shared workflow moves the + `v1` tag forward when it merges. Consumers pick it up on their next run, with + no change on their side — the propagation property `@main` was protecting. + +2. **A breaking change cuts `v2`.** Consumers move deliberately, one repository + at a time, and a half-migrated org is a valid state rather than a broken one. + Breaking means, precisely: removing or renaming an input or a secret; making + an optional input required; renaming a job, because consumers name jobs in + their branch rulesets as required checks ([OPS-R18](../../70-operations/README.md#the-ops-r-namespace)); + or changing behaviour such that a pull request which passed before now fails. + +3. **Every move is also an immutable tag.** `v1` is repointed onto a + `v1..` tag that is never moved, so what `v1` pointed at on any + given day is recoverable, and a consumer that needs to hold still can pin to + the immutable one. + +## Why this is not the mutable-tag risk ADR-0012 warned about + +ADR-0012's threat is real and unchanged: a third-party action's owner can move a +tag onto malicious code, and the consumer runs it with a write-capable token. The +2025 compromise it cites worked exactly that way. + +That threat does not transfer here, for a reason worth stating rather than +assuming. **The tag mover and the tag consumer are the same party.** Moving `v1` +requires a push to this repository, whose default branch is protected — linear +history, signed commits, required checks, owner approval on workflow files +([REPO-R8](../../30-repos/README.md#the-repo-r-namespace), +[REPO-R13](../../30-repos/README.md#the-repo-r-namespace)). An attacker who can +move `v1` can already push to `main`, which `@main` would have executed anyway. + +Against `@main`, this decision is strictly stronger: a breaking change no longer +reaches consumers the moment it merges. Against full SHA pinning, it is weaker by +exactly the amount of trust the org already places in its own protected branch. + +## Alternatives considered + +| Option | Why it lost | +|--------|-------------| +| **Keep `@main`** | Correct on propagation, but leaves no mechanism for a breaking change: every consumer breaks simultaneously, at merge time, in the pipeline that gates merges. | +| **SHA-pin first-party too** | A one-line fix to a shared check needs a bump pull request in every consumer before it takes effect. It also makes this repository pin itself to its own commits for its internal call, which is self-referential churn with no reader. | +| **Immutable tags only, no moving major** | Every shared fix becomes a bump in three repositories — SHA pinning with extra steps. | + +## Consequences + +### Positive + +- One reference form across every repository, matching what the specification says. +- Shared fixes still propagate in one merge. +- Breaking changes to a workflow contract become explicit and migratable. +- What `v1` pointed at on any date stays recoverable. + +### Negative + +- **This repository now has a release step it did not have.** Merging a change to + a shared workflow is no longer sufficient; the tag has to move, and a forgotten + move means the fix silently does not reach anyone. +- Judging whether a change is breaking is a human call, and getting it wrong + breaks consumers rather than a build here. + +### Neutral + +- Dependency automation does not propose major-tag moves, so `v1 → v2` is + deliberate work, which is the intent. + +## Revisit if + +- The tag move is forgotten often enough to be a real failure mode, at which + point it should be automated on merge rather than left to discipline. +- GitHub introduces immutable tags, which would let the moving major and the + audit trail be the same object. + +## Related + +- [ADR-0012](0012-action-pinning.md) — superseded in part; its third-party rule stands +- [50-governance/cross-repo-ci.md](../../50-governance/cross-repo-ci.md) — the shared workflows and how they are called +- [40-quality/ci-cd.md](../../40-quality/ci-cd.md) · [30-repos/README.md](../../30-repos/README.md) diff --git a/30-repos/README.md b/30-repos/README.md index 398e139..b26bd7d 100644 --- a/30-repos/README.md +++ b/30-repos/README.md @@ -25,7 +25,7 @@ the product's behaviour. | **REPO-R4** | Every repository MUST call the shared hygiene checks. | | **REPO-R5** | Every repository's `CODEOWNERS` MUST be generated from the single maintainer registry and MUST NOT be edited by hand. | | **REPO-R6** | Every repository MUST sync its labels from the canonical label set. | -| **REPO-R7** | Every third-party action MUST be pinned to a full commit hash with an inline version comment; first-party reusable workflows MUST be referenced on the default branch. | +| **REPO-R7** | Every third-party action MUST be pinned to a full commit hash with an inline version comment; first-party reusable workflows MUST be referenced by their major-version tag ([ADR-0021](../00-overview/decisions/0021-reusable-workflow-version-tags.md)). | | **REPO-R8** | Every repository MUST protect its default branch with linear history, signed commits, required status checks, and blocked force-push and deletion. | | **REPO-R9** | Every repository MUST enable platform secret scanning and push protection. | | **REPO-R10** | Every repository MUST enable dependency alerts and scheduled dependency updates. | diff --git a/40-quality/ci-cd.md b/40-quality/ci-cd.md index 9c645f8..193a82f 100644 --- a/40-quality/ci-cd.md +++ b/40-quality/ci-cd.md @@ -69,7 +69,7 @@ of build: **a bundle that boots but reports the wrong version**. | Rule | Why | |------|-----| | **Third-party actions pinned to a full commit hash**, with an inline version comment | Tags are mutable, and this attack has happened in the wild ([ADR-0012](../00-overview/decisions/0012-action-pinning.md)) | -| **First-party reusable workflows referenced on the default branch** | They are ours, gated by our own review; pinning costs coordination and buys nothing | +| **First-party reusable workflows referenced by major-version tag** | The tag mover and the consumer are the same protected repository, so fixes still propagate in one merge — but a breaking change can cut `v2` instead of breaking every sibling at once | | **No trigger that exposes secrets to a fork's code** | The canonical secret-exfiltration pattern, made impossible by construction | | **No build-time telemetry or third-party upload** | The local-only contract extends to the pipeline | | **No update path that skips verification** | The signed manifest is the only binary-integrity signal ([F6](../10-functional/features/f-platform/f6-updates.md)) | diff --git a/40-quality/tooling.md b/40-quality/tooling.md index 7d467ec..af6ae9a 100644 --- a/40-quality/tooling.md +++ b/40-quality/tooling.md @@ -41,7 +41,7 @@ from every repository. | Kind | Policy | |------|--------| | Third-party actions | **Full commit hash**, with an inline version comment ([ADR-0012](../00-overview/decisions/0012-action-pinning.md)) | -| First-party reusable workflows | The default branch | +| First-party reusable workflows | Their major-version tag (`@v1`) | | Language dependencies | A lock file, committed | | Runtime version | The bundled version, with the next one in the matrix | diff --git a/50-governance/cross-repo-ci.md b/50-governance/cross-repo-ci.md index 6bba75f..e24fbaa 100644 --- a/50-governance/cross-repo-ci.md +++ b/50-governance/cross-repo-ci.md @@ -21,14 +21,56 @@ One definition, no drift ([Q-R20](../40-quality/README.md#the-q-r-namespace)). ## How a repository calls one -A short caller workflow that names the shared definition. First-party reusable -workflows are referenced on the **default branch** -([ADR-0012](../00-overview/decisions/0012-action-pinning.md)) so a fix -propagates in one merge rather than needing a pin bump in every consumer. +A short caller workflow that names the shared definition, referenced by its +**major-version tag** +([ADR-0021](../00-overview/decisions/0021-reusable-workflow-version-tags.md)): + +```yaml +jobs: + dco: + uses: beatrax-app/spec/.github/workflows/dco.yml@v1 +``` + +`v1` moves forward as fixes merge here, so a fix still propagates in one merge +rather than needing a bump in every consumer. A **breaking** change cuts `v2` +instead, so consumers migrate deliberately rather than all failing at once. Every repository calls the governance gate, sign-off, commit lint, and hygiene ([REPO-R1](../30-repos/README.md#the-repo-r-namespace)–[REPO-R4](../30-repos/README.md#the-repo-r-namespace)). +## The workflow contract, and what breaks it + +The tag is a promise about the **calling interface**, not about the internals. +These are breaking, and cut a new major: + +| Change | Why it breaks a consumer | +|--------|--------------------------| +| Removing or renaming an input or secret | The caller's `with:`/`secrets:` block stops matching | +| Making an optional input required | Callers that omit it start failing | +| Renaming a job | Consumers name jobs as required status checks in their rulesets; a rename silently stops the check being required ([OPS-R18](../70-operations/README.md#the-ops-r-namespace)) | +| Tightening a check so a pull request that passed now fails | The gate moves under merges already in flight | + +Everything else — a bug fix, a new optional input, a faster implementation — moves +`v1` forward. + +**Every move is also an immutable `v1..` tag**, so what `v1` pointed +at on a given day stays recoverable and a consumer that must hold still can pin +to the immutable one. + +### Moving the tag + +After a change to a shared workflow merges here: + +```sh +git tag -s v1.2.0 -m "shared workflows v1.2.0" +git tag -f -s v1 -m "shared workflows v1 → v1.2.0" +git push origin v1.2.0 +git push --force origin v1 +``` + +Forgetting this means the fix reaches nobody, which is the one real cost of this +scheme ([ADR-0021](../00-overview/decisions/0021-reusable-workflow-version-tags.md#negative)). + ## How the governance gate works 1. Check out the calling repository's pull request. From 98d72c89aff4cfce044f444f1ca29c18125f075f Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Mon, 27 Jul 2026 20:23:06 +0200 Subject: [PATCH 3/3] docs(release): any prerelease identifier is a preview tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tag table named exactly two shapes, vX.Y.Z and vX.Y.Z-rc.N, while the pipeline has always keyed on something broader: a hyphen anywhere in the ref name. So -beta and -develop already worked and were already correct, and the specification described a narrower system than the one that exists. Widened rather than narrowed, because the broader behaviour is the one worth having: a snapshot off the integration branch is a legitimate preview, and forcing it to call itself a release candidate would make the label lie. The identifier now carries no meaning to the pipeline and says so — it is for the humans reading the release list. REPO-R31, OPS-R4 and the pipeline diagram move from "release candidate" to "prerelease tag" to match, and the staged-versus-releasable rule the preview channel depends on is stated here rather than left to staging.md alone. ADR-0019 is untouched: its decision — stable drafts, previews publish immediately — is unchanged, and only its examples were narrow. Signed-off-by: Wessel Verheij --- 30-repos/beatrax.md | 2 +- 40-quality/ci-cd.md | 2 +- 70-operations/README.md | 2 +- 70-operations/releasing.md | 27 +++++++++++++++++++-------- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/30-repos/beatrax.md b/30-repos/beatrax.md index dac38d2..662df0e 100644 --- a/30-repos/beatrax.md +++ b/30-repos/beatrax.md @@ -90,7 +90,7 @@ The next release is cut from the default branch inside the new organisation | **REPO-R28** | Every platform build MUST succeed before the publish step runs. | | **REPO-R29** | Each platform bundle MUST be smoke-tested — launched and asked for its health endpoint — before upload. | | **REPO-R30** | Release manifests MUST be signed, and every binary hash MUST be recorded in the signed manifest. | -| **REPO-R31** | A stable tag MUST publish as a draft; a release-candidate tag MUST publish immediately as a prerelease. | +| **REPO-R31** | A stable tag MUST publish as a draft; a tag carrying any semver prerelease identifier MUST publish immediately as a prerelease. | | **REPO-R32** | The release body MUST be generated from the commit history at tag time; no hand-maintained changelog file may be the release-note source. | | **REPO-R33** | A user-visible change MUST carry a conventional commit subject written as release-note copy; a subject written for the implementer MUST read that way in the release. | | **REPO-R34** | The bundled environment template MUST contain only placeholders, and the application key MUST be minted on first launch behind a sentinel. | diff --git a/40-quality/ci-cd.md b/40-quality/ci-cd.md index 193a82f..8f1777d 100644 --- a/40-quality/ci-cd.md +++ b/40-quality/ci-cd.md @@ -55,7 +55,7 @@ tag push generate the update manifests with binary hashes sign each manifest create the release with every binary and manifest attached - stable → DRAFT · release candidate → published prerelease + stable → DRAFT · prerelease tag → published prerelease ``` The asymmetric publish is diff --git a/70-operations/README.md b/70-operations/README.md index 1cb650e..97ecbda 100644 --- a/70-operations/README.md +++ b/70-operations/README.md @@ -25,7 +25,7 @@ and the runbooks. | **OPS-R1** | The pushed tag MUST be the single source of truth for a build's version string. | | **OPS-R2** | A build produced outside the release pipeline MUST self-identify as a development build. | | **OPS-R3** | The release workflow MUST trigger only on a tag push. | -| **OPS-R4** | A stable tag MUST publish as a draft for human review; a release-candidate tag MUST publish immediately as a prerelease. | +| **OPS-R4** | A stable tag MUST publish as a draft for human review; a tag carrying any semver prerelease identifier MUST publish immediately as a prerelease. | | **OPS-R5** | Every platform build MUST pass its smoke test before the publish step runs. | | **OPS-R6** | Labels MUST be synced to every repository from one canonical file; a label MUST NOT be created by hand. | | **OPS-R7** | Maintainership MUST be declared in one registry, and every ownership file MUST be generated from it. | diff --git a/70-operations/releasing.md b/70-operations/releasing.md index c89e8b2..28171f2 100644 --- a/70-operations/releasing.md +++ b/70-operations/releasing.md @@ -41,14 +41,26 @@ shape. | Shape | Channel | Publish behaviour | |-------|---------|-------------------| | `vX.Y.Z` | stable | Builds, smoke-tests, uploads, and creates a **draft**. A human reviews and publishes. | -| `vX.Y.Z-rc.N` | preview | Published immediately as a prerelease. | +| `vX.Y.Z-` | preview | Published immediately as a prerelease. Any semver prerelease identifier — `-rc.1`, `-beta`, `-develop`. | + +**The hyphen is what decides the channel.** A tag with a prerelease identifier +is a preview; one without is stable. The identifier itself carries no meaning to +the pipeline — it is there for the humans reading the release list, so name it +for what the build is (`-rc.1` before a release, `-develop` for a snapshot off +the integration branch). The asymmetry is [ADR-0019](../00-overview/decisions/0019-asymmetric-release-publish.md): a mistaken stable tag push costs a deleted draft, not a bad release in the -field. +field. That record's examples predate this generalisation and name `-rc.N` +specifically; the decision it records — stable drafts, previews publish — is +unchanged. + +There is **one** preview channel, not a tier ladder. Anything needing a tester +before stable rides it, whatever the identifier says. -There is no alpha tier. Anything needing a tester before stable rides the -preview channel. +A preview tag rides a **staged** version; a stable tag needs a **releasable** +one ([staging.md](staging.md)). That is the point of a preview: to exercise +goals that are not all satisfied yet. **The pushed tag is the single source of truth for the version string** ([OPS-R1](README.md#the-ops-r-namespace)). The workflow strips the leading @@ -88,8 +100,7 @@ there is nothing to move by hand. 3. **Publish**, only if all three succeeded: generate the release notes from the commits since the previous tag, generate the update manifests with binary hashes, sign each manifest, and create the release with every binary - and manifest attached — as a draft for stable, published for a release - candidate. + and manifest attached — as a draft for stable, published for a preview. ### After the tag @@ -109,8 +120,8 @@ Fix and re-tag with a new patch version; do not move a tag. manifest stops the updater offering it, but users who already installed it have it — which is exactly why the draft step exists for stable. -**A tag was pushed by mistake.** Delete the draft. For a release candidate, -which publishes immediately, publish a corrected one. +**A tag was pushed by mistake.** Delete the draft. For a preview tag, which +publishes immediately, publish a corrected one — it is already in the field. ## What is never overridable here