Skip to content

Commit 911965e

Browse files
authored
Merge pull request #8650 from BitGo/zahinmohammad/WCN-308-recovery-mode
feat(ci): add recovery-mode to npmjs-release workflow
2 parents 447d400 + c0dbca6 commit 911965e

1 file changed

Lines changed: 117 additions & 3 deletions

File tree

.github/workflows/npmjs-release.yml

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,33 @@ on:
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

1429
permissions:
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+
1940
env:
2041
NX_NO_CLOUD: true
2142
NX_SKIP_NX_CACHE: true
@@ -24,6 +45,7 @@ env:
2445
jobs:
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

Comments
 (0)