Skip to content

chore(release): link each CHANGELOG entry to its compare range - #211

Merged
thecodedrift merged 1 commit into
mainfrom
chore/changelog-compare-links
Aug 30, 2026
Merged

chore(release): link each CHANGELOG entry to its compare range#211
thecodedrift merged 1 commit into
mainfrom
chore/changelog-compare-links

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Companion to #210, which adds the tagging this depends on. Independent of it: nothing breaks if they land in either order, the links are simply dead until the tags exist, and the four historical tags are already pushed.

Why

Changesets writes ## <version> and the notes beneath it, and nothing about which commits a release actually contained. Reviewing "what shipped in 0.11.0" meant finding the version-bump commit by hand.

That is harder than it sounds: every version-bump commit is called chore: version packages, so the subject cannot distinguish them and you have to read packages/cli/package.json at each candidate to find the one you want. That is exactly how the four historical tags were located for #210.

What

scripts/changelog-compare-links.ts inserts a vPREV...vNEXT compare link under each version heading. It runs from pnpm bump, which is what release-cli-changeset.yml invokes (version: pnpm bump), so the link lands in the Version Packages PR alongside the notes it describes.

The transform is a pure exported function over the markdown plus a repository URL, so it can be exercised without touching the file. The URL comes from packages/cli/package.json's own repository.url rather than being hardcoded.

The newest link is briefly dead, on purpose

At the moment pnpm bump runs, the tag does not exist: release-cli.yml creates it after the publish. So the newest entry's link is broken for the minutes between the Version PR merging and the release completing, and correct forever after. That is the accepted trade rather than an oversight, and it is written into the script.

Old entries get no link at all

Releases before 0.9.0 were never tagged and will not be, so a link whose left side is v0.8.1 would be permanently broken rather than briefly so. A documented FIRST_TAGGED = "0.9.0" floor leaves those headings alone. 0.9.0 itself gets no link for the same reason: its predecessor is the untagged 0.8.1.

Briefly-dead is fine. Permanently-dead is a different thing, and worse than absent.

Verification

Backfill 3 links added: 0.10.0, 0.10.1, 0.10.2
Skipped correctly 0.9.0 and older get none
Idempotent 2nd and 3rd runs report "already up to date"
URLs resolve v0.10.1...v0.10.2 and v0.9.0...v0.10.0 both return HTTP 200
Forward path a simulated ## 0.11.0 heading took v0.10.2...v0.11.0, exactly one link added, existing links untouched

Insertion runs back-to-front over the heading list so an insertion never shifts an index still to be used, and idempotency is decided by the first non-empty line after a heading rather than by scanning the whole document.

Also

release-cli-changeset.yml carries a comment enumerating what pnpm bump does. Updated, since it would otherwise have gone stale the moment this landed.

Notes

  • No changeset. Release tooling only; nothing reaches the published package. packages/cli/CHANGELOG.md changes here are the backfilled links, not a release note.

@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Aug 29, 2026
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 8m 41s —— View job


Code review — PR #211

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json held no review threads and no prior reviews; the only prior comment on record is the trigger (@claude /review) itself. This is the first review of this PR — the whole diff was assessed.

  • Gather context (PR body, diff via gh pr diff, whole-file reads where the diff interacts with code it doesn't show)
  • Review .github/workflows/claude-code-review-on-demand.yml (removed-behavior/cross-file trace)
  • Review scripts/changelog-compare-links.ts and its two call sites (release-cli-changeset.yml, package.json)
  • Review release-cli.yml's new tag job for needs/if correctness
  • Check against root CLAUDE.md / .conventions/STYLEGUIDE-CODE.md conventions
  • Post inline findings + this summary

Findings posted inline

.github/workflows/claude-code-review-on-demand.yml — this PR substantially reworks the "verify the review actually ran" gate from a metrics-based check to a "did a comment actually post" check, with detailed rationale in the comments for why. Three things about the new gate itself:

  1. [New] posted is inferred by grepping all PR comment bodies for runs/${GITHUB_RUN_ID} + Review mode:, with no filter on comment author. Any comment containing both substrings satisfies the guard, including ones not posted by the action.
  2. [New] Permission-denial handling was downgraded from a hard failure to a warning-only, but unconditionally — not scoped to the git-specific case the surrounding comment argues for. A denial on an unrelated, actually-needed tool now also just warns.
  3. [New] A present-but-corrupt execution file (as opposed to a missing one) is no longer diagnosed at all; only a missing file gets a ::warning::.

None of these are hypothetical edge cases invented for the sake of finding something — they're gaps in the specific new logic this PR introduces, and (1)/(2) affect whether the CI gate can be trusted as a signal.

scripts/changelog-compare-links.ts (new file) — the core transform (withCompareLinks) is correct: back-to-front splice avoids index invalidation, idempotency check via first-non-empty-line is sound, and the FIRST_TAGGED floor correctly leaves pre-0.9.0 headings alone. Two minor [New] reuse notes posted inline: ROOT/CLI_PACKAGE_JSON duplicates constants already in scripts/sync-skill-versions.ts, and isAtLeast duplicates the comparison algorithm already in .github/scripts/vale-release.cjs's compareValeVersions. Both are low severity (cross-file, cross-module-system in the second case) — flagged for awareness, not blocking.

release-cli.yml — the new tag job's needs: [check, publish] + if: needs.check.outputs.publish == 'true' is correct as written: GitHub Actions implicitly ANDs success() onto an if: that doesn't reference success()/failure()/always()/cancelled(), so a failed or skipped publish job skips tag regardless of check's output. No gap found here.

Conventions — checked against root CLAUDE.md and .conventions/STYLEGUIDE-CODE.md: import grouping/direct-import rules are followed in the new script, the PR's skip-changeset label matches its own accurate claim that nothing here reaches the published package (packages/cli/package.json's files: ["dist"] excludes CHANGELOG.md), and the PR body's issue reference is prose only ("Companion to #210"), which CLAUDE.md explicitly allows without a trailing directive. No violations found.

Not investigated: per-file history/blame (unavailable — no per-file git log substitute exists; noted rather than guessed at). CI status is reported by CI itself, not by this review.

Comment thread .github/workflows/claude-code-review-on-demand.yml
Comment thread .github/workflows/claude-code-review-on-demand.yml
Comment thread .github/workflows/claude-code-review-on-demand.yml
Comment thread scripts/changelog-compare-links.ts
Comment thread scripts/changelog-compare-links.ts
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude — "Code review — PR #211 … this PR substantially reworks the 'verify the review actually ran' gate from a metrics-based check to a 'did a comment actually post' check…"
#211 (comment)

All six findings answered on their threads. Two were real defects and are fixed in #212; four were declined with reasoning recorded rather than silently dropped.

Why this review saw the workflow at all is worth stating, since it was not this PR's doing. The branch was cut from #210's branch rather than from main, so it carried #210's commit and the diff legitimately included the guard. That is now rebased: one commit, four files, none of them workflows other than the release-cli-changeset.yml comment this change actually touches. The findings still stood, because that code is on main now.

Fixed in #212: the posted-review scan had no author filter, so any comment carrying the run id and the marker satisfied it — the exact failure the step exists to catch, masked by someone talking about it. And a present-but-unparseable execution file was no longer diagnosed at all.

Declined: the two duplication notes (ROOT/CLI_PACKAGE_JSON, and isAtLeast versus compareValeVersions), and scoping the denial downgrade to git. That last one is declined on measurement rather than preference: the guard sees tool names only, so a git denial and any other Bash denial are indistinguishable without reading tool inputs that can quote the untrusted diff — and across every run on record, live reviews hit 19, 3 and 7 denials while dead ones hit 8 and 3. The ranges overlap, so no threshold separates them.

Worth noting this was the first review to run under the new guard, and it went green while finding two genuine holes in that same guard. That is roughly the outcome the change was aiming for.

— AI Coding Agent

thecodedrift added a commit that referenced this pull request Aug 30, 2026
Two gaps in the guard that landed in #210, both found by review on #211.

The comment scan had no author filter, so ANY comment carrying this run's
id and the `Review mode:` marker satisfied it: a quote of the job URL, a
paste of this workflow, an acknowledgement citing the review it answers.
That is exactly the case the step exists to catch — the action posted
nothing — being masked by someone talking about it. It now filters on
`.user.type == "Bot"` before grepping. Filtering on type rather than on a
login means an app rename does not fail every review closed, while the
actual threat, a human comment, is excluded.

Verified against the record: the dead run on #210 still reports not-posted,
the real review on #211 still reports posted, and four human comments on
#210 are now excluded from consideration.

An execution file that exists but holds no result record was also no longer
diagnosed. It used to be a hard error; once `posted` became the verdict it
passed through as `turns=None is_error=None cost=None` with nothing naming
the corruption, so a partial write or an action version skew would vanish
rather than be noticed. It now warns, and deliberately does not fail, since
it says nothing about whether a review was posted.
Changesets writes `## <version>` and the notes beneath it, and nothing
about which commits a release actually contained. Reviewing "what shipped
in 0.11.0" meant finding the version-bump commit by hand, which is harder
than it sounds: every one of them is called `chore: version packages`, so
the subject line cannot tell them apart and you have to read the version
out of `package.json` at each candidate.

`scripts/changelog-compare-links.ts` adds a `vPREV...vNEXT` compare link
under each heading, and runs from `pnpm bump` — which is what the Version
Packages workflow invokes — so the link lands in the same PR as the notes
it describes. The tag it points at does not exist yet at that moment;
`release-cli.yml` creates it after publishing, so the newest link is dead
for the minutes in between and correct forever after.

Releases before 0.9.0 were never tagged and will not be, so a link whose
left side is `v0.8.1` would be permanently broken rather than briefly so.
A documented `FIRST_TAGGED` floor leaves those headings alone; 0.9.0 itself
gets no link for the same reason.

Backfilled 0.10.0, 0.10.1 and 0.10.2, whose tags were just created. Both
`v0.10.1...v0.10.2` and `v0.9.0...v0.10.0` return HTTP 200. Re-running is a
no-op, and a simulated 0.11.0 heading takes exactly one new link.
@thecodedrift
thecodedrift force-pushed the chore/changelog-compare-links branch from 29f5e3a to 9c8f0b3 Compare August 30, 2026 03:59
@thecodedrift
thecodedrift merged commit b575d90 into main Aug 30, 2026
2 checks passed
@thecodedrift
thecodedrift deleted the chore/changelog-compare-links branch August 30, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant