Skip to content

Commit 7c6323d

Browse files
Add version bump script for automated releases
Usage: node scripts/bump-version.mjs patch|minor|major [--dry-run] [--no-push] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dfb2c93 commit 7c6323d

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

scripts/bump-version.mjs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Bump version across all publishable packages, commit, tag, and push.
5+
*
6+
* Usage:
7+
* node scripts/bump-version.mjs patch # 0.1.10 → 0.1.11
8+
* node scripts/bump-version.mjs minor # 0.1.10 → 0.2.0
9+
* node scripts/bump-version.mjs major # 0.1.10 → 1.0.0
10+
* node scripts/bump-version.mjs 0.2.0 # explicit version
11+
*
12+
* What it does:
13+
* 1. Reads current version from packages/core/package.json
14+
* 2. Computes new version based on bump type
15+
* 3. Updates version in all publishable package.json files
16+
* 4. Commits: "Bump all packages to <version>"
17+
* 5. Tags: v<version>
18+
* 6. Pushes commit + tag to both remotes (origin + github)
19+
*
20+
* Flags:
21+
* --dry-run Show what would happen without making changes
22+
* --no-push Commit and tag locally but don't push
23+
*/
24+
25+
import { readFileSync, writeFileSync } from 'node:fs';
26+
import { execSync } from 'node:child_process';
27+
import { join, resolve } from 'node:path';
28+
import { fileURLToPath } from 'node:url';
29+
30+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
31+
const ROOT = resolve(__dirname, '..');
32+
33+
/** Packages that get version bumps (all publishable + private that share version). */
34+
const PACKAGES = [
35+
'packages/core',
36+
'packages/cli',
37+
'packages/mcp-server',
38+
'packages/api-server',
39+
];
40+
41+
function readPkgJson(dir) {
42+
const path = join(ROOT, dir, 'package.json');
43+
return { path, data: JSON.parse(readFileSync(path, 'utf-8')) };
44+
}
45+
46+
function writePkgJson(path, data) {
47+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
48+
}
49+
50+
function bumpVersion(current, type) {
51+
const [major, minor, patch] = current.split('.').map(Number);
52+
switch (type) {
53+
case 'patch': return `${major}.${minor}.${patch + 1}`;
54+
case 'minor': return `${major}.${minor + 1}.0`;
55+
case 'major': return `${major + 1}.0.0`;
56+
default: {
57+
// Treat as explicit version
58+
if (/^\d+\.\d+\.\d+/.test(type)) return type;
59+
console.error(`Unknown bump type: ${type}`);
60+
console.error('Usage: node scripts/bump-version.mjs patch|minor|major|<version>');
61+
process.exit(1);
62+
}
63+
}
64+
}
65+
66+
function run(cmd, opts = {}) {
67+
return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', stdio: 'pipe', ...opts }).trim();
68+
}
69+
70+
function hasRemote(name) {
71+
try {
72+
run(`git remote get-url ${name}`);
73+
return true;
74+
} catch {
75+
return false;
76+
}
77+
}
78+
79+
// --- Main ---
80+
81+
const args = process.argv.slice(2).filter((a) => !a.startsWith('--'));
82+
const flags = new Set(process.argv.slice(2).filter((a) => a.startsWith('--')));
83+
const dryRun = flags.has('--dry-run');
84+
const noPush = flags.has('--no-push');
85+
86+
if (args.length === 0) {
87+
console.error('Usage: node scripts/bump-version.mjs patch|minor|major|<version> [--dry-run] [--no-push]');
88+
process.exit(1);
89+
}
90+
91+
// 1. Read current version
92+
const { data: corePkg } = readPkgJson('packages/core');
93+
const currentVersion = corePkg.version;
94+
const newVersion = bumpVersion(currentVersion, args[0]);
95+
96+
console.log(`Version: ${currentVersion}${newVersion}`);
97+
98+
if (dryRun) {
99+
console.log('\n[dry-run] Would update:');
100+
for (const dir of PACKAGES) {
101+
console.log(` ${dir}/package.json`);
102+
}
103+
console.log(`\n[dry-run] Would commit: "Bump all packages to ${newVersion}"`);
104+
console.log(`[dry-run] Would tag: v${newVersion}`);
105+
console.log('[dry-run] Would push to: origin, github');
106+
process.exit(0);
107+
}
108+
109+
// 2. Check working tree is clean
110+
const status = run('git status --porcelain');
111+
if (status.length > 0) {
112+
console.error('\nWorking tree is not clean. Commit or stash changes first.');
113+
console.error(status);
114+
process.exit(1);
115+
}
116+
117+
// 3. Check tag doesn't already exist
118+
try {
119+
run(`git rev-parse v${newVersion}`);
120+
console.error(`\nTag v${newVersion} already exists. Choose a different version.`);
121+
process.exit(1);
122+
} catch {
123+
// Tag doesn't exist — good
124+
}
125+
126+
// 4. Update all package.json files
127+
const updatedFiles = [];
128+
for (const dir of PACKAGES) {
129+
const { path, data } = readPkgJson(dir);
130+
data.version = newVersion;
131+
writePkgJson(path, data);
132+
updatedFiles.push(`${dir}/package.json`);
133+
console.log(` Updated ${dir}/package.json`);
134+
}
135+
136+
// 5. Commit
137+
const fileArgs = updatedFiles.join(' ');
138+
run(`git add ${fileArgs}`);
139+
run(`git commit -m "Bump all packages to ${newVersion}"`);
140+
console.log(`\nCommitted: Bump all packages to ${newVersion}`);
141+
142+
// 6. Tag
143+
run(`git tag v${newVersion}`);
144+
console.log(`Tagged: v${newVersion}`);
145+
146+
// 7. Push
147+
if (noPush) {
148+
console.log('\n--no-push: skipping push. Run manually:');
149+
console.log(` git push origin main && git push origin v${newVersion}`);
150+
if (hasRemote('github')) {
151+
console.log(` git push github main && git push github v${newVersion}`);
152+
}
153+
} else {
154+
const remotes = ['origin'];
155+
if (hasRemote('github')) remotes.push('github');
156+
157+
for (const remote of remotes) {
158+
run(`git push ${remote} main`);
159+
run(`git push ${remote} v${newVersion}`);
160+
console.log(`Pushed to ${remote}`);
161+
}
162+
console.log(`\nDone! v${newVersion} released.`);
163+
console.log('GitHub Actions will now build, test, and publish to npm + ghcr.io.');
164+
}

0 commit comments

Comments
 (0)