diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6175de4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,129 @@ +name: Publish + +# Stages a release on npm; a maintainer then approves it on npmjs.com with 2FA. +# npm authenticates this workflow by OIDC (trusted publishing), so there is no +# npm token anywhere. The trusted publisher on npmjs.com is bound to this file +# name and the `npm` environment, and only allows `npm stage publish`. +on: + release: + types: [published] + +concurrency: + group: publish + cancel-in-progress: false + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + dist-tag: ${{ steps.release.outputs.dist-tag }} + tarball: ${{ steps.pack.outputs.tarball }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + # Full history, so the tagged commit can be checked against master. + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version-file: .nvmrc + package-manager-cache: false + + - name: Enable Corepack + run: corepack enable + + - name: Check release + id: release + env: + TAG: ${{ github.event.release.tag_name }} + run: | + version=$(node -p 'require("./package.json").version') + if [ "$TAG" != "$version" ]; then + echo "::error::Release tag '$TAG' does not match package.json version '$version'" + exit 1 + fi + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::Release tag '$TAG' points at a commit that is not on master" + exit 1 + fi + # A prerelease version must never become `latest`. + if [[ "$version" == *-* ]]; then + echo "dist-tag=next" >> "$GITHUB_OUTPUT" + else + echo "dist-tag=latest" >> "$GITHUB_OUTPUT" + fi + + - name: Install dependencies + # No dependency needs an install script; refusing them all keeps a + # compromised package from running code at install time. + run: yarn install --frozen-lockfile --ignore-scripts + + - name: Test + # Runs mocha under c8 coverage, then eslint. + run: yarn ci-test + + - name: Pack + id: pack + run: | + tarball=$(npm pack --ignore-scripts --json | node -p 'JSON.parse(require("fs").readFileSync(0))[0].filename') + echo "tarball=$tarball" >> "$GITHUB_OUTPUT" + + - name: Upload package + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: package + path: ${{ steps.pack.outputs.tarball }} + if-no-files-found: error + retention-days: 30 + + stage: + name: Stage on npm + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + # Bound in the npm trusted publisher config, and limited to release tags. + environment: + name: npm + url: https://www.npmjs.com/package/prolly + # The only job that can mint an npm OIDC token. It never checks out or + # installs anything; it stages the tarball the build job produced. + permissions: + id-token: write + steps: + - name: Download package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: package + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + # Staged publishing needs npm >= 11.15.0, which Node 24 bundles. + node-version: 24 + package-manager-cache: false + + - name: Stage + env: + TARBALL: ${{ needs.build.outputs.tarball }} + DIST_TAG: ${{ needs.build.outputs.dist-tag }} + run: | + npm stage publish "./$TARBALL" --tag "$DIST_TAG" --provenance + { + echo "### Staged \`$TARBALL\` on npm with dist-tag \`$DIST_TAG\`" + echo + echo "Nothing is live yet. A package maintainer must approve it with 2FA," + echo "either under Staged Packages on npmjs.com or with:" + echo + echo '```sh' + echo "npm stage list prolly" + echo "npm stage approve " + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index c710e43..fd264aa 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,14 @@ Prolly.poll(isReady, 5000, validateFn, 1000, 10) 4. Open PR * (Optional) link to your development soundtrack +## Releasing + +Maintainers only. Releases are staged on npm by the [Publish](.github/workflows/publish.yml) workflow and go live only once an npm maintainer approves them with 2FA. + +1. Open a PR that sets `version` in `package.json` (e.g. `npm version 0.5.5 --no-git-tag-version`), and merge it. +2. Create a GitHub release whose tag is exactly that version, with no `v` prefix: `gh release create 0.5.5 --target master --generate-notes`. A prerelease version (`0.5.5-rc.0`) is staged under the `next` dist-tag instead of `latest`. +3. Once the workflow has passed, approve the staged version under Staged Packages on npmjs.com, or run `npm stage list prolly` and then `npm stage approve `. + ## License The MIT License (MIT) diff --git a/package.json b/package.json index 6f820d3..dd879f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prolly", - "version": "0.5.4", + "version": "0.5.5", "description": "A minimalist utility library for ES6 Native Promises", "main": "index.js", "scripts": {