@decohere/ci-tools 0.0.1-dev.111 → 0.0.1-dev.113
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 +2 -2
- package/src/audit/upsert-issue.mjs +5 -39
- package/src/bench/bench.mjs +5 -5
- package/src/bench/file-regressions.mjs +15 -14
- package/src/bench/history.mjs +64 -41
- package/src/bench/nightly.mjs +118 -0
- package/src/bench/regression-issue.mjs +15 -22
- package/src/github/credential-nudge.mjs +22 -97
- package/src/github/oidc-bootstrap-nudge.mjs +24 -182
- package/src/lib/github-issue.mjs +73 -0
- package/src/pages/bench-page.mjs +13 -3
- package/tests/bench/history.test.mjs +58 -30
- package/tests/bench/regression-issue.test.mjs +12 -20
- package/tests/github/credential-nudge.test.mjs +53 -2
- package/tests/github/oidc-bootstrap-nudge.test.mjs +14 -29
- package/tests/lib/github-issue.test.mjs +74 -0
- package/tests/pages/bench-page.test.mjs +17 -0
- package/src/audit/upsert-clickup.mjs +0 -117
|
@@ -4,19 +4,21 @@ import { issueBody, issueTitle } from "../../src/bench/regression-issue.mjs";
|
|
|
4
4
|
const regression = {
|
|
5
5
|
id: "flux.load.cold",
|
|
6
6
|
label: "flux load, first import (cold process)",
|
|
7
|
-
|
|
7
|
+
base: 300,
|
|
8
|
+
head: 400,
|
|
8
9
|
percent: 33.3,
|
|
9
|
-
|
|
10
|
-
{
|
|
11
|
-
{
|
|
10
|
+
rounds: [
|
|
11
|
+
{ base: 300, head: 395 },
|
|
12
|
+
{ base: 300, head: 405 },
|
|
12
13
|
],
|
|
13
|
-
lastGood: { date: "2026-10-20T03:17:00Z", sha: "aaaaaaaaaaaaaaaa" },
|
|
14
14
|
};
|
|
15
15
|
const context = {
|
|
16
16
|
repoUrl: "https://github.com/typesmiths/decohere",
|
|
17
17
|
runUrl: "https://github.com/typesmiths/decohere/actions/runs/1",
|
|
18
18
|
machine: "gh-actions-node24, Test CPU × 4",
|
|
19
19
|
threshold: 0.2,
|
|
20
|
+
base: "aaaaaaaaaaaaaaaa",
|
|
21
|
+
head: "cccccccccccccccc",
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
describe("regression issue", () => {
|
|
@@ -24,27 +26,17 @@ describe("regression issue", () => {
|
|
|
24
26
|
expect(issueTitle(regression)).toBe("perf(bench): flux.load.cold is slower");
|
|
25
27
|
});
|
|
26
28
|
|
|
27
|
-
it("gives
|
|
29
|
+
it("gives both rounds, the commit range and how to reproduce", () => {
|
|
28
30
|
const body = issueBody(regression, context);
|
|
29
31
|
expect(body).toContain(
|
|
30
|
-
"
|
|
31
|
-
);
|
|
32
|
-
expect(body).toContain("- Baseline (median of the runs before): **300.0 ms**");
|
|
33
|
-
expect(body).toContain(
|
|
34
|
-
"- 2026-10-22, [`ccccccc`](https://github.com/typesmiths/decohere/commit/cccccccccccccccc): **400.0 ms**",
|
|
32
|
+
"is 33% slower on [`ccccccc`](https://github.com/typesmiths/decohere/commit/cccccccccccccccc)",
|
|
35
33
|
);
|
|
34
|
+
expect(body).toContain("| 1 | 300.0 ms | 395.0 ms |");
|
|
35
|
+
expect(body).toContain("| 2 | 300.0 ms | 405.0 ms |");
|
|
36
36
|
expect(body).toContain(
|
|
37
37
|
"(https://github.com/typesmiths/decohere/compare/aaaaaaaaaaaaaaaa...cccccccccccccccc)",
|
|
38
38
|
);
|
|
39
|
-
expect(body).toContain("`pnpm bench:save`");
|
|
39
|
+
expect(body).toContain("check out `aaaaaaa`, run `pnpm bench:save`");
|
|
40
40
|
expect(body).toContain(context.runUrl);
|
|
41
41
|
});
|
|
42
|
-
|
|
43
|
-
it("says the range is unknown when the baseline run has no commit (converted old history)", () => {
|
|
44
|
-
const body = issueBody(
|
|
45
|
-
{ ...regression, lastGood: { date: "2026-08-12T00:00:00Z", sha: null } },
|
|
46
|
-
context,
|
|
47
|
-
);
|
|
48
|
-
expect(body).toContain("unknown (no baseline commit recorded)");
|
|
49
|
-
});
|
|
50
42
|
});
|
|
@@ -5,12 +5,32 @@ import {
|
|
|
5
5
|
CREDENTIALS,
|
|
6
6
|
credentialProfile,
|
|
7
7
|
ghScopeFlags,
|
|
8
|
+
issueTitle,
|
|
8
9
|
parseExpiresAt,
|
|
9
10
|
rotationCommands,
|
|
11
|
+
runNudge,
|
|
10
12
|
suggestedNextExpiry,
|
|
11
|
-
taskName,
|
|
12
13
|
} from "../../src/github/credential-nudge.mjs";
|
|
13
14
|
|
|
15
|
+
/** Records each upsert and answers like a newly opened issue. */
|
|
16
|
+
function recordUpserts() {
|
|
17
|
+
/** @type {{ title: string, body: string }[]} */
|
|
18
|
+
const upserts = [];
|
|
19
|
+
const upsert = (options) => {
|
|
20
|
+
upserts.push(options);
|
|
21
|
+
return {
|
|
22
|
+
number: 9,
|
|
23
|
+
url: "https://github.com/typesmiths/decohere/issues/9",
|
|
24
|
+
created: true,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
return { upserts, upsert };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function inDays(days) {
|
|
31
|
+
return suggestedNextExpiry(Date.now(), days);
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
describe("credential-nudge", () => {
|
|
15
35
|
it("parses YYYY-MM-DD as a UTC day", () => {
|
|
16
36
|
expect(parseExpiresAt("2026-11-19")).toBe(Date.UTC(2026, 10, 19));
|
|
@@ -48,7 +68,7 @@ describe("credential-nudge", () => {
|
|
|
48
68
|
expect(rotationCommands(npm, "2027-02-17", "typesmiths/decohere")).toContain(
|
|
49
69
|
"gh variable set NODE_AUTH_TOKEN_EXPIRES_AT --repo typesmiths/decohere --body 2027-02-17",
|
|
50
70
|
);
|
|
51
|
-
expect(
|
|
71
|
+
expect(issueTitle(npm, "2026-11-19")).toBe(
|
|
52
72
|
"chore(security): rotate NODE_AUTH_TOKEN (expires 2026-11-19)",
|
|
53
73
|
);
|
|
54
74
|
const body = buildDescription(ghcr, {
|
|
@@ -62,4 +82,35 @@ describe("credential-nudge", () => {
|
|
|
62
82
|
expect(suggestedNextExpiry(Date.UTC(2026, 7, 21), 90)).toBe("2026-11-19");
|
|
63
83
|
expect(() => credentialProfile("nope")).toThrow(/Unknown NUDGE_CREDENTIAL/);
|
|
64
84
|
});
|
|
85
|
+
|
|
86
|
+
it("files nothing and passes while the token has more than 14 days left", async () => {
|
|
87
|
+
const { upserts, upsert } = recordUpserts();
|
|
88
|
+
const env = { NUDGE_CREDENTIAL: "npm", NODE_AUTH_TOKEN_EXPIRES_AT: inDays(30) };
|
|
89
|
+
expect(await runNudge(env, upsert)).toBe(0);
|
|
90
|
+
expect(upserts).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("files a rotation issue but lets Publish continue inside the warning window", async () => {
|
|
94
|
+
const { upserts, upsert } = recordUpserts();
|
|
95
|
+
const expires = inDays(10);
|
|
96
|
+
const env = { NUDGE_CREDENTIAL: "npm", NODE_AUTH_TOKEN_EXPIRES_AT: expires };
|
|
97
|
+
expect(await runNudge(env, upsert)).toBe(0);
|
|
98
|
+
expect(upserts).toHaveLength(1);
|
|
99
|
+
expect(upserts[0].title).toBe(
|
|
100
|
+
`chore(security): rotate NODE_AUTH_TOKEN (expires ${expires})`,
|
|
101
|
+
);
|
|
102
|
+
expect(upserts[0].body).toContain("Close this issue, then retry Publish");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("files a rotation issue and stops Publish once the token has expired or was rejected", async () => {
|
|
106
|
+
const expired = recordUpserts();
|
|
107
|
+
const env = { NUDGE_CREDENTIAL: "ghcr", GHCR_TOKEN_EXPIRES_AT: inDays(0) };
|
|
108
|
+
expect(await runNudge(env, expired.upsert)).toBe(1);
|
|
109
|
+
expect(expired.upserts).toHaveLength(1);
|
|
110
|
+
|
|
111
|
+
const rejected = recordUpserts();
|
|
112
|
+
const rejectedEnv = { ...env, GHCR_TOKEN_EXPIRES_AT: inDays(60), NUDGE_REJECTED: "1" };
|
|
113
|
+
expect(await runNudge(rejectedEnv, rejected.upsert)).toBe(1);
|
|
114
|
+
expect(rejected.upserts[0].body).toContain(CREDENTIALS.ghcr.rejectedWhy);
|
|
115
|
+
});
|
|
65
116
|
});
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
bootstrapTaskNames,
|
|
4
3
|
buildDescription,
|
|
5
4
|
extractGitHubReviewerLogins,
|
|
6
5
|
firstPublishCommands,
|
|
7
|
-
|
|
6
|
+
issueTitle,
|
|
8
7
|
needsPrereleaseTag,
|
|
9
|
-
parseAssigneeIds,
|
|
10
8
|
parseUnpublished,
|
|
11
9
|
runNudge,
|
|
12
|
-
taskName,
|
|
13
10
|
} from "../../src/github/oidc-bootstrap-nudge.mjs";
|
|
14
11
|
|
|
15
12
|
describe("oidc-bootstrap-nudge", () => {
|
|
16
|
-
it("parses unpublished names
|
|
13
|
+
it("parses unpublished names", () => {
|
|
17
14
|
expect(parseUnpublished(" @decohere/scaffold @decohere/cli ")).toEqual([
|
|
18
15
|
"@decohere/scaffold",
|
|
19
16
|
"@decohere/cli",
|
|
20
17
|
]);
|
|
21
|
-
expect(
|
|
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
|
-
);
|
|
18
|
+
expect(issueTitle()).toBe("deploy(publish): OIDC bootstrap unpublished packages");
|
|
27
19
|
});
|
|
28
20
|
|
|
29
|
-
it("extracts GitHub user reviewers and
|
|
21
|
+
it("extracts GitHub user reviewers and skips teams", () => {
|
|
30
22
|
expect(
|
|
31
23
|
extractGitHubReviewerLogins({
|
|
32
24
|
protection_rules: [
|
|
@@ -41,15 +33,6 @@ describe("oidc-bootstrap-nudge", () => {
|
|
|
41
33
|
],
|
|
42
34
|
}),
|
|
43
35
|
).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
36
|
});
|
|
54
37
|
|
|
55
38
|
it("puts first-publish commands and Trusted Publisher fields in the task body", () => {
|
|
@@ -79,7 +62,7 @@ describe("oidc-bootstrap-nudge", () => {
|
|
|
79
62
|
expect(body).toContain("https://github.com/typesmiths/decohere/actions/runs/9");
|
|
80
63
|
});
|
|
81
64
|
|
|
82
|
-
it("assigns
|
|
65
|
+
it("assigns the bootstrap environment's reviewers to the issue", async () => {
|
|
83
66
|
/** @type {object[]} */
|
|
84
67
|
const upserts = [];
|
|
85
68
|
const code = await runNudge(
|
|
@@ -87,18 +70,20 @@ describe("oidc-bootstrap-nudge", () => {
|
|
|
87
70
|
{
|
|
88
71
|
listPackages: () => [{ name: "@decohere/scaffold", dir: "scaffold" }],
|
|
89
72
|
githubReviewers: async () => ["das974"],
|
|
90
|
-
|
|
91
|
-
{ id: 308432336, username: "Daniel Scalamogna", email: "das974@gmail.com" },
|
|
92
|
-
],
|
|
93
|
-
upsert: async (options) => {
|
|
73
|
+
upsert: (options) => {
|
|
94
74
|
upserts.push(options);
|
|
95
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
number: 5,
|
|
77
|
+
url: "https://github.com/typesmiths/decohere/issues/5",
|
|
78
|
+
created: true,
|
|
79
|
+
};
|
|
96
80
|
},
|
|
97
81
|
runUrl: "https://github.com/typesmiths/decohere/actions/runs/1",
|
|
98
82
|
},
|
|
99
83
|
);
|
|
100
84
|
expect(code).toBe(0);
|
|
101
|
-
expect(upserts[0].
|
|
102
|
-
expect(upserts[0].
|
|
85
|
+
expect(upserts[0].title).toBe(issueTitle());
|
|
86
|
+
expect(upserts[0].assignees).toEqual(["das974"]);
|
|
87
|
+
expect(upserts[0].body).toContain("`@decohere/scaffold`");
|
|
103
88
|
});
|
|
104
89
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
it("opens an issue with assignees when none has the title", () => {
|
|
32
|
+
const { run, calls } = fakeGh([]);
|
|
33
|
+
const result = upsertIssue({ title: TITLE, body: "body", assignees: ["das974"] }, run);
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
number: 42,
|
|
36
|
+
url: "https://github.com/typesmiths/decohere/issues/42",
|
|
37
|
+
created: true,
|
|
38
|
+
});
|
|
39
|
+
expect(calls[1]).toEqual({
|
|
40
|
+
args: [
|
|
41
|
+
"issue",
|
|
42
|
+
"create",
|
|
43
|
+
"--title",
|
|
44
|
+
TITLE,
|
|
45
|
+
"--body-file",
|
|
46
|
+
"-",
|
|
47
|
+
"--add-assignee",
|
|
48
|
+
"das974",
|
|
49
|
+
],
|
|
50
|
+
input: "body",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("updates an open issue's body in place", () => {
|
|
55
|
+
const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "OPEN", url: "u7" }]);
|
|
56
|
+
const result = upsertIssue({ title: TITLE, body: "new body" }, run);
|
|
57
|
+
expect(result).toEqual({ number: 7, url: "u7", created: false });
|
|
58
|
+
expect(calls.map((call) => call.args[1])).toEqual(["list", "edit"]);
|
|
59
|
+
expect(calls[1]).toEqual({
|
|
60
|
+
args: ["issue", "edit", "7", "--body-file", "-"],
|
|
61
|
+
input: "new body",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("reopens a closed issue before updating it", () => {
|
|
66
|
+
const { run, calls } = fakeGh([{ number: 7, title: TITLE, state: "CLOSED", url: "u7" }]);
|
|
67
|
+
upsertIssue({ title: TITLE, body: "b", repo: "typesmiths/decohere" }, run);
|
|
68
|
+
expect(calls.map((call) => call.args.slice(0, 3))).toEqual([
|
|
69
|
+
["issue", "list", "--repo"],
|
|
70
|
+
["issue", "reopen", "7"],
|
|
71
|
+
["issue", "edit", "7"],
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -81,6 +81,23 @@ describe("buildBenchmarkHtml", () => {
|
|
|
81
81
|
expect(html).toContain('<p class="when">Last run 2026-10-20 03:17 UTC</p>');
|
|
82
82
|
});
|
|
83
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
|
+
|
|
84
101
|
it("escapes labels from the history", () => {
|
|
85
102
|
const html = buildBenchmarkHtml(history);
|
|
86
103
|
expect(html).toContain("spawn <to> ready");
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upsert a ClickUp task with the current npm audit report.
|
|
3
|
-
*
|
|
4
|
-
* Env: CLICKUP_API_KEY (required), CLICKUP_LIST_ID (default Decohere list).
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { formatMarkdown, parseAudit } from "./format.mjs";
|
|
8
|
-
|
|
9
|
-
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
10
|
-
const TASK_NAME = "chore(security): npm audit findings";
|
|
11
|
-
const LIST_ID = process.env.CLICKUP_LIST_ID ?? "901220171084";
|
|
12
|
-
const API = "https://api.clickup.com/api/v2";
|
|
13
|
-
|
|
14
|
-
function runUrl() {
|
|
15
|
-
if (
|
|
16
|
-
process.env.GITHUB_SERVER_URL &&
|
|
17
|
-
process.env.GITHUB_REPOSITORY &&
|
|
18
|
-
process.env.GITHUB_RUN_ID
|
|
19
|
-
) {
|
|
20
|
-
return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
21
|
-
}
|
|
22
|
-
return "(run URL unavailable)";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function clickup(method, path, body) {
|
|
26
|
-
const key = process.env.CLICKUP_API_KEY;
|
|
27
|
-
if (key == null || key.length === 0) {
|
|
28
|
-
throw new Error("Missing CLICKUP_API_KEY");
|
|
29
|
-
}
|
|
30
|
-
const response = await fetch(`${API}${path}`, {
|
|
31
|
-
method,
|
|
32
|
-
headers: {
|
|
33
|
-
Authorization: key,
|
|
34
|
-
"Content-Type": "application/json",
|
|
35
|
-
},
|
|
36
|
-
body: body == null ? undefined : JSON.stringify(body),
|
|
37
|
-
});
|
|
38
|
-
const text = await response.text();
|
|
39
|
-
if (!response.ok) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`ClickUp ${method} ${path} failed (${response.status}): ${text.slice(0, 500)}`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return text.length === 0 ? undefined : JSON.parse(text);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function findTaskByName(name) {
|
|
48
|
-
for (let page = 0; page < 10; page++) {
|
|
49
|
-
const qs = new URLSearchParams({
|
|
50
|
-
include_closed: "true",
|
|
51
|
-
subtasks: "false",
|
|
52
|
-
page: String(page),
|
|
53
|
-
});
|
|
54
|
-
const data = await clickup("GET", `/list/${LIST_ID}/task?${qs}`);
|
|
55
|
-
const tasks = Array.isArray(data) ? data : (data?.tasks ?? []);
|
|
56
|
-
const match = tasks.find((t) => t.name === name);
|
|
57
|
-
if (match) return match;
|
|
58
|
-
if (tasks.length < 100) break;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function firstOpenStatus() {
|
|
64
|
-
const list = await clickup("GET", `/list/${LIST_ID}`);
|
|
65
|
-
return (list.statuses ?? []).find((s) => s.type === "open")?.status;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildDescription(parsed) {
|
|
69
|
-
const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 12) : "unknown";
|
|
70
|
-
return [
|
|
71
|
-
formatMarkdown(parsed).trim(),
|
|
72
|
-
"",
|
|
73
|
-
`- Commit: \`${sha}\``,
|
|
74
|
-
`- Workflow run: ${runUrl()}`,
|
|
75
|
-
"",
|
|
76
|
-
"Updated by the nightly npm-audit workflow. PR CI still gates high/critical.",
|
|
77
|
-
].join("\n");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!existsSync(auditFilePath)) {
|
|
81
|
-
console.log(`No audit file at ${auditFilePath}; skipping ClickUp upsert.`);
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const parsed = parseAudit(JSON.parse(readFileSync(auditFilePath, "utf8")));
|
|
86
|
-
const existing = await findTaskByName(TASK_NAME);
|
|
87
|
-
|
|
88
|
-
if (parsed.total === 0) {
|
|
89
|
-
if (existing) {
|
|
90
|
-
await clickup("PUT", `/task/${existing.id}`, {
|
|
91
|
-
markdown_description: buildDescription(parsed),
|
|
92
|
-
});
|
|
93
|
-
console.log(`Cleared findings on ClickUp task ${existing.id}`);
|
|
94
|
-
} else {
|
|
95
|
-
console.log("No vulnerabilities and no existing ClickUp task; nothing to upsert.");
|
|
96
|
-
}
|
|
97
|
-
process.exit(0);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const payload = {
|
|
101
|
-
name: TASK_NAME,
|
|
102
|
-
markdown_description: buildDescription(parsed),
|
|
103
|
-
custom_item_id: 1004,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (existing) {
|
|
107
|
-
const update = { markdown_description: payload.markdown_description };
|
|
108
|
-
if (existing.closed || existing.status?.type === "closed") {
|
|
109
|
-
const status = await firstOpenStatus();
|
|
110
|
-
if (status) update.status = status;
|
|
111
|
-
}
|
|
112
|
-
await clickup("PUT", `/task/${existing.id}`, update);
|
|
113
|
-
console.log(`Updated ClickUp task ${existing.id} (${existing.url ?? ""})`);
|
|
114
|
-
} else {
|
|
115
|
-
const created = await clickup("POST", `/list/${LIST_ID}/task`, payload);
|
|
116
|
-
console.log(`Created ClickUp task ${created.id} ${created.url ?? ""}`);
|
|
117
|
-
}
|