fix(release): pushed tag by commit id to dodge maintenance prune

- Pushed HEAD sha into the remote tag ref so the push has no dependency on a local tag.
- Avoided both "cannot be resolved to branch" and "src refspec does not match" prune races.
- Dropped the retry loop and explicit git lfs push; the atomic push uploads LFS objects.
This commit is contained in:
can1357
2026-06-06 16:18:55 +02:00
parent b65d173dcd
commit 7ffafae1cc
+28 -41
View File
@@ -328,49 +328,37 @@ async function cmdRelease(version: string): Promise<void> {
await git(["commit", "-m", `chore: bump version to ${version}`]);
console.log();
// 8. Upload LFS objects, then tag + push Git refs atomically.
// 8. Tag, then push branch + tag atomically — pushing the tag by object id.
//
// The release push sends both refs/heads/main and refs/tags/v… in one
// atomic push. The default Git LFS pre-push hook reads every ref Git is
// about to send; when the tag ref is present it can abort with
// "refs/tags/v… cannot be resolved to branch" before Git pushes anything.
// Upload LFS objects for the branch explicitly, then make the atomic Git
// ref push with GIT_LFS_SKIP_PUSH so only that LFS hook becomes a no-op.
// This repo is in the global `[maintenance] repo = …` list, so a scheduled
// `git maintenance run` fetches origin with `fetch.pruneTags=true` (set
// globally) and deletes any local tag not yet on the remote — i.e. the
// brand-new release tag. The `-c fetch.pruneTags=false` on our git wrapper
// only governs our own git calls, not the concurrent maintenance process, so
// a local tag ref may vanish before or while the push resolves it.
//
// Background `git maintenance run` (scheduled via the global `[maintenance]
// repo = …` list) fetches origin with `fetch.pruneTags=true` set globally,
// which deletes any local tag that does not yet exist on the remote — i.e.
// the brand-new release tag. The `-c fetch.pruneTags=false` we pass to our
// git wrapper only applies to our git invocations, not to the concurrent
// maintenance process, so we have to defend against the race ourselves:
// (re)create the tag immediately before the push and retry on the specific
// "src refspec … does not match any" symptom that means it got pruned.
// A bare push refspec (`refs/tags/v…` with no `:dst`) re-resolves the tag on
// disk during refspec matching (git's remote.c:match_explicit); if the prune
// lands in that window git dies with
// "refs/tags/v… cannot be resolved to branch", and if it lands before the
// push it dies with "src refspec … does not match any". We sidestep both by
// pushing the HEAD commit object id straight into the remote tag ref
// (`<sha>:refs/tags/v…`): the push has no dependency on a local tag, and the
// commit is reachable from main so maintenance cannot prune it. The local
// tag we still create is only for `git describe`; losing it is harmless. The
// default Git LFS pre-push hook uploads the branch's LFS objects as part of
// this same atomic push — no separate `git lfs push` is needed.
console.log("Tagging and pushing to remote...");
const tagRef = `v${version}`;
await git(["lfs", "push", "origin", "main"]);
for (let attempt = 1; ; attempt++) {
await git(["tag", "-f", tagRef]);
const result = await git([
"push",
"--atomic",
"origin",
"refs/heads/main:refs/heads/main",
`refs/tags/${tagRef}:refs/tags/${tagRef}`,
])
.env({ ...Bun.env, GIT_LFS_SKIP_PUSH: "1" })
.quiet()
.nothrow();
if (result.exitCode === 0) break;
const stderr = result.stderr.toString();
process.stderr.write(stderr);
const pruned = /src refspec .* does not match any/.test(stderr);
if (!pruned || attempt >= 3) {
throw new Error(`git push failed for ${tagRef} (attempt ${attempt})`);
}
console.warn(
` Tag ${tagRef} pruned by background maintenance, retrying (${attempt + 1}/3)...`,
);
}
const sha = (await git(["rev-parse", "HEAD"]).text()).trim();
await git(["tag", "-f", tagRef]);
await git([
"push",
"--atomic",
"origin",
"refs/heads/main:refs/heads/main",
`${sha}:refs/tags/${tagRef}`,
]);
console.log();
// 9. Watch CI
@@ -383,9 +371,8 @@ async function cmdRelease(version: string): Promise<void> {
console.log("\nTo retry after fixing (repeat until CI passes):");
console.log(" git commit -m \"fix: <brief description>\"");
console.log(` git tag -f v${version}`);
console.log(" git lfs push origin main");
console.log(
` GIT_LFS_SKIP_PUSH=1 git push --atomic origin refs/heads/main:refs/heads/main +refs/tags/v${version}:refs/tags/v${version}`,
` git push --atomic origin refs/heads/main:refs/heads/main "+$(git rev-parse HEAD):refs/tags/v${version}"`,
);
console.log(" bun scripts/release.ts watch");
process.exit(1);