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/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 9c645f8..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 @@ -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/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/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. 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/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/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/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 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.