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
6 changes: 3 additions & 3 deletions .github/badges/conformance.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion .github/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -56,6 +56,7 @@ criteria:
R04: pass
R05: na
R06: pass
R07: pass
I01: na
I02: na
I03: na
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
79 changes: 79 additions & 0 deletions docs/decisions/0010-release-notes-come-from-the-changelog.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
28 changes: 27 additions & 1 deletion docs/repository-quality-standard.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -192,6 +192,32 @@ assessment backlog.
| <a id="r04"></a>R04 | Tag, package version, and release title are consistent | Release workflow validation |
| <a id="r05"></a>R05 | Built artifacts are smoke-tested in a clean environment | CI or release workflow |
| <a id="r06"></a>R06 | Release notes describe meaningful changes and upgrade concerns | GitHub release or changelog |
| <a id="r07"></a>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

Expand Down
10 changes: 9 additions & 1 deletion docs/self-assessment.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
75 changes: 75 additions & 0 deletions templates/release-notes/release.yml
Original file line number Diff line number Diff line change
@@ -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