From c8d01f7715000d09585a22c61f1fd2864a88902b Mon Sep 17 00:00:00 2001 From: Mathis Mensing Date: Thu, 15 Oct 2026 08:41:43 +0000 Subject: [PATCH] take the release version from the tag Publishing a GitHub release with the tag v is enough to release: the release check takes the version from the tag (instead of requiring package.json to match it), npm, JSR and the docs are built with it, and for regular releases a final job commits it to the default branch on top of whatever was merged since, so package.json there shows the latest release. Prereleases and fixes for older majors leave the branch alone. scripts/version.js sets the version and keeps its copies (jsr.json, src/version.ts) in sync; the build and `npm version` use it too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J --- .github/workflows/docs.yml | 9 +++++++ .github/workflows/jsr.yml | 13 ++++++++++ .github/workflows/release.yml | 46 +++++++++++++++++++++++++++++++---- package.json | 2 +- scripts/build.js | 9 ++----- scripts/release-check.js | 17 ++++++------- scripts/version.js | 34 ++++++++++++++++++++++++++ src/version.ts | 2 +- 8 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 scripts/version.js diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 20a9307e..e8875fea 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,11 @@ name: docs # npm, so the site documents what is on npm; can also be run by hand from the Actions tab on: workflow_call: + inputs: + version: + description: version to document (release.yml passes the released one; empty keeps package.json's) + type: string + default: '' workflow_dispatch: permissions: @@ -25,6 +30,10 @@ jobs: node-version: 24.x - run: npm install + - if: inputs.version != '' + run: node scripts/version.js "$VERSION" + env: + VERSION: ${{ inputs.version }} - run: npm run docs - uses: actions/configure-pages@v6 diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml index f17dc764..a47bdb5a 100644 --- a/.github/workflows/jsr.yml +++ b/.github/workflows/jsr.yml @@ -4,6 +4,11 @@ name: jsr # linked to this repository on jsr.io). release.yml runs it after npm; can also be run by hand from the Actions tab on: workflow_call: + inputs: + version: + description: version to publish (release.yml passes the released one; empty keeps jsr.json's) + type: string + default: '' workflow_dispatch: permissions: @@ -18,6 +23,14 @@ jobs: url: https://jsr.io/@matmen/imagescript/versions steps: - uses: actions/checkout@v7 + - if: inputs.version != '' + uses: actions/setup-node@v7 + with: + node-version: 24.x + - if: inputs.version != '' + run: node scripts/version.js "$VERSION" + env: + VERSION: ${{ inputs.version }} - uses: denoland/setup-deno@v2 with: deno-version: v2.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b85aa631..fc3c289b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ name: release -# publishing a GitHub release with the tag v publishes that version to npm and JSR: -# the release is checked (tag, semver, changelog, jsr.json, not yet on npm/JSR), tested on every supported platform, -# published to npm with provenance through trusted publishing and to JSR through GitHub OIDC (no tokens), -# then the docs are deployed +# publishing a GitHub release with the tag v publishes that version to npm and JSR. The tag is the version: +# the release is checked (semver, changelog, not yet on npm/JSR), tested on every supported platform, published with +# the tag's version to npm (with provenance, through trusted publishing) and JSR (through GitHub OIDC, no tokens), +# then the docs are deployed and, for regular releases, the version is committed to the default branch on: release: types: [published] @@ -19,6 +19,7 @@ jobs: check: runs-on: ubuntu-latest outputs: + version: ${{ steps.check.outputs.version }} dist-tag: ${{ steps.check.outputs.dist-tag }} steps: - uses: actions/checkout@v7 @@ -53,17 +54,22 @@ jobs: registry-url: https://registry.npmjs.org - run: npm install + - run: node scripts/version.js "$VERSION" + env: + VERSION: ${{ needs.check.outputs.version }} # prepack builds dist; trusted publishing needs npm 11.5.1 or newer, which node 24 ships - run: npm publish --provenance --access public --tag "$DIST_TAG" env: DIST_TAG: ${{ needs.check.outputs.dist-tag }} jsr: - needs: publish + needs: [check, publish] permissions: contents: read id-token: write uses: ./.github/workflows/jsr.yml + with: + version: ${{ needs.check.outputs.version }} docs: needs: [check, publish] @@ -73,3 +79,33 @@ jobs: pages: write id-token: write uses: ./.github/workflows/docs.yml + with: + version: ${{ needs.check.outputs.version }} + + # regular releases commit their version to the default branch (on top of what was merged since the tag), so + # package.json there shows the latest release; prereleases and fixes for older majors leave it alone + version: + needs: [check, publish, jsr] + if: needs.check.outputs.dist-tag == 'latest' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + - uses: actions/setup-node@v7 + with: + node-version: 24.x + - run: node scripts/version.js "$VERSION" + env: + VERSION: ${{ needs.check.outputs.version }} + # pushes with GITHUB_TOKEN don't trigger other workflows + - run: | + if git diff --quiet; then echo "the default branch already has version $VERSION"; exit 0; fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -am "release $VERSION" + git push + env: + VERSION: ${{ needs.check.outputs.version }} diff --git a/package.json b/package.json index 1286685c..b721d6b9 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "docs": "node scripts/docs.js", "test": "npm run build && node ./tests/run.js", "prepack": "npm run build", - "version": "node scripts/build.js && git add src/version.ts jsr.json" + "version": "node scripts/version.js && git add src/version.ts jsr.json" }, "repository": { "type": "git", diff --git a/scripts/build.js b/scripts/build.js index 6e0ea2e1..ee0b09e5 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -8,13 +8,8 @@ const root = path.join(__dirname, '..'); const tsc = require.resolve('typescript/bin/tsc'); const run = args => execFileSync(process.execPath, [tsc, ...args], { cwd: root, stdio: 'inherit' }); -// keep the version reported in PNG metadata in sync with package.json -const { version } = require('../package.json'); -fs.writeFileSync(path.join(root, 'src/version.ts'), `// kept in sync with package.json by scripts/build.js\nexport const VERSION = '${version}';\n`); -// and the one JSR publishes -const jsrPath = path.join(root, 'jsr.json'); -const jsr = JSON.parse(fs.readFileSync(jsrPath, 'utf8')); -if (jsr.version !== version) fs.writeFileSync(jsrPath, JSON.stringify({ ...jsr, version }, null, 2) + '\n'); +// keep the version in PNG metadata and the one JSR publishes in sync with package.json +require('./version.js').setVersion(); fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true }); // declarations keep the doc comments (editors show them), the JavaScript doesn't need them diff --git a/scripts/release-check.js b/scripts/release-check.js index 35416eb5..f2c4d4d5 100644 --- a/scripts/release-check.js +++ b/scripts/release-check.js @@ -1,14 +1,16 @@ -// checks a GitHub release before it is published to npm and JSR, run by .github/workflows/release.yml: -// the tag matches package.json and jsr.json, the version is valid semver, newer than what npm has, not on JSR yet and -// listed in the changelog, prereleases are marked as such. Prints the npm dist-tag to publish under (and writes it to GITHUB_OUTPUT) +// checks a GitHub release before it is published to npm and JSR, run by .github/workflows/release.yml. +// The release tag (v) is the version: it has to be valid semver, newer than what npm has, not on JSR yet and +// listed in the changelog, prereleases are marked as such. Prints the version and the npm dist-tag to publish under +// (and writes both to GITHUB_OUTPUT); the workflow then sets the version with scripts/version.js // usage: node scripts/release-check.js [prerelease flag of the GitHub release: true/false] const fs = require('fs'); const path = require('path'); const root = path.join(__dirname, '..'); -const { name, version } = require('../package.json'); +const { name } = require('../package.json'); const jsr = require('../jsr.json'); const [tag = '', prereleaseFlag = 'false'] = process.argv.slice(2); +const version = tag.replace(/^v/, ''); const errors = []; const fail = message => errors.push(message); @@ -43,16 +45,13 @@ function compare(a, b) { async function main() { const current = parse(version); - if (!current) fail(`package.json version ${version} is not valid semver`); - if (tag !== `v${version}`) fail(`release tag ${tag || '(none)'} doesn't match package.json, expected v${version}`); + if (!/^v/.test(tag) || !current) fail(`release tag ${tag || '(none)'} isn't v with a valid semver version (like v3.1.0)`); const prerelease = prereleaseFlag === 'true'; if (current && prerelease !== current.pre.length > 0) fail(current.pre.length ? `${version} is a prerelease version, mark the GitHub release as prerelease` : `the GitHub release is marked as prerelease, but ${version} has no prerelease suffix (like -beta.1)`); - if (jsr.version !== version) fail(`jsr.json has version ${jsr.version}, package.json ${version} (npm run build syncs them)`); - const changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); if (!changelog.split('\n').some(line => /^##\s/.test(line) && line.replace(/^##\s+v?/, '').trim().split(/\s/)[0] === version)) fail(`CHANGELOG.md has no "## ${version}" section`); @@ -98,7 +97,7 @@ async function main() { } console.log(`${name}@${version} can be published under the dist-tag "${distTag}"`); - if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `dist-tag=${distTag}\n`); + if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ndist-tag=${distTag}\n`); } if (require.main === module) main().catch(e => { diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 00000000..982e0a88 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,34 @@ +// sets the package version and keeps its copies in sync: package.json, jsr.json and src/version.ts (reported in +// PNG metadata). The release workflow sets the version from the release tag, the build syncs the copies. +// usage: node scripts/version.js [version] (without a version only the copies are synced to package.json) +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..'); +const readJSON = file => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8')); +const writeJSON = (file, data) => fs.writeFileSync(path.join(root, file), JSON.stringify(data, null, 2) + '\n'); + +function setVersion(version) { + const pkg = readJSON('package.json'); + if (version && pkg.version !== version) writeJSON('package.json', { ...pkg, version }); + version = version || pkg.version; + + const jsr = readJSON('jsr.json'); + if (jsr.version !== version) writeJSON('jsr.json', { ...jsr, version }); + + const file = path.join(root, 'src/version.ts'); + const source = `// kept in sync with package.json by scripts/version.js\nexport const VERSION = '${version}';\n`; + if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== source) fs.writeFileSync(file, source); + return version; +} + +if (require.main === module) { + const version = process.argv[2]; + if (version && !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.test(version)) { + console.error(`invalid version ${version}`); + process.exit(1); + } + console.log(setVersion(version)); +} + +module.exports = { setVersion }; diff --git a/src/version.ts b/src/version.ts index 37fc7b61..26b61f3b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -// kept in sync with package.json by scripts/build.js +// kept in sync with package.json by scripts/version.js export const VERSION = '3.0.0';