1010 type : boolean
1111 required : false
1212 default : false
13+ recovery-mode :
14+ description : |
15+ Recover from a partial-publish failure. Skips version bumping,
16+ master→rel/latest merge, and GPG signing. Runs `lerna publish
17+ from-package` against rel/latest, publishing only versions
18+ missing from npm. Release notes, GitHub release, and Express
19+ Docker publish still run.
20+
21+ IMPORTANT: from-package publishes whatever versions are in the
22+ rel/latest package.json files at trigger time. Verify rel/latest
23+ HEAD matches the failed release before triggering — the workflow
24+ logs the resolved SHA and the planned publish list.
25+ type : boolean
26+ required : false
27+ default : false
1328
1429permissions :
1530 contents : write
1631 id-token : write
1732 pull-requests : read
1833
34+ # Prevent overlapping releases. workflow_dispatch runs are serialized;
35+ # a normal release and a recovery run cannot race against rel/latest.
36+ concurrency :
37+ group : npmjs-release
38+ cancel-in-progress : false
39+
1940env :
2041 NX_NO_CLOUD : true
2142 NX_SKIP_NX_CACHE : true
2445jobs :
2546 get-release-context :
2647 name : Get release context
48+ if : ${{ !inputs.recovery-mode }}
2749 runs-on : ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
2850 timeout-minutes : 10
2951 outputs :
@@ -109,23 +131,79 @@ jobs:
109131
110132 echo "" >> "$GITHUB_STEP_SUMMARY"
111133
134+ get-recovery-context :
135+ name : Get recovery context
136+ if : inputs.recovery-mode
137+ runs-on : ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
138+ timeout-minutes : 10
139+ outputs :
140+ # Pinned SHA. release-bitgojs checks out this exact commit, not the
141+ # rel/latest branch tip, so the publish cannot drift from what the
142+ # env reviewer approved.
143+ sha : ${{ steps.resolve.outputs.sha }}
144+ steps :
145+ - name : Checkout rel/latest
146+ uses : actions/checkout@v6
147+ with :
148+ ref : rel/latest
149+ fetch-depth : 1
150+
151+ - name : Resolve SHA and show recovery target
152+ id : resolve
153+ run : |
154+ # Pin the SHA at preview time and surface it (plus the planned
155+ # publish list) BEFORE the env-gated publish job runs, so
156+ # reviewers approving the `npmjs-release` environment can
157+ # sanity-check what will be published.
158+ sha="$(git rev-parse HEAD)"
159+ if [ -z "$sha" ]; then
160+ echo "::error::Failed to resolve rel/latest SHA. Refusing to proceed."
161+ exit 1
162+ fi
163+ echo "sha=$sha" >> "$GITHUB_OUTPUT"
164+ {
165+ echo "## Recovery target"
166+ echo ""
167+ echo "Branch: \`rel/latest\`"
168+ echo "Resolved SHA: \`$sha\`"
169+ echo "Subject: $(git log -1 --pretty=format:'%s')"
170+ echo ""
171+ echo "### Versions in rel/latest package.jsons"
172+ echo ""
173+ echo '```'
174+ for f in modules/*/package.json; do
175+ jq -r '"\(.name)@\(.version)\(if .private then " (private)" else "" end)"' "$f"
176+ done | sort
177+ echo '```'
178+ } >> "$GITHUB_STEP_SUMMARY"
179+
112180 release-bitgojs :
113181 name : Release BitGoJS
114182 needs :
115183 - get-release-context
184+ - get-recovery-context
185+ if : ${{ always() && needs.get-release-context.result != 'failure' && needs.get-recovery-context.result != 'failure' }}
116186 runs-on : ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
117187 timeout-minutes : 60
118188 environment : npmjs-release
119189 steps :
120190 - name : Checkout repository
121191 uses : actions/checkout@v6
122192 with :
123- ref : ${{ needs.get-release-context.outputs.current-master-sha }}
193+ # Recovery mode pins to the SHA resolved by get-recovery-context so
194+ # the publish cannot drift from the commit the env reviewer approved
195+ # (rel/latest could otherwise advance during the approval wait).
196+ # Normal mode uses the master SHA captured by get-release-context.
197+ ref : ${{ inputs.recovery-mode && needs.get-recovery-context.outputs.sha || needs.get-release-context.outputs.current-master-sha }}
124198 token : ${{ secrets.BITGOBOT_PAT_TOKEN || github.token }}
125199 fetch-depth : 0
200+ # version-bump-summary uses `git tag --points-at HEAD`. In recovery
201+ # mode the bump tags were created by a prior failed run and live
202+ # only on origin, so we must fetch them.
203+ fetch-tags : true
126204
127205 - name : Configure GPG
128- if : inputs.dry-run == false
206+ if : ${{ inputs.dry-run == false && !inputs.recovery-mode }}
129207 run : |
130208 echo "${{ secrets.BITGOBOT_GPG_PRIVATE_KEY }}" | gpg --batch --import
131209 git config --global user.signingkey 67A9A0B77F0BD445E45CC8B719828A304678A92F
@@ -143,11 +221,13 @@ jobs:
143221 node-version-file : " .nvmrc"
144222
145223 - name : Switch to rel/latest branch
224+ if : ${{ !inputs.recovery-mode }}
146225 run : |
147226 git checkout rel/latest
148227 git pull origin rel/latest
149228
150229 - name : Merge master into rel/latest
230+ if : ${{ !inputs.recovery-mode }}
151231 run : |
152232 echo "Merging master commit ${{ needs.get-release-context.outputs.current-master-sha }} into rel/latest"
153233 git merge ${{ needs.get-release-context.outputs.current-master-sha }} --no-edit
@@ -171,12 +251,46 @@ jobs:
171251 uses : ./.github/actions/verify-npm-packages
172252
173253 - name : Publish new version
174- if : inputs.dry-run == false
254+ if : ${{ inputs.dry-run == false && !inputs.recovery-mode }}
175255 run : |
176256 yarn lerna publish --sign-git-tag --sign-git-commit --include-merged-tags --conventional-commits --conventional-graduate --yes
177257 env :
178258 NPM_CONFIG_PROVENANCE : true
179259
260+ - name : Publish missing versions (recovery)
261+ if : ${{ inputs.dry-run == false && inputs.recovery-mode }}
262+ run : |
263+ # `from-package` reads each package.json's `version`, queries npm,
264+ # and publishes only versions missing from the registry. No bump,
265+ # no tag, no git push.
266+ yarn lerna publish from-package --yes
267+ env :
268+ NPM_CONFIG_PROVENANCE : true
269+
270+ - name : Verify recovery published the missing versions
271+ if : ${{ inputs.dry-run == false && inputs.recovery-mode }}
272+ run : |
273+ # Walk every non-private package and confirm the version on
274+ # rel/latest is now reachable on the npm registry. Catches the
275+ # case where lerna reports success but a package didn't land
276+ # (e.g., another transient registry error).
277+ missing=()
278+ for f in modules/*/package.json; do
279+ if [ "$(jq -r '.private // false' "$f")" = "true" ]; then continue; fi
280+ name=$(jq -r '.name' "$f")
281+ version=$(jq -r '.version' "$f")
282+ code=$(curl -sL -o /dev/null -w "%{http_code}" -- "https://registry.npmjs.org/${name}/${version}")
283+ if [ "$code" != "200" ]; then
284+ missing+=("${name}@${version} (HTTP ${code})")
285+ fi
286+ done
287+ if [ "${#missing[@]}" -ne 0 ]; then
288+ echo "::error::Recovery left versions still missing from npm:"
289+ printf ' - %s\n' "${missing[@]}"
290+ exit 1
291+ fi
292+ echo "✅ All public package versions on rel/latest are present on npm."
293+
180294 - name : Generate version bump summary
181295 id : version-bump-summary
182296 if : inputs.dry-run == false
0 commit comments