diff --git a/.github/badges/conformance.svg b/.github/badges/conformance.svg index 6c3b41d..e314d96 100644 --- a/.github/badges/conformance.svg +++ b/.github/badges/conformance.svg @@ -1,10 +1,10 @@ - - trsdn standard: v1.6.1 - Healthy + + trsdn standard: v1.7.0 - Healthy trsdn standard - v1.6.1 - Healthy + v1.7.0 - Healthy diff --git a/.github/conformance.yml b/.github/conformance.yml index 66a9b41..4739085 100644 --- a/.github/conformance.yml +++ b/.github/conformance.yml @@ -6,7 +6,7 @@ # The badge in .github/badges/ is generated from this file. Change the evidence # first, then this record, then regenerate. Never the other way round. -standard_version: "1.6.1" +standard_version: "1.7.0" assessed_on: "2026-08-31" state: "Healthy" evidence: "docs/self-assessment.md" @@ -56,6 +56,7 @@ criteria: R04: pass R05: na R06: pass + R07: pass I01: na I02: na I03: na diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 000a35b..4b1959d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release # A tag publishes a release of the standard. The notes come from CHANGELOG.md so # that the changelog stays the single place a change is described, and the # consistency check guarantees the tag, the document version, and the changelog -# agree before anything is published. +# agree before anything is published. Together these steps are the `R07` gate. on: push: @@ -34,6 +34,19 @@ jobs: exit 1 fi + - name: Reject entries left in an unreleased section + run: | + set -euo pipefail + held="$(awk ' + tolower($0) ~ /^##[ \t]+\[?unreleased\]?/ { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md | tr -d '[:space:]')" + if [ -n "$held" ]; then + echo "release: entries are still held in the unreleased section; promote them into $GITHUB_REF_NAME" >&2 + exit 1 + fi + - name: Extract release notes for this version run: | set -euo pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index 042c148..067762e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ recorded here. Versions follow the compatibility policy in the [Repository Quality Standard](docs/repository-quality-standard.md). +## 1.7.0 - 2026-08-31 + +- Added `R07`, requiring that published release notes are generated from the + changelog entry for the version being released, gated by automation that fails + the release when the entry is missing, empty, or still held in an unreleased + section. A repository can keep an exemplary changelog and still publish + releases whose notes are fixed boilerplate, because nothing connects the two. + The entries then reach nobody, and the release page is the surface a consumer + actually lands on. +- Left `R06` unchanged. Narrowing it so that boilerplate notes over an unread + changelog could no longer pass would be a major change, and it would buy no + coverage that `R07` does not already provide. The two now divide the work: + `R06` is about content, `R07` about provenance. See + [decision 0010](docs/decisions/0010-release-notes-come-from-the-changelog.md). +- Published [`templates/release-notes/`](templates/release-notes/) as the + reference gate for `R07`, linked from the criterion's prose. A criterion that + mandates automation has to say where the automation comes from, or every + repository invents it again. + ## 1.6.1 - 2026-08-31 - Closed an ambiguity in the badge image rule, found by the first repository diff --git a/README.md b/README.md index 1770e30..82aab14 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ repositories. Those controls must be enabled and verified per repository. readiness criteria `G01` to `G08`. - [`templates/repo-stats/`](templates/repo-stats/) — caller workflow and README snippet for the `P09` repository statistics card. +- [`templates/release-notes/`](templates/release-notes/) — reference release + workflow implementing the `R07` gate, which publishes the changelog entry for + the tag as the release notes. ## Contributing diff --git a/docs/decisions/0010-release-notes-come-from-the-changelog.md b/docs/decisions/0010-release-notes-come-from-the-changelog.md new file mode 100644 index 0000000..0dbb5d6 --- /dev/null +++ b/docs/decisions/0010-release-notes-come-from-the-changelog.md @@ -0,0 +1,79 @@ +# 0010 - Release notes are generated from the changelog, and R06 stays as it is + +- Status: Accepted +- Date: 2026-08-31 + +## Context + +`R06` asks that release notes describe meaningful changes and upgrade concerns, +and accepts "GitHub release **or** changelog" as evidence. That disjunction is +the hole. + +A repository in the estate demonstrated it. It keeps a Keep-a-Changelog file +with an `## Unreleased` section and one dated section per release, and its +release workflow publishes every GitHub release with a hardcoded notes string +describing the artifacts rather than the change. The changelog is never read by +anything. Nothing fails when a tag has no changelog section, and nothing fails +when entries are still sitting under `## Unreleased` at tag time — those entries +ship and then appear in no release notes at all. + +Assessed against `R06` as written, that repository passes: a maintained +changelog exists, and it does describe meaningful changes. The criterion was +satisfied by a document nobody consumes while the surface consumers actually +land on said nothing. + +## Decision + +Add `R07`, requiring that the notes published for a tag are generated from the +changelog entry for exactly that version, enforced by a release gate that fails +when the entry is missing, empty, or still held in an unreleased section. + +Leave `R06` unchanged. + +## Consequences + +Sharpening `R06` was the obvious alternative and was rejected for two reasons. + +It buys nothing. Once `R07` holds, boilerplate notes over an unread changelog +cannot pass, because the notes are no longer written separately from the entry. +The combination "exemplary changelog, meaningless release page" is closed by +provenance, not by restating the content requirement more forcefully. + +It is expensive. Narrowing a criterion so that a recorded `Pass` could become a +`Fail` is a major change under +[Versioning And Compatibility](../repository-quality-standard.md#versioning-and-compatibility). +Every recorded result in the estate would be invalidated and every repository +would be due for reassessment — to reach an outcome `R07` already reaches as a +minor change, where a recorded result stays valid for the version it names. + +So the two criteria divide the work and stay orthogonal. `R06` is about content: +a changelog entry reading "bug fixes" still fails it, and `R07` cannot catch +that, because such an entry is faithfully published. `R07` is about provenance: +whatever the entry says, it is what the consumer receives. Neither subsumes the +other, and keeping them separate means an assessment says which of the two +things is wrong. + +The cost is that `R06` still reads permissively in isolation, and a reader who +finds it without `R07` may draw the old conclusion. The prose under the Package +And Release table names the division for that reader, which is the remedy +available without a major bump. + +`R07` is deliberately conditional on the second half. A repository that keeps no +unreleased section has nowhere to strand an entry and satisfies that condition by +construction; requiring a guard against a failure mode the repository cannot have +would be evidence theatre, which +[decision 0005](0005-proportionate-accessibility-and-privacy.md) rules out. + +This repository already satisfies `R07`: its release workflow extracts the +section for the tag from `CHANGELOG.md`, fails when the extraction is empty, and +publishes that text as the notes body. The unreleased guard was added alongside +this decision so the gate is complete rather than complete-by-absence. + +A criterion that mandates automation has to say where the automation comes from, +or every repository invents it again and the estate ends up with as many gates as +it has release workflows. A reference workflow is therefore published as +[`templates/release-notes/`](../../templates/release-notes/) and linked from the +criterion's prose, the same way `templates/AGENTS.md` backs `G01`-`G08` and +`templates/repo-stats/` backs `P09`. It is a starting point, not a mandate: +`R07` asks for the outcome, and a repository that reaches it with a shell script, +a release tool, or a different CI system passes just as well. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 47e5e59..9aac8a5 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -15,3 +15,4 @@ future reader can see why it is the way it is before changing it. | [0007](0007-testing-the-validation-scripts.md) | The validation scripts are tested through their command line, asserting exit code and diagnostic | Accepted | | [0008](0008-internal-links-are-checked-external-links-are-not.md) | Internal links and anchors are checked in CI; external links are deliberately not | Accepted | | [0009](0009-published-sites-and-content-boundaries.md) | Published sites carry the shared design language, and each fact has one home | Accepted | +| [0010](0010-release-notes-come-from-the-changelog.md) | Release notes are generated from the changelog, and `R06` stays as it is | Accepted | diff --git a/docs/repository-quality-standard.md b/docs/repository-quality-standard.md index 20516e5..88d9442 100644 --- a/docs/repository-quality-standard.md +++ b/docs/repository-quality-standard.md @@ -1,6 +1,6 @@ # Repository Quality Standard -- Version: 1.6.1 +- Version: 1.7.0 - Last reviewed: 2026-08-31 - Review cadence: every six months, even when nothing changes @@ -192,6 +192,32 @@ assessment backlog. | R04 | Tag, package version, and release title are consistent | Release workflow validation | | R05 | Built artifacts are smoke-tested in a clean environment | CI or release workflow | | R06 | Release notes describe meaningful changes and upgrade concerns | GitHub release or changelog | +| R07 | Release notes are generated from the changelog entry for the version being released, and automation fails the release when that entry is missing, empty, or still held in an unreleased section | Release workflow gate plus a published release whose notes match its changelog entry | + +`R06` and `R07` divide the work. `R06` is about content: notes a reader can act +on. `R07` is about provenance: the notes a consumer actually receives are the +maintained entry for that exact version, and not a second description written at +tag time. + +The gap `R07` closes is specific. A repository can keep an exemplary changelog +and still publish releases whose notes are fixed boilerplate, because nothing +connects the two. The entries then reach nobody — the changelog is read only by +someone who already knows to open it, and the release page, which is the surface +a consumer actually lands on, says nothing. `R06` on its own is satisfiable by a +changelog nobody consumes, which is why `R07` asks for a gate rather than a +habit. + +The gate fails the release when the changelog has no section for the version +being tagged, when that section is empty, or when entries are still held in an +unreleased section that the tagged version did not absorb. A repository that +keeps no unreleased section satisfies the last condition by construction, having +nowhere to strand an entry. A minimal gate extracts the section for the tag, +exits non-zero when the result is empty, and passes that same text to the +release command as the notes body, so the published notes and the maintained +entry cannot disagree. + +A reusable starting point is published as +[`templates/release-notes/`](../templates/release-notes/). ## Product Identity diff --git a/docs/self-assessment.md b/docs/self-assessment.md index ff23f87..9793553 100644 --- a/docs/self-assessment.md +++ b/docs/self-assessment.md @@ -1,6 +1,6 @@ # Self-Assessment: trsdn/.github -- Standard version: 1.6.1 +- Standard version: 1.7.0 - Assessed on: 2026-08-31 - State: **Healthy** - Record: [`.github/conformance.yml`](../.github/conformance.yml) @@ -153,6 +153,14 @@ environment. - `R02`, `R04` — the versioning policy is documented and a check enforces that the document version, the changelog, and the release tag agree, so they cannot drift apart silently. +- `R07` — passes, and the criterion was written from a failure found elsewhere in + the estate rather than from this repository. `.github/workflows/release.yml` + extracts the changelog section for the tag, exits non-zero when the result is + empty, and publishes that same text as the notes body, so no release can carry + notes that were written separately from the entry. The unreleased guard was + added in the same change: this repository keeps no unreleased section and could + not strand an entry today, but a gate that holds only while a convention holds + is not a gate. - `G01`-`G08` — the agent readiness criteria are satisfied by `AGENTS.md`, a Copilot configuration that defers to it instead of duplicating it, generated paths marked explicitly, and a single documented validation command. diff --git a/standard.yml b/standard.yml index 6381a37..502c436 100644 --- a/standard.yml +++ b/standard.yml @@ -3,7 +3,7 @@ # Generated by scripts/standard.py from # docs/repository-quality-standard.md. Do not edit by hand. -version: "1.6.1" +version: "1.7.0" last_reviewed: "2026-08-31" prefixes: @@ -198,6 +198,10 @@ criteria: section: "Package And Release Repositories" requirement: "Release notes describe meaningful changes and upgrade concerns" evidence: "GitHub release or changelog" + - id: "R07" + section: "Package And Release Repositories" + requirement: "Release notes are generated from the changelog entry for the version being released, and automation fails the release when that entry is missing, empty, or still held in an unreleased section" + evidence: "Release workflow gate plus a published release whose notes match its changelog entry" - id: "I01" section: "Product Identity" requirement: "The built artifact embeds its product name and exact version" diff --git a/templates/release-notes/release.yml b/templates/release-notes/release.yml new file mode 100644 index 0000000..9737a2c --- /dev/null +++ b/templates/release-notes/release.yml @@ -0,0 +1,75 @@ +name: Release + +# Reference implementation of the `R07` gate: the notes published for a tag are +# the maintained changelog entry for exactly that version, and the release fails +# rather than publishing something written here instead. +# +# Adopt this workflow as it stands, or lift the two gate steps into an existing +# release workflow. Everything project-specific — building, signing, and +# uploading artifacts — belongs between the gate and the publish step. +# +# The extraction accepts both `## 1.2.3 - 2026-01-01` and the +# Keep-a-Changelog `## [1.2.3] - 2026-01-01` heading form. + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + name: Publish release + runs-on: ubuntu-latest + steps: + - name: Check out the tag + uses: actions/checkout@v7 + + # Gate, part one. Nothing ships while it is still described as unreleased. + # Without this, entries stay under `## Unreleased`, ship anyway, and then + # appear in no release notes at all. + # + # A repository that keeps no unreleased section can drop this step: it has + # nowhere to strand an entry. + - name: Reject entries left in an unreleased section + run: | + set -euo pipefail + held="$(awk ' + tolower($0) ~ /^##[ \t]+\[?unreleased\]?/ { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md | tr -d '[:space:]')" + if [ -n "$held" ]; then + echo "release: entries are still held in the unreleased section; promote them into $GITHUB_REF_NAME" >&2 + exit 1 + fi + + # Gate, part two. The notes are extracted, never authored here. A missing + # or empty entry fails the release instead of publishing boilerplate. + - name: Extract release notes for this version + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + awk -v version="$version" ' + BEGIN { gsub(/\./, "\\.", version) } + $0 ~ "^## \\[?" version "\\]?([ \t]|$)" { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + if [ ! -s release-notes.md ]; then + echo "release: no changelog entry for $version" >&2 + exit 1 + fi + + # Build, sign, and stage artifacts here. Keep them after the gate, so a + # release that cannot describe itself fails before anything is built. + + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --notes-file release-notes.md