mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
chore(release): prepare the release commit with one command (#1672)
pnpm release:prepare <version> sets the app version in Cargo.toml, package.json and tauri.conf.json, refreshes Cargo.lock's workspace entries, and copies wit/*.wit over the plugin API snapshot when wit/ declares a newer API. The release workflow refused a tag with a stale snapshot, but the copy itself was a manual step.
This commit is contained in:
1 parent
ed08b96382
commit
37f0897eb7
5 files changed
+206
-4
No files matched your search
@@ -79,6 +79,8 @@ Shared agent skills live in `.agents/skills/` (`.claude/skills/` links to them):
|
||||
`pnpm gen:bindings`.
|
||||
- `pnpm perf` (UI interaction latency / frames / leaks), `pnpm perf:rust` (criterion
|
||||
benches vs budgets). Budgets: `perf/budgets.json`.
|
||||
- `pnpm release:prepare <version>` — the release commit's changes: app version everywhere,
|
||||
`Cargo.lock`, and the plugin API snapshot when `wit/` moved to a new version.
|
||||
|
||||
## Before opening a PR
|
||||
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@
|
||||
"dev:status": "node scripts/dev-status.ts",
|
||||
"e2e": "playwright test",
|
||||
"perf": "node perf/run.ts",
|
||||
"perf:rust": "node perf/rust.ts"
|
||||
"perf:rust": "node perf/rust.ts",
|
||||
"release:prepare": "node scripts/release-prepare.ts"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
|
||||
+4
-3
@@ -978,9 +978,10 @@ Checked automatically:
|
||||
released version `wit/` must match it exactly, so additions start with a
|
||||
version bump. Each snapshot passed this check against the one before, so
|
||||
one snapshot covers every release.
|
||||
- Releasing: when the `wit/` version changes, copy `wit/*.wit` over
|
||||
`tests/api/released/` in the release commit. The release workflow refuses a
|
||||
tag whose `wit/` version differs from the snapshot's.
|
||||
- Releasing: `pnpm release:prepare <version>` makes the release commit's
|
||||
changes; when the `wit/` version changed, it copies `wit/*.wit` over
|
||||
`tests/api/released/`. The release workflow refuses a tag whose `wit/`
|
||||
version differs from the snapshot's.
|
||||
- `crates/ide-plugin/tests/api_compat.rs`: the versions agree, the SDK's copy
|
||||
matches, the version check (rules 4, 5); a simulated next-minor host (every
|
||||
import stubbed at the newer version, plus extra functions and an extra
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
SNAPSHOT,
|
||||
apiVersion,
|
||||
cargoVersion,
|
||||
parseVersion,
|
||||
setCargoVersion,
|
||||
setJsonVersion,
|
||||
syncApiSnapshot,
|
||||
} from "./release-prepare.ts";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterEach(() => dirs.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true })));
|
||||
|
||||
function repo(wit: Record<string, string>, snapshot: Record<string, string>): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "release-prepare-"));
|
||||
dirs.push(root);
|
||||
for (const [dir, files] of [
|
||||
["wit", wit],
|
||||
[SNAPSHOT, snapshot],
|
||||
] as const) {
|
||||
mkdirSync(join(root, dir), { recursive: true });
|
||||
for (const [name, text] of Object.entries(files)) writeFileSync(join(root, dir, name), text);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
const pkg = (version: string) => `package openidea:plugin@${version};\n`;
|
||||
|
||||
describe("versions", () => {
|
||||
it("accepts x.y.z with or without a leading v", () => {
|
||||
expect(parseVersion("0.1.38")).toBe("0.1.38");
|
||||
expect(parseVersion("v0.1.38")).toBe("0.1.38");
|
||||
expect(() => parseVersion("0.1")).toThrow();
|
||||
expect(() => parseVersion(undefined)).toThrow();
|
||||
});
|
||||
|
||||
it("changes only the workspace version in Cargo.toml", () => {
|
||||
const toml = [
|
||||
"[workspace]",
|
||||
'members = ["crates/*"]',
|
||||
"",
|
||||
"[workspace.package]",
|
||||
'authors = ["a"]',
|
||||
'version = "0.1.37"',
|
||||
'edition = "2024"',
|
||||
"",
|
||||
"[workspace.dependencies]",
|
||||
'foo = { version = "0.1.37" }',
|
||||
"",
|
||||
].join("\n");
|
||||
expect(cargoVersion(toml)).toBe("0.1.37");
|
||||
const next = setCargoVersion(toml, "0.1.38");
|
||||
expect(cargoVersion(next)).toBe("0.1.38");
|
||||
expect(next).toContain('foo = { version = "0.1.37" }');
|
||||
expect(next.split("\n")).toHaveLength(toml.split("\n").length);
|
||||
});
|
||||
|
||||
it("keeps a JSON file's formatting", () => {
|
||||
const json = '{\n "name": "openidea",\n "version": "0.1.37",\n "private": true\n}\n';
|
||||
expect(setJsonVersion(json, "0.1.38")).toBe(json.replace("0.1.37", "0.1.38"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin API snapshot", () => {
|
||||
it("copies wit/ over the snapshot when the API version moved, dropping stale files", () => {
|
||||
const root = repo(
|
||||
{ "plugin.wit": pkg("1.2.0"), "indexing.wit": pkg("1.2.0") },
|
||||
{ "plugin.wit": pkg("1.1.0"), "old.wit": pkg("1.1.0"), "README.md": "kept" },
|
||||
);
|
||||
expect(syncApiSnapshot(root)).toEqual({ from: "1.1.0", to: "1.2.0", copied: true });
|
||||
expect(readdirSync(join(root, SNAPSHOT)).sort()).toEqual(["README.md", "indexing.wit", "plugin.wit"]);
|
||||
expect(apiVersion(readFileSync(join(root, SNAPSHOT, "plugin.wit"), "utf8"))).toBe("1.2.0");
|
||||
});
|
||||
|
||||
it("leaves the snapshot alone when the API is unchanged", () => {
|
||||
const root = repo({ "plugin.wit": pkg("1.1.0") + "// edited\n" }, { "plugin.wit": pkg("1.1.0") });
|
||||
expect(syncApiSnapshot(root)).toEqual({ from: "1.1.0", to: "1.1.0", copied: false });
|
||||
expect(readFileSync(join(root, SNAPSHOT, "plugin.wit"), "utf8")).toBe(pkg("1.1.0"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Prepares the release commit: `pnpm release:prepare 0.1.38` sets the app version in
|
||||
* Cargo.toml, package.json and src-tauri/tauri.conf.json, refreshes Cargo.lock, and,
|
||||
* when wit/ carries a newer plugin API than the released snapshot, copies wit/*.wit
|
||||
* over sdk/rust/openidea-plugin/tests/api/released/ (the release workflow refuses a
|
||||
* tag whose wit/ and snapshot versions differ). Commit the result as `chore: v<version>`.
|
||||
*/
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const SNAPSHOT = "sdk/rust/openidea-plugin/tests/api/released";
|
||||
|
||||
/** `0.1.38` from `0.1.38` or `v0.1.38`; throws for anything else. */
|
||||
export function parseVersion(arg: string | undefined): string {
|
||||
const version = arg?.replace(/^v/, "");
|
||||
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`expected a version like 0.1.38, got ${arg}`);
|
||||
return version;
|
||||
}
|
||||
|
||||
function compare(a: string, b: string): number {
|
||||
const [x, y] = [a, b].map((v) => v.split(".").map(Number));
|
||||
for (let i = 0; i < 3; i++) if (x![i] !== y![i]) return x![i]! - y![i]!;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** The line index and value of `version` in `[workspace.package]`. */
|
||||
function workspaceVersionLine(lines: string[]): { index: number; version: string } {
|
||||
const start = lines.findIndex((line) => line.trim() === "[workspace.package]");
|
||||
for (let i = start + 1; start >= 0 && i < lines.length && !lines[i]!.startsWith("["); i++) {
|
||||
const match = /^version = "([^"]+)"$/.exec(lines[i]!.trim());
|
||||
if (match) return { index: i, version: match[1]! };
|
||||
}
|
||||
throw new Error("Cargo.toml: no version in [workspace.package]");
|
||||
}
|
||||
|
||||
export function cargoVersion(toml: string): string {
|
||||
return workspaceVersionLine(toml.split("\n")).version;
|
||||
}
|
||||
|
||||
export function setCargoVersion(toml: string, version: string): string {
|
||||
const lines = toml.split("\n");
|
||||
const { index } = workspaceVersionLine(lines);
|
||||
lines[index] = `version = "${version}"`;
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Replaces the top-level `"version"` of a JSON file, keeping its formatting. */
|
||||
export function setJsonVersion(json: string, version: string): string {
|
||||
const current = (JSON.parse(json) as { version?: unknown }).version;
|
||||
if (typeof current !== "string") throw new Error("no top-level version");
|
||||
return json.replace(`"version": "${current}"`, `"version": "${version}"`);
|
||||
}
|
||||
|
||||
/** `1.2.0` from a `package openidea:[email protected];` line. */
|
||||
export function apiVersion(wit: string): string {
|
||||
const match = /^package openidea:plugin@([^;]+);$/m.exec(wit);
|
||||
if (!match) throw new Error("no `package openidea:plugin@…;` line");
|
||||
return match[1]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies `wit/*.wit` over the snapshot when wit/ declares another API version, and
|
||||
* returns the versions; leaves the snapshot alone otherwise (no API change to release).
|
||||
*/
|
||||
export function syncApiSnapshot(root: string): { from: string; to: string; copied: boolean } {
|
||||
const wit = join(root, "wit");
|
||||
const snapshot = join(root, SNAPSHOT);
|
||||
const to = apiVersion(readFileSync(join(wit, "plugin.wit"), "utf8"));
|
||||
const from = apiVersion(readFileSync(join(snapshot, "plugin.wit"), "utf8"));
|
||||
if (from === to) return { from, to, copied: false };
|
||||
for (const file of readdirSync(snapshot)) if (file.endsWith(".wit")) rmSync(join(snapshot, file));
|
||||
for (const file of readdirSync(wit)) if (file.endsWith(".wit")) copyFileSync(join(wit, file), join(snapshot, file));
|
||||
return { from, to, copied: true };
|
||||
}
|
||||
|
||||
function edit(path: string, change: (text: string) => string) {
|
||||
const before = readFileSync(path, "utf8");
|
||||
const after = change(before);
|
||||
if (after === before) throw new Error(`${path}: nothing changed`);
|
||||
writeFileSync(path, after);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const root = fileURLToPath(new URL("..", import.meta.url));
|
||||
const version = parseVersion(process.argv[2]);
|
||||
const current = cargoVersion(readFileSync(join(root, "Cargo.toml"), "utf8"));
|
||||
if (compare(version, current) <= 0) throw new Error(`${version} is not newer than the current ${current}`);
|
||||
|
||||
edit(join(root, "Cargo.toml"), (text) => setCargoVersion(text, version));
|
||||
edit(join(root, "package.json"), (text) => setJsonVersion(text, version));
|
||||
edit(join(root, "src-tauri/tauri.conf.json"), (text) => setJsonVersion(text, version));
|
||||
const cargo = spawnSync("cargo", ["update", "--workspace"], { cwd: root, stdio: "inherit" });
|
||||
if (cargo.status !== 0) throw new Error("cargo update --workspace failed");
|
||||
console.log(`version ${current} -> ${version} (Cargo.toml, Cargo.lock, package.json, tauri.conf.json)`);
|
||||
|
||||
const api = syncApiSnapshot(root);
|
||||
console.log(
|
||||
api.copied
|
||||
? `plugin API ${api.from} -> ${api.to}: copied wit/*.wit to ${SNAPSHOT}/`
|
||||
: `plugin API ${api.to}: unchanged since the last release`,
|
||||
);
|
||||
console.log(`next: commit as "chore: v${version}", then tag v${version}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`release:prepare: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user