From 701f11e3f30deb075342c0afec996385382cef8f Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:49:55 +0800 Subject: [PATCH] fix(release): push version tags in one atomic push (avoid commit_refs race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit changeset publish creates a git tag per package; changesets/action then pushes them with one concurrent `git push origin ` per tag. With the large Changesets "fixed" group (~73 packages bumping in lockstep), that burst of simultaneous ref-creation pushes races GitHub's ref backend, which returns `remote: fatal error in commit_refs` and rejects ~half the tags — failing the Release job even though npm publishing already fully succeeded (#2191: 54/73 tags pushed, 19 rejected). Route the publish command through scripts/release-publish.sh, which runs `changeset publish` then pushes ALL new tags in a SINGLE atomic `git push origin --tags`. One push is one ref transaction, so there is no concurrency to race; the action's later per-tag pushes become no-ops because every tag already exists on the remote. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 5 +++++ package.json | 2 +- scripts/release-publish.sh | 36 +++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100755 scripts/release-publish.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57a2988fba..df77fb882f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,11 @@ jobs: id: changesets uses: changesets/action@v1 with: + # publish (pnpm run release) ends in scripts/release-publish.sh, + # which pushes all new version tags in ONE atomic git push. This + # pre-empts changesets/action's own concurrent per-tag pushes, which + # otherwise race GitHub's ref backend (remote: fatal error in + # commit_refs) and reject ~half the tags on a large fixed-group bump (#2191). publish: pnpm run release version: pnpm run version commit: 'chore: version packages' diff --git a/package.json b/package.json index 72d0c7e9d1..73dd8cbe8e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "clean": "turbo run clean && rm -rf dist", "setup": "pnpm install && pnpm --filter @objectstack/spec build", "version": "changeset version", - "release": "pnpm run build && bash scripts/build-console.sh && changeset publish", + "release": "pnpm run build && bash scripts/build-console.sh && bash scripts/release-publish.sh", "docs:dev": "pnpm --filter @objectstack/docs dev", "docs:build": "pnpm --filter @objectstack/docs build", "docs:start": "pnpm --filter @objectstack/docs start", diff --git a/scripts/release-publish.sh b/scripts/release-publish.sh new file mode 100755 index 0000000000..8d881a5051 --- /dev/null +++ b/scripts/release-publish.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# release-publish.sh — npm publish + atomic git-tag push for the Release workflow. +# +# Why this exists (the bug it fixes): +# `changeset publish` publishes every package to npm and creates a local +# annotated git tag (`@`) for each one. changesets/action +# then pushes those tags — but it fires one `git push origin ` per tag +# *concurrently* (Promise.all). With this monorepo's large Changesets "fixed" +# group (~70+ packages all bumping in lockstep), that burst of simultaneous +# ref-creation pushes races on GitHub's ref backend, which responds with +# `remote: fatal error in commit_refs` and rejects a chunk of the tags. npm +# publishing has already fully succeeded by then, yet the job fails and the +# rejected version tags never make it to the remote (#2191). +# +# The fix: +# Push ALL new tags ourselves in a SINGLE atomic `git push origin --tags` +# immediately after `changeset publish`. One push = one ref transaction, so +# there is no concurrency to race. By the time changesets/action runs its own +# per-tag pushes, every tag already exists on the remote at the same SHA, so +# each of those pushes is a harmless no-op ("Everything up-to-date"). +# +# git push auth comes from the persisted actions/checkout credentials. The +# tags themselves are already created by `changeset publish` (which configures +# the CI git identity); this script only pushes them. Run as the `publish:` +# command of changesets/action (see +# .github/workflows/release.yml) so the atomic push happens before the action +# pushes tags itself. +set -euo pipefail + +# Publish to npm and create the local version tags. +changeset publish + +# Push every new tag in one atomic transaction (see header). --tags pushes ALL +# local tags regardless of reachability; --follow-tags would push nothing here +# since this is a bare push with no branch ref attached. +git push origin --tags