From e55b4a01c50d2710a022099ad6636fdb20ecf8b9 Mon Sep 17 00:00:00 2001 From: KT <677465+kevintseng@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:51:15 +0800 Subject: [PATCH] fix(release): fetch the one tag, not every tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `release:finish` ended by running `git fetch --tags origin` to bring the tag it had just created into the local checkout. That form asks for every tag `origin` has and exits non-zero if any single one cannot be written, so an unrelated divergence anywhere in the repository's history makes the step look failed. This repository has 26 of them. Measured across all 73 tags: the version tags from v2.10.1 through v4.1.7, plus benchmark/longmemeval-public-r1, point at different objects locally than on origin, while everything from v4.2.0 onward agrees. The cause is measured too, not inferred — origin's v4.1.7 tree lacks four internal documents the local v4.1.7 tree still carries, which is a history rewrite. Those refs will not converge, so every release has printed "could not `git fetch --tags origin`" immediately after that same fetch wrote the new tag successfully. Measured on the v4.6.2 release, same checkout, same moment: git fetch --tags origin exit=1, 26 rejected git fetch origin refs/tags/v4.6.2:refs/tags/v4.6.2 exit=0, * [new tag] Restored afterwards and confirmed with `git show-ref --tags` diffed against a pre-experiment capture — identical. Not `--tags --force`: that would silently rewrite 26 local refs as a side effect of cutting a release, which is a larger action than the one being asked for. The line that follows is unchanged and was already right: it asks whether the tag is in fact present rather than treating the fetch's own report as the answer. That is why the false alarm was never load-bearing — but a warning that fires on every release is a warning nobody reads. Pinned by a sixth wiring assertion in tests/release-preconditions.test.ts, scoped to the fetch because `git ls-remote --tags` is a different call with the same flag and is correct. Break-tested both ways: deleting the refspec goes red, and re-inserting `--tags` on the fetch goes red. --- CHANGELOG.md | 20 ++++++++++++++++++++ scripts/finish-release.mjs | 29 ++++++++++++++++++++++++++--- tests/release-preconditions.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c024ef6..7ffe5d7aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to MeMesh are documented here. ## [Unreleased] +### Fixed + +- **`release:finish` no longer warns about a fetch that worked.** Its last step + brought the new tag into the local checkout with `git fetch --tags origin` — + a command that asks for every tag `origin` has and exits non-zero if any one + of them cannot be written. This repository has 26 that cannot: measured + across all 73 tags, the version tags from `v2.10.1` through `v4.1.7` plus + `benchmark/longmemeval-public-r1` point at different objects locally than on + `origin`, while everything from `v4.2.0` onward agrees. The cause is a + history rewrite that removed internal documents — `origin`'s `v4.1.7` tree + lacks four files the local one still carries. So that step printed + ``could not `git fetch --tags origin` `` on every release, immediately after + writing the new tag successfully. Measured on the v4.6.2 release, same + checkout: `--tags` exited 1 with 26 rejections while + `refs/tags/v4.6.2:refs/tags/v4.6.2` exited 0. It now fetches the one tag it + needs — not `--tags --force`, which would silently rewrite 26 local refs as a + side effect of cutting a release. The line that follows is unchanged and was + already sound: it asks whether the tag is in fact present rather than + treating the fetch's own report as the answer. + ## [4.6.2] — 2026-08-23 ### Added diff --git a/scripts/finish-release.mjs b/scripts/finish-release.mjs index 930f2a731..8c2d65c1f 100644 --- a/scripts/finish-release.mjs +++ b/scripts/finish-release.mjs @@ -218,11 +218,34 @@ console.log(`\n✓ released: ${releaseUrl}`); // Bring the tag into this checkout so `verify:release` here stops failing — // the check reads `git tag --list`, and until the tag is fetched, main still // looks like it declares an untagged version. -if (capture('git', ['fetch', '--tags', 'origin']) === null) { - console.log(' (could not `git fetch --tags origin` — run it to sync this checkout)'); +// +// ONE tag, by explicit refspec, not `--tags`. The wide form asks for every tag +// the remote has and fails if ANY of them cannot be written, so one unrelated +// divergence anywhere in the repository's history turns this step red. +// +// This repository has 26 of them. Measured 2026-08-23 across all 73 tags: 26 +// refs disagree with origin — the 25 version tags from v2.10.1 through v4.1.7, +// plus `benchmark/longmemeval-public-r1` — while everything from v4.2.0 onward +// agrees. The cause is a history rewrite that removed internal documents: +// origin's v4.1.7 tree lacks four files the local v4.1.7 tree still carries. +// (Their names are deliberately not repeated here — putting them back into a +// public file would undo part of what the rewrite was for.) Those old refs are +// not going to converge, so `--tags` fails here on every release — right after +// writing the new tag it was actually asked for. Measured on v4.6.2, same +// checkout: `--tags` exited 1 with 26 rejections, while +// `refs/tags/v4.6.2:refs/tags/v4.6.2` exited 0. +// +// Not `--tags --force` either: that would silently rewrite 26 local refs as a +// side effect of cutting a release. A warning that always fires is a warning +// nobody reads; the fix is to stop asking a wider question than we need. +const fetchSpec = `refs/tags/${tag}:refs/tags/${tag}`; +if (capture('git', ['fetch', 'origin', fetchSpec]) === null) { + console.log(` (could not \`git fetch origin ${fetchSpec}\` — run it to sync this checkout)`); } const nowTagged = (captureLines('git', ['tag', '--list', 'v*']) ?? []).includes(tag); -console.log(` local checkout has ${tag}: ${nowTagged ? 'yes' : 'no — run `git fetch --tags origin`'}`); +console.log( + ` local checkout has ${tag}: ${nowTagged ? 'yes' : `no — run \`git fetch origin ${fetchSpec}\``}` +); // Where to look next. The publish is a workflow run, and npm's registry lags // that run by minutes: `npm view` answering with the OLD version right after a diff --git a/tests/release-preconditions.test.ts b/tests/release-preconditions.test.ts index 8635956d8..621360c36 100644 --- a/tests/release-preconditions.test.ts +++ b/tests/release-preconditions.test.ts @@ -217,6 +217,30 @@ describe('finish-release cuts the release in one call', () => { expect(code).toMatch(/'release',\s*'view'/); }); + it('fetches the one tag it needs, not every tag the remote has', () => { + // `git fetch --tags` asks for every tag origin has and exits non-zero if + // ANY of them cannot be written. Measured 2026-08-23 across all 73 tags in + // this repository: 26 refs disagree with origin (v2.10.1 through v4.1.7, + // plus benchmark/longmemeval-public-r1) because a history rewrite removed + // internal documents from those commits. So the wide form fails on every + // release, immediately after successfully writing the tag it was asked + // for, and the script printed a warning about a fetch that had worked. + // + // Both halves are pinned. The refspec must be built — delete it and this + // goes red. And `--tags` must not come back on the FETCH — the negative + // half is break-tested by INSERTING it, not by deleting the refspec, + // because a not.toMatch also passes when the thing it forbids has become + // impossible to express. + // + // Scoped to the fetch on purpose. `git ls-remote --tags origin` above is a + // different call with the same flag and is correct: listing every remote + // tag is exactly what the precondition check needs. A blanket ban on the + // string would have failed on it — as the first version of this test did. + expect(code).toMatch(/'fetch',\s*'origin',\s*fetchSpec/); + expect(code).toMatch(/refs\/tags\/\$\{tag\}:refs\/tags\/\$\{tag\}/); + expect(code).not.toMatch(/'fetch'[^\]]*'--tags'/); + }); + it('is wired to an npm script, so it is the documented way in', () => { const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); expect(pkg.scripts['release:finish']).toContain('scripts/finish-release.mjs');