Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 61 additions & 20 deletions scripts/bump-and-publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,31 +238,72 @@ function main() {
// Only the annotated tag is pushed. Tag pushes bypass branch-protection
// "require pull request" rules.

// Create tag
// Create and push the annotated tag — idempotently.
//
// The tag is created and pushed BEFORE the npm publish loop below. If a
// previous run pushed the tag but then failed partway through publishing,
// the remote tag already exists. A naive rerun would die on `git tag` /
// `git push` for the existing tag before it ever reached the publish retry,
// leaving the release stuck until someone deletes the tag by hand.
//
// To make reruns safe we check the remote for the tag: if it already exists
// we simply skip the tag step and fall straight through to the publish retry.
// We do NOT compare it to HEAD — on a rerun `alpha` may have moved past the
// original release commit, and the goal here is only to retry publishing the
// already-tagged version, not to police where the tag points. Only when the
// tag is genuinely absent do we create + push a fresh annotated tag.
//
// For master the version-bump commit already lives on the branch (it came
// from the release PR). Only the annotated tag is pushed — tag pushes bypass
// branch-protection "require pull request" rules. Alpha follows the same
// pattern: the version bump arrived via the alpha release PR.
const tagName = `v${nextVersion}`;
console.log(`🏷️ Creating git tag: ${tagName}...`);
if (!dryRun) {

// Resolve the commit a remote tag points at. `^{}` dereferences an
// annotated tag to the commit it wraps; lightweight tags have no `^{}` line
// and the plain ref already IS the commit. Returns null when absent.
function getRemoteTagCommit(name) {
const out = execSync(`git ls-remote origin "refs/tags/${name}" "refs/tags/${name}^{}"`, {
cwd: ROOT_DIR,
encoding: 'utf8'
}).trim();
if (!out) return null;
const lines = out.split('\n').filter(Boolean);
// Prefer the dereferenced (annotated) commit line if present.
const derefLine = lines.find(l => l.endsWith(`refs/tags/${name}^{}`));
const plainLine = lines.find(l => l.endsWith(`refs/tags/${name}`));
const line = derefLine || plainLine;
return line ? line.split('\t')[0] : null;
}

console.log(`🏷️ Preparing git tag: ${tagName}...`);
const remoteTagCommit = getRemoteTagCommit(tagName);

if (remoteTagCommit) {
// Rerun-after-failed-publish path: the version is already tagged on the
// remote. Skip create/push and fall through to the publish retry.
console.log(`✅ Remote tag ${tagName} already exists — skipping tag create/push, proceeding to publish.`);
const headCommit = execSync('git rev-parse HEAD', { cwd: ROOT_DIR, encoding: 'utf8' }).trim();
if (remoteTagCommit !== headCommit) {
console.warn(`⚠️ Remote tag ${tagName} points at ${remoteTagCommit}, which differs from HEAD ${headCommit}. Proceeding to publish anyway.`);
}
Comment on lines +286 to +289

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reject tag conflicts

When the remote tag exists but points at a different commit, this branch only warns and still runs the publish loop from the current checkout. That lets a real vX.Y.Z tag collision proceed as if it were a retry, so npm packages can be published for a checkout that is not represented by the release tag. The retry path should skip tag creation only when the existing remote tag is the intended release commit; otherwise it should stop before publishing.

Suggested change
const headCommit = execSync('git rev-parse HEAD', { cwd: ROOT_DIR, encoding: 'utf8' }).trim();
if (remoteTagCommit !== headCommit) {
console.warn(`⚠️ Remote tag ${tagName} points at ${remoteTagCommit}, which differs from HEAD ${headCommit}. Proceeding to publish anyway.`);
}
const headCommit = execSync('git rev-parse HEAD', { cwd: ROOT_DIR, encoding: 'utf8' }).trim();
if (remoteTagCommit !== headCommit) {
throw new Error(`Remote tag ${tagName} points at ${remoteTagCommit}, which differs from HEAD ${headCommit}. Refusing to publish.`);
}

} else if (dryRun) {
console.log(` [DRY RUN] Remote tag ${tagName} not found — would create annotated tag and push to origin.`);
} else {
// Tag is absent on the remote — always create a FRESH annotated tag. Delete
// any pre-existing local tag of any type first (a no-op when none exists) so
// we never reuse or push a stale/lightweight tag.
try {
execSync(`git tag -a "${tagName}" -m "Release ${tagName}"`, {
cwd: ROOT_DIR,
stdio: 'inherit'
});
execSync(`git tag -d "${tagName}"`, { cwd: ROOT_DIR, stdio: 'inherit' });
} catch (e) {
console.log(`⚠️ Tag might already exist, continuing...`);
// No local tag to delete — fine.
}
} else {
console.log(` [DRY RUN] Would create tag: ${tagName}`);
}

// For master the version-bump commit already lives in master (it came from
// the release PR). Only push the git tag — tag pushes bypass branch
// protection "require pull request" rules.
// Alpha follows the same pattern: the version bump arrived via the alpha
// release PR, so we only push the tag here too. console.log(`📤 Pushing tag ${tagName}...`);
if (!dryRun) {

console.log(`🏷️ Creating git tag: ${tagName}...`);
execSync(`git tag -a "${tagName}" -m "Release ${tagName}"`, { cwd: ROOT_DIR, stdio: 'inherit' });

console.log(`📤 Pushing tag ${tagName}...`);
execSync(`git push origin "${tagName}"`, { cwd: ROOT_DIR, stdio: 'inherit' });
} else {
console.log(` [DRY RUN] Would push tag: origin ${tagName}`);
}

// Validate alpha branch requirements
Expand Down
Loading