Skip to content

Commit 6909c68

Browse files
rhyslbwclaude
andcommitted
ci(release): nominate a ref + guarded publish-only recovery mode
Adds two workflow_dispatch inputs to the release workflow: - `ref` (default `master`): branch/tag/commit to build & publish from, so a specific already-versioned release can be back-published. - `publish_only` (default false): skips the Phase 1 version step and runs only Phase 2 (`lerna publish from-package`). The version step is guarded to run only for a real master release (non-master/ detached refs would fail `lerna version --allow-branch master` anyway). Phase 2 is inherently idempotent — it publishes only tree versions not already on the registry — so re-running with publish_only=true safely ships whatever a failed run left behind, without touching git history. Also adds a pre-publish guard: `from-package` trusts package.json blindly, so a mis-nominated `ref` could ship drifted source under an already-versioned number. The guard asserts every publishable package's current version has a matching `<name>@<version>` tag and its src+manifest at HEAD is byte-identical to that tag, aborting otherwise. No-op for a normal release, since Phase 1 creates those tags at HEAD. Note documented inline: back-publishing an OLD ref still requires that ref's own @lerna-lite to be publish-capable (>=4 for the tar-7 line); refs predating that bump crash exactly as they originally did. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 24dd527 commit 6909c68

1 file changed

Lines changed: 44 additions & 5 deletions

File tree

.github/workflows/release.yaml

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ name: release
22

33
on:
44
workflow_dispatch:
5+
inputs:
6+
ref:
7+
description: 'Git ref (branch, tag or commit) to build & publish from. Defaults to master for a normal release.'
8+
required: false
9+
default: master
10+
publish_only:
11+
description: 'Skip the version phase and only publish (from-package). Enable when back-publishing an already-versioned ref.'
12+
type: boolean
13+
required: false
14+
default: false
515

616
jobs:
717
publish:
@@ -15,7 +25,7 @@ jobs:
1525
with:
1626
# Fetch all history for all tags and branches
1727
fetch-depth: 0
18-
ref: master
28+
ref: ${{ inputs.ref || 'master' }}
1929
token: ${{ secrets.BOT_GH_TOKEN }}
2030

2131
- name: 🧰 Setup Node.js
@@ -46,19 +56,48 @@ jobs:
4656
4757
# Phase 1 — decide versions, commit, tag, push to master, and create GitHub Releases.
4858
# No-ops when there are no new conventional commits (e.g. re-running after a publish
49-
# failure), so it never double-bumps an already-versioned release.
59+
# failure), so it never double-bumps an already-versioned release. Skipped entirely when
60+
# publishing from a nominated ref or in publish_only mode: those refs are already versioned,
61+
# and `lerna version --allow-branch master` would reject a non-master / detached checkout.
5062
- name: 🏷️ Version
63+
if: ${{ !inputs.publish_only && (inputs.ref == '' || inputs.ref == 'master') }}
5164
run: |
5265
if [ "$RELEASE_MODE" = "stable" ]; then yarn version:stable; else yarn version:rc; fi
5366
env:
5467
# Used for making a GitHub Release
5568
GH_TOKEN: ${{ secrets.BOT_GH_TOKEN }}
5669
RELEASE_MODE: ${{ vars.RELEASE_MODE }}
5770

71+
# Guard — refuse to publish a tree that isn't a genuine release state. `from-package`
72+
# publishes whatever version sits in each package.json with no check that the source has
73+
# stayed put since that version was tagged, so a mis-nominated `ref` could ship drifted
74+
# source under an already-versioned number. Assert every publishable package's current
75+
# version has a matching `<name>@<version>` tag AND its src+manifest at HEAD is identical
76+
# to that tag. Passes trivially for a normal release (Phase 1 just created the tags at HEAD).
77+
- name: 🔎 Verify tree aligns with release tags
78+
run: |
79+
node -e '
80+
const fs=require("fs"),cp=require("child_process");
81+
const bad=[];
82+
for(const d of fs.readdirSync("packages")){
83+
let p; try{p=JSON.parse(fs.readFileSync("packages/"+d+"/package.json","utf8"))}catch{continue}
84+
if(p.private) continue;
85+
const tag=p.name+"@"+p.version;
86+
if(cp.spawnSync("git",["rev-parse","-q","--verify","refs/tags/"+tag]).status!==0){bad.push("no tag: "+tag);continue}
87+
if(cp.spawnSync("git",["diff","--quiet",tag,"HEAD","--","packages/"+d+"/src","packages/"+d+"/package.json"]).status!==0)
88+
bad.push("drift vs tag: "+tag);
89+
}
90+
if(bad.length){console.error("Refusing to publish — tree does not align with release tags:\n "+bad.join("\n "));process.exit(1)}
91+
console.log("All publishable packages align with their release tags.");
92+
'
93+
5894
# Phase 2 — publish to npm via `lerna publish from-package`, which queries the registry
59-
# and publishes ONLY versions that are not yet there. This makes the job idempotent and
60-
# recoverable: if a previous run versioned/tagged but failed to publish, simply re-running
61-
# the workflow ships the missing packages without touching git history.
95+
# and publishes ONLY the versions in the checked-out tree that are not yet there. This makes
96+
# the job idempotent and recoverable: re-running (optionally with publish_only=true) ships
97+
# the missing packages without touching git history. To back-publish an older release, set
98+
# `ref` to that release's tag/commit AND publish_only=true — BUT note the tree's own
99+
# `@lerna-lite` must be a version that can actually publish (>=4 for the tar-7 line); older
100+
# refs that predate that bump will crash here exactly as they did originally.
62101
- name: 📦 Publish to npm
63102
run: |
64103
if [ "$RELEASE_MODE" = "stable" ]; then yarn publish:stable; else yarn publish:rc; fi

0 commit comments

Comments
 (0)