Repository navigation
contract: history resolutions come from the power history #3186
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: changeset-check | |
| # Gate on PRs targeting master: require a `.changeset/*.md` file unless | |
| # the PR only touches paths in the allowlist below (docs, repo config, | |
| # CI tooling, markdown). Pattern lifted from umara/u-front. | |
| # | |
| # Escape layers, in order: | |
| # 1. PR identity exactly matches the auto-generated Version PR → skip | |
| # (trusted repository, head branch, base branch and title). | |
| # 2. PR carries the `no-changeset` label → skip (manual override for | |
| # cases the allowlist doesn't anticipate). | |
| # 3. All changed files are in the path allowlist → skip. | |
| # 4. Otherwise: require at least one added .changeset/<name>.md file. | |
| # | |
| # Layer 3 is decided before Changesets runs, because `changeset status` | |
| # reports "packages have been changed but no changesets were found" for | |
| # any diff inside a package — a docs-only PR included. Validating the | |
| # queue first therefore failed exactly the PRs the allowlist exempts. | |
| on: | |
| pull_request: | |
| branches: [master] | |
| types: [opened, synchronize, reopened, labeled, unlabeled] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| # A label change or a new push re-asks the same question of a newer diff, so | |
| # the older answer is worthless. Cancelling is also what makes the `labeled` | |
| # trigger cheap: adding `no-changeset` mid-run drops the run it would fail. | |
| concurrency: | |
| group: changeset-check-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| changeset-required: | |
| name: changeset present (or auto-exempt) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Short-circuit on auto-generated Version PR | |
| id: version_pr | |
| env: | |
| TITLE: ${{ github.event.pull_request.title }} | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${TITLE}" == "chore(release): version packages" \ | |
| && "${HEAD_REPO}" == "${GITHUB_REPOSITORY}" \ | |
| && "${HEAD_REF}" == "changeset-release/master" \ | |
| && "${BASE_REF}" == "master" ]]; then | |
| echo "skip=true" >> "${GITHUB_OUTPUT}" | |
| echo "Auto-generated Version PR — gate skipped." | |
| else | |
| echo "skip=false" >> "${GITHUB_OUTPUT}" | |
| fi | |
| - name: Short-circuit on no-changeset label | |
| if: steps.version_pr.outputs.skip != 'true' | |
| id: label | |
| env: | |
| LABELS: ${{ toJson(github.event.pull_request.labels) }} | |
| run: | | |
| set -euo pipefail | |
| if echo "${LABELS}" | jq -e '.[] | select(.name == "no-changeset")' >/dev/null; then | |
| echo "skip=true" >> "${GITHUB_OUTPUT}" | |
| echo "PR carries 'no-changeset' label — release-notes gate satisfied." | |
| { | |
| echo "### Changeset exemption" | |
| echo "The \`no-changeset\` label skips Changesets setup, install, validation, and entry checks." | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| else | |
| echo "skip=false" >> "${GITHUB_OUTPUT}" | |
| fi | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node for version policy | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: package-lock.json | |
| - name: Reject Core major bumps | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin "${BASE_SHA}" "${HEAD_SHA}" | |
| mapfile -t changed_changesets < <(git diff -M --name-only --diff-filter=ACMR "${BASE_SHA}"..."${HEAD_SHA}" -- '.changeset/*.md') | |
| npm ci --ignore-scripts --no-audit --no-fund | |
| node --test .github/scripts/check-changeset-bumps.test.mjs | |
| node .github/scripts/check-changeset-bumps.mjs "${changed_changesets[@]}" | |
| - name: Validate package and lockfile metadata | |
| run: node --test web/release-metadata.test.mjs | |
| - name: Verify changeset present (or PR is allowlisted) | |
| id: gate | |
| if: steps.version_pr.outputs.skip != 'true' && steps.label.outputs.skip != 'true' | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin "${BASE_SHA}" "${HEAD_SHA}" | |
| CHANGED="$(git diff --name-only "${BASE_SHA}"..."${HEAD_SHA}")" | |
| # Accept any added or modified changeset file. Exclude | |
| # deletions (--diff-filter=d) so a PR that *deletes* a | |
| # changeset can't satisfy the rule. Exclude the README + | |
| # config files which are infrastructure, not real entries. | |
| # Computed up front: the verdict below and the Changesets | |
| # validation that follows need the same answer. | |
| CHANGESETS="$(git diff --name-only --diff-filter=d "${BASE_SHA}"..."${HEAD_SHA}" \ | |
| | grep -E '^\.changeset/[^/]+\.md$' \ | |
| | grep -vE '^\.changeset/(README)\.md$' \ | |
| || true)" | |
| if [ -n "${CHANGESETS}" ]; then | |
| echo "has_changesets=true" >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "has_changesets=false" >> "${GITHUB_OUTPUT}" | |
| fi | |
| if [ -z "${CHANGED}" ]; then | |
| echo "exempt=true" >> "${GITHUB_OUTPUT}" | |
| echo "No file changes — passing." | |
| exit 0 | |
| fi | |
| # Paths in this allowlist do NOT require a changeset. Tweak | |
| # if a new "obviously doesn't ship to users" path is added. | |
| # Patterns are extended-grep regexes: | |
| # - any *.md / *.mdx / *.txt anywhere in the tree | |
| # - the docs/ tree | |
| # - .changeset/ (the changesets themselves + README/config) | |
| # - .github/ (workflows, CODEOWNERS, issue templates…) | |
| # - .vscode/ (editor settings) | |
| # - .cursor/ (agent skills and policies; nothing ships to users) | |
| # - dotfiles at repo root | |
| REQUIRES_CHANGESET="$(echo "${CHANGED}" | grep -vE \ | |
| -e '(^|/)[^/]+\.(md|mdx|txt)$' \ | |
| -e '^docs/' \ | |
| -e '^\.changeset/' \ | |
| -e '^\.github/' \ | |
| -e '^\.vscode/' \ | |
| -e '^\.cursor/' \ | |
| -e '^\.editorconfig$' \ | |
| -e '^\.gitignore$' \ | |
| -e '^\.gitattributes$' \ | |
| -e '^LICENSE$' \ | |
| || true)" | |
| if [ -z "${REQUIRES_CHANGESET}" ]; then | |
| echo "exempt=true" >> "${GITHUB_OUTPUT}" | |
| echo "✓ PR changes only docs / repo config / CI tooling (allowlist) — changeset not required." | |
| echo "" | |
| echo "Files in this PR:" | |
| echo "${CHANGED}" | sed 's/^/ /' | |
| exit 0 | |
| fi | |
| echo "exempt=false" >> "${GITHUB_OUTPUT}" | |
| if [ -n "${CHANGESETS}" ]; then | |
| echo "✓ Found changeset(s):" | |
| echo "${CHANGESETS}" | sed 's/^/ /' | |
| exit 0 | |
| fi | |
| echo "::error::No changeset found and this PR touches files outside the docs/config/CI allowlist." | |
| echo "" | |
| echo "Files that triggered the changeset requirement:" | |
| echo "${REQUIRES_CHANGESET}" | sed 's/^/ /' | |
| echo "" | |
| echo "Add one with:" | |
| echo " npx changeset" | |
| echo "" | |
| echo "Or hand-create '.changeset/<descriptive-name>.md' following an existing example." | |
| echo "" | |
| echo "If this PR genuinely needs no release entry, either:" | |
| echo " - apply the 'no-changeset' label to skip the gate, or" | |
| echo " - expand the allowlist in .github/workflows/changeset-check.yml in the same PR." | |
| exit 1 | |
| # Presence alone is not enough: a Changeset can reference a package that | |
| # no longer exists after a rename. Validate the complete pending queue | |
| # with Changesets itself so an invalid entry fails on the PR instead of | |
| # breaking every release run after it reaches master. | |
| # | |
| # Runs when the PR needs a changeset, and also when an allowlisted PR | |
| # carries one anyway — a `.changeset/*.md` file matches the markdown | |
| # allowlist, so a changeset-only PR is exempt from the gate and would | |
| # otherwise ship its entry unvalidated. | |
| - name: Setup Node for Changesets validation | |
| if: >- | |
| steps.version_pr.outputs.skip != 'true' | |
| && steps.label.outputs.skip != 'true' | |
| && (steps.gate.outputs.exempt != 'true' || steps.gate.outputs.has_changesets == 'true') | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: package-lock.json | |
| - name: Install Changesets dependencies | |
| if: >- | |
| steps.version_pr.outputs.skip != 'true' | |
| && steps.label.outputs.skip != 'true' | |
| && (steps.gate.outputs.exempt != 'true' || steps.gate.outputs.has_changesets == 'true') | |
| run: npm ci --prefer-offline --no-audit --no-fund | |
| - name: Validate pending Changesets | |
| if: >- | |
| steps.version_pr.outputs.skip != 'true' | |
| && steps.label.outputs.skip != 'true' | |
| && (steps.gate.outputs.exempt != 'true' || steps.gate.outputs.has_changesets == 'true') | |
| # pull_request workflows run on a detached merge ref. Compare against | |
| # the fetched remote base because no local `master` branch exists. | |
| run: npx changeset status --since="origin/${{ github.base_ref }}" |