@decohere/ci-tools 0.0.1-dev.3 → 0.0.1-dev.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bench/bench.mjs +3 -3
- package/src/bench/trace.mjs +2 -2
- package/src/coverage/badge.mjs +2 -2
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/pages/keep-prior-bench-badge.mjs +38 -0
- package/src/pages/restore-bench-history.mjs +37 -0
- package/src/release/capture-version.mjs +27 -0
- package/src/release/npm-links.mjs +39 -0
- package/src/release/push-prerelease-commit.mjs +85 -0
- package/src/release/sync-dependencies.mjs +27 -0
- package/tests/lib/lcov.test.mjs +8 -8
package/package.json
CHANGED
package/src/bench/bench.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* bench.mjs — run the
|
|
4
|
+
* bench.mjs — run the forge benchmark and compare against the last saved baseline.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* node bench.mjs — run and compare, do not save
|
|
@@ -34,9 +34,9 @@ const HISTORY_FILE = join(
|
|
|
34
34
|
"projects/ci-tools/dist/ci/bench/bench-history.json",
|
|
35
35
|
);
|
|
36
36
|
const VITEST_BIN = join(WORKSPACE_ROOT, "node_modules/.bin/vitest");
|
|
37
|
-
const BENCH_FILE = "projects/
|
|
37
|
+
const BENCH_FILE = "projects/terminal/e2e/build.benchmark.bench.ts";
|
|
38
38
|
const CONFIG = join(WORKSPACE_ROOT, "vitest.e2e.config.ts");
|
|
39
|
-
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".
|
|
39
|
+
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".decohere", "logs");
|
|
40
40
|
const MAX_RUNS = 10;
|
|
41
41
|
const command = process.argv[2] ?? "run";
|
|
42
42
|
|
package/src/bench/trace.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { join } from "node:path";
|
|
|
7
7
|
const WORKSPACE_ROOT = process.cwd();
|
|
8
8
|
const VITEST_BIN = join(WORKSPACE_ROOT, "node_modules/.bin/vitest");
|
|
9
9
|
const CONFIG = join(WORKSPACE_ROOT, "vitest.e2e.config.ts");
|
|
10
|
-
const TRACE_SPEC = "projects/
|
|
11
|
-
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".
|
|
10
|
+
const TRACE_SPEC = "projects/terminal/e2e/build.trace.e2e.spec.ts";
|
|
11
|
+
const TRACE_LOG_DIR = join(WORKSPACE_ROOT, ".decohere", "logs");
|
|
12
12
|
|
|
13
13
|
function truthy(value, defaultValue = false) {
|
|
14
14
|
if (value == null) return defaultValue;
|
package/src/coverage/badge.mjs
CHANGED
|
@@ -10,8 +10,8 @@ import path from "node:path";
|
|
|
10
10
|
import { parseLcovByProject } from "../lib/lcov.mjs";
|
|
11
11
|
import { createBadgeSvg, getColor } from "../lib/svg.mjs";
|
|
12
12
|
|
|
13
|
-
const summaryPath = path.resolve("
|
|
14
|
-
const lcovPath = path.resolve("
|
|
13
|
+
const summaryPath = path.resolve("dist/coverage/coverage-summary.json");
|
|
14
|
+
const lcovPath = path.resolve("dist/coverage/lcov.info");
|
|
15
15
|
const badgesDirectory = path.resolve("projects/ci-tools/dist/ci/badges");
|
|
16
16
|
const globalBadgePath = path.join(badgesDirectory, "coverage.svg");
|
|
17
17
|
const projectBadgesDirectory = path.join(badgesDirectory, "projects");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Clear benchmark history for this publish (workflow_dispatch with clear-history=true)
|
|
4
|
+
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
6
|
+
import { mkdir } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
|
|
9
|
+
const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(HISTORY_DIR, { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
HISTORY_FILE,
|
|
15
|
+
JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
runs: {},
|
|
18
|
+
},
|
|
19
|
+
null,
|
|
20
|
+
2,
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
console.log("✓ Cleared benchmark history");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Error clearing benchmark history:", error.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Keep prior bench badge if no code changes; create fallback if none exists
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const BADGES_DIR = "projects/ci-tools/dist/ci/badges";
|
|
10
|
+
const BADGE_FILE = `${BADGES_DIR}/bench.svg`;
|
|
11
|
+
const PAGES_URL = "https://typesmiths.github.io/decohere/badges/bench.svg";
|
|
12
|
+
|
|
13
|
+
const FALLBACK_BADGE = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20" role="img" aria-label="bench: unavailable">
|
|
14
|
+
<title>bench: unavailable</title>
|
|
15
|
+
<rect width="120" height="20" fill="#555"/>
|
|
16
|
+
<rect x="46" width="74" height="20" fill="#9f9f9f"/>
|
|
17
|
+
<text x="24" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">bench</text>
|
|
18
|
+
<text x="83" y="14" fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">unavailable</text>
|
|
19
|
+
</svg>`;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(BADGE_FILE)) {
|
|
23
|
+
await mkdir(BADGES_DIR, { recursive: true });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
execSync(`curl -fsSL "${PAGES_URL}" -o "${BADGE_FILE}"`, {
|
|
27
|
+
stdio: "pipe",
|
|
28
|
+
});
|
|
29
|
+
console.log("✓ Restored prior bench badge from Pages");
|
|
30
|
+
} catch {
|
|
31
|
+
console.warn("::warning::No prior bench badge found on Pages; creating fallback badge.");
|
|
32
|
+
writeFileSync(BADGE_FILE, FALLBACK_BADGE);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error keeping prior bench badge:", error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Restore benchmark history from Pages, or initialize empty if not found
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { writeFileSync } from "node:fs";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
const HISTORY_DIR = "projects/ci-tools/dist/ci/bench";
|
|
10
|
+
const HISTORY_FILE = `${HISTORY_DIR}/bench-history.json`;
|
|
11
|
+
const PAGES_URL = "https://typesmiths.github.io/decohere/data/bench-history.json";
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(HISTORY_DIR, { recursive: true });
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
execSync(`curl -fsSL "${PAGES_URL}" -o "${HISTORY_FILE}"`, {
|
|
18
|
+
stdio: "pipe",
|
|
19
|
+
});
|
|
20
|
+
console.log("✓ Restored benchmark history from Pages");
|
|
21
|
+
} catch {
|
|
22
|
+
console.log("No benchmark history on Pages; starting from empty history");
|
|
23
|
+
writeFileSync(
|
|
24
|
+
HISTORY_FILE,
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
runs: {},
|
|
28
|
+
},
|
|
29
|
+
null,
|
|
30
|
+
2,
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Error restoring benchmark history:", error.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capture final version metadata from package.json
|
|
4
|
+
* Outputs version and is_prerelease flags for GitHub Actions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8"));
|
|
12
|
+
const version = packageJson.version;
|
|
13
|
+
|
|
14
|
+
if (!version) {
|
|
15
|
+
console.error("No version found in package.json");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`version=${version}`);
|
|
20
|
+
|
|
21
|
+
// Check if prerelease (has hyphen in version)
|
|
22
|
+
const isPrerelease = version.includes("-");
|
|
23
|
+
console.log(`is_prerelease=${isPrerelease}`);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(`Failed to capture version: ${e.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate npm package links for all @decohere packages
|
|
4
|
+
* Outputs markdown formatted links for GitHub release notes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const VERSION = process.argv[2] || process.env.VERSION;
|
|
11
|
+
|
|
12
|
+
if (!VERSION) {
|
|
13
|
+
console.error("VERSION not provided as argument or env var");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let links = "## Published Packages\n\n";
|
|
18
|
+
const projectsDir = join(process.cwd(), "projects");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const projects = readdirSync(projectsDir);
|
|
22
|
+
|
|
23
|
+
for (const project of projects) {
|
|
24
|
+
const pkgPath = join(projectsDir, project, "package.json");
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
27
|
+
if (pkg.name?.startsWith("@decohere/")) {
|
|
28
|
+
links += `- [\`${pkg.name}@${VERSION}\`](https://www.npmjs.com/package/${pkg.name}/v/${VERSION})\n`;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Skip projects without package.json
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(links);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(`Failed to generate npm links: ${e.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Publish prerelease version commit via PR
|
|
4
|
+
* Validates working tree, pushes prerelease commit to a release branch,
|
|
5
|
+
* and creates/updates a PR targeting the protected base branch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
function run(command) {
|
|
11
|
+
return execSync(command, { encoding: "utf8" }).trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function runInherit(command, env) {
|
|
15
|
+
execSync(command, { stdio: "inherit", env });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Check if working tree is dirty
|
|
20
|
+
const status = run("git status --porcelain");
|
|
21
|
+
if (status.trim().length > 0) {
|
|
22
|
+
console.error(
|
|
23
|
+
"::error::Working tree is dirty after version/dependency sync; refusing to publish with uncommitted changes.",
|
|
24
|
+
);
|
|
25
|
+
console.log(status);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Compare HEAD with origin and create a PR branch targeting an explicit base branch.
|
|
30
|
+
const head = run("git rev-parse HEAD");
|
|
31
|
+
const sourceBranch = process.env.GITHUB_REF_NAME || run("git rev-parse --abbrev-ref HEAD");
|
|
32
|
+
const channel = process.env.RELEASE_CHANNEL ?? "beta";
|
|
33
|
+
const baseBranch = process.env.RELEASE_BASE_BRANCH || sourceBranch;
|
|
34
|
+
const releasePrBranch = `release/${channel}-${sourceBranch}-to-${baseBranch}`;
|
|
35
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const origin = run(`git rev-parse origin/${sourceBranch}`);
|
|
39
|
+
|
|
40
|
+
if (head !== origin) {
|
|
41
|
+
console.log(
|
|
42
|
+
`Publishing release commit via PR branch origin/${releasePrBranch} (base: origin/${baseBranch})`,
|
|
43
|
+
);
|
|
44
|
+
runInherit(`git push origin HEAD:${releasePrBranch} --force`, process.env);
|
|
45
|
+
|
|
46
|
+
if (token == null || token.length === 0) {
|
|
47
|
+
throw new Error("Missing GITHUB_TOKEN/GH_TOKEN for PR upsert");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const version = process.env.RELEASE_VERSION ?? "(unknown)";
|
|
51
|
+
const ownerRepo = run("git config --get remote.origin.url")
|
|
52
|
+
.replace(/^.*github\.com[:/]/, "")
|
|
53
|
+
.replace(/\.git$/, "");
|
|
54
|
+
|
|
55
|
+
runInherit(
|
|
56
|
+
[
|
|
57
|
+
"node projects/ci-tools/src/github/create-or-update-pr.mjs",
|
|
58
|
+
`--title "chore(release): ${channel} ${version}"`,
|
|
59
|
+
`--base "${baseBranch}"`,
|
|
60
|
+
`--head "${releasePrBranch}"`,
|
|
61
|
+
`--body "Automated ${channel} version bump and dependency sync.\\n\\n- Channel: ${channel}\\n- Version: ${version}\\n- Source branch: ${sourceBranch}\\n- Base branch: ${baseBranch}\\n- Source commit: ${head}\\n\\nGenerated by pull-request workflow."`,
|
|
62
|
+
`--owner "${ownerRepo.split("/")[0]}"`,
|
|
63
|
+
`--repo "${ownerRepo.split("/")[1]}"`,
|
|
64
|
+
].join(" "),
|
|
65
|
+
{ ...process.env, GITHUB_TOKEN: token },
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.log("No prerelease version commit to push.");
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const message = err?.message ?? "";
|
|
72
|
+
if (
|
|
73
|
+
message.includes(`origin/${sourceBranch}`) &&
|
|
74
|
+
message.includes("Needed a single revision")
|
|
75
|
+
) {
|
|
76
|
+
console.log(`Remote branch origin/${sourceBranch} not found, skipping push.`);
|
|
77
|
+
} else {
|
|
78
|
+
console.error("::error::Failed to publish release commit via PR branch.");
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(`Push failed: ${e.message}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sync cross-library dependency versions
|
|
4
|
+
* Conditionally runs sync with or without commit based on DRY_RUN flag
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
const DRY_RUN = process.env.DRY_RUN === "true";
|
|
10
|
+
const TERMINAL_CLI = process.env.TERMINAL_CLI || "dist/debug/projects/terminal/src/cli.js";
|
|
11
|
+
const DECOHERE_CONFIG = process.env.DECOHERE_CONFIG || ".decohere.js";
|
|
12
|
+
|
|
13
|
+
const baseCmd = `node "${TERMINAL_CLI}" --log-level trace --config-file "${DECOHERE_CONFIG}" forge dependencies --configuration release sync`;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (DRY_RUN) {
|
|
17
|
+
console.log(`[DRY RUN] ${baseCmd}`);
|
|
18
|
+
execSync(baseCmd, { stdio: "inherit" });
|
|
19
|
+
} else {
|
|
20
|
+
const commitCmd = `${baseCmd} --commit --message "chore: version packages and sync cross-library dependencies [skip ci]"`;
|
|
21
|
+
console.log(`Syncing with commit...`);
|
|
22
|
+
execSync(commitCmd, { stdio: "inherit" });
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(`Dependency sync failed: ${e.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
package/tests/lib/lcov.test.mjs
CHANGED
|
@@ -5,15 +5,15 @@ import {
|
|
|
5
5
|
parseLcovFiles,
|
|
6
6
|
} from "../../src/lib/lcov.mjs";
|
|
7
7
|
|
|
8
|
-
const sampleLcov = `SF:projects/forge/
|
|
8
|
+
const sampleLcov = `SF:projects/forge/src/index.ts
|
|
9
9
|
LF:10
|
|
10
10
|
LH:9
|
|
11
11
|
end_of_record
|
|
12
|
-
SF:projects/forge/
|
|
12
|
+
SF:projects/forge/src/build/index.ts
|
|
13
13
|
LF:20
|
|
14
14
|
LH:15
|
|
15
15
|
end_of_record
|
|
16
|
-
SF:projects/
|
|
16
|
+
SF:projects/nexus/src/index.ts
|
|
17
17
|
LF:5
|
|
18
18
|
LH:5
|
|
19
19
|
end_of_record
|
|
@@ -32,7 +32,7 @@ end_of_record
|
|
|
32
32
|
|
|
33
33
|
describe("getProjectFromPath", () => {
|
|
34
34
|
it("extracts project name from projects/ path", () => {
|
|
35
|
-
expect(getProjectFromPath("projects/forge/
|
|
35
|
+
expect(getProjectFromPath("projects/forge/src/index.ts")).toBe("forge");
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("returns undefined for unrecognized paths", () => {
|
|
@@ -46,13 +46,13 @@ describe("parseLcovByProject", () => {
|
|
|
46
46
|
expect(result.get("forge")).toBeDefined();
|
|
47
47
|
expect(result.get("forge").totalLines).toBe(30);
|
|
48
48
|
expect(result.get("forge").coveredLines).toBe(24);
|
|
49
|
-
expect(result.get("
|
|
50
|
-
expect(result.get("
|
|
51
|
-
expect(result.get("
|
|
49
|
+
expect(result.get("nexus")).toBeDefined();
|
|
50
|
+
expect(result.get("nexus").totalLines).toBe(5);
|
|
51
|
+
expect(result.get("nexus").coveredLines).toBe(5);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("ignores records with LF:0", () => {
|
|
55
|
-
const lcov = `SF:projects/forge/
|
|
55
|
+
const lcov = `SF:projects/forge/src/empty.ts\nLF:0\nLH:0\nend_of_record\n`;
|
|
56
56
|
const result = parseLcovByProject(lcov);
|
|
57
57
|
expect(result.size).toBe(0);
|
|
58
58
|
});
|