diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 00000000000..f0408ac94fb --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,78 @@ +name: Publish package to npm + +on: + release: + # "created" does not fire when a draft release is published, "published" does + types: [published] + +concurrency: + group: "${{ github.workflow }} ✨ ${{ github.ref }}" + cancel-in-progress: false + +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "lts/*" + registry-url: "https://registry.npmjs.org" + + # npm stage publish requires npm >= 11.15.0 + - name: Upgrade npm + run: npm install -g npm@latest + + # Derive the npm dist-tag from the package version rather than from the + # release target branch, which is just whatever the "Target" dropdown held + # when the release was created and has been wrong in the past. Publishing + # a maintenance line must never overwrite "latest": + # prerelease (e.g. 3.0.0-beta.1) -> next + # major >= current npm "latest" major -> latest + # major < current npm "latest" major -> latest- + # No data from the release event is used, so nothing untrusted reaches + # the script. + - name: Resolve npm dist-tag from package version + id: dist-tag + run: | + node <<'EOF' + const fs = require('fs') + const { execFileSync } = require('child_process') + + const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/ + const { name, version } = JSON.parse(fs.readFileSync('package.json', 'utf8')) + const parsed = SEMVER.exec(version) + if (!parsed) throw new Error(`Invalid version in package.json: ${version}`) + + const latest = execFileSync('npm', ['view', name, 'version'], { encoding: 'utf8' }).trim() + const latestParsed = SEMVER.exec(latest) + if (!latestParsed) throw new Error(`Unexpected "latest" version on npm: ${latest}`) + + const major = Number(parsed[1]) + const prerelease = parsed[4] + const latestMajor = Number(latestParsed[1]) + + let tag + if (prerelease) tag = 'next' + else if (major >= latestMajor) tag = 'latest' + else tag = `latest-${major}` + + console.log(`${name}@${version} (npm latest: ${latest}) -> dist-tag "${tag}"`) + fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\n`) + EOF + + - name: Stage publish to npm + env: + NPM_DIST_TAG: ${{ steps.dist-tag.outputs.tag }} + run: npm stage publish --tag "$NPM_DIST_TAG" \ No newline at end of file