Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
take the release version from the tag
Publishing a GitHub release with the tag v<version> 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
  • Loading branch information
matmen and claude committed Oct 15, 2026
commit c8d01f7715000d09585a22c61f1fd2864a88902b
9 changes: 9 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/jsr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
46 changes: 41 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: release

# publishing a GitHub release with the tag v<version> 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<version> 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]
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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 }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 2 additions & 7 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 8 additions & 9 deletions scripts/release-check.js
Original file line number Diff line number Diff line change
@@ -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<version>) 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 <tag> [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);
Expand Down Expand Up @@ -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<version> 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`);
Expand Down Expand Up @@ -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 => {
Expand Down
34 changes: 34 additions & 0 deletions scripts/version.js
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading