Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/awaiting-maintainer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/hygiene.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/spec-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/spec-references.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
8 changes: 7 additions & 1 deletion 00-overview/decisions/0012-action-pinning.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
124 changes: 124 additions & 0 deletions 00-overview/decisions/0021-reusable-workflow-version-tags.md
Original file line number Diff line number Diff line change
@@ -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.<minor>.<patch>` 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)
2 changes: 1 addition & 1 deletion 30-repos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
2 changes: 1 addition & 1 deletion 30-repos/beatrax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
4 changes: 2 additions & 2 deletions 40-quality/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)) |
Expand Down
2 changes: 1 addition & 1 deletion 40-quality/tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
5 changes: 2 additions & 3 deletions 50-governance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 18 additions & 20 deletions 50-governance/ai-contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions 50-governance/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading