Skip to content
Open
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
9 changes: 9 additions & 0 deletions docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,12 @@ print order are asserted by:
```bash
node --test scripts/check-bindings-cli.test.mjs
```

## tlock checks

Run the blocking test and typecheck suite for tlock locally and in CI:

```bash
pnpm tlock:ci
node --test scripts/tlock-ci.test.mjs
```
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"errors:check": "node scripts/check-round-errors.mjs",
"errors:test": "node --test scripts/check-round-errors.test.mjs",
"bindings:generate": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && cp tmp-bindings/src/index.ts packages/round-bindings/src/index.ts && rm -rf tmp-bindings",
"bindings:check": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && (diff tmp-bindings/src/index.ts packages/round-bindings/src/index.ts; diff_exit=$?; rm -rf tmp-bindings; if [ $diff_exit -ne 0 ]; then echo '❌ Round bindings are stale. Run pnpm bindings:generate to update them.'; exit 1; else echo '✅ Round bindings are up to date.'; fi)",
"bindings:check": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && (diff tmp-bindings/src/index.ts packages/round-bindings/src/index.ts; diff_exit=$?; rm -rf tmp-bindings; if [ $diff_exit -ne 0 ]; then echo '\u274c Round bindings are stale. Run pnpm bindings:generate to update them.'; exit 1; else echo '\u2705 Round bindings are up to date.'; fi)",
"sdk:test": "pnpm --filter @sub-rosa/sdk test",
"sdk:typecheck": "pnpm --filter @sub-rosa/sdk typecheck",
"bindings:test": "pnpm --filter @sub-rosa/round-bindings test",
Expand Down Expand Up @@ -87,6 +87,7 @@
"errors:normalize:check": "node scripts/check-error-normalization.mjs",
"errors:normalize:test": "pnpm --filter @sub-rosa/logging test && node --test scripts/check-error-normalization.test.mjs",
"line-lengths:check": "node scripts/check-line-lengths.mjs",
"line-lengths:test": "node --test scripts/check-line-lengths.test.mjs"
"line-lengths:test": "node --test scripts/check-line-lengths.test.mjs",
"tlock:ci": "node --test scripts/tlock-ci.test.mjs && pnpm tlock:test && pnpm tlock:typecheck"
}
}
68 changes: 68 additions & 0 deletions scripts/run-tlock-ci.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env node
// Copyright (c) 2026 Sub Rosa contributors
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createLogger } from "../packages/logging/src/index.cjs";
import { normalizeError } from "../packages/logging/src/errors.cjs";

const diagnostics = createLogger("scripts.run-tlock-ci");
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");

export const CI_STEPS = Object.freeze(["tlock:test", "tlock:typecheck"]);
export const TARGET_WORKSPACES = Object.freeze([{"path": "packages/tlock", "name": "@sub-rosa/tlock"}]);

export function verifyTlockCiConfig(root = ROOT) {
const rootPkgPath = resolve(root, "package.json");
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8"));
const scripts = rootPkg.scripts ?? {};

for (const step of CI_STEPS) {
if (!scripts[step]) {
throw new Error(`Missing required root script: ${step}`);
}
}

for (const ws of TARGET_WORKSPACES) {
const wsPkgPath = resolve(root, ws.path, "package.json");
if (!existsSync(wsPkgPath)) {
throw new Error(`Missing workspace package.json: ${ws.path}`);
}
const wsPkg = JSON.parse(readFileSync(wsPkgPath, "utf8"));
if (wsPkg.name !== ws.name) {
throw new Error(`Unexpected package name in ${ws.path}: ${wsPkg.name}`);
}
if (!wsPkg.scripts?.test) {
throw new Error(`Missing test script in ${ws.path}`);
}
if (!wsPkg.scripts?.typecheck) {
throw new Error(`Missing typecheck script in ${ws.path}`);
}
}

return { steps: CI_STEPS, workspaces: TARGET_WORKSPACES };
}

export function runTlockCi(root = ROOT, runner = execFileSync) {
verifyTlockCiConfig(root);
const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
for (const step of CI_STEPS) {
diagnostics.info("ci-step-start", `Running pnpm ${step}`);
try {
runner(pnpmBin, ["run", step], {
cwd: root,
stdio: "inherit",
env: { ...process.env },
});
} catch (error) {
diagnostics.error("ci-step-failed", normalizeError(error));
throw new Error(`Blocking CI step failed: ${step}`);
}
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
runTlockCi();
}
57 changes: 57 additions & 0 deletions scripts/tlock-ci.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2026 Sub Rosa contributors
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import {
CI_STEPS,
TARGET_WORKSPACES,
runTlockCi,
verifyTlockCiConfig,
} from "./run-tlock-ci.mjs";

const rootPkg = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
);

test("tlock CI configuration defines blocking test and typecheck steps in order", () => {
const verified = verifyTlockCiConfig();
assert.deepEqual(verified.steps, ["tlock:test", "tlock:typecheck"]);
assert.ok(TARGET_WORKSPACES.length >= 1);

assert.ok(
rootPkg.scripts["tlock:ci"],
"root script tlock:ci must exist",
);

let previousIndex = -1;
for (const step of CI_STEPS) {
assert.ok(rootPkg.scripts[step], `root script ${step} must exist`);
const idx = rootPkg.scripts["tlock:ci"].indexOf(`pnpm ${step}`);
assert.ok(
idx > previousIndex,
`${step} must run as a blocking step in tlock:ci`,
);
previousIndex = idx;
}
});

test("tlock CI runner executes each step sequentially and blocks on failure", () => {
const executed = [];
runTlockCi(undefined, (_bin, args) => {
executed.push(args.join(" "));
});
assert.deepEqual(
executed,
CI_STEPS.map((step) => `run ${step}`),
);

assert.throws(
() =>
runTlockCi(undefined, (_bin, args) => {
if (args[1] === CI_STEPS[0]) {
throw new Error("simulated step failure");
}
}),
/Blocking CI step failed/,
);
});