@decohere/ci-tools 0.0.1-dev.20 → 0.0.1-dev.20261105.103
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/LICENSE +15 -0
- package/package.json +17 -3
- package/src/audit/enforce.mjs +7 -5
- package/src/audit/format.mjs +133 -0
- package/src/audit/summarize.mjs +10 -32
- package/src/audit/upsert-issue.mjs +25 -74
- package/src/bench/bench.mjs +135 -368
- package/src/bench/file-regressions.mjs +76 -0
- package/src/bench/history.mjs +248 -0
- package/src/bench/nightly.mjs +118 -0
- package/src/bench/regression-issue.mjs +44 -0
- package/src/bench/trace.mjs +18 -14
- package/src/github/first-publish-check.mjs +70 -0
- package/src/github/npm-bootstrap.mjs +182 -0
- package/src/github/npm-trust-promote.mjs +139 -0
- package/src/github/publish-verify.mjs +204 -0
- package/src/github/smoke-create-launcher.mjs +85 -0
- package/src/lib/github-issue.mjs +75 -0
- package/src/lib/lcov.mjs +1 -1
- package/src/lib/workspace-packages.mjs +30 -0
- package/src/pages/artifact.mjs +38 -183
- package/src/pages/bench-page.mjs +324 -0
- package/src/pages/restore-badges.mjs +35 -0
- package/src/pages/restore-bench-history.mjs +15 -28
- package/src/release/npm-links.mjs +6 -13
- package/src/release/promote-latest.mjs +122 -0
- package/src/release/publish-to-temporary-registry.mjs +65 -0
- package/src/release/sync-dependencies.mjs +1 -2
- package/tests/audit/format.test.mjs +115 -0
- package/tests/bench/history.test.mjs +279 -0
- package/tests/bench/regression-issue.test.mjs +42 -0
- package/tests/github/ci-workflow.test.mjs +30 -0
- package/tests/github/first-publish-check.test.mjs +51 -0
- package/tests/github/npm-bootstrap.test.mjs +125 -0
- package/tests/github/package-metadata.test.mjs +69 -0
- package/tests/github/publish-verify.test.mjs +115 -0
- package/tests/github/smoke-create-launcher.test.mjs +132 -0
- package/tests/lib/github-issue.test.mjs +85 -0
- package/tests/lib/lcov.test.mjs +8 -8
- package/tests/pages/bench-page.test.mjs +123 -0
- package/src/coverage/enforce-thresholds.mjs +0 -144
- package/src/pages/clear-bench-history.mjs +0 -27
- package/src/pages/keep-prior-bench-badge.mjs +0 -38
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Typesmiths
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decohere/ci-tools",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.20261105.103",
|
|
4
4
|
"description": "CI utility scripts for badge generation, coverage reporting, pages artifact assembly, and GitHub API tooling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -10,18 +10,28 @@
|
|
|
10
10
|
"github-actions",
|
|
11
11
|
"decohere"
|
|
12
12
|
],
|
|
13
|
+
"homepage": "https://github.com/typesmiths/decohere/tree/main/projects/ci-tools#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/typesmiths/decohere/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/typesmiths/decohere.git",
|
|
20
|
+
"directory": "projects/ci-tools"
|
|
21
|
+
},
|
|
13
22
|
"license": "ISC",
|
|
14
23
|
"author": "Typesmiths",
|
|
15
24
|
"type": "module",
|
|
16
25
|
"exports": {
|
|
17
26
|
"./lib/svg": "./src/lib/svg.mjs",
|
|
18
27
|
"./lib/lcov": "./src/lib/lcov.mjs",
|
|
28
|
+
"./lib/github-issue": "./src/lib/github-issue.mjs",
|
|
19
29
|
"./coverage/badge": "./src/coverage/badge.mjs",
|
|
20
30
|
"./coverage/report": "./src/coverage/report.mjs",
|
|
21
|
-
"./coverage/enforce-thresholds": "./src/coverage/enforce-thresholds.mjs",
|
|
22
31
|
"./badges/ci-status": "./src/badges/ci-status.mjs",
|
|
23
32
|
"./pages/artifact": "./src/pages/artifact.mjs",
|
|
24
33
|
"./pages/smoke": "./src/pages/smoke.mjs",
|
|
34
|
+
"./audit/format": "./src/audit/format.mjs",
|
|
25
35
|
"./audit/summarize": "./src/audit/summarize.mjs",
|
|
26
36
|
"./audit/enforce": "./src/audit/enforce.mjs",
|
|
27
37
|
"./audit/upsert-issue": "./src/audit/upsert-issue.mjs",
|
|
@@ -31,7 +41,11 @@
|
|
|
31
41
|
"./changeset/check-status": "./src/changeset/check-status.mjs",
|
|
32
42
|
"./github/create-issue": "./src/github/create-issue.mjs",
|
|
33
43
|
"./github/ensure-metadata": "./src/github/ensure-metadata.mjs",
|
|
34
|
-
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs"
|
|
44
|
+
"./github/create-or-update-pr": "./src/github/create-or-update-pr.mjs",
|
|
45
|
+
"./github/publish-verify": "./src/github/publish-verify.mjs",
|
|
46
|
+
"./github/first-publish-check": "./src/github/first-publish-check.mjs",
|
|
47
|
+
"./github/npm-bootstrap": "./src/github/npm-bootstrap.mjs",
|
|
48
|
+
"./github/smoke-create-launcher": "./src/github/smoke-create-launcher.mjs"
|
|
35
49
|
},
|
|
36
50
|
"scripts": {
|
|
37
51
|
"test": "vitest run"
|
package/src/audit/enforce.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { formatLog, highCritical, parseAudit } from "./format.mjs";
|
|
2
3
|
|
|
3
4
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
4
5
|
|
|
@@ -10,12 +11,13 @@ try {
|
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const critical = vulnerabilities.critical ?? 0;
|
|
14
|
+
const parsed = parseAudit(data);
|
|
15
|
+
const blocking = highCritical(parsed);
|
|
16
16
|
|
|
17
|
-
if (
|
|
18
|
-
console.error(
|
|
17
|
+
if (blocking.length > 0) {
|
|
18
|
+
console.error(
|
|
19
|
+
`Security gate failed: high=${parsed.high}, critical=${parsed.critical}\n${formatLog({ ...parsed, advisories: blocking })}`,
|
|
20
|
+
);
|
|
19
21
|
process.exit(1);
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared npm/pnpm audit JSON helpers.
|
|
3
|
+
*
|
|
4
|
+
* Supports pnpm's classic `{ advisories }` report and npm audit v2
|
|
5
|
+
* `{ auditReportVersion: 2, vulnerabilities }` (pnpm 9+/10).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
|
|
9
|
+
|
|
10
|
+
function ghsaFromUrl(url) {
|
|
11
|
+
return typeof url === "string" ? (url.match(/GHSA-[a-z0-9-]+/i)?.[0] ?? "") : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fromClassicAdvisories(data) {
|
|
15
|
+
return Object.values(data?.advisories ?? {}).map((raw) => {
|
|
16
|
+
const paths = (raw.findings ?? []).flatMap((f) => f.paths ?? []);
|
|
17
|
+
const url = raw.url ?? "";
|
|
18
|
+
return {
|
|
19
|
+
id: raw.id,
|
|
20
|
+
module: raw.module_name ?? "(unknown)",
|
|
21
|
+
severity: String(raw.severity ?? "info").toLowerCase(),
|
|
22
|
+
title: raw.title ?? "",
|
|
23
|
+
url,
|
|
24
|
+
vulnerable: raw.vulnerable_versions ?? "",
|
|
25
|
+
patched: raw.patched_versions ?? "",
|
|
26
|
+
cves: raw.cves ?? [],
|
|
27
|
+
ghsa: ghsaFromUrl(url),
|
|
28
|
+
paths: paths.slice(0, 3),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromNpmV2(data) {
|
|
34
|
+
const byKey = new Map();
|
|
35
|
+
for (const vuln of Object.values(data?.vulnerabilities ?? {})) {
|
|
36
|
+
const vias = Array.isArray(vuln.via) ? vuln.via : [];
|
|
37
|
+
for (const via of vias) {
|
|
38
|
+
if (via == null || typeof via !== "object") continue;
|
|
39
|
+
const url = via.url ?? "";
|
|
40
|
+
const key = url || `${via.name ?? vuln.name}:${via.title ?? ""}`;
|
|
41
|
+
const existing = byKey.get(key);
|
|
42
|
+
const node = Array.isArray(vuln.nodes) ? vuln.nodes[0] : undefined;
|
|
43
|
+
if (existing) {
|
|
44
|
+
if (node && existing.paths.length < 3 && !existing.paths.includes(node)) {
|
|
45
|
+
existing.paths.push(node);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
byKey.set(key, {
|
|
50
|
+
id: via.source,
|
|
51
|
+
module: via.name ?? vuln.name ?? "(unknown)",
|
|
52
|
+
severity: String(via.severity ?? vuln.severity ?? "info").toLowerCase(),
|
|
53
|
+
title: via.title ?? "",
|
|
54
|
+
url,
|
|
55
|
+
vulnerable: via.range ?? vuln.range ?? "",
|
|
56
|
+
patched: "",
|
|
57
|
+
cves: [],
|
|
58
|
+
ghsa: ghsaFromUrl(url),
|
|
59
|
+
paths: node ? [node] : [],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...byKey.values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseAudit(data) {
|
|
67
|
+
const counts = data?.metadata?.vulnerabilities ?? {};
|
|
68
|
+
const info = counts.info ?? 0;
|
|
69
|
+
const low = counts.low ?? 0;
|
|
70
|
+
const moderate = counts.moderate ?? 0;
|
|
71
|
+
const high = counts.high ?? 0;
|
|
72
|
+
const critical = counts.critical ?? 0;
|
|
73
|
+
const total = counts.total ?? info + low + moderate + high + critical;
|
|
74
|
+
|
|
75
|
+
const classic = data?.advisories != null && Object.keys(data.advisories).length > 0;
|
|
76
|
+
const advisories = classic ? fromClassicAdvisories(data) : fromNpmV2(data);
|
|
77
|
+
|
|
78
|
+
advisories.sort((a, b) => {
|
|
79
|
+
const d = SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
|
|
80
|
+
if (d !== 0) return d;
|
|
81
|
+
return String(a.module).localeCompare(String(b.module));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return { total, critical, high, moderate, low, info, advisories };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatLog(parsed) {
|
|
88
|
+
const lines = [
|
|
89
|
+
`npm audit: total=${parsed.total} critical=${parsed.critical} high=${parsed.high} moderate=${parsed.moderate} low=${parsed.low} info=${parsed.info}`,
|
|
90
|
+
];
|
|
91
|
+
for (const a of parsed.advisories) {
|
|
92
|
+
const id = a.ghsa || a.cves[0] || a.id;
|
|
93
|
+
lines.push(
|
|
94
|
+
` [${a.severity}] ${a.module} ${id}: ${a.title} (vulnerable ${a.vulnerable}; patched ${a.patched})`,
|
|
95
|
+
);
|
|
96
|
+
if (a.url) lines.push(` ${a.url}`);
|
|
97
|
+
if (a.paths[0]) lines.push(` ${a.paths[0]}`);
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join("\n")}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatMarkdown(parsed) {
|
|
103
|
+
const lines = [
|
|
104
|
+
"## npm audit",
|
|
105
|
+
"",
|
|
106
|
+
"| Severity | Count |",
|
|
107
|
+
"|---|---|",
|
|
108
|
+
`| Critical | ${parsed.critical} |`,
|
|
109
|
+
`| High | ${parsed.high} |`,
|
|
110
|
+
`| Moderate | ${parsed.moderate} |`,
|
|
111
|
+
`| Low | ${parsed.low} |`,
|
|
112
|
+
`| Info | ${parsed.info} |`,
|
|
113
|
+
`| **Total** | **${parsed.total}** |`,
|
|
114
|
+
"",
|
|
115
|
+
];
|
|
116
|
+
if (parsed.advisories.length === 0) {
|
|
117
|
+
lines.push("No advisories.");
|
|
118
|
+
return `${lines.join("\n")}\n`;
|
|
119
|
+
}
|
|
120
|
+
lines.push("### Advisories", "");
|
|
121
|
+
for (const a of parsed.advisories) {
|
|
122
|
+
const id = a.ghsa || a.cves.join(", ") || a.id;
|
|
123
|
+
const link = a.url ? `[${id}](${a.url})` : id;
|
|
124
|
+
lines.push(`- **${a.severity}** \`${a.module}\` ${link} — ${a.title}`);
|
|
125
|
+
lines.push(` - vulnerable: \`${a.vulnerable}\`; patched: \`${a.patched}\``);
|
|
126
|
+
if (a.paths[0]) lines.push(` - ${a.paths[0]}`);
|
|
127
|
+
}
|
|
128
|
+
return `${lines.join("\n")}\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function highCritical(parsed) {
|
|
132
|
+
return parsed.advisories.filter((a) => a.severity === "high" || a.severity === "critical");
|
|
133
|
+
}
|
package/src/audit/summarize.mjs
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
1
1
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
2
|
+
import { formatLog, formatMarkdown, parseAudit } from "./format.mjs";
|
|
2
3
|
|
|
3
4
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
4
5
|
|
|
5
|
-
function writeSummary(
|
|
6
|
-
const summaryText = `${lines.join("\n")}\n`;
|
|
6
|
+
function writeSummary(text) {
|
|
7
7
|
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
8
|
-
appendFileSync(process.env.GITHUB_STEP_SUMMARY,
|
|
9
|
-
return;
|
|
8
|
+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, text);
|
|
10
9
|
}
|
|
11
|
-
process.stdout.write(summaryText);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
let data = null;
|
|
15
13
|
try {
|
|
16
14
|
data = JSON.parse(readFileSync(auditFilePath, "utf8"));
|
|
17
15
|
} catch {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`- See uploaded \`${auditFilePath}\` artifact for raw output.`,
|
|
23
|
-
]);
|
|
24
|
-
process.exit(0);
|
|
16
|
+
const msg = `npm audit output could not be parsed (${auditFilePath}).\n`;
|
|
17
|
+
process.stderr.write(msg);
|
|
18
|
+
writeSummary(`## Security Audit\n\n- ${msg}`);
|
|
19
|
+
process.exit(1);
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const moderate = vulnerabilities.moderate ?? 0;
|
|
31
|
-
const high = vulnerabilities.high ?? 0;
|
|
32
|
-
const critical = vulnerabilities.critical ?? 0;
|
|
33
|
-
const total = vulnerabilities.total ?? info + low + moderate + high + critical;
|
|
34
|
-
|
|
35
|
-
writeSummary([
|
|
36
|
-
"## Security Audit",
|
|
37
|
-
"",
|
|
38
|
-
`| Severity | Count |`,
|
|
39
|
-
`|-----------|-------|`,
|
|
40
|
-
`| Critical | ${critical} |`,
|
|
41
|
-
`| High | ${high} |`,
|
|
42
|
-
`| Moderate | ${moderate} |`,
|
|
43
|
-
`| Low | ${low} |`,
|
|
44
|
-
`| Info | ${info} |`,
|
|
45
|
-
`| **Total** | **${total}** |`,
|
|
46
|
-
]);
|
|
22
|
+
const parsed = parseAudit(data);
|
|
23
|
+
process.stdout.write(formatLog(parsed));
|
|
24
|
+
writeSummary(formatMarkdown(parsed));
|
|
@@ -1,68 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import path from "node:path";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { upsertIssue } from "../lib/github-issue.mjs";
|
|
3
|
+
import { formatMarkdown, parseAudit } from "./format.mjs";
|
|
5
4
|
|
|
6
5
|
const auditFilePath = process.argv[2] ?? "npm-audit.json";
|
|
7
6
|
const ISSUE_TITLE = "security(ci): npm audit vulnerabilities detected";
|
|
8
7
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
function runUrl() {
|
|
9
|
+
if (
|
|
10
|
+
process.env.GITHUB_SERVER_URL &&
|
|
11
|
+
process.env.GITHUB_REPOSITORY &&
|
|
12
|
+
process.env.GITHUB_RUN_ID
|
|
13
|
+
) {
|
|
14
|
+
return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
13
15
|
}
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readAuditSummary(filePath) {
|
|
18
|
-
const data = JSON.parse(readFileSync(filePath, "utf8"));
|
|
19
|
-
const vulnerabilities = data.metadata?.vulnerabilities ?? {};
|
|
20
|
-
const info = vulnerabilities.info ?? 0;
|
|
21
|
-
const low = vulnerabilities.low ?? 0;
|
|
22
|
-
const moderate = vulnerabilities.moderate ?? 0;
|
|
23
|
-
const high = vulnerabilities.high ?? 0;
|
|
24
|
-
const critical = vulnerabilities.critical ?? 0;
|
|
25
|
-
const total = vulnerabilities.total ?? info + low + moderate + high + critical;
|
|
26
|
-
return { total, critical, high, moderate, low, info };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildBody(summary) {
|
|
30
|
-
const runUrl =
|
|
31
|
-
process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
|
|
32
|
-
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
|
|
33
|
-
: "(run URL unavailable)";
|
|
34
|
-
const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 12) : "unknown";
|
|
35
|
-
|
|
36
|
-
return [
|
|
37
|
-
"## npm audit detected vulnerabilities",
|
|
38
|
-
"",
|
|
39
|
-
`- Total: ${summary.total}`,
|
|
40
|
-
`- Critical: ${summary.critical}`,
|
|
41
|
-
`- High: ${summary.high}`,
|
|
42
|
-
`- Moderate: ${summary.moderate}`,
|
|
43
|
-
`- Low: ${summary.low}`,
|
|
44
|
-
`- Info: ${summary.info}`,
|
|
45
|
-
"",
|
|
46
|
-
`- Commit: ${sha}`,
|
|
47
|
-
`- Workflow run: ${runUrl}`,
|
|
48
|
-
"",
|
|
49
|
-
"This issue is managed by CI and will be updated on subsequent main-branch runs while vulnerabilities remain.",
|
|
50
|
-
].join("\n");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function findExistingIssueNumber() {
|
|
54
|
-
const output = runGh([
|
|
55
|
-
"issue",
|
|
56
|
-
"list",
|
|
57
|
-
"--state",
|
|
58
|
-
"open",
|
|
59
|
-
"--limit",
|
|
60
|
-
"200",
|
|
61
|
-
"--json",
|
|
62
|
-
"number,title",
|
|
63
|
-
]);
|
|
64
|
-
const issues = JSON.parse(output);
|
|
65
|
-
return issues.find((issue) => issue.title === ISSUE_TITLE)?.number ?? null;
|
|
16
|
+
return "(run URL unavailable)";
|
|
66
17
|
}
|
|
67
18
|
|
|
68
19
|
if (!existsSync(auditFilePath)) {
|
|
@@ -70,21 +21,21 @@ if (!existsSync(auditFilePath)) {
|
|
|
70
21
|
process.exit(0);
|
|
71
22
|
}
|
|
72
23
|
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
24
|
+
const parsed = parseAudit(JSON.parse(readFileSync(auditFilePath, "utf8")));
|
|
25
|
+
if (parsed.total === 0) {
|
|
75
26
|
console.log("No vulnerabilities found; skipping issue create/update.");
|
|
76
27
|
process.exit(0);
|
|
77
28
|
}
|
|
78
29
|
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
30
|
+
const sha = process.env.GITHUB_SHA ? process.env.GITHUB_SHA.slice(0, 12) : "unknown";
|
|
31
|
+
const body = [
|
|
32
|
+
formatMarkdown(parsed).trim(),
|
|
33
|
+
"",
|
|
34
|
+
`- Commit: \`${sha}\``,
|
|
35
|
+
`- Workflow run: ${runUrl()}`,
|
|
36
|
+
"",
|
|
37
|
+
"This issue is managed by CI: main-branch runs and the nightly audit update it in place while vulnerabilities remain.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
const result = upsertIssue({ title: ISSUE_TITLE, body });
|
|
41
|
+
console.log(`${result.created ? "Opened" : "Updated"} issue #${result.number} ${result.url}`);
|