@decohere/ci-tools 0.0.1-dev.119 → 0.0.1-dev.12

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 (36) hide show
  1. package/package.json +3 -8
  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/src/audit/format.mjs +0 -133
  16. package/src/bench/file-regressions.mjs +0 -76
  17. package/src/bench/history.mjs +0 -248
  18. package/src/bench/nightly.mjs +0 -118
  19. package/src/bench/regression-issue.mjs +0 -44
  20. package/src/github/credential-nudge.mjs +0 -245
  21. package/src/github/oidc-bootstrap-nudge.mjs +0 -210
  22. package/src/github/publish-verify.mjs +0 -206
  23. package/src/github/smoke-create-launcher.mjs +0 -70
  24. package/src/lib/github-issue.mjs +0 -75
  25. package/src/lib/workspace-packages.mjs +0 -30
  26. package/src/pages/bench-page.mjs +0 -323
  27. package/src/pages/restore-badges.mjs +0 -58
  28. package/tests/audit/format.test.mjs +0 -115
  29. package/tests/bench/history.test.mjs +0 -279
  30. package/tests/bench/regression-issue.test.mjs +0 -42
  31. package/tests/github/credential-nudge.test.mjs +0 -116
  32. package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -89
  33. package/tests/github/publish-verify.test.mjs +0 -115
  34. package/tests/github/smoke-create-launcher.test.mjs +0 -71
  35. package/tests/lib/github-issue.test.mjs +0 -85
  36. package/tests/pages/bench-page.test.mjs +0 -123
@@ -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
- // #681: 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
- });