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';