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:
+28
-41
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user