Skip to content

Commit 24d350b

Browse files
fix(release): a changeset a revert restored is credited to the commit that first added it in the CHANGELOG (#25669)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D8YtQfsG3aDPvEyQz2wfMbk)_ Part of #25638 Clause-②: no Metadata-protocol: none ## What happens today `.changeset/config.json` named `@changesets/cli/changelog`. It stamps each entry with the commit `@changesets/git` returns: the **newest** commit that added the changeset file. #25605 (`3937697`) reverted the unpublished first 18.1.0 cut (`74fa3cb6d`), which re-added the 260 changesets that cut had consumed. The re-cut (#25606, `e649c2d91e`) therefore credited all 260 to the revert: 260 of 265 distinct entries, and 6000 `- Updated dependencies [3937697]` lines, across 79 CHANGELOGs. ## The fix (a): future cuts - `.changeset/config.json` loads `../scripts/changeset-changelog.mjs` (resolved from `.changeset/`, as `@changesets/apply-release-plan` resolves it). It is the stock generator's two functions with each changeset's `commit` replaced by `createChangelogAttribution`. The line format is the stock one byte for byte. - `createChangelogAttribution` lives in `scripts/release-pending-publish.mjs`, beside `annotate-shipped`, which already knows reverted cuts (#25607). One `git log --no-renames --diff-filter=AD --name-status -- .changeset/` pass indexes every add and delete. An add whose blob is exactly the blob the previous delete removed is a **restoration**. The walk passes over it, and over that delete, to the add before. A changeset written afresh under a name a published cut consumed has other bytes and keeps its own add. A changeset the walk cannot find as an add keeps the stock commit. - Shallow clones (the version-PR job checks out at depth 1): the clone is deepened, in `annotate-shipped`'s bounded rounds, to the last version commit (every delete that matters lies after it), then further while a restoration's first add is still beyond the boundary. A shallow boundary commit diffs against nothing and seems to add every file it carries, so its adds are never trusted. - It never stops the version pass. Anything it cannot decide (history out of reach, not a git repo, any error) keeps the stock commit, with one `::warning::changeset-changelog:` line. A restoring commit is named once, as a plain line. - `annotate-shipped`'s fetch is factored into a shared `deepenBy` helper. Its reasons and behaviour are unchanged, and its batteries pass unchanged. ## Not in this pull: the 18.1.0 amendment (b) The seat split the card. After this merges, the same branch restarts from `main` and regenerates the 18.1.0 amendment with this generator as its own pull (`Fixes #25638`). The method, already run once against this branch's generator (that commit is reverted here): - The 18.1.0 sections are **regenerated, not hand-edited**: the cut re-run on `e649c2d91e^` (`8801f2bb37`, the tree #25606 cut) with this generator (`annotate-shipped`, then `changeset version`), and the resulting CHANGELOGs copied. - Compared with `e649c2d91e`, every changed line differs **only in its 7-hex commit**. With hashes normalised, all 79 files are identical. `main` has not touched any CHANGELOG since `e649c2d91e`. - 265 distinct entries, none naming `3937697`. The eight **Shipped in 18.0.0** entries name `6b9e6df 7cacad5 950f949 9a06f0b abd5acd de9cd1f df8bb40 e5b3de0`, exactly the adding commits #25607 measured. The five post-revert changesets keep `a57643d 9f39094 4a25192 4395cbc cb38a8c`. - The same re-cut from a **depth-1 clone** (annotate-shipped deepened it by 700 commits) produced byte-identical CHANGELOGs. - The published tarballs are not touched. ⛔ No republish of 18.1.0. ## Evidence - `node scripts/release-pending-publish.mjs --self-test`: 124 cases across 31 batteries pass, two of them new: - *add → version → revert → re-cut*: the fixture first proves the stock lookup names the revert for every restored changeset. Then `a`/`b` name their own adds, and `l`, a leak added before the 1.0.0 cut, names its own add. `n`, rewritten under a consumed name, keeps its second add, and `c`, landed after the revert, is untouched. The re-cut runs through `scripts/changeset-changelog.mjs`'s own `getReleaseLine`/`getDependencyReleaseLine`. - *shallow clone*: bounded deepening reaches each first add, including past a graft commit that only seems to add `l`. With deepening off, or outside a repository, the stock commit stays with one warning and nothing throws. - Ablations, each reverted after: dropping the blob-equality test fails the reused-name case. Dropping the boundary guard fails the shallow-clone case. - `dispatch-gates --commands`: 108 derived, all 108 run and exit 0 (two after `pnpm build`, which they need first); `--ran` reconciles 108 of 108, 0 not measured. That run covered the combined diff before the split, a superset of this one. - `check:commit-card-trailers --range origin/main..HEAD` and `check-nul-bytes` pass. ## Tier and surface - 339 changed lines: `.changeset/config.json`, `scripts/changeset-changelog.mjs`, `scripts/release-pending-publish.mjs`. No governed path. - ⛔ `release.yml` and the root `version` script are **not** touched. The generator is reached through `.changeset/config.json` (loaded by `@changesets/apply-release-plan` through `import()`), and its self-test rides the `release-pending-publish.mjs --self-test` step lint.yml already runs. ## Acceptance notes - The 18.1.0 amendment is the second pull, by the method above. - `unconsumedChangesets` (the `unconsumed` report) still names the newest add. It reads changesets a version commit left pending, which a revert does not produce, so it was left alone. Noted, not filed. `skip-changeset`: `scripts/` and `.changeset/config.json` publish nothing from any released package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01X36HeTaoAfX2pS589N9FYP --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 384bb7b commit 24d350b

3 files changed

Lines changed: 331 additions & 8 deletions

File tree

‎.changeset/config.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
33
"changelog": [
4-
"@changesets/cli/changelog",
4+
"../scripts/changeset-changelog.mjs",
55
{
66
"repo": "objectstack-ai/objectstack"
77
}

‎scripts/changeset-changelog.mjs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// changeset-changelog (#25638) -- the changelog generator `.changeset/config.json`
4+
// names: `@changesets/cli/changelog`'s own lines, with each entry's commit read
5+
// by `createChangelogAttribution` in scripts/release-pending-publish.mjs.
6+
//
7+
// The stock generator stamps an entry with the newest commit that added its
8+
// changeset file. A revert of a version commit re-adds every changeset that cut
9+
// consumed, so after a re-cut every entry names the revert: 260 of the 265
10+
// entries in 18.1.0's CHANGELOGs open `- 3937697:`, the commit of #25605. Here
11+
// an add that restores the bytes an earlier commit deleted is passed over, and
12+
// the entry names the commit that first added the changeset.
13+
//
14+
// Nothing else moves: the line format is the stock generator's, and whatever
15+
// the attribution cannot decide keeps the stock commit with a `::warning::`.
16+
// The pins live in `node scripts/release-pending-publish.mjs --self-test`,
17+
// which lint.yml runs: an add -> version -> revert -> re-cut fixture through
18+
// these two functions.
19+
20+
import stock from '@changesets/cli/changelog';
21+
import { createChangelogAttribution } from './release-pending-publish.mjs';
22+
23+
/** The two functions `changeset version` calls, attributing from the repository at `cwd`. */
24+
export function changelogFunctions(options = {}) {
25+
const attribute = createChangelogAttribution({ cwd: process.cwd(), ...options });
26+
const attributed = (changeset) => ({ ...changeset, commit: attribute(changeset) });
27+
return {
28+
getReleaseLine: (changeset, type, opts) => stock.getReleaseLine(attributed(changeset), type, opts),
29+
getDependencyReleaseLine: (changesets, dependenciesUpdated, opts) =>
30+
stock.getDependencyReleaseLine(changesets.map(attributed), dependenciesUpdated, opts),
31+
};
32+
}
33+
34+
export default changelogFunctions();

0 commit comments

Comments
 (0)