Skip to content

feat(release-notes): format-2 release pages and a release-note check - #880

Merged
pronskiy merged 31 commits into
ModernRelay:mainfrom
pronskiy:release-notes-v2
Oct 27, 2026
Merged

pronskiy merged 31 commits into
ModernRelay:mainfrom
pronskiy:release-notes-v2

Conversation

@pronskiy

@pronskiy pronskiy commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

The v0.13.0 release page was 113K characters: every changelog.d note pasted in order, with no summary, no upgrade actions up front and no link to the pull request behind each note. Notes written early in the cycle described behaviour that later pull requests had changed, and nothing asked a feature or fix to carry a note at all.

This PR adds a second page format in `scripts/release_notes.py:

  • intro - main things in the release in one short paragraph, human readable
  • highlights - heading (in "benefits fro user" terms), description (technical DB terms)
  • all changes grouped into sections, each one line linked to its pull request

@cursor

cursor Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review of ede1fb70: request changes (rebase and rework needed)

Why. The v0.12.0 GitHub release page was 79K characters. It had no summary and no PR links. This PR makes release pages short and easy to scan, and it adds a CI gate for release notes.

What. The PR has two parts:

  1. Format 2 in scripts/release_notes.py. Each release gets a changelog.d/vX.Y.Z.md file with an intro, a "why", 3–5 highlights, the contributors, and an optional note order. Each section is one tight list, and each note links its PR. Soft-wrapped lines are joined. New caps apply: 200 characters per note (400 for a breaking note), and 80/80/150 words for the intro, the why, and each highlight.
  2. Release Note Gate (release-note-gate.yml + a new scripts/check-pr-title.py). The title must read type(scope)!: summary. A feat/fix/perf PR must add a note, and a ! PR must add a .breaking.md note, unless the PR has the skip-changelog label.

How. The composer selects format 2 for every version except v0.12.0. The snapshot provenance records the release-file digest, the previous tag, and each note's PR link. Verify recomputes all three from history. The gate runs base-branch code on pull_request_target and reads the head only as a diff range.

Impact. The format-2 design is good. The snapshot and provenance reuse is clean, and the PR's own tests pass at its head. But main has moved 100 commits since 2026-10-05. On current main, this PR does not merge cleanly. Its title rules contradict a required check that landed after it opened. Its format rule would also turn CI red on every PR and stop the daily edge release. Please rebase and make the changes below before merge.

Blocking

B1. The title part duplicates the required PR Title check, and the two checks disagree. This breaks invariant 12 (one source of truth).

  • ci: refuse pull request titles without a Conventional Commits type #916 and ci: add typed PR title diagnostics with exact ranges and repair guidance #944 added .github/workflows/pr-title.yml, a 500+ line scripts/check-pr-title.py and scripts/test_check_pr_title.py. PR Title is a required context in .github/branch-protection.json. CONTRIBUTING.md, docs/dev/ci.md and AGENTS.md already document its rule.
  • This PR adds a different file at the same path. git merge reports an add/add conflict in scripts/check-pr-title.py and a content conflict in docs/dev/ci.md.
  • The two rules differ. I tested five titles, and the two checkers disagreed on all five:
    • chore:, bench:, GitHub's Revert "…" and feat: Add a thing. pass this gate but fail PR Title.
    • feat(gq_parser): … fails this gate but passes PR Title.
  • Affected surfaces: every PR title, the PR template, AGENTS.md, CONTRIBUTING.md, docs/dev/ci.md and docs/dev/documentation.md. Two parsers of one rule drift again each time someone adds a type.
  • Fix: Remove this PR's title regex, type list and title docs. Keep only the rule that main does not enforce yet: a feat/fix/perf PR adds a note, and a ! PR adds a .breaking.md note, unless the PR has the skip-changelog label. Get the type and ! from main's checker (its parsed report or --format json), not from a second regex.
    • Prefer to put the note check in pr-title.yml. It needs a head fetch (as data only), the labeled/unlabeled events and a diff range. This approach needs no new required context, branch-protection entry or merge-group trigger.
    • If you keep a separate workflow, make it a pure consumer of main's parser.

B2. A hardcoded version set chooses the format, and that set is already wrong. On current main it breaks CI Gate and the daily edge release.

  • format_for() returns 2 for every version except v0.12.0 (FORMAT_ONE_VERSIONS). Two things happened after this PR opened:
    • v0.13.0 shipped in format 1 (docs/releases/v0.13.0.md records "format":1).
    • changelog.d/release.json now names v0.14.0 with base v0.13.0. The 86 pending v0.14.0 notes follow the current guide, which allows one to three sentences, examples and nested lists.
  • With this PR's release_notes.py on current main, 83 of the 86 notes fail the new caps. As a result, scripts/check-docs.py fails and release_notes.py preview --target HEAD exits 1. Both run in the check_agents_md job, which CI Gate needs. After merge, every PR and main would go red. release-edge.yml also runs preview, so edge builds would stop.
  • Published pages are safe. body --tag v0.13.0 gives the same bytes before and after, because verify reads the format from the snapshot.
  • The exact-head CI is green only because the PR's base still had release.json at v0.12.0. No format-2 rule runs there.
  • Root cause: The release format and its caps belong to a release cycle. The code derives them from a version literal instead of from the release configuration, which already owns the cycle. The PR description notes that it depends on the post-release bump. The bump has now happened twice, and the literal did not change.
  • Fix at the owning layer: Record the format in changelog.d/release.json (for example "format": 2; no field means 1). Set it in the post-release bump that opens the first format-2 cycle, and derive format_for from the config. Verify already uses the format recorded in the snapshot.
    • The caps then apply only to new notes, as the guide says. v0.14.0 can ship in format 1 without a rewrite of 83 reviewed notes.
    • The alternative is to rewrite the v0.14.0 notes in this PR. That is a large content change, and it leaves the stale literal in place.
    • After five more releases, the config field is still one line. The literal set needs an edit every time a cycle stays on an older format.
  • Tests:
    • test_format_follows_the_version asserts that v0.13.0 is format 2, which contradicts the published page. Replace it with a test driven by the config.
    • Add a regression test: in a cycle configured as format 1, check_working_notes accepts a long note.

B3. Add the changelog note. The PR leaves out its note because release.json named v0.12.0 at the time. It now names v0.14.0, so add the one-line note from the PR description (AGENTS.md, Change discipline).

Optional

O1. The edge release text (#1031) parses preview output. In scripts/edge_release.py, unreleased_notes() keeps only the provenance line before the first ## heading, and it changes each ## to ### . I rendered a fixture format-2 page and passed it through compose(). The result:

  • The intro is dropped.
  • ### Highlights and ### Highlight 1 end up at the same heading level.
  • The first section is no longer ### Upgrade actions, which the over-limit fallback expects.
  • The footer's compare link points to a tag that does not exist yet.

Add a format-2 case to scripts/test_edge_release.py, then adjust the parser or give edge a render that has only the sections. This matters as soon as the first release file lands.

O2. Refuse ## Why these changes when the release has no breaking note. The PR lists the empty "Upgrade actions" heading as a known minor issue. check_release_file already receives has_breaking, so one check closes it. That is cheaper than documenting the issue.

Liability assessment

  • Removed: the unreadable 79K-character page and the unlinked notes. PR links come from squash subjects and are rechecked at verify. No new state lives outside git.
  • Added: a second format path in the composer (about 420 lines), a per-release file with its own grammar, and caps that check-docs enforces on every PR. These are worth it if the config selects format 2 (B2). The PR also adds a second title parser and a candidate second required context, which main already covers. Keep format 2 after B2, and remove the duplicate title code per B1.

Validation

All runs are local: Python 3.12.3, markdown-it-py 4.0.0. Commits: head ede1fb70, PR base 420d7424, main 4d7536d6.

Check Tree Result
python3 scripts/test_release_notes.py head 94 tests OK
python3 scripts/check-pr-title.py --self-test head 25/25
python3 scripts/check-docs.py head OK
release_notes.py body --tag v0.12.0 --target v0.12.0 | sha256sum head code; PR-base code both 60f73177…db752
git merge --no-commit ede1fb70 main conflicts: docs/dev/ci.md, scripts/check-pr-title.py (add/add)
check-docs.py; release_notes.py preview --target HEAD main OK; exit 0
the same two commands main + this PR's release_notes.py and test file (these merge cleanly) FAIL: changelog.d/aggregate-comparison.added.md: 382 characters, over the 200-character limit; exit 1
caps over pending notes same 83 of 86 v0.14.0 notes refused
test_release_notes.py same 1 error: test_release_note_gate_matches_the_composer_note_names loads main's check-pr-title.py
body --tag v0.13.0 | sha256sum main; main + PR both 81c11247…95530
5 disputed titles this gate vs main's PR Title 5 of 5 disagree
fixture format-2 page through edge_release.compose main + PR intro dropped; highlights flattened
PR-link lookup on real history main + PR 86 of 86 notes linked; about 0.3 s added per select

Before/after proof. This PR is a feature, not a bug fix, so there is no defect to reproduce. For its compatibility claims, I ran the old and new code on the same input: the v0.12.0 and v0.13.0 bodies are identical. For B2, the same tree passes check-docs with main's code and fails with this PR's code. GQT does not apply, because this is Python release tooling with no query behavior. The test owners are scripts/test_release_notes.py, scripts/test_check_pr_title.py and scripts/test_edge_release.py.

CI and limits. All checks pass at ede1fb70, but on the October 5 base. I did not run the gate workflow on GitHub; pull_request_target runs the base copy, so it cannot run on this PR. I did not re-render the linked v0.12.0 gist, and I did not attempt a full conflict resolution.

Recommendation: request changes. Rebase on main and fix B1–B3. Then re-run check-docs and preview on the rebased tree.

Comment thread scripts/release_notes.py
Comment thread AGENTS.md Outdated
Comment thread scripts/release_notes.py
Comment thread scripts/test_release_notes.py
Comment thread scripts/check-pr-title.py Outdated
Comment thread scripts/test_release_notes.py Outdated
Comment thread .github/workflows/release-note-gate.yml Outdated
Comment thread scripts/release_notes.py Outdated
Comment thread scripts/release_notes.py Outdated
@pronskiy pronskiy changed the title feat(release-notes): format-2 release pages and the Release Note Gate feat(release-notes): format-2 release pages and a release-note check Oct 22, 2026
@pronskiy

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I rebased on 242ab02f and pushed the fixes as separate commits on top.

B1 (duplicate title check): fixed.

  • scripts/check-pr-title.py is main's file, unchanged. The note rule lives in scripts/check-release-note.py. It refuses any title main's checker refuses and only then reads the type and !.
  • That still takes a small regex, applied to a title the checker has already accepted. Getting the type out of the checker itself would mean changing the versioned JSON report it emits, and that isn't worth it for two fields.
  • The note step now runs in pr-title.yml: labeled/unlabeled, a full-history base checkout, and the head fetched as data only. release-note-gate.yml is gone, so there is no new required context or branch-protection entry.
  • The duplicate title text is out of AGENTS.md, the PR template and documentation.md. CONTRIBUTING.md states the note rule next to the title rule.
  • The categories test loads the new script.

B2 (format chosen by a version literal): fixed as you proposed.

  • release.json takes an optional "format" (absent means 1), and format_for reads it. A snapshot whose format differs from the config is refused.
  • This PR sets "format": 2 for v0.14.1. That cycle has no notes yet, so nothing has to be rewritten.
  • The v0.12.0 to v0.14.0 bodies give the same bytes as main's code.
  • Tests: the config drives the format, and a format-1 cycle accepts a long note. I confirmed that test fails when the config says format 2.

B3 (missing note): added. changelog.d/release-pages.changed.md.

O1 (edge release text): partly done.

  • Edge pages drop the format-2 footer, which was the dead compare link. There is a test for it.
  • The intro is only a placeholder on edge pages, so dropping it is fine.
  • I left the highlights' heading level as is and listed it as a follow-up.

O2 (why section without breaking notes): done. A why section with no breaking note is now refused, and the empty "Upgrade actions" heading can no longer occur.

Also since the review:

  • Format-2 pages spell the product Omnigraph. Format-1 pages keep their bytes.
  • The guide asks for an ASD-STE100 intro of up to 100 words, and for highlight headings that name the change and what it gives users.
  • I replayed v0.14.0 through format 2 to test the rules: all 158 notes rewritten to one line and linked to their PRs, about 4,700 words against the published 12,470.

I kept the fixtures on v0.13.0 rather than moving them to v0.15.0. With the format coming from the config, the version in a fixture no longer picks it.

@pronskiy

Copy link
Copy Markdown
Contributor Author

@aaltshuler can we merge this?

@pronskiy
pronskiy added this pull request to the merge queue Oct 27, 2026
Merged via the queue into ModernRelay:main with commit aee2a02 Oct 27, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant