From f0c979fd5e65653888007195b309d8a30fd38aa8 Mon Sep 17 00:00:00 2001 From: Cristhofer Pincetti Date: Fri, 28 Aug 2026 16:35:04 -0400 Subject: [PATCH] ci: skip npm publish when only CI, tests, or scripts change Match workit's path-gated releases: semantic-release cuts a version only if src, plugin, or catalog files changed since the last tag. Co-authored-by: Cursor --- CHANGELOG.md | 4 + bun.lock | 44 ++++++--- package.json | 2 +- release.config.cjs | 9 +- scripts/analyze-release-scope.ts | 79 ++++++++++++++++ tests/unit/analyze-release-scope.test.ts | 111 +++++++++++++++++++++++ tests/unit/release-workflow.test.ts | 13 ++- 7 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 scripts/analyze-release-scope.ts create mode 100644 tests/unit/analyze-release-scope.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e503e7..22d8638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- npm publishes only when plugin or catalog files change since the last tag — CI, tests, and scripts-only merges skip a release. + ## [0.6.1] - 2026-08-28 ### Added diff --git a/bun.lock b/bun.lock index b7ff817..c31902a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "commandcode-go-opencode-provider", "devDependencies": { - "@semantic-release/commit-analyzer": "13.0.1", + "@semantic-release/exec": "7.1.0", "@semantic-release/github": "12.0.9", "@semantic-release/npm": "13.1.5", "@semantic-release/release-notes-generator": "14.1.1", @@ -154,6 +154,8 @@ "@semantic-release/error": ["@semantic-release/error@4.0.0", "", {}, "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ=="], + "@semantic-release/exec": ["@semantic-release/exec@7.1.0", "", { "dependencies": { "@semantic-release/error": "^4.0.0", "aggregate-error": "^3.0.0", "debug": "^4.0.0", "execa": "^9.0.0", "lodash-es": "^4.17.21", "parse-json": "^8.0.0" }, "peerDependencies": { "semantic-release": ">=24.1.0" } }, "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ=="], + "@semantic-release/github": ["@semantic-release/github@12.0.9", "", { "dependencies": { "@octokit/core": "^7.0.0", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-retry": "^8.0.0", "@octokit/plugin-throttling": "^11.0.0", "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", "debug": "^4.3.4", "dir-glob": "^3.0.1", "http-proxy-agent": "^9.0.0", "https-proxy-agent": "^9.0.0", "issue-parser": "^7.0.0", "lodash-es": "^4.17.21", "mime": "^4.0.0", "p-filter": "^4.0.0", "tinyglobby": "^0.2.14", "undici": "^7.0.0", "url-join": "^5.0.0" }, "peerDependencies": { "semantic-release": ">=24.1.0" } }, "sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ=="], "@semantic-release/npm": ["@semantic-release/npm@13.1.5", "", { "dependencies": { "@actions/core": "^3.0.0", "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", "env-ci": "^11.2.0", "execa": "^9.0.0", "fs-extra": "^11.0.0", "lodash-es": "^4.17.21", "nerf-dart": "^1.0.0", "normalize-url": "^9.0.0", "npm": "^11.6.2", "rc": "^1.2.8", "read-pkg": "^10.0.0", "registry-auth-token": "^5.0.0", "semver": "^7.1.2", "tempy": "^3.0.0" }, "peerDependencies": { "semantic-release": ">=20.1.0" } }, "sha512-Hq5UxzoatN3LHiq2rTsWS54nCdqJHlsssGERCo8WlvdfFA9LoN0vO+OuKVSjtNapIc/S8C2LBj206wKLHg62mg=="], @@ -176,7 +178,7 @@ "agent-base": ["agent-base@9.0.0", "", {}, "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA=="], - "aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], "ai": ["ai@6.0.191", "", { "dependencies": { "@ai-sdk/gateway": "3.0.120", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zAxvjKebQE7YkSyyNIl0OM7i6/zygnKeF+yNUjD4nWOelYrG+LpDd6RnH6mjySI4zUpZ7o4wbnmAy8jc6u98vQ=="], @@ -206,7 +208,7 @@ "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], - "clean-stack": ["clean-stack@5.3.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], @@ -320,7 +322,7 @@ "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], @@ -464,7 +466,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], @@ -568,7 +570,7 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], @@ -620,6 +622,10 @@ "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], + "@semantic-release/github/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], + + "@semantic-release/npm/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], @@ -636,13 +642,11 @@ "execa/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "fdir/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - "make-asynchronous/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "npm/@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], @@ -932,13 +936,13 @@ "npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - "parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], "read-package-up/read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], - "read-package-up/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "read-pkg/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + + "semantic-release/aggregate-error": ["aggregate-error@5.0.0", "", { "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" } }, "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw=="], "semantic-release/read-package-up": ["read-package-up@12.0.0", "", { "dependencies": { "find-up-simple": "^1.0.1", "read-pkg": "^10.0.0", "type-fest": "^5.2.0" } }, "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw=="], @@ -952,14 +956,20 @@ "tempy/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], - "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@semantic-release/github/aggregate-error/clean-stack": ["clean-stack@5.3.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg=="], + + "@semantic-release/github/aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "@semantic-release/npm/aggregate-error/clean-stack": ["clean-stack@5.3.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg=="], + + "@semantic-release/npm/aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -992,6 +1002,12 @@ "read-package-up/read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + "semantic-release/aggregate-error/clean-stack": ["clean-stack@5.3.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg=="], + + "semantic-release/aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "semantic-release/read-package-up/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], + "signale/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "signale/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], diff --git a/package.json b/package.json index 33a2d91..e65514b 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "catalog:ci": "bun run scripts/catalog-sync-ci.ts" }, "devDependencies": { - "@semantic-release/commit-analyzer": "13.0.1", + "@semantic-release/exec": "7.1.0", "@semantic-release/github": "12.0.9", "@semantic-release/npm": "13.1.5", "@semantic-release/release-notes-generator": "14.1.1", diff --git a/release.config.cjs b/release.config.cjs index ff984c4..8cfd86d 100644 --- a/release.config.cjs +++ b/release.config.cjs @@ -1,7 +1,14 @@ module.exports = { branches: ["main"], plugins: [ - "@semantic-release/commit-analyzer", + // Path-gated: prints major|minor|patch only when src/plugin/catalog files + // changed since the previous v* tag. Empty output skips npm + GitHub. + [ + "@semantic-release/exec", + { + analyzeCommitsCmd: "bun scripts/analyze-release-scope.ts", + }, + ], "./scripts/semantic-release-catalog-notes.cjs", "@semantic-release/release-notes-generator", "./scripts/semantic-release-changelog.cjs", diff --git a/scripts/analyze-release-scope.ts b/scripts/analyze-release-scope.ts new file mode 100644 index 0000000..9369dd8 --- /dev/null +++ b/scripts/analyze-release-scope.ts @@ -0,0 +1,79 @@ +#!/usr/bin/env bun +// Path-gated releases (workit AR-16): a releasable commit counts only when it +// touches package payload. CI/docs/tests/scripts-only merges cut no npm publish. +import { execFileSync } from "node:child_process"; +import { resolve } from "node:path"; + +const PRODUCT_FILES = new Set([ + "plugin.ts", + "index.ts", + "models.json", + "manifest.json", + "_version.txt", +]); + +export function isProductPath(file: string): boolean { + return PRODUCT_FILES.has(file) || file.startsWith("src/"); +} + +const g = (root: string, args: string[]): string => + execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim(); + +const SEMVER_TAG = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; + +export function latestTag(root = process.cwd()): string | null { + const out = g(root, ["tag", "--list", "v*", "--sort=-v:refname"]) + .split("\n") + .map((l) => l.trim()) + .filter((l) => SEMVER_TAG.test(l)); + return out[0] ?? null; +} + +type Level = "major" | "minor" | "patch"; +const LEVEL_RANK: Record = { patch: 1, minor: 2, major: 3 }; +const TYPE_LEVEL: Record = { fix: "patch", perf: "patch", feat: "minor" }; + +const subjectLevel = (commit: string): Level | null => { + const firstLine = commit.split("\n")[0] ?? ""; + const m = /^(?:fix|perf|feat)(?:\([^)]*\))?!?:/.exec(firstLine); + if (!m) return null; + if (m[0].includes("!")) return "major"; + const body = commit.split("\n").slice(1).join("\n"); + const type = m[0].replace(/\(.*$/, "").replace(/!$/, "").replace(/:$/, ""); + return /BREAKING[- ]CHANGE:/.test(body) ? "major" : (TYPE_LEVEL[type] ?? null); +}; + +const commitsSince = (root: string, from: string): { message: string; files: string[] }[] => { + const hashes = g(root, ["log", "--reverse", "--format=%H", `${from}..HEAD`]) + .split("\n") + .filter(Boolean); + return hashes.map((h) => ({ + message: g(root, ["show", "-s", "--format=%B", h]), + files: g(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-m", "--root", "-z", h]) + .split("\0") + .filter(Boolean), + })); +}; + +export function analyzeReleaseScope(root = process.cwd()): { level: Level | null } { + const from = latestTag(root); + if (from === null) return { level: "minor" }; + const levels: Level[] = []; + for (const { message, files } of commitsSince(root, from)) { + if (!files.some(isProductPath)) continue; + const lvl = subjectLevel(message); + if (lvl) levels.push(lvl); + } + if (levels.length === 0) return { level: null }; + const level = levels.reduce( + (best, l) => (LEVEL_RANK[l] > LEVEL_RANK[best] ? l : best), + "patch", + ); + return { level }; +} + +if (import.meta.main) { + const root = process.argv[2] ? resolve(process.argv[2]) : process.cwd(); + const { level } = analyzeReleaseScope(root); + if (level) process.stdout.write(`${level}\n`); +} diff --git a/tests/unit/analyze-release-scope.test.ts b/tests/unit/analyze-release-scope.test.ts new file mode 100644 index 0000000..6a0388c --- /dev/null +++ b/tests/unit/analyze-release-scope.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { analyzeReleaseScope, latestTag } from "../../scripts/analyze-release-scope.ts"; + +type Repo = { + root: string; + cleanup(): void; + commit(msg: string, files: Record): void; + tag(name: string): void; +}; + +function repo(): Repo { + const root = mkdtempSync(path.join(os.tmpdir(), "cc-relscope-")); + const g = (args: string[]) => execFileSync("git", args, { cwd: root, encoding: "utf8" }); + g(["init", "-q", "-b", "main"]); + g(["config", "user.name", "t"]); + g(["config", "user.email", "t@t"]); + writeFileSync(path.join(root, "seed.md"), "seed\n"); + g(["add", "-A"]); + g(["commit", "-q", "-m", "chore: seed"]); + return { + root, + cleanup: () => rmSync(root, { recursive: true, force: true }), + commit: (msg, files) => { + for (const [rel, body] of Object.entries(files)) { + mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + writeFileSync(path.join(root, rel), body); + } + g(["add", "-A"]); + g(["commit", "-q", "-m", msg]); + }, + tag: (name) => g(["tag", name]), + }; +} + +describe("latestTag", () => { + test("returns the newest semver-sorted v* tag", () => { + const r = repo(); + try { + r.commit("chore: a", { "x.txt": "a" }); + r.tag("v0.6.0"); + r.commit("chore: b", { "x.txt": "b" }); + r.tag("v0.6.1"); + expect(latestTag(r.root)).toBe("v0.6.1"); + } finally { + r.cleanup(); + } + }); +}); + +describe("analyzeReleaseScope", () => { + test("CI-only commits yield no release", () => { + const r = repo(); + r.tag("v0.6.0"); + try { + r.commit("fix(ci): workflow tweak", { ".github/workflows/ci.yml": "on: push\n" }); + expect(analyzeReleaseScope(r.root)).toEqual({ level: null }); + } finally { + r.cleanup(); + } + }); + + test("tests or scripts alone yield no release", () => { + const r = repo(); + r.tag("v0.6.0"); + try { + r.commit("fix: coverage", { "tests/unit/x.test.ts": "test\n" }); + r.commit("ci: release helper", { "scripts/analyze-release-scope.ts": "export {}\n" }); + expect(analyzeReleaseScope(r.root)).toEqual({ level: null }); + } finally { + r.cleanup(); + } + }); + + test("plugin source fix yields patch", () => { + const r = repo(); + r.tag("v0.6.0"); + try { + r.commit("fix: table header", { "src/catalog-release-notes.ts": "export {}\n" }); + expect(analyzeReleaseScope(r.root)).toEqual({ level: "patch" }); + } finally { + r.cleanup(); + } + }); + + test("catalog file updates still release", () => { + const r = repo(); + r.tag("v0.6.0"); + try { + r.commit("fix(catalog): sync command-code@1.39.0", { "models.json": "[]\n" }); + expect(analyzeReleaseScope(r.root)).toEqual({ level: "patch" }); + } finally { + r.cleanup(); + } + }); + + test("feat beats fix", () => { + const r = repo(); + r.tag("v0.6.0"); + try { + r.commit("fix: bug", { "plugin.ts": "export {}\n" }); + r.commit("feat: variants", { "index.ts": "export {}\n" }); + expect(analyzeReleaseScope(r.root).level).toBe("minor"); + } finally { + r.cleanup(); + } + }); +}); diff --git a/tests/unit/release-workflow.test.ts b/tests/unit/release-workflow.test.ts index 7666dfc..5991812 100644 --- a/tests/unit/release-workflow.test.ts +++ b/tests/unit/release-workflow.test.ts @@ -12,6 +12,7 @@ describe("CHANGELOG.md", () => { test("records shipped versions under Keep a Changelog headings", () => { const log = read("CHANGELOG.md"); expect(log).toContain("## [Unreleased]"); + expect(log).toContain("## [0.6.1]"); expect(log).toContain("## [0.6.0]"); expect(log).toContain("## [0.5.1]"); expect(log).toContain("## [0.5.0]"); @@ -38,6 +39,7 @@ describe("package.json publish identity", () => { homepage?: string; files?: string[]; scripts?: Record; + devDependencies?: Record; }>("package.json"); expect(pkg.name).toBe("@brainervirus/opencode-commandcode"); expect(pkg.publishConfig?.access).toBe("public"); @@ -50,6 +52,8 @@ describe("package.json publish identity", () => { expect(pkg.scripts?.format).toContain("oxfmt"); expect(pkg.scripts?.["format:check"]).toContain("oxfmt --check"); expect(pkg.scripts?.check).toContain("format:check"); + expect(pkg.devDependencies?.["@semantic-release/exec"]).toBeDefined(); + expect(pkg.devDependencies?.["@semantic-release/commit-analyzer"]).toBeUndefined(); }); }); @@ -148,13 +152,16 @@ describe("catalog-sync.yml", () => { describe("release.config.cjs", () => { test("publishes the root package then GitHub Release", () => { const cfg = read("release.config.cjs"); - expect(cfg).toContain("@semantic-release/commit-analyzer"); + expect(cfg).toContain("@semantic-release/exec"); + expect(cfg).toContain("analyzeCommitsCmd"); + expect(cfg).toContain("scripts/analyze-release-scope.ts"); + expect(cfg).not.toContain("@semantic-release/commit-analyzer"); expect(cfg).toContain("./scripts/semantic-release-catalog-notes.cjs"); expect(cfg).toContain("./scripts/semantic-release-changelog.cjs"); expect(cfg).toContain("@semantic-release/npm"); expect(cfg).toContain("@semantic-release/github"); - expect(cfg.indexOf("semantic-release-catalog-notes.cjs")).toBeLessThan( - cfg.indexOf("@semantic-release/release-notes-generator"), + expect(cfg.indexOf("@semantic-release/exec")).toBeLessThan( + cfg.indexOf("semantic-release-catalog-notes.cjs"), ); expect(cfg.indexOf("semantic-release-changelog.cjs")).toBeLessThan( cfg.indexOf("@semantic-release/npm"),