From 37bcb4ff83c04db499ed782e896df2027f91e695 Mon Sep 17 00:00:00 2001 From: Carter Campbell Date: Sat, 19 Sep 2026 16:19:55 -0700 Subject: [PATCH] ci: require a version bump on every PR Adds a Version Bump Check workflow that runs on every PR into main and fails unless package.json's "version" is strictly greater than the base branch's (compared with semver) and CHANGELOG.md has a matching "## [x.y.z]" entry. The skip-version-bump label bypasses it for PRs that intentionally ship no release. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/version-bump.yml | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/version-bump.yml diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 0000000..afc6d06 --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,59 @@ +name: Version Bump Check + +# Every PR into main must increase the "version" in package.json and add a +# matching `## [x.y.z]` entry to CHANGELOG.md, because publishing to npm is +# release-driven and a release is cut from main after merge. +# Add the `skip-version-bump` label to a PR that intentionally ships no +# release (CI-only, docs-only, etc.) to bypass this check. +on: + pull_request: + branches: ["main"] + types: [opened, synchronize, reopened, labeled, unlabeled] + +concurrency: + group: concurrency-group::${{ github.workflow }}::pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + version-bump: + name: Version incremented + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-version-bump') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Full history so the base branch's package.json is available locally. + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Compare version against base branch + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + BASE_VERSION=$(git show "${BASE_SHA}:package.json" | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).version') + HEAD_VERSION=$(node -p 'require("./package.json").version') + echo "base: ${BASE_VERSION} -> head: ${HEAD_VERSION}" + + for v in "${BASE_VERSION}" "${HEAD_VERSION}"; do + if ! npx -y semver@7.6.3 "${v}" > /dev/null 2>&1; then + echo "::error file=package.json::Could not parse version '${v}' as semver" + exit 1 + fi + done + + # `semver -r` prints only the versions that satisfy the range (-p so a prerelease head also counts); empty output means head <= base. + if [ -z "$(npx -y semver@7.6.3 -p -r ">${BASE_VERSION}" "${HEAD_VERSION}")" ]; then + echo "::error file=package.json::\"version\" must be incremented in every PR (base is ${BASE_VERSION}, PR has ${HEAD_VERSION}). Bump it, or add the 'skip-version-bump' label if this PR intentionally ships no release." + exit 1 + fi + echo "Version bump OK" + + if ! grep -qE "^## \[${HEAD_VERSION//./\\.}\]" CHANGELOG.md; then + echo "::error file=CHANGELOG.md::CHANGELOG.md has no \"## [${HEAD_VERSION}]\" entry. Add one describing this release at the top of the file." + exit 1 + fi + echo "CHANGELOG entry OK"