mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 15:05:25 +00:00
added release scripts, gh pages builds on release branch
This commit is contained in:
1 parent
3ac80cfe99
commit
bec3f4416b
6 files changed
+247
-2
No files matched your search
@@ -5,7 +5,8 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
# master runs CI; release is what gets deployed to GitHub Pages.
|
||||
branches: [ master, release ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
# Testers can build an installable VSIX from any branch on demand.
|
||||
@@ -63,8 +64,9 @@ jobs:
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
# Publish only from the periodic release branch, never from master/HEAD.
|
||||
- name: Deploy
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
|
||||
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
|
||||
with:
|
||||
branch: gh-pages
|
||||
|
||||
@@ -85,6 +85,8 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
- Build is now only `esbuild` through `scripts/build.mjs`; code split into a headless `src/common/projectcore.ts` / `toolselect.ts`.
|
||||
- `.xa` assembler and the Dialog interactive-fiction compiler added.
|
||||
- New `buildpresets` script that builds and runs every preset.
|
||||
- `npm run release -- patch` bumps the app version, moves the changelog's unreleased notes under the new version, commits, and tags `v<version>`; add `--push` to push the tag.
|
||||
- The web site is deployed to GitHub Pages from the `release` branch instead of `master`, so a release can be staged and published separately from ongoing work.
|
||||
|
||||
## 3.12.1 (2025-11-06)
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"buildpresets": "NODE_PATH=$(pwd) node gen/tools/buildpresets.js",
|
||||
"toolversions": "node gen/tools/toolversions.js",
|
||||
"notices": "node scripts/url-licenses.mjs --notices --write",
|
||||
"release": "node scripts/release.mjs",
|
||||
"doctools": "node gen/tools/doctools.js",
|
||||
"mkdoc": "typedoc --out web/jsdoc src/common/"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Pure helpers for the release script. Kept separate from `release.mjs` so
|
||||
* they can be unit tested without touching git or the filesystem.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compute the next version. `kind` is `patch`, `minor`, `major`, or an
|
||||
* explicit `x.y.z`. An explicit version may equal the current one (useful for
|
||||
* the first release, where `package.json` already carries the version).
|
||||
*
|
||||
* @param {string} current
|
||||
* @param {string} kind
|
||||
* @returns {string}
|
||||
*/
|
||||
export function bumpVersion(current, kind) {
|
||||
if (/^\d+\.\d+\.\d+$/.test(kind)) return kind;
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(current);
|
||||
if (!match) {
|
||||
throw new Error(`current version "${current}" is not x.y.z`);
|
||||
}
|
||||
let [major, minor, patch] = match.slice(1).map(Number);
|
||||
switch (kind) {
|
||||
case 'major':
|
||||
major += 1;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
break;
|
||||
case 'minor':
|
||||
minor += 1;
|
||||
patch = 0;
|
||||
break;
|
||||
case 'patch':
|
||||
patch += 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown bump "${kind}" (use patch, minor, major, or x.y.z)`);
|
||||
}
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the entries currently under `## [Unreleased]` under a new
|
||||
* `## [version] - date` heading, leaving an empty Unreleased section on top.
|
||||
*
|
||||
* Older entries in this repo use `## <version> (unreleased)` instead. When no
|
||||
* standard Unreleased heading is found, the first such heading is renamed and
|
||||
* an empty Unreleased section is inserted above it.
|
||||
*
|
||||
* @param {string} text changelog contents
|
||||
* @param {string} version
|
||||
* @param {string} date `YYYY-MM-DD`
|
||||
* @returns {string}
|
||||
*/
|
||||
export function releaseChangelog(text, version, date) {
|
||||
const heading = '## [Unreleased]';
|
||||
const released = `## [${version}] - ${date}`;
|
||||
const index = text.indexOf(heading);
|
||||
if (index !== -1) {
|
||||
// Insert after the Unreleased heading and the blank line that follows it, so
|
||||
// existing content ends up beneath the new version heading.
|
||||
let insertAt = text.indexOf('\n', index) + 1;
|
||||
if (text[insertAt] === '\n') insertAt += 1;
|
||||
return `${text.slice(0, insertAt)}${released}\n\n${text.slice(insertAt)}`;
|
||||
}
|
||||
const unreleased = /^## [^\n]*\(unreleased\)[^\n]*$/im.exec(text);
|
||||
if (unreleased) {
|
||||
const start = unreleased.index;
|
||||
const lineEnd = text.indexOf('\n', start);
|
||||
const end = lineEnd === -1 ? text.length : lineEnd + 1;
|
||||
return `${text.slice(0, start)}${heading}\n\n${released}\n${text.slice(end)}`;
|
||||
}
|
||||
return `${heading}\n\n${released}\n\n${text}`;
|
||||
}
|
||||
|
||||
/** Local calendar date as `YYYY-MM-DD`. */
|
||||
export function today(date = new Date()) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Release helper: bump the app version, move the changelog's unreleased
|
||||
* entries under the new version, commit, and tag.
|
||||
*
|
||||
* npm run release -- patch
|
||||
* npm run release -- minor --push
|
||||
* npm run release -- 4.1.0 --push
|
||||
*
|
||||
* Runs the type check and test suite first (skip with --no-tests) and refuses
|
||||
* to run on a dirty (tracked) tree. `--dry-run` prints the plan and stops
|
||||
* before anything is written.
|
||||
*
|
||||
* The tag is what CI and the distro build see. The web site is deployed from
|
||||
* the `release` branch, so to publish:
|
||||
*
|
||||
* git push origin release
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { bumpVersion, releaseChangelog, today } from './release-lib.mjs';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const push = args.includes('--push');
|
||||
const skipTests = args.includes('--no-tests');
|
||||
const kind = args.find((arg) => !arg.startsWith('--'));
|
||||
|
||||
function fail(message) {
|
||||
console.error(`release: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!kind) {
|
||||
fail('usage: npm run release -- <patch|minor|major|x.y.z> [--push] [--dry-run] [--no-tests]');
|
||||
}
|
||||
|
||||
const run = (cmd, cmdArgs) => execFileSync(cmd, cmdArgs, { cwd: root, stdio: 'inherit' });
|
||||
const capture = (cmd, cmdArgs) =>
|
||||
execFileSync(cmd, cmdArgs, { cwd: root, encoding: 'utf8' }).trim();
|
||||
|
||||
// Ignore untracked scratch files (there are usually a few in this repo), but
|
||||
// refuse to commit a release on top of uncommitted tracked changes. Only the
|
||||
// three release files are staged below, so untracked files are never added.
|
||||
if (capture('git', ['status', '--porcelain', '--untracked-files=no'])) {
|
||||
fail('working tree has uncommitted tracked changes; commit or stash first');
|
||||
}
|
||||
if (capture('git', ['rev-parse', '--abbrev-ref', 'HEAD']) === 'HEAD') {
|
||||
fail('detached HEAD; check out a branch first');
|
||||
}
|
||||
|
||||
const packagePath = join(root, 'package.json');
|
||||
const packageLockPath = join(root, 'package-lock.json');
|
||||
const changelogPath = join(root, 'CHANGELOG.md');
|
||||
const changelog = readFileSync(changelogPath, 'utf8');
|
||||
const current = JSON.parse(readFileSync(packagePath, 'utf8')).version;
|
||||
const next = bumpVersion(current, kind);
|
||||
const tag = `v${next}`;
|
||||
const updatedChangelog = releaseChangelog(changelog, next, today());
|
||||
|
||||
if (capture('git', ['tag', '--list', tag])) {
|
||||
fail(`tag ${tag} already exists`);
|
||||
}
|
||||
|
||||
if (next === current) {
|
||||
console.log(`release: releasing current version ${next} (no bump)`);
|
||||
} else {
|
||||
console.log(`release: ${current} -> ${next}`);
|
||||
}
|
||||
console.log(`release: changelog ${updatedChangelog === changelog ? 'unchanged' : 'updated'}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`release: dry run, would commit and tag ${tag}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!skipTests) {
|
||||
try {
|
||||
run('npm', ['run', 'tslint']);
|
||||
run('npm', ['test']);
|
||||
} catch {
|
||||
fail('typecheck or tests failed; nothing was bumped');
|
||||
}
|
||||
}
|
||||
|
||||
// Bump package.json and package-lock.json. --no-git-tag-version keeps npm from
|
||||
// committing/tagging, so this script controls the commit contents and message.
|
||||
run('npm', ['version', next, '--no-git-tag-version', '--allow-same-version']);
|
||||
writeFileSync(changelogPath, updatedChangelog);
|
||||
run('git', ['add', packagePath, packageLockPath, changelogPath]);
|
||||
run('git', ['commit', '-m', tag]);
|
||||
run('git', ['tag', '-a', tag, '-m', tag]);
|
||||
|
||||
if (push) {
|
||||
run('git', ['push', '--follow-tags']);
|
||||
console.log(`release: pushed ${tag}`);
|
||||
} else {
|
||||
console.log(`release: done. next:\n git push --follow-tags\n git push origin release # when the release branch is ready to deploy`);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
// Plain .mjs helpers so the release script and these tests share one copy.
|
||||
var release;
|
||||
|
||||
describe('Release helpers', function () {
|
||||
before(async function () {
|
||||
release = await import('../../scripts/release-lib.mjs');
|
||||
});
|
||||
|
||||
it('should bump patch, minor, and major versions', function () {
|
||||
assert.strictEqual(release.bumpVersion('1.2.3', 'patch'), '1.2.4');
|
||||
assert.strictEqual(release.bumpVersion('1.2.3', 'minor'), '1.3.0');
|
||||
assert.strictEqual(release.bumpVersion('1.2.3', 'major'), '2.0.0');
|
||||
});
|
||||
|
||||
it('should accept an explicit version, including the current one', function () {
|
||||
assert.strictEqual(release.bumpVersion('1.2.3', '0.2.0'), '0.2.0');
|
||||
assert.strictEqual(release.bumpVersion('1.2.3', '1.2.3'), '1.2.3');
|
||||
});
|
||||
|
||||
it('should reject unknown bumps and malformed versions', function () {
|
||||
assert.throws(() => release.bumpVersion('1.2.3', 'huge'), /unknown bump/);
|
||||
assert.throws(() => release.bumpVersion('1.2', 'patch'), /not x\.y\.z/);
|
||||
});
|
||||
|
||||
it('should move Unreleased entries under the new version', function () {
|
||||
var changelog = '## [Unreleased]\n\n### Added\n- thing\n';
|
||||
assert.strictEqual(
|
||||
release.releaseChangelog(changelog, '1.0.0', '2026-01-02'),
|
||||
'## [Unreleased]\n\n## [1.0.0] - 2026-01-02\n\n### Added\n- thing\n');
|
||||
});
|
||||
|
||||
it('should keep already-released sections intact', function () {
|
||||
var changelog = '## [Unreleased]\n\n### Added\n- thing\n\n## [0.1.0] - 2025-01-01\n\n- old\n';
|
||||
assert.strictEqual(
|
||||
release.releaseChangelog(changelog, '1.0.0', '2026-01-02'),
|
||||
'## [Unreleased]\n\n## [1.0.0] - 2026-01-02\n\n### Added\n- thing\n\n## [0.1.0] - 2025-01-01\n\n- old\n');
|
||||
});
|
||||
|
||||
it('should handle the older "version (unreleased)" heading', function () {
|
||||
var changelog = '# Changelog\n\n## 4.0.0 (unreleased)\n\n### Added\n- thing\n';
|
||||
assert.strictEqual(
|
||||
release.releaseChangelog(changelog, '4.0.1', '2026-01-02'),
|
||||
'# Changelog\n\n## [Unreleased]\n\n## [4.0.1] - 2026-01-02\n\n### Added\n- thing\n');
|
||||
});
|
||||
|
||||
it('should add an Unreleased section when the changelog has none', function () {
|
||||
assert.ok(
|
||||
release.releaseChangelog('# Changelog\n', '1.0.0', '2026-01-02')
|
||||
.includes('## [Unreleased]\n\n## [1.0.0] - 2026-01-02\n'));
|
||||
});
|
||||
|
||||
it('should format a local date as YYYY-MM-DD', function () {
|
||||
assert.strictEqual(release.today(new Date(2026, 0, 2)), '2026-01-02');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user