diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..a2234db7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +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: read # Everything that writes uses the GitHub App token below + id-token: write # Mint the OIDC token npm trusted publishing requires + + defaults: + run: + 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 + 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: ${{ steps.app-token.outputs.token }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c590d32..7d6ba3a9 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,24 @@ 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. + +`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. 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",