Skip to content

Commit f13bc33

Browse files
committed
chore: automate version bumps and release 0.3.0
1 parent 184973c commit f13bc33

7 files changed

Lines changed: 191 additions & 3 deletions

File tree

‎.github/workflows/version-bump.yml‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: version-bump
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
paths-ignore:
7+
- "package.json"
8+
workflow_dispatch:
9+
inputs:
10+
release:
11+
description: SemVer component to increment
12+
required: true
13+
default: patch
14+
type: choice
15+
options:
16+
- patch
17+
- minor
18+
- major
19+
20+
permissions:
21+
contents: write
22+
23+
concurrency:
24+
group: version-bump-main
25+
cancel-in-progress: false
26+
27+
jobs:
28+
bump:
29+
if: github.actor != 'github-actions[bot]'
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v6
33+
with:
34+
ref: main
35+
fetch-depth: 0
36+
37+
- name: Decide whether this push already changed the version
38+
id: decide
39+
env:
40+
EVENT_NAME: ${{ github.event_name }}
41+
BEFORE_SHA: ${{ github.event.before }}
42+
AFTER_SHA: ${{ github.sha }}
43+
run: |
44+
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
45+
echo "bump=true" >> "$GITHUB_OUTPUT"
46+
exit 0
47+
fi
48+
before_version=$(git show "$BEFORE_SHA:package.json" 2>/dev/null | node -e "let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).version))")
49+
after_version=$(git show "$AFTER_SHA:package.json" 2>/dev/null | node -e "let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).version))")
50+
if [ "$before_version" != "$after_version" ]; then
51+
echo "Version already changed: $before_version -> $after_version"
52+
echo "bump=false" >> "$GITHUB_OUTPUT"
53+
else
54+
echo "bump=true" >> "$GITHUB_OUTPUT"
55+
fi
56+
57+
- name: Bump version
58+
if: steps.decide.outputs.bump == 'true'
59+
env:
60+
RELEASE: ${{ inputs.release || 'patch' }}
61+
run: |
62+
npm run check
63+
node scripts/bump-version.mjs "$RELEASE"
64+
65+
- name: Commit and push
66+
if: steps.decide.outputs.bump == 'true'
67+
run: |
68+
version=$(node -p "require('./package.json').version")
69+
git config user.name "github-actions[bot]"
70+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
71+
git add package.json
72+
git commit -m "chore: bump version to $version"
73+
git pull --rebase origin main
74+
git push origin main

‎AGENTS.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ docs/upgrading.md upgrade + single-repo-era migration guide
196196
Shipped: Global Hub architecture (registry, superseding `start`, systemd
197197
persistence, self-`update`), rich issue creation + epics, inline issue edits,
198198
tmux session introspection, the prompt scheduler, and the Preact/htm
199-
frontend rewrite (5-preset theming, hash routing). Verified against real
199+
frontend rewrite (6-preset theming, hash routing). Verified against real
200200
beads-backed repos via `npm run smoke`.
201201

202202
## Natural next steps (not built)
@@ -219,6 +219,11 @@ beads-backed repos via `npm run smoke`.
219219
architecture — see `docs/upgrading.md` for the migration notes.
220220
- For release or packaging work, run `npm run check`, `npm run smoke`, and
221221
`npm_config_cache=/tmp/bd-console-npm-cache npm pack --dry-run`.
222+
- `package.json` is the canonical installed version. Ordinary pushes to `main`
223+
receive an automatic patch bump from `.github/workflows/version-bump.yml`.
224+
For an intentional release boundary, run `npm run version:bump -- patch`
225+
(or `minor`/`major`) in the same commit; the workflow detects and respects
226+
that manual bump instead of adding another one.
222227
- `node scripts/smoke.mjs browser` is an **opt-in** domain, excluded from
223228
`npm run smoke` because it boots a real headless Chrome (~19s). It drives
224229
Chrome over the DevTools Protocol with Node's built-in `WebSocket` — never

‎CLAUDE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ npm run smoke -- usage # or: node scripts/smoke.mjs usage
7171
node scripts/smoke.mjs --list # the domain names
7272
```
7373

74+
`package.json` is the canonical installed version. Ordinary pushes to `main`
75+
receive an automatic patch bump from `.github/workflows/version-bump.yml`.
76+
For an intentional release boundary, run `npm run version:bump -- patch` (or
77+
`minor`/`major`) in the same commit; the workflow detects and respects that
78+
manual bump instead of adding another one.
79+
7480
Add new coverage to the domain module it belongs to, not to a new tail on one
7581
file. Shared fixtures, the scratch-port server, and the `BD_CONSOLE_*_DIR`
7682
isolation live in `scripts/smoke/harness.mjs` — use `ctx`, never the real

‎README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,26 @@ through the same superseding `start` path, so the new code goes live
174174
immediately. `--dry-run` prints the detected flavor and exact command(s)
175175
without running them.
176176

177+
### Versioning
178+
179+
The version in `package.json` is the installed bd-console version. A GitHub
180+
Actions workflow automatically increments its patch component after each push
181+
to `main`. If the push already changes the version, the workflow respects that
182+
deliberate major/minor/patch choice instead of incrementing it again.
183+
184+
For an intentional release bump, maintainers can run one of:
185+
186+
```bash
187+
npm run version:bump -- patch
188+
npm run version:bump -- minor
189+
npm run version:bump -- major
190+
```
191+
192+
The command only updates `package.json`; it does not implicitly commit, tag,
193+
publish, or push. The workflow can also be dispatched manually with the same
194+
three choices. This keeps GitHub-installed copies distinguishable even though
195+
bd-console is not yet published to the npm registry.
196+
177197
### Persistence (systemd)
178198

179199
On Linux, when `systemctl --user` is usable, `start` installs/refreshes a

‎package.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bd-console",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Standalone, project-agnostic viewer + light editor for beads issues and a project's docs.",
55
"type": "module",
66
"bin": {
@@ -10,8 +10,9 @@
1010
"scripts": {
1111
"start": "node serve.mjs",
1212
"init": "node scripts/init.mjs",
13-
"check": "node --check serve.mjs && node --check public/app.js && node --check scripts/init.mjs && node --check scripts/smoke.mjs && node --check scripts/serve-static.mjs && node --check lib/paths.mjs && node --check lib/registry.mjs && node --check lib/config.mjs && node --check lib/bd.mjs && node --check lib/docs.mjs && node --check lib/routes.mjs && node --check lib/daemon.mjs && node --check lib/systemd.mjs && node --check lib/update.mjs && node --check lib/settings.mjs && node --check lib/tmux.mjs && node --check lib/health.mjs && node --check lib/termix.mjs && node --check lib/schedule.mjs && node --check lib/events.mjs && node --check lib/git.mjs && node --check lib/usage.mjs && node --check lib/usage/harness.mjs && node --check lib/usage/read.mjs && node --check lib/usage/claude.mjs && node --check lib/usage/codex.mjs && node --check lib/usage/kimi.mjs && node --check lib/usage/gemini.mjs && node --check lib/usage-history.mjs && node --check lib/bdversion.mjs && node --check lib/cliversions.mjs && for f in public/ui/*.js public/ui/components/*.js public/ui/components/hub/*.js public/ui/console2/*.js scripts/smoke/*.mjs; do node --check \"$f\" || exit 1; done && node scripts/check-imports.mjs",
13+
"check": "node --check serve.mjs && node --check public/app.js && node --check scripts/init.mjs && node --check scripts/bump-version.mjs && node --check scripts/smoke.mjs && node --check scripts/serve-static.mjs && node --check lib/paths.mjs && node --check lib/registry.mjs && node --check lib/config.mjs && node --check lib/bd.mjs && node --check lib/docs.mjs && node --check lib/routes.mjs && node --check lib/daemon.mjs && node --check lib/systemd.mjs && node --check lib/update.mjs && node --check lib/settings.mjs && node --check lib/tmux.mjs && node --check lib/health.mjs && node --check lib/termix.mjs && node --check lib/schedule.mjs && node --check lib/events.mjs && node --check lib/git.mjs && node --check lib/usage.mjs && node --check lib/usage/harness.mjs && node --check lib/usage/read.mjs && node --check lib/usage/claude.mjs && node --check lib/usage/codex.mjs && node --check lib/usage/kimi.mjs && node --check lib/usage/gemini.mjs && node --check lib/usage-history.mjs && node --check lib/bdversion.mjs && node --check lib/cliversions.mjs && for f in public/ui/*.js public/ui/components/*.js public/ui/components/hub/*.js public/ui/console2/*.js scripts/smoke/*.mjs; do node --check \"$f\" || exit 1; done && node scripts/check-imports.mjs",
1414
"smoke": "node scripts/smoke.mjs",
15+
"version:bump": "node scripts/bump-version.mjs",
1516
"docs:serve": "node scripts/serve-static.mjs site 4179"
1617
},
1718
"engines": {
@@ -24,6 +25,7 @@
2425
"serve.mjs",
2526
"lib",
2627
"scripts/init.mjs",
28+
"scripts/bump-version.mjs",
2729
"public",
2830
"bd-console.json.example",
2931
"README.md"

‎scripts/bump-version.mjs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env node
2+
// Increment bd-console's stable SemVer without npm's implicit git tag/commit
3+
// behavior. This is used by the version-bump workflow and is also available to
4+
// maintainers as `npm run version:bump -- major|minor|patch`.
5+
6+
import { readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
7+
import { dirname, resolve } from 'node:path';
8+
import { fileURLToPath, pathToFileURL } from 'node:url';
9+
10+
const STABLE_SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
11+
const RELEASES = new Set(['major', 'minor', 'patch']);
12+
13+
export function nextVersion(version, release = 'patch') {
14+
const match = String(version || '').match(STABLE_SEMVER_RE);
15+
if (!match) throw new Error(`expected a stable SemVer version, got ${JSON.stringify(version)}`);
16+
if (!RELEASES.has(release)) throw new Error(`release must be major, minor, or patch; got ${JSON.stringify(release)}`);
17+
18+
let major = Number(match[1]);
19+
let minor = Number(match[2]);
20+
let patch = Number(match[3]);
21+
if (release === 'major') { major += 1; minor = 0; patch = 0; }
22+
if (release === 'minor') { minor += 1; patch = 0; }
23+
if (release === 'patch') patch += 1;
24+
return `${major}.${minor}.${patch}`;
25+
}
26+
27+
export function bumpPackageText(text, release = 'patch') {
28+
let pkg;
29+
try { pkg = JSON.parse(text); } catch (err) { throw new Error(`package.json is invalid JSON: ${err.message}`); }
30+
const current = pkg?.version;
31+
const next = nextVersion(current, release);
32+
const escaped = current.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33+
const versionField = new RegExp(`("version"\\s*:\\s*)"${escaped}"`);
34+
if (!versionField.test(text)) throw new Error('could not locate package.json version field');
35+
return { current, next, text: text.replace(versionField, `$1"${next}"`) };
36+
}
37+
38+
export function bumpPackageFile(packagePath, release = 'patch') {
39+
const source = readFileSync(packagePath, 'utf8');
40+
const result = bumpPackageText(source, release);
41+
const tempPath = `${packagePath}.${process.pid}.tmp`;
42+
try {
43+
writeFileSync(tempPath, result.text, { mode: statSync(packagePath).mode & 0o777 });
44+
renameSync(tempPath, packagePath);
45+
} finally {
46+
try { unlinkSync(tempPath); } catch { /* rename succeeded or no temp was written */ }
47+
}
48+
return result;
49+
}
50+
51+
function main() {
52+
const release = process.argv[2] || 'patch';
53+
const packagePath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
54+
const result = bumpPackageFile(packagePath, release);
55+
console.log(`${result.current} -> ${result.next}`);
56+
}
57+
58+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
59+
try { main(); } catch (err) {
60+
console.error(`bd-console version bump failed: ${err.message}`);
61+
process.exitCode = 1;
62+
}
63+
}

‎scripts/smoke/versions.mjs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,28 @@ import { spawn } from 'node:child_process';
1111
import { createServer } from 'node:http';
1212
import { parseBdVersionStdout, compareVersions, isBehind } from '../../lib/bdversion.mjs';
1313
import { parseCliVersionStdout } from '../../lib/cliversions.mjs';
14+
import { bumpPackageText, nextVersion } from '../bump-version.mjs';
1415

1516
export async function runVersions(ctx) {
1617
const { assert, getPort, waitFor, tempRoot, serverEntry } = ctx;
1718

19+
// --- bd-console package version automation ------------------------------
20+
assert(nextVersion('0.2.0', 'patch') === '0.2.1', 'patch bump should increment only patch');
21+
assert(nextVersion('0.2.9', 'minor') === '0.3.0', 'minor bump should increment minor and reset patch');
22+
assert(nextVersion('0.9.9', 'major') === '1.0.0', 'major bump should increment major and reset minor/patch');
23+
assert(bumpPackageText('{\n "name": "x",\n "version": "1.2.3"\n}\n', 'patch').text
24+
=== '{\n "name": "x",\n "version": "1.2.4"\n}\n',
25+
'package bump should preserve surrounding formatting');
26+
for (const bad of ['1.2', 'v1.2.3', '1.2.3-beta']) {
27+
let rejected = false;
28+
try { nextVersion(bad, 'patch'); } catch { rejected = true; }
29+
assert(rejected, `unstable or malformed package version must be rejected: ${bad}`);
30+
}
31+
let badReleaseRejected = false;
32+
try { nextVersion('1.2.3', 'banana'); } catch { badReleaseRejected = true; }
33+
assert(badReleaseRejected, 'unknown release component must be rejected');
34+
console.log('smoke ok (bd-console version bump: major/minor/patch + formatting + invalid input)');
35+
1836
// --- bd (beads CLI) version check (lib/bdversion.mjs + GET /api/bd-version)
1937
// Pure-function assertions first (no server, no network), then the live
2038
// route against the real installed `bd`, with the GitHub lookup redirected

0 commit comments

Comments
 (0)