Skip to content

Commit 3a8d41c

Browse files
committed
chore: add commit conventions to CLAUDE.md, release script and justfile task
1 parent fd18043 commit 3a8d41c

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- use turbo for builds
55
- keep timeouts under 1 minute and avoid running full test suites unless necessary
66
- use one-line Conventional Commit messages; never add any co-authors (including agents)
7+
- use one-line Conventional Commit messages; never add any co-authors (including agents)
78
- never mark work complete until typechecks pass and all tests pass in the current turn; if they fail, report the failing command and first concrete error
89
- always add or update tests that cover plausible exploit/abuse paths introduced by each feature or behavior change
910
- treat host memory buildup and CPU amplification as critical risks; avoid unbounded buffering/work (for example, default in-memory log buffering)

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ dev-website:
1010
build-website:
1111
pnpm --filter @secure-exec/website build
1212

13+
release *args:
14+
npx tsx scripts/release.ts {{args}}
15+

scripts/release.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env npx tsx
2+
3+
import { execSync } from "node:child_process";
4+
import { readFileSync, writeFileSync } from "node:fs";
5+
import { join } from "node:path";
6+
import { createInterface } from "node:readline";
7+
8+
const ROOT = join(import.meta.dirname, "..");
9+
10+
// ── Helpers ──
11+
12+
function run(cmd: string, opts?: { cwd?: string; stdio?: "pipe" | "inherit" }) {
13+
return execSync(cmd, {
14+
cwd: opts?.cwd ?? ROOT,
15+
stdio: opts?.stdio ?? "pipe",
16+
encoding: "utf-8",
17+
}).trim();
18+
}
19+
20+
function fatal(msg: string): never {
21+
console.error(`\x1b[31mError:\x1b[0m ${msg}`);
22+
process.exit(1);
23+
}
24+
25+
async function confirm(msg: string): Promise<boolean> {
26+
const rl = createInterface({ input: process.stdin, output: process.stdout });
27+
return new Promise((resolve) => {
28+
rl.question(`${msg} (y/N) `, (answer) => {
29+
rl.close();
30+
resolve(answer.trim().toLowerCase() === "y");
31+
});
32+
});
33+
}
34+
35+
function bumpVersion(current: string, type: "patch" | "minor" | "major"): string {
36+
const [major, minor, patch] = current.split(".").map(Number);
37+
switch (type) {
38+
case "patch": return `${major}.${minor}.${patch + 1}`;
39+
case "minor": return `${major}.${minor + 1}.0`;
40+
case "major": return `${major + 1}.0.0`;
41+
}
42+
}
43+
44+
// ── Parse args ──
45+
46+
function parseArgs(): { version: string; tag: "latest" | "rc" } {
47+
const args = process.argv.slice(2);
48+
49+
// RC: exact version required
50+
if (args.includes("--rc")) {
51+
const idx = args.indexOf("--rc");
52+
const ver = args[idx + 1];
53+
if (!ver || ver.startsWith("--")) {
54+
fatal("--rc requires an exact version (e.g. --rc 0.2.0-rc.1)");
55+
}
56+
if (!ver.includes("-")) {
57+
fatal(`RC version should contain a prerelease identifier (got "${ver}")`);
58+
}
59+
return { version: ver, tag: "rc" };
60+
}
61+
62+
// Semver bump
63+
const rootPkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
64+
const current = rootPkg.version;
65+
66+
for (const type of ["patch", "minor", "major"] as const) {
67+
if (args.includes(`--${type}`)) {
68+
return { version: bumpVersion(current, type), tag: "latest" };
69+
}
70+
}
71+
72+
fatal("Usage: release --patch | --minor | --major | --rc <version>");
73+
}
74+
75+
// ── Find publishable packages ──
76+
77+
function getPublishablePackages(): string[] {
78+
const output = run("pnpm -r ls --json --depth -1");
79+
const packages = JSON.parse(output) as Array<{ name: string; path: string; private?: boolean }>;
80+
return packages
81+
.filter((p) => !p.private && p.path !== ROOT)
82+
.map((p) => p.path);
83+
}
84+
85+
// ── Update version in a package.json ──
86+
87+
function setVersion(pkgPath: string, version: string) {
88+
const file = join(pkgPath, "package.json");
89+
const content = readFileSync(file, "utf-8");
90+
const pkg = JSON.parse(content);
91+
pkg.version = version;
92+
// Preserve original formatting (indent detection)
93+
const indent = content.match(/^(\s+)"/m)?.[1] ?? " ";
94+
writeFileSync(file, JSON.stringify(pkg, null, indent) + "\n");
95+
}
96+
97+
// ── Main ──
98+
99+
async function main() {
100+
const { version, tag } = parseArgs();
101+
102+
// Git checks
103+
const branch = run("git branch --show-current");
104+
if (branch !== "main") {
105+
fatal(`Must be on main branch (currently on "${branch}")`);
106+
}
107+
108+
run("git fetch origin main");
109+
const local = run("git rev-parse HEAD");
110+
const remote = run("git rev-parse origin/main");
111+
if (local !== remote) {
112+
fatal("Local main is not even with origin/main. Pull or push first.");
113+
}
114+
115+
const status = run("git status --porcelain");
116+
if (status) {
117+
fatal("Working tree is not clean. Commit or stash changes first.");
118+
}
119+
120+
// Find packages
121+
const packages = getPublishablePackages();
122+
const pkgNames = packages.map((p) => {
123+
const pkg = JSON.parse(readFileSync(join(p, "package.json"), "utf-8"));
124+
return pkg.name as string;
125+
});
126+
127+
// Confirmation
128+
console.log(`\n\x1b[1mRelease Plan\x1b[0m`);
129+
console.log(` Version: \x1b[36m${version}\x1b[0m`);
130+
console.log(` NPM tag: \x1b[36m${tag}\x1b[0m`);
131+
console.log(` Git tag: \x1b[36mv${version}\x1b[0m`);
132+
console.log(` Packages (${pkgNames.length}):`);
133+
for (const name of pkgNames) {
134+
console.log(` - ${name}`);
135+
}
136+
console.log();
137+
138+
if (!(await confirm("Proceed?"))) {
139+
console.log("Aborted.");
140+
process.exit(0);
141+
}
142+
143+
// Typecheck & build
144+
console.log("\n\x1b[1mRunning typecheck...\x1b[0m");
145+
run("pnpm turbo check-types", { stdio: "inherit" });
146+
147+
console.log("\n\x1b[1mRunning build...\x1b[0m");
148+
run("pnpm turbo build", { stdio: "inherit" });
149+
150+
// Bump versions
151+
console.log(`\n\x1b[1mBumping versions to ${version}...\x1b[0m`);
152+
setVersion(ROOT, version);
153+
for (const pkg of packages) {
154+
setVersion(pkg, version);
155+
}
156+
157+
// Commit & push
158+
console.log("\n\x1b[1mCommitting version bump...\x1b[0m");
159+
run("git add -A");
160+
run(`git commit -m "release: v${version}"`);
161+
run("git push origin main");
162+
163+
// Git tag & GitHub release
164+
console.log(`\n\x1b[1mCreating git tag v${version}...\x1b[0m`);
165+
run(`git tag v${version}`);
166+
run(`git push origin v${version}`);
167+
168+
const prerelease = tag === "rc" ? "--prerelease" : "";
169+
console.log("\n\x1b[1mCreating GitHub release...\x1b[0m");
170+
run(
171+
`gh release create v${version} --title "v${version}" --generate-notes ${prerelease}`.trim(),
172+
{ stdio: "inherit" },
173+
);
174+
175+
// Publish
176+
console.log(`\n\x1b[1mPublishing to npm (tag: ${tag})...\x1b[0m`);
177+
for (const pkg of packages) {
178+
const name = JSON.parse(readFileSync(join(pkg, "package.json"), "utf-8")).name;
179+
console.log(` Publishing ${name}...`);
180+
run(`pnpm publish --access public --tag ${tag} --no-git-checks`, { cwd: pkg, stdio: "inherit" });
181+
}
182+
183+
console.log(`\n\x1b[32m✓ Released v${version}\x1b[0m`);
184+
}
185+
186+
main().catch((err) => {
187+
console.error(err);
188+
process.exit(1);
189+
});

0 commit comments

Comments
 (0)