Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,8 @@ Other wave items (read bounds, shell truncation, process-group kill, grep caps,
## Build and Validation

```bash
bun run build # bun build ./src/index.ts --outdir ./dist --target bun
bun run build # bun build ./src/index.ts --outdir ./dist --target bun, stamping
# the version badge via --define "$(bun scripts/build-info.ts)"
bun run typecheck # tsc --noEmit
bun test ./src ./tests
```
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"corbits": "./dist/corbits"
},
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target bun --external '@opentui/core-*' && bun scripts/copy-repo-plugins.ts",
"build:bin": "bun build ./src/index.ts --compile --minify --sourcemap --define process.env.NODE_ENV='\"production\"' --outfile ./dist/corbits && ([ \"$(uname -s)\" != Darwin ] || codesign -s - --force ./dist/corbits) && bun scripts/copy-repo-plugins.ts",
"build": "bun build ./src/index.ts --outdir ./dist --target bun --external '@opentui/core-*' --define \"$(bun scripts/build-info.ts)\" && bun scripts/copy-repo-plugins.ts",
"build:bin": "bun build ./src/index.ts --compile --minify --sourcemap --define process.env.NODE_ENV='\"production\"' --define \"$(bun scripts/build-info.ts)\" --outfile ./dist/corbits && ([ \"$(uname -s)\" != Darwin ] || codesign -s - --force ./dist/corbits) && bun scripts/copy-repo-plugins.ts",
"typecheck": "tsc --noEmit",
"test": "bun test ./src ./e2e ./evals ./scripts ./testkit --randomize --seed 424242",
"test:paths": "bun scripts/test-paths.ts",
Expand Down
251 changes: 251 additions & 0 deletions scripts/build-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { join } from "node:path";

import pkg from "../package.json" with { type: "json" };
import { withTempDir } from "../testkit/temporary-dirs.js";
import {
collectGitInfo,
composeDisplayVersion,
type GitInfo,
type RunResult,
} from "./build-info.js";

const repoRoot = join(import.meta.dir, "..");
const scriptPath = join(repoRoot, "scripts", "build-info.ts");

const DISPLAY_VERSION_RE =
/^v\d+\.\d+\.\d+(-\d+-g[0-9a-f]+)?(\+g[0-9a-f]+)?(-dirty)?$/;

describe("composeDisplayVersion", () => {
test("on tag, clean tree -> plain version", () => {
const info: GitInfo = {
tag: "v0.3.36",
count: 0,
hash: "9af7e1e",
dirty: false,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36");
});

test("on tag, dirty tree -> -dirty suffix", () => {
const info: GitInfo = {
tag: "v0.3.36",
count: 0,
hash: "9af7e1e",
dirty: true,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36-dirty");
});

test("N commits past tag -> -N-g<hash>", () => {
const info: GitInfo = {
tag: "v0.3.36",
count: 7,
hash: "9af7e1e",
dirty: false,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36-7-g9af7e1e");
});

test("N commits past tag, dirty -> -N-g<hash>-dirty", () => {
const info: GitInfo = {
tag: "v0.3.36",
count: 7,
hash: "9af7e1e",
dirty: true,
};
expect(composeDisplayVersion("0.3.36", info)).toBe(
"v0.3.36-7-g9af7e1e-dirty",
);
});

test("no tags -> +g<hash>", () => {
const info: GitInfo = {
tag: null,
count: null,
hash: "9af7e1e",
dirty: false,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36+g9af7e1e");
});

test("no tags, dirty -> +g<hash>-dirty", () => {
const info: GitInfo = {
tag: null,
count: null,
hash: "9af7e1e",
dirty: true,
};
expect(composeDisplayVersion("0.3.36", info)).toBe(
"v0.3.36+g9af7e1e-dirty",
);
});

test("not a git repo / metadata unavailable -> plain version", () => {
const info: GitInfo = {
tag: null,
count: null,
hash: null,
dirty: false,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36");
});

test("tag present but commit count unavailable -> plain version", () => {
const info: GitInfo = {
tag: "v0.3.36",
count: null,
hash: "9af7e1e",
dirty: false,
};
expect(composeDisplayVersion("0.3.36", info)).toBe("v0.3.36");
});
});

interface FakeSpec {
stdout?: string;
exitCode?: number;
throws?: boolean;
}

function fakeRun(specs: Record<string, FakeSpec>) {
return (args: string[]): RunResult => {
const spec = specs[args.join(" ")] ?? {};
if (spec.throws) throw new Error(`git ${args.join(" ")} exploded`);
return { stdout: spec.stdout ?? "", exitCode: spec.exitCode ?? 0 };
};
}

describe("collectGitInfo", () => {
test("git repo, clean tree", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "true" },
"describe --tags --match v* --abbrev=0": { stdout: "v0.3.36" },
"rev-list --count v0.3.36..HEAD": { stdout: "7" },
"rev-parse --short=7 HEAD": { stdout: "9af7e1e" },
"diff-index --quiet HEAD --": { exitCode: 0 },
});
expect(collectGitInfo("/repo", run)).toEqual({
tag: "v0.3.36",
count: 7,
hash: "9af7e1e",
dirty: false,
});
});

test("git repo, dirty tree (diff-index exits 1)", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "true" },
"describe --tags --match v* --abbrev=0": { stdout: "v0.3.36" },
"rev-list --count v0.3.36..HEAD": { stdout: "7" },
"rev-parse --short=7 HEAD": { stdout: "9af7e1e" },
"diff-index --quiet HEAD --": { exitCode: 1 },
});
expect(collectGitInfo("/repo", run)).toEqual({
tag: "v0.3.36",
count: 7,
hash: "9af7e1e",
dirty: true,
});
});

test("git repo, exactly on tag", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "true" },
"describe --tags --match v* --abbrev=0": { stdout: "v0.3.36" },
"rev-list --count v0.3.36..HEAD": { stdout: "0" },
"rev-parse --short=7 HEAD": { stdout: "9af7e1e" },
"diff-index --quiet HEAD --": { exitCode: 0 },
});
expect(collectGitInfo("/repo", run)).toEqual({
tag: "v0.3.36",
count: 0,
hash: "9af7e1e",
dirty: false,
});
});

test("no tags (describe throws) degrades tag/count, keeps hash", () => {
Comment thread
bwachman marked this conversation as resolved.
Resolved
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "true" },
"describe --tags --match v* --abbrev=0": { throws: true },
"rev-parse --short=7 HEAD": { stdout: "9af7e1e" },
"diff-index --quiet HEAD --": { exitCode: 0 },
});
expect(collectGitInfo("/repo", run)).toEqual({
tag: null,
count: null,
hash: "9af7e1e",
dirty: false,
});
});

test("no tags (describe exits 128 with empty stdout) degrades tag/count, keeps hash", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "true" },
"describe --tags --match v* --abbrev=0": { exitCode: 128 },
"rev-parse --short=7 HEAD": { stdout: "9af7e1e" },
"diff-index --quiet HEAD --": { exitCode: 0 },
});
expect(collectGitInfo("/repo", run)).toEqual({
tag: null,
count: null,
hash: "9af7e1e",
dirty: false,
});
});

test("not a git repo (--is-inside-work-tree -> false)", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { stdout: "false" },
});
expect(collectGitInfo("/outside", run)).toEqual({
tag: null,
count: null,
hash: null,
dirty: false,
});
});

test("git missing (rev-parse exits non-zero) -> all null", () => {
const run = fakeRun({
"rev-parse --is-inside-work-tree": { exitCode: 128 },
});
expect(collectGitInfo("/outside", run)).toEqual({
tag: null,
count: null,
hash: null,
dirty: false,
});
});
});

describe("build-info CLI", () => {
test("outside a git repo prints a plain-version define", async () => {
await withTempDir("build-info-not-git-", async (dir) => {
const res = spawnSync(process.execPath, [scriptPath], {
cwd: dir,
encoding: "utf8",
});
expect(res.status).toBe(0);
expect(res.stdout.trim()).toBe(
`process.env.CORBITS_BUILD_INFO="v${pkg.version}"`,
);
});
});

test("from the repo root prints a define matching the suffix regex", () => {
const res = spawnSync(process.execPath, [scriptPath], {
cwd: repoRoot,
encoding: "utf8",
});
expect(res.status).toBe(0);
const line = res.stdout.trim();
expect(line.startsWith("process.env.CORBITS_BUILD_INFO=")).toBe(true);
const value: string = JSON.parse(
line.slice("process.env.CORBITS_BUILD_INFO=".length),
);
expect(value).toMatch(DISPLAY_VERSION_RE);
});
});
123 changes: 123 additions & 0 deletions scripts/build-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { spawnSync } from "node:child_process";

import pkg from "../package.json" with { type: "json" };

/**
* Build-time git metadata for the version badge. Run once by `bun
* scripts/build-info.ts` (wired into `build` and `build:bin` through
* `--define "$(bun scripts/build-info.ts)"`); the single stdout line is the
* define token, e.g. `process.env.CORBITS_BUILD_INFO="v0.3.36-7-g9af7e1e"`.
*
* A build must never fail for metadata: every field degrades to null/false
* and the script prints a stderr warning instead of throwing.
*/

export interface GitInfo {
tag: string | null;
count: number | null;
hash: string | null;
dirty: boolean;
}

export interface RunResult {
stdout: string;
exitCode: number;
}

export function gitRunner(cwd: string, args: string[]): RunResult {
const res = spawnSync("git", args, { cwd, encoding: "utf8" });
return {
stdout: typeof res.stdout === "string" ? res.stdout.trim() : "",
exitCode: res.status ?? 1,
};
}

function safeRun(
run: (args: string[]) => RunResult,
args: string[],
): RunResult | null {
try {
return run(args);
} catch (err) {
console.error(`[build-info] git ${args.join(" ")} failed: ${String(err)}`);
return null;
}
}

/**
* Gathers git metadata against `cwd`. Not a git repo (or git missing) yields
* `{ tag: null, count: null, hash: null, dirty: false }`; individual
* failures degrade just their own field.
*/
export function collectGitInfo(
cwd: string,
run: (args: string[]) => RunResult = (args) => gitRunner(cwd, args),
): GitInfo {
const inside = safeRun(run, ["rev-parse", "--is-inside-work-tree"]);
if (inside === null || inside.stdout !== "true") {
return { tag: null, count: null, hash: null, dirty: false };
}

const tagResult = safeRun(run, [
"describe",
"--tags",
"--match",
"v*",
"--abbrev=0",
]);
const tag =
tagResult !== null && tagResult.stdout !== "" ? tagResult.stdout : null;

let count: number | null = null;
if (tag !== null) {
const countResult = safeRun(run, ["rev-list", "--count", `${tag}..HEAD`]);
if (countResult !== null) {
const parsed = Number.parseInt(countResult.stdout, 10);
count = Number.isNaN(parsed) ? null : parsed;
}
}

const hashResult = safeRun(run, ["rev-parse", "--short=7", "HEAD"]);
const hash =
hashResult !== null && hashResult.stdout !== "" ? hashResult.stdout : null;

const diffResult = safeRun(run, ["diff-index", "--quiet", "HEAD", "--"]);
const dirty = diffResult !== null && diffResult.exitCode === 1;

return { tag, count, hash, dirty };
}

/**
* Pure composition of the display version from the six SOLUTION_SCOPE §3
* cases:
* on tag, clean -> v0.3.36
* on tag, dirty -> v0.3.36-dirty
* past tag -> v0.3.36-7-g9af7e1e (+ -dirty)
* no tags -> v0.3.36+g9af7e1e (+ -dirty)
* metadata unavailable -> v0.3.36
*/
export function composeDisplayVersion(version: string, info: GitInfo): string {
const base = `v${version}`;
const dirty = info.dirty ? "-dirty" : "";
if (info.tag !== null && info.count === 0) {
Comment thread
bwachman marked this conversation as resolved.
Resolved
return `${base}${dirty}`;
}
if (
info.tag !== null &&
info.count !== null &&
info.count > 0 &&
info.hash !== null
) {
return `${base}-${info.count}-g${info.hash}${dirty}`;
}
if (info.tag === null && info.hash !== null) {
return `${base}+g${info.hash}${dirty}`;
}
return base;
}

if (import.meta.main) {
const version = typeof pkg.version === "string" ? pkg.version : "0.0.0";
const display = composeDisplayVersion(version, collectGitInfo(process.cwd()));
console.log(`process.env.CORBITS_BUILD_INFO=${JSON.stringify(display)}`);
}
Loading
Loading