@decohere/ci-tools 0.0.1-dev.126 → 0.0.1-dev.13

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.
Files changed (40) hide show
  1. package/package.json +3 -18
  2. package/src/audit/enforce.mjs +5 -7
  3. package/src/audit/summarize.mjs +32 -10
  4. package/src/audit/upsert-issue.mjs +74 -25
  5. package/src/bench/bench.mjs +368 -134
  6. package/src/bench/trace.mjs +14 -18
  7. package/src/coverage/enforce-thresholds.mjs +144 -0
  8. package/src/pages/artifact.mjs +183 -38
  9. package/src/pages/clear-bench-history.mjs +27 -0
  10. package/src/pages/keep-prior-bench-badge.mjs +38 -0
  11. package/src/pages/restore-bench-history.mjs +28 -15
  12. package/src/release/npm-links.mjs +13 -6
  13. package/src/release/sync-dependencies.mjs +2 -1
  14. package/tests/lib/lcov.test.mjs +4 -4
  15. package/LICENSE +0 -15
  16. package/src/audit/format.mjs +0 -133
  17. package/src/bench/file-regressions.mjs +0 -76
  18. package/src/bench/history.mjs +0 -248
  19. package/src/bench/nightly.mjs +0 -118
  20. package/src/bench/regression-issue.mjs +0 -44
  21. package/src/github/credential-nudge.mjs +0 -226
  22. package/src/github/npm-bootstrap.mjs +0 -179
  23. package/src/github/oidc-bootstrap-nudge.mjs +0 -214
  24. package/src/github/publish-verify.mjs +0 -206
  25. package/src/github/smoke-create-launcher.mjs +0 -70
  26. package/src/lib/github-issue.mjs +0 -75
  27. package/src/lib/workspace-packages.mjs +0 -30
  28. package/src/pages/bench-page.mjs +0 -323
  29. package/src/pages/restore-badges.mjs +0 -58
  30. package/tests/audit/format.test.mjs +0 -115
  31. package/tests/bench/history.test.mjs +0 -279
  32. package/tests/bench/regression-issue.test.mjs +0 -42
  33. package/tests/github/credential-nudge.test.mjs +0 -112
  34. package/tests/github/npm-bootstrap.test.mjs +0 -125
  35. package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
  36. package/tests/github/package-metadata.test.mjs +0 -69
  37. package/tests/github/publish-verify.test.mjs +0 -115
  38. package/tests/github/smoke-create-launcher.test.mjs +0 -71
  39. package/tests/lib/github-issue.test.mjs +0 -85
  40. package/tests/pages/bench-page.test.mjs +0 -123
@@ -1,69 +0,0 @@
1
- import { join, relative } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { vol } from "memfs";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { listPublishablePackages } from "../../src/github/publish-verify.mjs";
6
-
7
- const fs = await vi.importActual("node:fs");
8
- const REPO_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
9
- const REPO = "https://github.com/typesmiths/decohere";
10
-
11
- /** The repo's real package.json files, loaded into the in-memory fs the tests run on. */
12
- function loadRealManifests() {
13
- /** @type {Record<string, string>} */
14
- const files = {};
15
- const walk = (dir) => {
16
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
17
- const path = join(dir, entry.name);
18
- if (entry.isDirectory() && !["node_modules", "dist"].includes(entry.name)) {
19
- walk(path);
20
- } else if (entry.name === "package.json") {
21
- files[join("/repo", relative(REPO_ROOT, path))] = fs.readFileSync(path, "utf8");
22
- }
23
- }
24
- };
25
- walk(join(REPO_ROOT, "projects"));
26
- return files;
27
- }
28
-
29
- afterEach(() => {
30
- vol.reset();
31
- });
32
-
33
- // npm shows these on each package's page, and Trusted Publishing's provenance is
34
- // checked against `repository.url`, so a package without it fails to publish.
35
- describe("published package metadata", () => {
36
- vol.fromJSON(loadRealManifests());
37
- const packages = listPublishablePackages("/repo").map((pkg) => ({
38
- ...pkg,
39
- json: JSON.parse(String(vol.readFileSync(`/repo/projects/${pkg.dir}/package.json`))),
40
- }));
41
- vol.reset();
42
-
43
- it("finds the publishable packages", () => {
44
- expect(packages.length).toBeGreaterThan(0);
45
- });
46
-
47
- it.each(packages.map((pkg) => [pkg.name, pkg]))(
48
- "%s links back to the repo",
49
- (_name, pkg) => {
50
- expect(pkg.json.repository).toEqual({
51
- type: "git",
52
- url: `git+${REPO}.git`,
53
- directory: `projects/${pkg.dir}`,
54
- });
55
- expect(pkg.json.homepage).toBe(`${REPO}/tree/main/projects/${pkg.dir}#readme`);
56
- expect(pkg.json.bugs).toEqual({ url: `${REPO}/issues` });
57
- },
58
- );
59
-
60
- it.each(packages.map((pkg) => [pkg.name, pkg]))(
61
- "%s has a description, keywords, license and author",
62
- (_name, pkg) => {
63
- expect(pkg.json.description).toBeTruthy();
64
- expect(pkg.json.keywords?.length).toBeGreaterThan(0);
65
- expect(pkg.json.license).toBe("ISC");
66
- expect(pkg.json.author).toBeTruthy();
67
- },
68
- );
69
- });
@@ -1,115 +0,0 @@
1
- import { vol } from "memfs";
2
- import { afterEach, describe, expect, it } from "vitest";
3
- import {
4
- collectPublishVerify,
5
- formatGithubOutput,
6
- listPublishablePackages,
7
- parseGhcrRepository,
8
- } from "../../src/github/publish-verify.mjs";
9
-
10
- function seedWorkspace() {
11
- const root = "/workspace";
12
- vol.fromJSON({
13
- [`${root}/package.json`]: JSON.stringify({ version: "0.0.1-dev.79" }),
14
- [`${root}/projects/cli/package.json`]: JSON.stringify({
15
- name: "@decohere/cli",
16
- version: "0.0.1-dev.79",
17
- }),
18
- [`${root}/projects/scaffold/package.json`]: JSON.stringify({
19
- name: "@decohere/scaffold",
20
- version: "0.0.1-dev.79",
21
- private: true,
22
- }),
23
- [`${root}/projects/create/package.json`]: JSON.stringify({
24
- name: "@decohere/create",
25
- version: "0.0.1-dev.79",
26
- decohere: { dockerImageRepository: "ghcr.io/typesmiths/decohere/create" },
27
- }),
28
- [`${root}/projects/devcontainer/package.json`]: JSON.stringify({
29
- name: "@decohere/devcontainer",
30
- private: true,
31
- decohere: { dockerImageRepository: "ghcr.io/typesmiths/decohere/devcontainer" },
32
- }),
33
- });
34
- return root;
35
- }
36
-
37
- afterEach(() => {
38
- vol.reset();
39
- });
40
-
41
- describe("publish-verify", () => {
42
- it("lists non-private workspace packages", () => {
43
- const root = seedWorkspace();
44
- expect(listPublishablePackages(root).map((pkg) => pkg.name)).toEqual([
45
- "@decohere/cli",
46
- "@decohere/create",
47
- ]);
48
- });
49
-
50
- it("lists grouped packages (projects/<group>/<name>) and skips nested and build folders", () => {
51
- const root = seedWorkspace();
52
- vol.fromJSON({
53
- [`${root}/projects/data/data/package.json`]: JSON.stringify({ name: "@decohere/data" }),
54
- [`${root}/projects/data/data-typeorm/package.json`]: JSON.stringify({
55
- name: "@decohere/data-typeorm",
56
- }),
57
- [`${root}/projects/cli/examples/demo/package.json`]: JSON.stringify({ name: "nested" }),
58
- [`${root}/projects/__fixtures__/app/package.json`]: JSON.stringify({ name: "fixture" }),
59
- [`${root}/projects/node_modules/dep/package.json`]: JSON.stringify({ name: "dep" }),
60
- [`${root}/projects/dist/out/package.json`]: JSON.stringify({ name: "built" }),
61
- });
62
- expect(listPublishablePackages(root).map((pkg) => pkg.name)).toEqual([
63
- "@decohere/cli",
64
- "@decohere/create",
65
- "@decohere/data",
66
- "@decohere/data-typeorm",
67
- ]);
68
- });
69
-
70
- it("parses GHCR repository coordinates", () => {
71
- expect(parseGhcrRepository("ghcr.io/typesmiths/decohere/create")).toEqual({
72
- org: "typesmiths",
73
- packageName: "decohere/create",
74
- });
75
- });
76
-
77
- it("marks names missing from npm as unpublished and skip_npm when every version exists", async () => {
78
- const root = seedWorkspace();
79
- const views = [];
80
- const spawnSync = (_cmd, args) => {
81
- views.push(args[1]);
82
- if (args[1] === "@decohere/cli") {
83
- return { status: 1 };
84
- }
85
- return { status: 0 };
86
- };
87
- const result = await collectPublishVerify({
88
- root,
89
- spawnSync,
90
- githubToken: "",
91
- });
92
- expect(result.unpublished).toEqual(["@decohere/cli"]);
93
- expect(result.skipNpm).toBe(false);
94
- expect(views).toContain("@decohere/cli");
95
- expect(views).toContain("@decohere/create");
96
- expect(views).toContain("@decohere/create@0.0.1-dev.79");
97
- });
98
-
99
- it("sets skip_docker when both GHCR tags exist", async () => {
100
- const root = seedWorkspace();
101
- const fetch = async () => ({
102
- ok: true,
103
- status: 200,
104
- json: async () => [{ metadata: { container: { tags: ["0.0.1-dev.79"] } } }],
105
- });
106
- const result = await collectPublishVerify({
107
- root,
108
- spawnSync: () => ({ status: 0 }),
109
- githubToken: "ghs_test",
110
- fetch,
111
- });
112
- expect(result.skipDocker).toBe(true);
113
- expect(formatGithubOutput(result)).toContain("skip_docker=true");
114
- });
115
- });
@@ -1,71 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { runCreateLauncherSmoke } from "../../src/github/smoke-create-launcher.mjs";
4
-
5
- const fs = await vi.importActual("node:fs");
6
- const PUBLISH_YML = fileURLToPath(
7
- new URL("../../../../.github/workflows/publish.yml", import.meta.url),
8
- );
9
-
10
- describe("runCreateLauncherSmoke", () => {
11
- it("execs npx from a temp directory, not the repo root", () => {
12
- /** @type {{ cmd: string, args: string[], opts: { cwd?: string } }[]} */
13
- const calls = [];
14
- const written = [];
15
-
16
- const result = runCreateLauncherSmoke({
17
- createVersion: "0.0.1-dev.80",
18
- authToken: "tok",
19
- tmpdir: () => "/tmp",
20
- mkdtempSync: (prefix) => `${prefix}abc`,
21
- writeFileSync: (path, body) => {
22
- written.push({ path, body });
23
- },
24
- spawnSync: (cmd, args, opts) => {
25
- calls.push({ cmd, args, opts });
26
- return { status: 0 };
27
- },
28
- stdio: "pipe",
29
- });
30
-
31
- expect(result.workDir).toBe("/tmp/decohere-create-smoke-abc");
32
- expect(result.workDir).not.toBe(process.cwd());
33
- expect(calls).toHaveLength(1);
34
- expect(calls[0].cmd).toBe("npx");
35
- expect(calls[0].args).toEqual(["--yes", "@decohere/create@0.0.1-dev.80", "--help"]);
36
- expect(calls[0].opts.cwd).toBe("/tmp/decohere-create-smoke-abc");
37
- expect(calls[0].opts.cwd).not.toBe(process.cwd());
38
- expect(written).toEqual([
39
- {
40
- path: "/tmp/decohere-create-smoke-abc/.npmrc",
41
- body: "@decohere:registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=tok\n",
42
- },
43
- ]);
44
- });
45
-
46
- it("fails closed without version or token", () => {
47
- expect(() => runCreateLauncherSmoke({ createVersion: "", authToken: "tok" })).toThrow(
48
- "CREATE_VERSION is required",
49
- );
50
- expect(() =>
51
- runCreateLauncherSmoke({ createVersion: "0.0.1-dev.80", authToken: "" }),
52
- ).toThrow("NODE_AUTH_TOKEN is required");
53
- });
54
- });
55
-
56
- describe("publish.yml create smoke + tag", () => {
57
- const yml = fs.readFileSync(PUBLISH_YML, "utf8");
58
-
59
- it("wires create smoke through the isolated launcher", () => {
60
- expect(yml).toContain("node projects/ci-tools/src/github/smoke-create-launcher.mjs");
61
- expect(yml).toContain("CREATE_VERSION:");
62
- expect(yml).not.toContain("npx --yes");
63
- });
64
-
65
- it("grants the tag job actions:read so publish-prepare can download CI artifacts", () => {
66
- const tag = yml.slice(yml.indexOf("\n tag:"));
67
- const perms = tag.slice(tag.indexOf("permissions:"), tag.indexOf("steps:"));
68
- expect(perms).toMatch(/contents:\s*write/);
69
- expect(perms).toMatch(/actions:\s*read/);
70
- });
71
- });
@@ -1,85 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { pickIssue, upsertIssue } from "../../src/lib/github-issue.mjs";
3
-
4
- const TITLE = "chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)";
5
-
6
- /** A fake `gh` that answers `issue list` with `issues` and records every call. */
7
- function fakeGh(issues) {
8
- /** @type {{ args: string[], input?: string }[]} */
9
- const calls = [];
10
- const run = (args, input) => {
11
- calls.push({ args, input });
12
- if (args[1] === "list") return JSON.stringify(issues);
13
- if (args[1] === "create") return "https://github.com/typesmiths/decohere/issues/42\n";
14
- return "";
15
- };
16
- return { run, calls };
17
- }
18
-
19
- describe("github-issue", () => {
20
- it("matches the exact title only and prefers an open issue", () => {
21
- const issues = [
22
- { number: 1, title: TITLE, state: "CLOSED", url: "u1" },
23
- { number: 2, title: `${TITLE} (old)`, state: "OPEN", url: "u2" },
24
- { number: 3, title: TITLE, state: "OPEN", url: "u3" },
25
- ];
26
- expect(pickIssue(issues, TITLE)?.number).toBe(3);
27
- expect(pickIssue(issues.slice(0, 2), TITLE)?.number).toBe(1);
28
- expect(pickIssue([], TITLE)).toBeNull();
29
- });
30
-
31
- // GitHub's search can't match `security(ci): …` unquoted, so the lookup found nothing
32
- // and a new issue was opened on every run.
33
- it("searches for the title as a quoted phrase", () => {
34
- const { run, calls } = fakeGh([]);
35
- upsertIssue({ title: "security(ci): npm audit vulnerabilities detected", body: "b" }, run);
36
- const args = calls[0].args;
37
- expect(args[args.indexOf("--search") + 1]).toBe(
38
- '"security(ci): npm audit vulnerabilities detected" in:title',
39
- );
40
- });
41
-
42
- it("opens an issue with assignees when none has the title", () => {
43
- const { run, calls } = fakeGh([]);
44
- const result = upsertIssue({ title: TITLE, body: "body", assignees: ["das974"] }, run);
45
- expect(result).toEqual({
46
- number: 42,
47
- url: "https://github.com/typesmiths/decohere/issues/42",
48
- created: true,
49
- });
50
- expect(calls[1]).toEqual({
51
- args: [
52
- "issue",
53
- "create",
54
- "--title",
55
- TITLE,
56
- "--body-file",
57
- "-",
58
- "--add-assignee",
59
- "das974",
60
- ],
61
- input: "body",
62
- });
63
- });
64
-
65
- it("updates an open issue's body in place", () => {
66
- const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "OPEN", url: "u7" }]);
67
- const result = upsertIssue({ title: TITLE, body: "new body" }, run);
68
- expect(result).toEqual({ number: 7, url: "u7", created: false });
69
- expect(calls.map((call) => call.args[1])).toEqual(["list", "edit"]);
70
- expect(calls[1]).toEqual({
71
- args: ["issue", "edit", "7", "--body-file", "-"],
72
- input: "new body",
73
- });
74
- });
75
-
76
- it("reopens a closed issue before updating it", () => {
77
- const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "CLOSED", url: "u7" }]);
78
- upsertIssue({ title: TITLE, body: "b", repo: "typesmiths/decohere" }, run);
79
- expect(calls.map((call) => call.args.slice(0, 3))).toEqual([
80
- ["issue", "list", "--repo"],
81
- ["issue", "reopen", "7"],
82
- ["issue", "edit", "7"],
83
- ]);
84
- });
85
- });
@@ -1,123 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildBenchmarkHtml, trendSvg } from "../../src/pages/bench-page.mjs";
3
-
4
- const context = (benchEnv, cores) => ({
5
- benchEnv,
6
- os: { platform: "linux", arch: "x64" },
7
- cpu: { model: "Test CPU", count: cores },
8
- memoryGb: 16,
9
- node: "v24.1.0",
10
- });
11
-
12
- const run = (date, sha, results) => ({ date, sha, results });
13
- const result = (id, median, label = id) => ({
14
- id,
15
- label,
16
- unit: "ms",
17
- median,
18
- p90: median * 1.2,
19
- min: median,
20
- max: median,
21
- samples: 5,
22
- });
23
-
24
- const history = {
25
- version: 2,
26
- contexts: {
27
- ci: {
28
- context: context("gh-actions-node24", 4),
29
- runs: [
30
- run("2026-10-20T03:17:00Z", "bbbbbbbbbbbb", [
31
- result("flux.serve.start", 90, "spawn <to> ready"),
32
- result("forge.build.debug", 9),
33
- ]),
34
- run("2026-10-19T03:17:00Z", "aaaaaaaaaaaa", [
35
- result("flux.serve.start", 100, "spawn <to> ready"),
36
- ]),
37
- ],
38
- },
39
- laptop: {
40
- context: context("(local)", 24),
41
- runs: [run("2026-10-01T10:00:00Z", "cccccccccccc", [result("flux.serve.start", 40)])],
42
- },
43
- },
44
- };
45
-
46
- describe("buildBenchmarkHtml", () => {
47
- it("explains an empty history instead of rendering empty charts", () => {
48
- const html = buildBenchmarkHtml({ version: 2, contexts: {} });
49
- expect(html).toContain("No benchmark history yet");
50
- expect(html).not.toContain("<svg");
51
- });
52
-
53
- it("draws one card per benchmark, grouped, with a line per machine that ran it", () => {
54
- const html = buildBenchmarkHtml(history, { branch: "main", sha: "0123456789abcdef" });
55
-
56
- expect(html.match(/<article class="card">/g)).toHaveLength(2);
57
- expect(html.indexOf("flux serve (real process)")).toBeLessThan(
58
- html.indexOf("<h2>forge build</h2>"),
59
- );
60
- const serveCard = html.slice(
61
- html.indexOf("<code>flux.serve.start</code>"),
62
- html.indexOf("<code>forge.build.debug</code>"),
63
- );
64
- expect(serveCard.match(/<polyline/g)).toHaveLength(2);
65
- expect(html).toContain("built from main @ <code>0123456789ab</code>");
66
- });
67
-
68
- it("shows the latest median and the change from the previous run on the newest machine", () => {
69
- const html = buildBenchmarkHtml(history);
70
- expect(html).toContain("<strong>90.0 ms</strong> median · p90 108 ms");
71
- expect(html).toContain('<span class="change better">10.0% faster</span>');
72
- });
73
-
74
- it("dates each result and links its commit, in the latest table and on each card", () => {
75
- const html = buildBenchmarkHtml(history);
76
- expect(html).toContain("<th>Run</th><th>Commit</th>");
77
- expect(html).toContain("<td>2026-10-20 03:17 UTC</td>");
78
- expect(html).toContain(
79
- '<a href="https://github.com/typesmiths/decohere/commit/bbbbbbbbbbbb"><code>bbbbbbb</code></a>',
80
- );
81
- expect(html).toContain('<p class="when">Last run 2026-10-20 03:17 UTC</p>');
82
- });
83
-
84
- it("leaves out machine types with no run in the 30 days before the newest run", () => {
85
- const html = buildBenchmarkHtml({
86
- version: 2,
87
- contexts: {
88
- ...history.contexts,
89
- gone: {
90
- context: context("gh-actions-node24-retired", 2),
91
- runs: [
92
- run("2026-08-01T03:17:00Z", "dddddddddddd", [result("flux.serve.start", 70)]),
93
- ],
94
- },
95
- },
96
- });
97
- expect(html).not.toContain("gh-actions-node24-retired");
98
- expect(html).toContain("(local)");
99
- });
100
-
101
- it("escapes labels from the history", () => {
102
- const html = buildBenchmarkHtml(history);
103
- expect(html).toContain("spawn &lt;to&gt; ready");
104
- expect(html).not.toContain("spawn <to> ready");
105
- });
106
- });
107
-
108
- describe("trendSvg", () => {
109
- it("places a single point without dividing by a zero time range", () => {
110
- const svg = trendSvg([
111
- {
112
- color: "red",
113
- points: [{ time: 1, date: "2026-10-20", sha: "abc", median: 5, p90: 6 }],
114
- },
115
- ]);
116
- expect(svg).toContain("<circle");
117
- expect(svg).not.toContain("NaN");
118
- });
119
-
120
- it("draws nothing when no machine has a point", () => {
121
- expect(trendSvg([])).toBe("");
122
- });
123
- });