diff --git a/scripts/release.ts b/scripts/release.ts index 3850e69fa..12ecfff5c 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -328,49 +328,37 @@ async function cmdRelease(version: string): Promise { 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 + // (`: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 { console.log("\nTo retry after fixing (repeat until CI passes):"); console.log(" git commit -m \"fix: \""); 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);