actions/changelog-assembled — the release PR's section must be exactly the fragments it consumed #116

Closed
opened 2026-07-24 08:14:21 +00:00 by dan-claude-bot · 3 comments
dan-claude-bot commented 2026-07-24 08:14:21 +00:00 (Migrated from github.com)

Context

Part of #112. Under fragments the release PR's changelog edit is produced by a tool (#114) but committed by a person: bin/changelog-assemble is run by hand in the ceremony PR, because the assembled section has to land in the diff where the panel reads it. Nothing yet proves the section that landed is the one the fragments say.

That gap is the same shape as every failure this repo has already built a guard for: it leaves no trace. Drop one fragment from the delete list and its entry is simply absent from the release — the file is well-formed, changelog-armed is green (the section exists and has prose), changelog-monotonic is green (no heading was deleted), the publisher happily publishes the shortened section, and the only way anyone finds out is by reading the release body against a directory that no longer exists. Hand-edit the assembled prose and the published history quietly stops being what the authors wrote.

changelog-armed cannot ask this: "the section matches the fragments it consumed" is not a property of a tree, it is a property of a diff — exactly the argument changelog-monotonic makes for being its own git-aware action rather than a clause inside changelog-armed. This is the third guard, on the same reasoning.

Spec

actions/changelog-assembled — assert that a release PR's new section is exactly what the fragments it deleted assemble to.

When it applies: the merge base has changelog.d/ (fragment mode) and HEAD's version is bare — the ceremony PR and its merge commit. Every other tree is a pass with a NOTICE, not a skip in silence: an ordinary PR does not touch the changelog's sections and has nothing to prove here.

The check:

  1. Read the fragments as of the merge base (git show <base>:changelog.d/...), not the working tree — the working tree's are gone, that is the point.
  2. Run the assembler's --check over that set for HEAD's version, using the date already stamped in HEAD's top heading, so a date difference is never mistaken for a prose difference.
  3. Compare against changelog_section "$CHANGELOG" "$VER" on HEAD, byte for byte.
  4. Refuse on: any difference (print a unified diff, expected vs found); a fragment present at the merge base and still present at HEAD; the section absent.

Degradation: no resolvable merge base is a hard failure under the fail-closed switch CI sets, matching changelog-monotonic's stance and its fetch-depth: 0 requirement — a guard that can quietly stop guarding is the failure shape these checks exist to refuse.

Where it runs: the consumer CI guard list in docs/CONSUMERS.md, beside changelog-armed and changelog-monotonic, adopted at the same pin bump as fragments themselves.

The action is a thin wrapper: the assembly definition stays in lib/changelog.sh and the diffing lives in changelog-assembled.sh, its own file, so test/changelog-assembled.test.sh can drive it against constructed git repos.

Tasks

  • actions/changelog-assembled/changelog-assembled.sh — applicability, the four refusals, the fail-closed switch, a header comment stating the failure it catches and why it cannot live in changelog-armed.
  • actions/changelog-assembled/action.yml — inputs base-ref, changelog, fragments-dir, version-source, strict, mirroring the sibling guards' input names exactly.
  • test/changelog-assembled.test.sh — constructed git repos.
  • .github/workflows/ci.yml — run it on this repo, with fetch-depth: 0 already present for monotonic.
  • docs/CONSUMERS.md guard list + README's guard section — added in #117, not here (one doctrine PR, not three).
  • shellcheck + actionlint clean; test/run.sh green.

Acceptance criteria

  • A faithful ceremony PR (assembler-produced section, fragments deleted) → green.
  • Dropping one fragment from the deletion while its entry is absent from the section → red, and the diff names the missing entry.
  • Deleting a fragment whose entry never made it into the section → red.
  • Hand-editing one word of an assembled entry → red, with a unified diff.
  • Re-ordering entries away from the canonical order → red.
  • A fragment still present at HEAD → red, listing it.
  • An ordinary -dev PR → green NOTICE, whatever it does to changelog.d/.
  • A legacy-mode repo (no changelog.d/ at the merge base) → green NOTICE, always.
  • No merge base + strict → red; the only path that is a skip is strict explicitly off.
  • The date in HEAD's heading is not compared as prose.

Test plan

Constructed git repos in a temp dir (changelog-monotonic's suite is the pattern), each with a merge base commit and a HEAD commit.

Green rows: faithful assembly, flat; faithful assembly, grouped; an ordinary PR adding a fragment; an ordinary PR adding none; a legacy repo mid-ceremony (no fragments dir at base); strict off with no base ref.

Red rows: dropped entry; dropped fragment-with-no-entry; edited entry; re-ordered entries; surviving fragment at HEAD; missing section; strict on with an unresolvable base.

Interaction: run the trio (changelog-armed, changelog-monotonic, changelog-assembled) over one faithful ceremony tree and assert all three green — and over the dropped-entry tree and assert this one is the only one red. That row is the issue's whole argument, and it belongs in the suite.

Dependencies

Part of #112. Blocked by #114. Blocks #117.

## Context Part of #112. Under fragments the release PR's changelog edit is produced by a tool ([#114](https://github.com/heavy-duty/ceremony/issues/114)) but committed by a person: `bin/changelog-assemble` is run by hand in the ceremony PR, because the assembled section has to land in the diff where the panel reads it. Nothing yet proves the section that landed is the one the fragments say. That gap is the same shape as every failure this repo has already built a guard for: it leaves no trace. Drop one fragment from the delete list and its entry is simply absent from the release — the file is well-formed, `changelog-armed` is green (the section exists and has prose), `changelog-monotonic` is green (no heading was deleted), the publisher happily publishes the shortened section, and the only way anyone finds out is by reading the release body against a directory that no longer exists. Hand-edit the assembled prose and the published history quietly stops being what the authors wrote. `changelog-armed` cannot ask this: "the section matches the fragments it consumed" is not a property of a tree, it is a property of a **diff** — exactly the argument [`changelog-monotonic`](https://github.com/heavy-duty/ceremony/blob/2f58d9b/actions/changelog-monotonic/changelog-monotonic.sh#L44-L60) makes for being its own git-aware action rather than a clause inside `changelog-armed`. This is the third guard, on the same reasoning. ## Spec **`actions/changelog-assembled`** — assert that a release PR's new section is exactly what the fragments it deleted assemble to. **When it applies**: the merge base has `changelog.d/` (fragment mode) and HEAD's version is bare — the ceremony PR and its merge commit. Every other tree is a **pass with a NOTICE**, not a skip in silence: an ordinary PR does not touch the changelog's sections and has nothing to prove here. **The check**: 1. Read the fragments as of the **merge base** (`git show <base>:changelog.d/...`), not the working tree — the working tree's are gone, that is the point. 2. Run the assembler's `--check` over that set for HEAD's version, using the date already stamped in HEAD's top heading, so a date difference is never mistaken for a prose difference. 3. Compare against `changelog_section "$CHANGELOG" "$VER"` on HEAD, byte for byte. 4. Refuse on: any difference (print a unified diff, expected vs found); a fragment present at the merge base and still present at HEAD; the section absent. **Degradation**: no resolvable merge base is a **hard failure** under the fail-closed switch CI sets, matching `changelog-monotonic`'s stance and its `fetch-depth: 0` requirement — a guard that can quietly stop guarding is the failure shape these checks exist to refuse. **Where it runs**: the consumer CI guard list in [docs/CONSUMERS.md](https://github.com/heavy-duty/ceremony/blob/2f58d9b/docs/CONSUMERS.md#L81-L86), beside `changelog-armed` and `changelog-monotonic`, adopted at the same pin bump as fragments themselves. The action is a thin wrapper: the assembly definition stays in `lib/changelog.sh` and the diffing lives in `changelog-assembled.sh`, its own file, so `test/changelog-assembled.test.sh` can drive it against constructed git repos. ## Tasks - [ ] `actions/changelog-assembled/changelog-assembled.sh` — applicability, the four refusals, the fail-closed switch, a header comment stating the failure it catches and why it cannot live in `changelog-armed`. - [ ] `actions/changelog-assembled/action.yml` — inputs `base-ref`, `changelog`, `fragments-dir`, `version-source`, `strict`, mirroring the sibling guards' input names exactly. - [ ] `test/changelog-assembled.test.sh` — constructed git repos. - [ ] `.github/workflows/ci.yml` — run it on this repo, with `fetch-depth: 0` already present for monotonic. - [ ] docs/CONSUMERS.md guard list + README's guard section — added in #117, not here (one doctrine PR, not three). - [ ] shellcheck + actionlint clean; `test/run.sh` green. ## Acceptance criteria - [ ] A faithful ceremony PR (assembler-produced section, fragments deleted) → green. - [ ] Dropping one fragment from the deletion while its entry is absent from the section → red, and the diff names the missing entry. - [ ] Deleting a fragment whose entry never made it into the section → red. - [ ] Hand-editing one word of an assembled entry → red, with a unified diff. - [ ] Re-ordering entries away from the canonical order → red. - [ ] A fragment still present at HEAD → red, listing it. - [ ] An ordinary `-dev` PR → green NOTICE, whatever it does to `changelog.d/`. - [ ] A legacy-mode repo (no `changelog.d/` at the merge base) → green NOTICE, always. - [ ] No merge base + strict → red; the only path that is a skip is strict explicitly off. - [ ] The date in HEAD's heading is not compared as prose. ## Test plan Constructed git repos in a temp dir (`changelog-monotonic`'s suite is the pattern), each with a merge base commit and a HEAD commit. **Green rows**: faithful assembly, flat; faithful assembly, grouped; an ordinary PR adding a fragment; an ordinary PR adding none; a legacy repo mid-ceremony (no fragments dir at base); strict off with no base ref. **Red rows**: dropped entry; dropped fragment-with-no-entry; edited entry; re-ordered entries; surviving fragment at HEAD; missing section; strict on with an unresolvable base. **Interaction**: run the trio (`changelog-armed`, `changelog-monotonic`, `changelog-assembled`) over one faithful ceremony tree and assert all three green — and over the dropped-entry tree and assert this one is the *only* one red. That row is the issue's whole argument, and it belongs in the suite. ## Dependencies Part of #112. Blocked by #114. Blocks #117.
github-actions[bot] commented 2026-07-24 09:10:51 +00:00 (Migrated from github.com)

Every issue named by Blocked by is closed. The sweep is moving this issue to ready.

<!-- issueflow:blockers-cleared --> Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.
claude-bot-andresmgsl commented 2026-07-24 09:19:56 +00:00 (Migrated from github.com)

Claiming — starting now. Branch build/116-changelog-assembled; draft PR follows with the first commit.

Claiming — starting now. Branch `build/116-changelog-assembled`; draft PR follows with the first commit.
claude-bot-andresmgsl commented 2026-07-24 09:32:29 +00:00 (Migrated from github.com)

PR up and ready-for-review: #124 — built and verified (34-check suite over constructed histories including the trio interaction row: on the dropped-entry tree armed and monotonic stay green and changelog-assembled is the only red; full suite 17/17 files, shellcheck 37 scripts clean, actionlint clean), panel requested.

PR up and ready-for-review: #124 — built and verified (34-check suite over constructed histories including the trio interaction row: on the dropped-entry tree armed and monotonic stay green and `changelog-assembled` is the only red; full suite 17/17 files, shellcheck 37 scripts clean, actionlint clean), panel requested.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#116
No description provided.