From 0730566cc0a8f0a3c90fb6bb6600461d37a94d73 Mon Sep 17 00:00:00 2001 From: "Fredrik Liljegren (Claude Code Claude Fable 5)" Date: Fri, 28 Aug 2026 19:42:35 +0200 Subject: [PATCH 1/2] ci: test every pull request, publish every merged version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test.yml builds and runs the whole suite on pull requests and develop pushes. release.yml publishes @naturalcycles/diffity with provenance when a merge lands a version npm does not have, tags it and cuts a GitHub release — and fails loudly when the version is already published but the merge changed shipped code, which is the parallel-branch gap AGENTS.md could only warn about. The bump rule is now the enforced one: every PR touching packages/*/src, packages/skills or skills bumps. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs --- .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 20 ++++++++++ AGENTS.md | 12 ++++-- 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d929aea --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: release + +on: + push: + branches: [develop] + +# One release at a time, in merge order — two develop pushes must not race a publish. +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # HEAD^ is what this merge is judged against. + fetch-depth: 2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + registry-url: https://registry.npmjs.org + + - name: Decide whether this version needs publishing + id: decide + run: | + VERSION=$(node -p "require('./packages/cli/package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + PUBLISHED=$(npm view "@naturalcycles/diffity@$VERSION" version 2>/dev/null || true) + if [ "$PUBLISHED" != "$VERSION" ]; then + echo "publish=yes" >> "$GITHUB_OUTPUT" + echo "Version $VERSION is not on npm — publishing." + exit 0 + fi + echo "publish=no" >> "$GITHUB_OUTPUT" + # Already published. A merge that changed what a user gets must have re-bumped — + # this is the parallel-branch gap: two branches bump to the same number, the second + # to land would otherwise ship nothing, silently. + CHANGED=$(git diff --name-only HEAD^ HEAD -- 'packages/*/src' 'packages/skills' 'skills') + if [ -n "$CHANGED" ]; then + echo "::error::$VERSION is already on npm, but this merge changed shipped code. Re-bump the version (npx tsx scripts/release.ts patch --no-git) and merge again." + echo "$CHANGED" + exit 1 + fi + echo "Version $VERSION already published and nothing shipped changed — nothing to do." + + - if: steps.decide.outputs.publish == 'yes' + run: npm ci + - if: steps.decide.outputs.publish == 'yes' + run: npm run build + - if: steps.decide.outputs.publish == 'yes' + run: npm test + - if: steps.decide.outputs.publish == 'yes' + run: npm publish -w @naturalcycles/diffity --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - if: steps.decide.outputs.publish == 'yes' + name: Tag and release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION=${{ steps.decide.outputs.version }} + git tag "v$VERSION" + git push origin "v$VERSION" + gh release create "v$VERSION" --generate-notes --title "v$VERSION" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..05f8149 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: test + +on: + pull_request: + push: + branches: [develop] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - run: npm ci + # Typecheck resolves workspace imports through built dists, so the build comes first. + - run: npm run build + - run: npm test diff --git a/AGENTS.md b/AGENTS.md index e0d1e6f..e396820 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,12 @@ npx tsx scripts/release.ts patch --no-git # or: minor `develop` takes squash merges the commit is rewritten on the way in and the tag is left pointing at a commit that never lands. -There are no npm publish scripts: publishing `@naturalcycles/diffity` is a deliberate, separate -step (issue #60 gives it to CI on merges to develop). +There are no npm publish scripts: every merge to develop whose version is not yet on npm is +published by `.github/workflows/release.yml`, with a `v` tag and a GitHub release. The +workflow needs the `NPM_TOKEN` secret (publish rights on the `@naturalcycles` scope). -A pull request that changes nothing a user could observe — a test, a comment, a rename — does not -need a bump. Everything else does. +Bump in every pull request that touches `packages/*/src`, `packages/skills` or `skills`. The +release workflow enforces it: a merge to develop whose version is already on npm and whose diff +touched those paths fails, which is what catches two parallel branches bumping to the same number — +the second to land would otherwise ship nothing, silently. A comment-only source change trips the +same wire; bump anyway, a patch number costs nothing. From bebbde307a17c57c632d4b1a5bb6d77ea52f8bdd Mon Sep 17 00:00:00 2001 From: "Fredrik Liljegren (Claude Code Claude Fable 5)" Date: Fri, 28 Aug 2026 19:56:37 +0200 Subject: [PATCH 2/2] review fixes: the guard actually matches, and a re-run can finish the job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A quoted 'packages/*/src' pathspec matches nothing — git fnmatches wildcards against the whole path — so the enforcement wire was inert; :(glob)packages/** now provably fires on real commits. The guarded set is everything the artifact is built from, tests excluded, and AGENTS.md states the same set. Tagging is idempotent and the re-bump error names the manual recovery for a publish that failed at the tag. test.yml runs the engines floor and the release version, with read-only permissions; the concurrency comment says what the group actually gives. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs --- .github/workflows/release.yml | 24 +++++++++++++++--------- .github/workflows/test.yml | 9 ++++++++- AGENTS.md | 11 ++++++----- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d929aea..b30e8a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,8 @@ on: push: branches: [develop] -# One release at a time, in merge order — two develop pushes must not race a publish. +# Serialized: no two runs publish at once. GitHub keeps only the newest queued run, so a +# rapid-fire merge's own run can be superseded — the survivor publishes the latest version. concurrency: group: release cancel-in-progress: false @@ -39,12 +40,16 @@ jobs: exit 0 fi echo "publish=no" >> "$GITHUB_OUTPUT" - # Already published. A merge that changed what a user gets must have re-bumped — - # this is the parallel-branch gap: two branches bump to the same number, the second - # to land would otherwise ship nothing, silently. - CHANGED=$(git diff --name-only HEAD^ HEAD -- 'packages/*/src' 'packages/skills' 'skills') + # Already published. A merge that changes what ships must have re-bumped — this is + # the parallel-branch gap: two branches bump to the same number, and the second to + # land would otherwise ship nothing, silently. Everything the artifact is built from + # counts; tests do not. + CHANGED=$(git diff --name-only HEAD^ HEAD -- \ + ':(glob)packages/**' ':(glob)scripts/**' 'skills' \ + 'package.json' 'package-lock.json' 'tsconfig.json' \ + ':(exclude,glob)packages/*/tests/**' ':(exclude)scripts/lib/utils.test.ts') if [ -n "$CHANGED" ]; then - echo "::error::$VERSION is already on npm, but this merge changed shipped code. Re-bump the version (npx tsx scripts/release.ts patch --no-git) and merge again." + echo "::error::$VERSION is already on npm, but this merge changed shipped code. Re-bump the version (npx tsx scripts/release.ts patch --no-git) and merge again. If a previous run of this same commit already published and only tagging failed, instead create the tag by hand: git tag v$VERSION && git push origin v$VERSION && gh release create v$VERSION --generate-notes" echo "$CHANGED" exit 1 fi @@ -64,8 +69,9 @@ jobs: name: Tag and release env: GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.decide.outputs.version }} run: | - VERSION=${{ steps.decide.outputs.version }} - git tag "v$VERSION" + # Idempotent, so a re-run that already got partway does not fail on its own leftovers. + git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null || git tag "v$VERSION" git push origin "v$VERSION" - gh release create "v$VERSION" --generate-notes --title "v$VERSION" + gh release view "v$VERSION" >/dev/null 2>&1 || gh release create "v$VERSION" --generate-notes --title "v$VERSION" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05f8149..bb44d2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,14 +5,21 @@ on: push: branches: [develop] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + # The engines floor and the version releases build with. + node: [22, 24] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: ${{ matrix.node }} cache: npm - run: npm ci # Typecheck resolves workspace imports through built dists, so the build comes first. diff --git a/AGENTS.md b/AGENTS.md index e396820..4cf4b13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,8 +22,9 @@ There are no npm publish scripts: every merge to develop whose version is not ye published by `.github/workflows/release.yml`, with a `v` tag and a GitHub release. The workflow needs the `NPM_TOKEN` secret (publish rights on the `@naturalcycles` scope). -Bump in every pull request that touches `packages/*/src`, `packages/skills` or `skills`. The -release workflow enforces it: a merge to develop whose version is already on npm and whose diff -touched those paths fails, which is what catches two parallel branches bumping to the same number — -the second to land would otherwise ship nothing, silently. A comment-only source change trips the -same wire; bump anyway, a patch number costs nothing. +Bump in every pull request that touches anything the artifact is built from — `packages/`, +`scripts/`, `skills/`, or the root `package.json`/`package-lock.json`/`tsconfig.json`; tests are +exempt. The release workflow enforces exactly that set: a merge to develop whose version is already +on npm and whose diff touched it fails, which is what catches two parallel branches bumping to the +same number — the second to land would otherwise ship nothing, silently. A comment-only source +change trips the same wire; bump anyway, a patch number costs nothing.