diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d8adbe7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Summary + +## Test plan + +## Todo + +- [ ] PR title has a `fix:`/`feat:`/`feat!:` prefix (or intentionally none, for no release) \ No newline at end of file diff --git a/.github/workflows/npm-release.yml b/.github/workflows/npm-release.yml index 5e9ddc0..f369d85 100644 --- a/.github/workflows/npm-release.yml +++ b/.github/workflows/npm-release.yml @@ -2,35 +2,36 @@ name: NPM release on: push: - tags: - - '*' + branches: ["main"] + workflow_dispatch: {} + +# Run releases one at a time, so two runs can't publish the same version or move `latest` +# backwards. A queued run that gets superseded is dropped; its commits roll into the next release. +concurrency: + group: npm-release-${{ github.ref_name }} + cancel-in-progress: false env: CI: true +permissions: + id-token: write + contents: write + pull-requests: write + jobs: publish: name: "Publish ${{ github.ref_name }}" runs-on: ubuntu-latest + # npm's Trusted Publisher config for @mapbox/mr-ui is bound to this workflow file and environment. environment: npm-release - permissions: - id-token: write - contents: write steps: - - uses: actions/checkout@v4 - - - name: Extract package version from package.json - run: | - packageVersion=$(jq -r .version package.json) - echo "versionTag=v$packageVersion" >> "$GITHUB_ENV" - - - name: Verify tag matches package.json version - if: ${{ github.ref_name != env.versionTag }} - run: | - echo "Tag ${{ github.ref_name }} does not match package.json version (expected ${{ env.versionTag }})." - exit 1 + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: npm @@ -41,35 +42,70 @@ jobs: - name: Install npm dependencies run: npm ci --no-fund --no-audit --prefer-offline - - name: Build publishable package - # Publishes from pkg/, not the repo root - see scripts/build-package.js. - # This compiles src/components into pkg/, with a package.json that has - # `private`/`scripts`/`devDependencies` stripped out. - run: npm run build + # pretest runs lint and tsc + - name: Lint, typecheck, and test + run: npm test - - name: Release production - if: ${{ !contains(github.ref_name, '-') }} - working-directory: pkg - run: npm publish + # Reads the current version from public npmjs and git history + - id: version + uses: mapbox/gha@action-conventional-commit-version-v0 + with: + before-sha: ${{ github.event.before }} + after-sha: ${{ github.event.after }} - - name: Release development - if: ${{ contains(github.ref_name, '-') }} + # Versioned before building because scripts/build-package.js copies package.json into pkg/ + - name: Build publishable package + if: steps.version.outputs.skip != 'true' + run: | + npm version ${{ steps.version.outputs.version }} --no-git-tag-version + npm run build + + - name: Publish + if: steps.version.outputs.skip != 'true' working-directory: pkg - run: npm publish --tag dev + run: npm publish --tag ${{ steps.version.outputs.npm_publish_tag }} + + - id: release + name: Create GitHub Release + continue-on-error: true + if: steps.version.outputs.skip != 'true' && steps.version.outputs.npm_publish_tag == 'latest' + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + run: gh release create "v$VERSION" --target "$GITHUB_SHA" --title "v$VERSION" --generate-notes + + - name: Comment released version on PRs + continue-on-error: true + if: steps.release.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} + BEFORE_SHA: ${{ github.event.before }} + run: | + set -o pipefail + package_name="$(node -p 'require("./package.json").name')" + for sha in $(git rev-list "$BEFORE_SHA..$GITHUB_SHA"); do + gh api "repos/$GITHUB_REPOSITORY/commits/$sha/pulls" --jq '.[].number' + done | sort -u | while read -r pr; do + gh pr comment "$pr" --body "Released in \`$package_name@$VERSION\` (release [\`v$VERSION\`](https://github.com/$GITHUB_REPOSITORY/releases/tag/v$VERSION))." + done - name: Build docs - if: ${{ !contains(github.ref_name, '-') }} + if: steps.version.outputs.skip != 'true' && steps.version.outputs.npm_publish_tag == 'latest' run: npm run build-docs - name: Deploy docs - if: ${{ !contains(github.ref_name, '-') }} + if: steps.version.outputs.skip != 'true' && steps.version.outputs.npm_publish_tag == 'latest' + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" worktree=$(mktemp -d) git fetch origin gh-pages --depth=1 - git worktree add "$worktree" gh-pages + git worktree add "$worktree" FETCH_HEAD rsync -a --delete --exclude=.git build/ "$worktree/" @@ -78,6 +114,6 @@ jobs: if git diff --cached --quiet; then echo "No changes to deploy." else - git commit -m "Deploy docs for ${{ github.ref_name }}" - git push origin gh-pages + git commit -m "Deploy docs for v$VERSION" + git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:gh-pages fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2bfc542..e2818be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: npm diff --git a/CHANGELOG.md b/CHANGELOG.md index a4283a0..419ed7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Changelog +Starting after 2.25.0, release notes are published as [GitHub Releases](https://github.com/mapbox/mr-ui/releases). + ## HEAD ## 2.25.0 diff --git a/README.md b/README.md index 1da7146..ef82dee 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,17 @@ npm test The `build` command creates a `pkg/` directory that contains the code we want to publish, organized the way we want it. So `pkg/` is the directory that we publish. `pkg/package.json` is a clone of `package.json` but with `private`/`scripts`/`devDependencies` removed. -Publishing is automated via the `NPM release` GitHub Actions workflow, triggered by pushing a version tag: +Pull Requests merged to `main` with conventional commit prefixes are automatically published to npm by the `NPM release` GitHub Actions workflow, as new major/minor/patch versions (`feat!:`/`feat:`/`fix:` respectively). When using "Squash and merge", ensure your PR title includes the conventional commit prefix; a `BREAKING CHANGE:` footer only counts if it ends up in the squash commit's body. Other prefixes (`chore:`, `docs:`, etc.) or no prefix don't trigger a release. -- Increment version numbers in `package.json` and `package-lock.json`, and ensure the changelog has an entry for the latest version. Commit this. -- Create a Git tag matching the version, prefixed with `v`: `git tag v2.25.0`. -- Push the tag: `git push origin v2.25.0`. -- This triggers the workflow, which builds `pkg/` and publishes it via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC) — no npm token needed. It also builds and deploys the docs to https://mapbox.github.io/mr-ui/. -- Any prerelease tag (a version containing `-`, e.g. `v2.25.0-dev.0`) publishes under the npm `dev` dist-tag instead of `latest`, for testing a release without affecting `latest` consumers - dev releases don't touch the live docs site. +Examples: +- `fix: resolved issue with dropdowns` -> patch +- `feat: added dropdowns` -> minor +- `feat!: Overhauled dropdowns` (requires manual migration work) -> major + +The workflow builds `pkg/` and publishes it via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC) — no npm token needed. It then creates a GitHub Release with generated notes, comments the released version on the merged PRs, and builds and deploys the docs to https://mapbox.github.io/mr-ui/. + +`package.json`'s `version` may be incremented manually if needed (for example, to skip a minor version), but you shouldn't normally need to touch it; the published npm version, not `package.json`, is the source of truth for what gets released next. + +#### Pre-release versions on other branches + +For testing in downstream projects, you can manually trigger the "NPM release" workflow on a branch to publish a pre-release tagged with the short commit sha (e.g. `2.25.0-3ce8575`) under the npm `prerelease` dist-tag, without affecting `latest` or the live docs site. Check the workflow results for the published version.