Skip to content
Merged
Show file tree
Hide file tree
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
98 changes: 95 additions & 3 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ on:
types: [closed]
workflow_dispatch:
inputs:
release_mode:
description: 'Publish a prerelease or recover an existing stable GitHub release'
type: choice
options:
- prerelease
- recover-stable
default: prerelease
release_version:
description: 'Existing stable tag to recover (for example, 4.1.2); leave blank for prereleases'
type: string
npm_tag:
description: 'NPM tag for prerelease'
default: 'next'
Expand All @@ -14,6 +24,9 @@ jobs:
name: Publish to npm
runs-on: ubuntu-latest
environment: npm-publish
concurrency:
group: npm-publish-stable
cancel-in-progress: false
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains( github.event.pull_request.labels.*.name, '[ Type ] NPM version update' ) && startsWith( github.head_ref, 'release/')
permissions:
contents: write
Expand All @@ -33,13 +46,91 @@ jobs:
PROVENANCE: 'true'
CONVENTIONAL_COMMITS: 'true'

recover-stable:
name: Recover stable release
if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'recover-stable'
runs-on: ubuntu-latest
environment: npm-publish
concurrency:
group: npm-publish-stable
cancel-in-progress: false
permissions:
contents: read
id-token: write
env:
RELEASE_VERSION: ${{ inputs.release_version }}
NPM_CONFIG_LOGLEVEL: error
steps:
- name: Validate recovery target
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "$GITHUB_REF" != refs/heads/trunk || ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo '::error::Run recovery from trunk with an existing stable tag, such as 4.1.2.'
exit 1
fi
gh release view "$RELEASE_VERSION" --repo "$GITHUB_REPOSITORY" \
--json isDraft,isPrerelease,tagName > "$RUNNER_TEMP/recovery-release.json"
jq -e --arg version "$RELEASE_VERSION" \
'.tagName == $version and .isDraft == false and .isPrerelease == false' \
"$RUNNER_TEMP/recovery-release.json"

- uses: actions/checkout@v7
with:
ref: refs/tags/${{ inputs.release_version }}
persist-credentials: false

- name: Validate package and prepare local release branch
run: |
node -e 'const p = require("./package.json"); if (p.name !== "@automattic/vip" || p.version !== process.env.RELEASE_VERSION) process.exit(1);'
# prepublishOnly requires a branch named trunk for the latest npm tag.
# Keep HEAD at the existing release tag; do not fetch current trunk content.
git switch -C trunk

- uses: actions/setup-node@v7
with:
node-version: 'lts/*'
registry-url: https://registry.npmjs.org/

- name: Install npm and dependencies
run: |
npm install --global npm@11
npm ci --ignore-scripts

- name: Refuse already published versions and downgrades
run: |
npm view @automattic/vip versions dist-tags --json > "$RUNNER_TEMP/recovery-registry.json"
node <<'NODE'
const fs = require('node:fs');
const semver = require('semver');
const registry = JSON.parse(fs.readFileSync(`${process.env.RUNNER_TEMP}/recovery-registry.json`, 'utf8'));
const version = process.env.RELEASE_VERSION;
if (registry.versions.includes(version) || !semver.gt(version, registry['dist-tags'].latest)) {
console.error(`Refusing recovery: ${version} is already published or does not exceed latest (${registry['dist-tags'].latest}).`);
process.exit(1);
}
NODE

- name: Build and test release
env:
NODE_AUTH_TOKEN: ''
run: |
npm rebuild
npm run prepare --if-present
npm test
npm publish --access public --tag latest --dry-run

- name: Publish existing release to npm
run: npm publish --access public --tag latest --provenance --loglevel error

changelog:
name: Publish docs changelog
permissions:
contents: read
pull-requests: read
uses: ./.github/workflows/changelog.yml
needs: publish
needs: [publish, recover-stable]
if: always() && (needs.publish.result == 'success' || needs.recover-stable.result == 'success')
secrets:
CHANGELOG_DOCS_POST_TOKEN: ${{ secrets.CHANGELOG_DOCS_POST_TOKEN }}

Expand All @@ -48,15 +139,16 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/publish-docs.yml
needs: publish
needs: [publish, recover-stable]
if: always() && (needs.publish.result == 'success' || needs.recover-stable.result == 'success')
secrets:
DOCS_SECRET_TOKEN: ${{ secrets.DOCS_SECRET_TOKEN }}

publish-prerelease:
name: Publish prerelease
runs-on: ubuntu-latest
environment: npm-publish
if: github.event_name == 'workflow_dispatch'
if: github.event_name == 'workflow_dispatch' && inputs.release_mode != 'recover-stable'
permissions:
contents: write
id-token: write
Expand Down
26 changes: 26 additions & 0 deletions docs/NPM-RELEASE-RECOVERY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Recover an interrupted stable npm release

Use this when the stable publishing workflow created a GitHub release and tag,
but failed before publishing that version to npm.

1. Ensure the recovery workflow is merged into `trunk`.
2. Open **Actions → Publish to npm (if applicable) → Run workflow**.
3. Select branch **trunk**, set **release_mode** to **recover-stable**, and enter
the existing tag in **release_version** (for example, `4.1.2`).
4. Leave **npm_tag** unchanged; stable recovery always publishes to `latest`.
5. Select **Run workflow** and inspect the **Recover stable release** job.

Do not rerun the original failed run to pick up workflow changes: reruns retain
the original workflow. The original action also tries to recreate the existing
GitHub release before it reaches npm publishing.

Recovery builds and tests the existing tag, preserves the GitHub release, and
uses the same workflow identity and `npm-publish` environment for npm trusted
publishing. It enables npm error logging and refuses versions already published
or older than the current `latest`. Successful publication starts the changelog
and command-reference documentation jobs.

Recovery does not create the next development-version PR that the regular
publishing action normally opens after publication. Handle that version bump
separately once recovery succeeds. If npm publication succeeds but a downstream
documentation job fails, rerun only the failed jobs.
Loading