@decohere/ci-tools 0.0.1-dev.109 → 0.0.1-dev.11
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 +3 -8
- package/src/audit/enforce.mjs +5 -7
- package/src/audit/summarize.mjs +32 -10
- package/src/audit/upsert-issue.mjs +37 -22
- package/src/bench/bench.mjs +3 -13
- package/src/bench/trace.mjs +14 -18
- package/src/coverage/enforce-thresholds.mjs +144 -0
- package/src/pages/clear-bench-history.mjs +27 -0
- package/src/release/sync-dependencies.mjs +2 -1
- package/src/audit/format.mjs +0 -133
- package/src/audit/upsert-clickup.mjs +0 -117
- package/src/github/credential-nudge.mjs +0 -320
- package/src/github/oidc-bootstrap-nudge.mjs +0 -368
- package/src/github/publish-verify.mjs +0 -209
- package/src/github/smoke-create-launcher.mjs +0 -70
- package/tests/audit/format.test.mjs +0 -115
- package/tests/github/credential-nudge.test.mjs +0 -65
- package/tests/github/oidc-bootstrap-nudge.test.mjs +0 -104
- package/tests/github/publish-verify.test.mjs +0 -95
- package/tests/github/smoke-create-launcher.test.mjs +0 -71
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
formatLog,
|
|
4
|
-
formatMarkdown,
|
|
5
|
-
highCritical,
|
|
6
|
-
parseAudit,
|
|
7
|
-
} from "../../src/audit/format.mjs";
|
|
8
|
-
|
|
9
|
-
const fixture = {
|
|
10
|
-
metadata: {
|
|
11
|
-
vulnerabilities: { info: 0, low: 1, moderate: 0, high: 1, critical: 0, total: 2 },
|
|
12
|
-
},
|
|
13
|
-
advisories: {
|
|
14
|
-
1: {
|
|
15
|
-
id: 1,
|
|
16
|
-
module_name: "nanoid",
|
|
17
|
-
severity: "high",
|
|
18
|
-
title: "nanoid: custom generators can loop indefinitely when size is zero",
|
|
19
|
-
url: "https://github.com/advisories/GHSA-2v37-7h3g-55p8",
|
|
20
|
-
vulnerable_versions: "<3.3.18",
|
|
21
|
-
patched_versions: ">=3.3.18",
|
|
22
|
-
cves: [],
|
|
23
|
-
findings: [{ version: "3.3.17", paths: [". > vite > postcss > nanoid@3.3.17"] }],
|
|
24
|
-
},
|
|
25
|
-
2: {
|
|
26
|
-
id: 2,
|
|
27
|
-
module_name: "esbuild",
|
|
28
|
-
severity: "low",
|
|
29
|
-
title: "esbuild Windows file read",
|
|
30
|
-
url: "https://github.com/advisories/GHSA-g7r4-m6w7-qqqr",
|
|
31
|
-
vulnerable_versions: "<0.28.1",
|
|
32
|
-
patched_versions: ">=0.28.1",
|
|
33
|
-
cves: [],
|
|
34
|
-
findings: [{ version: "0.27.3", paths: [". > vite > esbuild"] }],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
describe("parseAudit", () => {
|
|
40
|
-
it("reads severity counts and sorts advisories critical/high first", () => {
|
|
41
|
-
const parsed = parseAudit(fixture);
|
|
42
|
-
expect(parsed.total).toBe(2);
|
|
43
|
-
expect(parsed.high).toBe(1);
|
|
44
|
-
expect(parsed.low).toBe(1);
|
|
45
|
-
expect(parsed.advisories.map((a) => a.module)).toEqual(["nanoid", "esbuild"]);
|
|
46
|
-
expect(parsed.advisories[0]?.severity).toBe("high");
|
|
47
|
-
expect(parsed.advisories[0]?.ghsa).toBe("GHSA-2v37-7h3g-55p8");
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("formatLog", () => {
|
|
52
|
-
it("prints severity counts and advisory lines including GHSA", () => {
|
|
53
|
-
const log = formatLog(parseAudit(fixture));
|
|
54
|
-
expect(log).toContain("high=1");
|
|
55
|
-
expect(log).toContain("[high] nanoid GHSA-2v37-7h3g-55p8");
|
|
56
|
-
expect(log).toContain("[low] esbuild");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("formatMarkdown", () => {
|
|
61
|
-
it("includes a severity table and linked advisories", () => {
|
|
62
|
-
const md = formatMarkdown(parseAudit(fixture));
|
|
63
|
-
expect(md).toContain("| High | 1 |");
|
|
64
|
-
expect(md).toContain("**high** `nanoid`");
|
|
65
|
-
expect(md).toContain("GHSA-2v37-7h3g-55p8");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("highCritical", () => {
|
|
70
|
-
it("filters to high and critical only", () => {
|
|
71
|
-
expect(highCritical(parseAudit(fixture)).map((a) => a.module)).toEqual(["nanoid"]);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("parseAudit npm v2", () => {
|
|
76
|
-
it("dedupes GHSA rows from package via objects", () => {
|
|
77
|
-
const v2 = {
|
|
78
|
-
auditReportVersion: 2,
|
|
79
|
-
metadata: {
|
|
80
|
-
vulnerabilities: { info: 0, low: 0, moderate: 0, high: 1, critical: 0, total: 1 },
|
|
81
|
-
},
|
|
82
|
-
vulnerabilities: {
|
|
83
|
-
nanoid: {
|
|
84
|
-
name: "nanoid",
|
|
85
|
-
severity: "high",
|
|
86
|
-
via: [
|
|
87
|
-
{
|
|
88
|
-
source: 1103848,
|
|
89
|
-
name: "nanoid",
|
|
90
|
-
title: "nanoid infinite loop",
|
|
91
|
-
url: "https://github.com/advisories/GHSA-2v37-7h3g-55p8",
|
|
92
|
-
severity: "high",
|
|
93
|
-
range: "<3.3.18",
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
range: "<3.3.18",
|
|
97
|
-
nodes: ["node_modules/.pnpm/nanoid@3.3.17/node_modules/nanoid"],
|
|
98
|
-
},
|
|
99
|
-
postcss: {
|
|
100
|
-
name: "postcss",
|
|
101
|
-
severity: "high",
|
|
102
|
-
via: ["nanoid"],
|
|
103
|
-
effects: [],
|
|
104
|
-
range: "*",
|
|
105
|
-
nodes: ["node_modules/.pnpm/postcss@8.5.26/node_modules/postcss"],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
const parsed = parseAudit(v2);
|
|
110
|
-
expect(parsed.advisories).toHaveLength(1);
|
|
111
|
-
expect(parsed.advisories[0]?.module).toBe("nanoid");
|
|
112
|
-
expect(parsed.advisories[0]?.ghsa).toBe("GHSA-2v37-7h3g-55p8");
|
|
113
|
-
expect(parsed.advisories[0]?.vulnerable).toBe("<3.3.18");
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
assessTokenLifetime,
|
|
4
|
-
buildDescription,
|
|
5
|
-
CREDENTIALS,
|
|
6
|
-
credentialProfile,
|
|
7
|
-
ghScopeFlags,
|
|
8
|
-
parseExpiresAt,
|
|
9
|
-
rotationCommands,
|
|
10
|
-
suggestedNextExpiry,
|
|
11
|
-
taskName,
|
|
12
|
-
} from "../../src/github/credential-nudge.mjs";
|
|
13
|
-
|
|
14
|
-
describe("credential-nudge", () => {
|
|
15
|
-
it("parses YYYY-MM-DD as a UTC day", () => {
|
|
16
|
-
expect(parseExpiresAt("2026-11-19")).toBe(Date.UTC(2026, 10, 19));
|
|
17
|
-
expect(parseExpiresAt("")).toBeNull();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("is ok more than 14 days out, warns inside the window, expires on the day", () => {
|
|
21
|
-
const expiresAtMs = Date.UTC(2026, 10, 19);
|
|
22
|
-
const now = (day) => Date.UTC(2026, 10, day, 15);
|
|
23
|
-
expect(assessTokenLifetime({ now: now(1), expiresAtMs }).kind).toBe("ok");
|
|
24
|
-
expect(assessTokenLifetime({ now: now(5), expiresAtMs })).toEqual({
|
|
25
|
-
kind: "warn",
|
|
26
|
-
daysLeft: 14,
|
|
27
|
-
});
|
|
28
|
-
expect(assessTokenLifetime({ now: now(18), expiresAtMs }).kind).toBe("warn");
|
|
29
|
-
expect(assessTokenLifetime({ now: now(19), expiresAtMs })).toEqual({
|
|
30
|
-
kind: "expired",
|
|
31
|
-
daysLeft: 0,
|
|
32
|
-
});
|
|
33
|
-
expect(assessTokenLifetime({ now: now(20), expiresAtMs }).kind).toBe("expired");
|
|
34
|
-
expect(assessTokenLifetime({ expiresAtMs: null }).kind).toBe("missing");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("parameterizes GHCR vs npm rotate commands without duplicating date logic", () => {
|
|
38
|
-
const ghcr = credentialProfile("ghcr");
|
|
39
|
-
const npm = credentialProfile("npm");
|
|
40
|
-
expect(ghScopeFlags(ghcr)).toBe("--env ghcr-publish ");
|
|
41
|
-
expect(ghScopeFlags(npm)).toBe("");
|
|
42
|
-
expect(rotationCommands(ghcr, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
43
|
-
"gh secret set GHCR_PUSH_TOKEN --env ghcr-publish --repo typesmiths/decohere",
|
|
44
|
-
);
|
|
45
|
-
expect(rotationCommands(npm, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
46
|
-
"gh secret set NODE_AUTH_TOKEN --repo typesmiths/decohere",
|
|
47
|
-
);
|
|
48
|
-
expect(rotationCommands(npm, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
49
|
-
"gh variable set NODE_AUTH_TOKEN_EXPIRES_AT --repo typesmiths/decohere --body 2027-02-17",
|
|
50
|
-
);
|
|
51
|
-
expect(taskName(npm, "2026-11-19")).toBe(
|
|
52
|
-
"chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)",
|
|
53
|
-
);
|
|
54
|
-
const body = buildDescription(ghcr, {
|
|
55
|
-
kind: "warn",
|
|
56
|
-
expiresYmd: "2026-11-19",
|
|
57
|
-
daysLeft: 14,
|
|
58
|
-
rejected: false,
|
|
59
|
-
});
|
|
60
|
-
expect(body).toContain("Docker login/push and the `v*` tag will **fail**");
|
|
61
|
-
expect(CREDENTIALS.npm.secret).toBe("NODE_AUTH_TOKEN");
|
|
62
|
-
expect(suggestedNextExpiry(Date.UTC(2026, 7, 21), 90)).toBe("2026-11-19");
|
|
63
|
-
expect(() => credentialProfile("nope")).toThrow(/Unknown NUDGE_CREDENTIAL/);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
bootstrapTaskNames,
|
|
4
|
-
buildDescription,
|
|
5
|
-
extractGitHubReviewerLogins,
|
|
6
|
-
firstPublishCommands,
|
|
7
|
-
mapLoginsToClickUpIds,
|
|
8
|
-
needsPrereleaseTag,
|
|
9
|
-
parseAssigneeIds,
|
|
10
|
-
parseUnpublished,
|
|
11
|
-
runNudge,
|
|
12
|
-
taskName,
|
|
13
|
-
} from "../../src/github/oidc-bootstrap-nudge.mjs";
|
|
14
|
-
|
|
15
|
-
describe("oidc-bootstrap-nudge", () => {
|
|
16
|
-
it("parses unpublished names and assignee ids", () => {
|
|
17
|
-
expect(parseUnpublished(" @decohere/scaffold @decohere/cli ")).toEqual([
|
|
18
|
-
"@decohere/scaffold",
|
|
19
|
-
"@decohere/cli",
|
|
20
|
-
]);
|
|
21
|
-
expect(parseAssigneeIds("308432336, 99")).toEqual([308432336, 99]);
|
|
22
|
-
expect(parseAssigneeIds("")).toEqual([]);
|
|
23
|
-
expect(taskName()).toBe("deploy(publish): OIDC bootstrap unpublished packages");
|
|
24
|
-
expect(bootstrapTaskNames()).toContain(
|
|
25
|
-
"chore(publish): OIDC bootstrap unpublished packages",
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("extracts GitHub user reviewers and maps login to ClickUp email local-part", () => {
|
|
30
|
-
expect(
|
|
31
|
-
extractGitHubReviewerLogins({
|
|
32
|
-
protection_rules: [
|
|
33
|
-
{ type: "branch_policy", reviewers: [] },
|
|
34
|
-
{
|
|
35
|
-
type: "required_reviewers",
|
|
36
|
-
reviewers: [
|
|
37
|
-
{ type: "User", reviewer: { login: "das974" } },
|
|
38
|
-
{ type: "Team", reviewer: { slug: "publishers", login: "should-skip" } },
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
}),
|
|
43
|
-
).toEqual(["das974"]);
|
|
44
|
-
expect(
|
|
45
|
-
mapLoginsToClickUpIds(
|
|
46
|
-
["das974"],
|
|
47
|
-
[{ id: 308432336, username: "Daniel Scalamogna", email: "das974@gmail.com" }],
|
|
48
|
-
),
|
|
49
|
-
).toEqual([308432336]);
|
|
50
|
-
expect(mapLoginsToClickUpIds(["other"], [{ id: 1, email: "das974@gmail.com" }])).toEqual(
|
|
51
|
-
[],
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("puts first-publish commands and Trusted Publisher fields in the task body", () => {
|
|
56
|
-
const pkg = { name: "@decohere/scaffold", dir: "scaffold", version: "0.0.1-dev.18" };
|
|
57
|
-
const body = buildDescription({
|
|
58
|
-
unpublished: ["@decohere/scaffold"],
|
|
59
|
-
packages: [pkg],
|
|
60
|
-
repo: "typesmiths/decohere",
|
|
61
|
-
runUrl: "https://github.com/typesmiths/decohere/actions/runs/9",
|
|
62
|
-
channel: "dev",
|
|
63
|
-
});
|
|
64
|
-
expect(needsPrereleaseTag("0.0.1-dev.18")).toBe(true);
|
|
65
|
-
expect(needsPrereleaseTag("1.2.3")).toBe(false);
|
|
66
|
-
expect(firstPublishCommands([pkg], "dev")).toEqual([
|
|
67
|
-
"npm publish ./projects/scaffold --tag dev",
|
|
68
|
-
]);
|
|
69
|
-
expect(firstPublishCommands([{ ...pkg, version: "1.0.0" }], "dev")).toEqual([
|
|
70
|
-
"npm publish ./projects/scaffold",
|
|
71
|
-
]);
|
|
72
|
-
expect(body).toContain("npm publish ./projects/scaffold --tag dev");
|
|
73
|
-
expect(body).not.toMatch(/# @decohere\/scaffold/);
|
|
74
|
-
expect(body).toContain("do **not** pass `--access public`");
|
|
75
|
-
expect(firstPublishCommands([pkg], "dev").join("\n")).not.toContain("--access public");
|
|
76
|
-
expect(body).toContain("Workflow filename: `publish.yml`");
|
|
77
|
-
expect(body).toContain("Environment: `npm-publish`");
|
|
78
|
-
expect(body).toContain("gh run rerun --failed");
|
|
79
|
-
expect(body).toContain("https://github.com/typesmiths/decohere/actions/runs/9");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("assigns mapped GitHub reviewers when upserting the ClickUp task", async () => {
|
|
83
|
-
/** @type {object[]} */
|
|
84
|
-
const upserts = [];
|
|
85
|
-
const code = await runNudge(
|
|
86
|
-
{ UNPUBLISHED: "@decohere/scaffold", GITHUB_REPOSITORY: "typesmiths/decohere" },
|
|
87
|
-
{
|
|
88
|
-
listPackages: () => [{ name: "@decohere/scaffold", dir: "scaffold" }],
|
|
89
|
-
githubReviewers: async () => ["das974"],
|
|
90
|
-
clickupMembers: async () => [
|
|
91
|
-
{ id: 308432336, username: "Daniel Scalamogna", email: "das974@gmail.com" },
|
|
92
|
-
],
|
|
93
|
-
upsert: async (options) => {
|
|
94
|
-
upserts.push(options);
|
|
95
|
-
return { id: "abc", url: "https://app.clickup.com/t/abc", created: true };
|
|
96
|
-
},
|
|
97
|
-
runUrl: "https://github.com/typesmiths/decohere/actions/runs/1",
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
expect(code).toBe(0);
|
|
101
|
-
expect(upserts[0].assigneeIds).toEqual([308432336]);
|
|
102
|
-
expect(upserts[0].unpublished).toEqual(["@decohere/scaffold"]);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1,95 +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("parses GHCR repository coordinates", () => {
|
|
51
|
-
expect(parseGhcrRepository("ghcr.io/typesmiths/decohere/create")).toEqual({
|
|
52
|
-
org: "typesmiths",
|
|
53
|
-
packageName: "decohere/create",
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("marks names missing from npm as unpublished and skip_npm when every version exists", async () => {
|
|
58
|
-
const root = seedWorkspace();
|
|
59
|
-
const views = [];
|
|
60
|
-
const spawnSync = (_cmd, args) => {
|
|
61
|
-
views.push(args[1]);
|
|
62
|
-
if (args[1] === "@decohere/cli") {
|
|
63
|
-
return { status: 1 };
|
|
64
|
-
}
|
|
65
|
-
return { status: 0 };
|
|
66
|
-
};
|
|
67
|
-
const result = await collectPublishVerify({
|
|
68
|
-
root,
|
|
69
|
-
spawnSync,
|
|
70
|
-
githubToken: "",
|
|
71
|
-
});
|
|
72
|
-
expect(result.unpublished).toEqual(["@decohere/cli"]);
|
|
73
|
-
expect(result.skipNpm).toBe(false);
|
|
74
|
-
expect(views).toContain("@decohere/cli");
|
|
75
|
-
expect(views).toContain("@decohere/create");
|
|
76
|
-
expect(views).toContain("@decohere/create@0.0.1-dev.79");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("sets skip_docker when both GHCR tags exist", async () => {
|
|
80
|
-
const root = seedWorkspace();
|
|
81
|
-
const fetch = async () => ({
|
|
82
|
-
ok: true,
|
|
83
|
-
status: 200,
|
|
84
|
-
json: async () => [{ metadata: { container: { tags: ["0.0.1-dev.79"] } } }],
|
|
85
|
-
});
|
|
86
|
-
const result = await collectPublishVerify({
|
|
87
|
-
root,
|
|
88
|
-
spawnSync: () => ({ status: 0 }),
|
|
89
|
-
githubToken: "ghs_test",
|
|
90
|
-
fetch,
|
|
91
|
-
});
|
|
92
|
-
expect(result.skipDocker).toBe(true);
|
|
93
|
-
expect(formatGithubOutput(result)).toContain("skip_docker=true");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -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
|
-
});
|