From 55361e83e811431dd8dc3338ac56feec4a5d78ee Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Tue, 18 Aug 2026 17:02:11 +0200 Subject: [PATCH 1/2] ci: publish releases with npm trusted publishing Move releases off the local machine and into GitHub Actions. The new Release workflow is dispatched manually with a bump type and dist-tag, then bumps the version, tags it, builds, publishes to npm over OIDC and creates the GitHub release. No npm token is needed anywhere, and packages get provenance for free. The local `release` scripts are removed so nobody publishes from a laptop by accident, along with the dead semantic-release config block that no longer matched how releases were cut. --- .github/workflows/release.yml | 78 +++++++++++++++++++ CONTRIBUTING.md | 25 ++++-- package.json | 1 - .../react-intersection-observer/package.json | 16 ---- 4 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3420a9db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,78 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: "Version bump" + type: choice + default: patch + options: + - patch + - minor + - major + - prepatch + - preminor + - premajor + - prerelease + tag: + description: "npm dist-tag" + type: string + default: latest + +concurrency: + group: release + cancel-in-progress: false + +permissions: {} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write # Push the version commit/tag and create the GitHub release + id-token: write # Mint the OIDC token npm trusted publishing requires + + defaults: + run: + working-directory: packages/react-intersection-observer + + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - run: corepack enable + working-directory: . + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: "pnpm" + registry-url: "https://registry.npmjs.org" + - name: Update npm + # Trusted publishing requires npm 11.5.1 or newer + run: npm install -g npm@latest + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: . + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Bump version, commit and tag + run: pnpm exec bumpp ${{ inputs.version }} --yes + - name: Read version + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" + - name: Build + run: pnpm build + working-directory: . + - name: Publish to npm + # No NODE_AUTH_TOKEN: npm authenticates through the OIDC token, and + # provenance is attested automatically by trusted publishing. + run: npm publish --tag ${{ inputs.tag }} + - name: Create GitHub release + run: gh release create "v${{ steps.version.outputs.version }}" --generate-notes ${{ startsWith(inputs.version, 'pre') && '--prerelease' || '' }} + working-directory: . + env: + GH_TOKEN: ${{ github.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c590d32..69cd3496 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,13 +50,10 @@ Fork the repository and create a branch for your feature/bug fix. ### Commit Message Conventions -- We use - [semantic-release](https://github.com/semantic-release/semantic-release) to - manage releases automatically. To ensure that releases are automatically - versioned correctly, we follow the +- We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - Conventions. This means that your commit messages should have the following - format: + Conventions, so the generated release notes stay readable. This means that + your commit messages should have the following format: `: ` @@ -92,3 +89,19 @@ Build every published and documentation surface with: ```shell pnpm build:all ``` + +## Releasing + +Releases are published from CI with +[npm trusted publishing](https://docs.npmjs.com/trusted-publishers), so no npm +token exists anywhere and packages are published with provenance. There is no +local publish step, and `npm publish` from a laptop will be rejected. + +To cut a release, a maintainer opens the **Actions** tab, selects the +**Release** workflow and runs it from the branch to release: + +- `version` picks the bump (`patch`, `minor`, `major` or a `pre*` variant). +- `tag` picks the npm dist-tag, `latest` by default. Use `beta` for prereleases. + +The workflow bumps the version, commits and tags it, builds the package, +publishes it to npm, and creates a GitHub release with generated notes. diff --git a/package.json b/package.json index b5239163..37b2bcc5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "dev:storybook": "pnpm --filter storybook dev", "docs:build": "pnpm --filter docs build", "lint": "pnpm --filter react-intersection-observer lint && pnpm --filter storybook lint && pnpm --filter docs lint", - "release": "pnpm --filter react-intersection-observer release", "test": "pnpm --filter react-intersection-observer test", "typecheck": "pnpm build && pnpm --filter react-intersection-observer typecheck && pnpm --filter storybook typecheck && pnpm --filter docs typecheck", "turbo:build": "turbo run build", diff --git a/packages/react-intersection-observer/package.json b/packages/react-intersection-observer/package.json index 4cc1368f..0316f65a 100644 --- a/packages/react-intersection-observer/package.json +++ b/packages/react-intersection-observer/package.json @@ -48,7 +48,6 @@ "postbuild": "npm_config_cache=.npm-cache attw --pack && publint && size-limit", "dev": "run-p dev:*", "dev:package": "tsup src/index.tsx --watch", - "release": "bumpp && npm publish", "preview": "pnpx pkg-pr-new publish --no-template", "lint": "biome check .", "version": "pnpm build", @@ -69,21 +68,6 @@ "useInView", "useIntersectionObserver" ], - "release": { - "branches": [ - "main", - { - "name": "beta", - "prerelease": true - } - ], - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/npm", - "@semantic-release/github" - ] - }, "size-limit": [ { "path": "dist/index.mjs", From 524b7c98d0f43dda2be65c9d7cbfd5a3fa59a4c5 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Tue, 18 Aug 2026 17:16:40 +0200 Subject: [PATCH 2/2] ci: push the version commit with a GitHub App token `main` is protected and the repository is user-owned, so the GitHub Actions app cannot be added as a ruleset bypass actor. Mint a short-lived token from a dedicated GitHub App instead, and use it for the checkout so bumpp can push the version commit and tag. GITHUB_TOKEN drops to contents: read, since everything that writes now goes through the app token. --- .github/workflows/release.yml | 15 +++++++++++++-- CONTRIBUTING.md | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3420a9db..a2234db7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: release: runs-on: ubuntu-latest permissions: - contents: write # Push the version commit/tag and create the GitHub release + contents: read # Everything that writes uses the GitHub App token below id-token: write # Mint the OIDC token npm trusted publishing requires defaults: @@ -38,9 +38,20 @@ jobs: working-directory: packages/react-intersection-observer steps: + - name: Mint GitHub App token + # `main` is protected, and the GitHub Actions app cannot be a ruleset + # bypass actor on a user-owned repository. This app can, so the version + # commit is pushed with a short-lived token minted here and revoked + # when the job ends. + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_KEY }} - uses: actions/checkout@v7 with: fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} - run: corepack enable working-directory: . - name: Setup Node.js @@ -75,4 +86,4 @@ jobs: run: gh release create "v${{ steps.version.outputs.version }}" --generate-notes ${{ startsWith(inputs.version, 'pre') && '--prerelease' || '' }} working-directory: . env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69cd3496..7d6ba3a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,3 +105,8 @@ To cut a release, a maintainer opens the **Actions** tab, selects the The workflow bumps the version, commits and tags it, builds the package, publishes it to npm, and creates a GitHub release with generated notes. + +`main` is protected, so the version commit is pushed with a short-lived token +minted from a GitHub App that is listed as a bypass actor on the branch +ruleset. The app's id lives in the `RELEASE_APP_ID` variable and its private key +in the `RELEASE_APP_KEY` secret.