diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b30e8a0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: release + +on: + push: + branches: [develop] + +# 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 + +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 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. 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 + 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 }} + VERSION: ${{ steps.decide.outputs.version }} + run: | + # 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 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 new file mode 100644 index 0000000..bb44d2c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: test + +on: + pull_request: + 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: ${{ matrix.node }} + 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..4cf4b13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,13 @@ 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 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.