name: Verify Changesets on: pull_request: types: [opened, synchronize, labeled, unlabeled, edited] branches: - main - next - 'release/**' paths: - '.changeset/**' - 'packages/**' permissions: {} jobs: verify: runs-on: ubuntu-latest if: > !startsWith(github.head_ref, 'changeset-release/') && !(github.head_ref == 'next' && github.base_ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository) && !(github.head_ref == 'main' && github.base_ref == 'next' && github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'github-actions[bot]' && github.actor != 'better-release[bot]' permissions: contents: read pull-requests: read steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false - name: Block pre.json on main if: github.base_ref == 'main' env: BASE_REF: ${{ github.base_ref }} run: | if git diff --diff-filter=d --name-only "origin/$BASE_REF"...HEAD | grep -q '^\.changeset/pre\.json$'; then echo "::error::.changeset/pre.json must not be committed to main. Pre-release mode belongs on the 'next' branch only." exit 1 fi - name: Get changed changeset files id: changed env: BASE_REF: ${{ github.base_ref }} run: | FILES=$(git diff --diff-filter=dr --name-only "origin/$BASE_REF"...HEAD -- '.changeset/*.md' | grep -v 'README.md' || true) COUNT=$(echo "$FILES" | grep -c '.' || true) { echo "files<> "$GITHUB_OUTPUT" - name: Check package changes require changeset if: steps.changed.outputs.has_changesets == 'false' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} BASE_REF: ${{ github.base_ref }} run: | LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' 2>/dev/null || echo "") if echo "$LABELS" | grep -q "skip-changeset"; then echo "Skipping changeset requirement (skip-changeset label found)" exit 0 fi PKG_CHANGES=$(git diff --name-only "origin/$BASE_REF"...HEAD -- 'packages/' | grep -c '.' || true) if [ "$PKG_CHANGES" -eq 0 ]; then echo "No package changes detected, changeset not required" exit 0 fi echo "::error::Missing changeset. Add one with 'pnpm changeset' or add the 'skip-changeset' label." exit 1 - name: Validate changeset files if: steps.changed.outputs.has_changesets == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} BASE_REF: ${{ github.base_ref }} CHANGESET_FILES: ${{ steps.changed.outputs.files }} run: | HAS_ERROR=0 for f in $CHANGESET_FILES; do if [ -L "$f" ]; then echo "::error::Changeset $f is a symlink, which is not allowed." HAS_ERROR=1 continue fi BASENAME=$(basename "$f") if ! echo "$BASENAME" | grep -qE '^[a-z][a-z0-9-]*\.md$'; then echo "::error::Changeset $f has an invalid filename. Use only lowercase letters, digits, and hyphens." HAS_ERROR=1 continue fi if ! head -1 "$f" | grep -q '^---$'; then echo "::error::Changeset $f is missing YAML frontmatter opening fence." HAS_ERROR=1 continue fi if ! sed -n '2,$ p' "$f" | grep -q '^---$'; then echo "::error::Changeset $f is missing YAML frontmatter closing fence." HAS_ERROR=1 continue fi BODY=$(sed -n '2,/^---$/p' "$f" | head -n -1) while IFS= read -r line; do [ -z "$line" ] && continue if ! echo "$line" | grep -qE '^"[^"]+"\s*:\s*(major|minor|patch|none)\s*$'; then echo "::error::Changeset $f has invalid entry: $line — expected '\"package-name\": patch|minor|major|none'" HAS_ERROR=1 fi done <<< "$BODY" done [ "$HAS_ERROR" -ne 0 ] && exit 1 if [ "$BASE_REF" = "main" ] || echo "$BASE_REF" | grep -q '^release/'; then for f in $CHANGESET_FILES; do BUMPS=$(sed -n '/^---$/,/^---$/p' "$f" | grep -oE '(major|minor|patch)' | sort -u) if echo "$BUMPS" | grep -q "major"; then echo "::error::Changeset $f contains a 'major' bump. Only 'patch' bumps are allowed on '$BASE_REF'. PRs with minor/major bumps should target 'next' (auto-retarget may not have run yet)." exit 1 fi if echo "$BUMPS" | grep -q "minor"; then echo "::error::Changeset $f contains a 'minor' bump. Only 'patch' bumps are allowed on '$BASE_REF'. PRs with minor/major bumps should target 'next' (auto-retarget may not have run yet)." exit 1 fi done echo "All changesets are patch-only ($BASE_REF branch policy)" else echo "Target is $BASE_REF, all bump types allowed" fi