@prodgator/report 3.5.0 → 3.19.1
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/dist/cli.js +265 -162
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -634,7 +634,7 @@ var require_scan = __commonJS({
|
|
|
634
634
|
var require_parse = __commonJS({
|
|
635
635
|
"node_modules/picomatch/lib/parse.js"(exports2, module2) {
|
|
636
636
|
"use strict";
|
|
637
|
-
var
|
|
637
|
+
var constants2 = require_constants();
|
|
638
638
|
var utils = require_utils();
|
|
639
639
|
var {
|
|
640
640
|
MAX_LENGTH,
|
|
@@ -642,7 +642,7 @@ var require_parse = __commonJS({
|
|
|
642
642
|
REGEX_NON_SPECIAL_CHARS,
|
|
643
643
|
REGEX_SPECIAL_CHARS_BACKREF,
|
|
644
644
|
REPLACEMENTS
|
|
645
|
-
} =
|
|
645
|
+
} = constants2;
|
|
646
646
|
var expandRange = (args, options) => {
|
|
647
647
|
if (typeof options.expandRange === "function") {
|
|
648
648
|
return options.expandRange(...args, options);
|
|
@@ -851,7 +851,7 @@ var require_parse = __commonJS({
|
|
|
851
851
|
if (options.maxExtglobRecursion === false) {
|
|
852
852
|
return { risky: false };
|
|
853
853
|
}
|
|
854
|
-
const max = typeof options.maxExtglobRecursion === "number" ? options.maxExtglobRecursion :
|
|
854
|
+
const max = typeof options.maxExtglobRecursion === "number" ? options.maxExtglobRecursion : constants2.DEFAULT_MAX_EXTGLOB_RECURSION;
|
|
855
855
|
const branches = splitTopLevel(body).map((branch) => branch.trim());
|
|
856
856
|
if (branches.length > 1) {
|
|
857
857
|
if (branches.some((branch) => branch === "") || branches.some((branch) => /^[*?]+$/.test(branch)) || hasRepeatedCharPrefixOverlap(branches)) {
|
|
@@ -897,8 +897,8 @@ var require_parse = __commonJS({
|
|
|
897
897
|
const bos = { type: "bos", value: "", output: opts.prepend || "" };
|
|
898
898
|
const tokens = [bos];
|
|
899
899
|
const capture = opts.capture ? "" : "?:";
|
|
900
|
-
const PLATFORM_CHARS =
|
|
901
|
-
const EXTGLOB_CHARS =
|
|
900
|
+
const PLATFORM_CHARS = constants2.globChars(opts.windows);
|
|
901
|
+
const EXTGLOB_CHARS = constants2.extglobChars(PLATFORM_CHARS);
|
|
902
902
|
const {
|
|
903
903
|
DOT_LITERAL,
|
|
904
904
|
PLUS_LITERAL,
|
|
@@ -1594,7 +1594,7 @@ var require_parse = __commonJS({
|
|
|
1594
1594
|
NO_DOTS_SLASH,
|
|
1595
1595
|
STAR,
|
|
1596
1596
|
START_ANCHOR
|
|
1597
|
-
} =
|
|
1597
|
+
} = constants2.globChars(opts.windows);
|
|
1598
1598
|
const nodot = opts.dot ? NO_DOTS : NO_DOT;
|
|
1599
1599
|
const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT;
|
|
1600
1600
|
const capture = opts.capture ? "" : "?:";
|
|
@@ -1652,7 +1652,7 @@ var require_picomatch = __commonJS({
|
|
|
1652
1652
|
var scan = require_scan();
|
|
1653
1653
|
var parse = require_parse();
|
|
1654
1654
|
var utils = require_utils();
|
|
1655
|
-
var
|
|
1655
|
+
var constants2 = require_constants();
|
|
1656
1656
|
var isObject = (val) => val && typeof val === "object" && !Array.isArray(val);
|
|
1657
1657
|
var picomatch = (glob2, options, returnState = false) => {
|
|
1658
1658
|
if (Array.isArray(glob2)) {
|
|
@@ -1780,7 +1780,7 @@ var require_picomatch = __commonJS({
|
|
|
1780
1780
|
return /$^/;
|
|
1781
1781
|
}
|
|
1782
1782
|
};
|
|
1783
|
-
picomatch.constants =
|
|
1783
|
+
picomatch.constants = constants2;
|
|
1784
1784
|
module2.exports = picomatch;
|
|
1785
1785
|
}
|
|
1786
1786
|
});
|
|
@@ -1826,7 +1826,7 @@ var require_dist = __commonJS({
|
|
|
1826
1826
|
enumerable: true
|
|
1827
1827
|
}) : target, mod));
|
|
1828
1828
|
var path = __toESM2(require("path"));
|
|
1829
|
-
var
|
|
1829
|
+
var fs2 = __toESM2(require("fs"));
|
|
1830
1830
|
function cleanPath(path$1) {
|
|
1831
1831
|
let normalized = (0, path.normalize)(path$1);
|
|
1832
1832
|
if (normalized.length > 1 && normalized[normalized.length - 1] === path.sep) normalized = normalized.substring(0, normalized.length - 1);
|
|
@@ -2123,7 +2123,7 @@ var require_dist = __commonJS({
|
|
|
2123
2123
|
symlinks: /* @__PURE__ */ new Map(),
|
|
2124
2124
|
visited: [""].slice(0, 0),
|
|
2125
2125
|
controller: new Aborter(),
|
|
2126
|
-
fs: options.fs ||
|
|
2126
|
+
fs: options.fs || fs2
|
|
2127
2127
|
};
|
|
2128
2128
|
this.joinPath = build$7(this.root, options);
|
|
2129
2129
|
this.pushDirectory = build$6(this.root, options);
|
|
@@ -2358,7 +2358,7 @@ var require_dist2 = __commonJS({
|
|
|
2358
2358
|
value: mod,
|
|
2359
2359
|
enumerable: true
|
|
2360
2360
|
}) : target, mod));
|
|
2361
|
-
var
|
|
2361
|
+
var fs2 = require("fs");
|
|
2362
2362
|
var path = require("path");
|
|
2363
2363
|
var url = require("url");
|
|
2364
2364
|
var fdir = require_dist();
|
|
@@ -2607,12 +2607,12 @@ var require_dist2 = __commonJS({
|
|
|
2607
2607
|
opts.cwd = (opts.cwd instanceof URL ? (0, url.fileURLToPath)(opts.cwd) : (0, path.resolve)(opts.cwd || process.cwd())).replace(BACKSLASHES, "/");
|
|
2608
2608
|
opts.ignore = ensureStringArray(opts.ignore);
|
|
2609
2609
|
opts.fs && (opts.fs = {
|
|
2610
|
-
readdir: opts.fs.readdir ||
|
|
2611
|
-
readdirSync: opts.fs.readdirSync ||
|
|
2612
|
-
realpath: opts.fs.realpath ||
|
|
2613
|
-
realpathSync: opts.fs.realpathSync ||
|
|
2614
|
-
stat: opts.fs.stat ||
|
|
2615
|
-
statSync: opts.fs.statSync ||
|
|
2610
|
+
readdir: opts.fs.readdir || fs2.readdir,
|
|
2611
|
+
readdirSync: opts.fs.readdirSync || fs2.readdirSync,
|
|
2612
|
+
realpath: opts.fs.realpath || fs2.realpath,
|
|
2613
|
+
realpathSync: opts.fs.realpathSync || fs2.realpathSync,
|
|
2614
|
+
stat: opts.fs.stat || fs2.stat,
|
|
2615
|
+
statSync: opts.fs.statSync || fs2.statSync
|
|
2616
2616
|
});
|
|
2617
2617
|
if (opts.debug) log("globbing with options:", opts);
|
|
2618
2618
|
return opts;
|
|
@@ -2626,12 +2626,12 @@ var require_dist2 = __commonJS({
|
|
|
2626
2626
|
return patterns.length > 0 ? buildCrawler(options, patterns) : [];
|
|
2627
2627
|
}
|
|
2628
2628
|
async function glob2(globInput, options) {
|
|
2629
|
-
const [crawler,
|
|
2630
|
-
return crawler ? formatPaths(await crawler.withPromise(),
|
|
2629
|
+
const [crawler, relative3] = getCrawler(globInput, options);
|
|
2630
|
+
return crawler ? formatPaths(await crawler.withPromise(), relative3) : [];
|
|
2631
2631
|
}
|
|
2632
2632
|
function globSync(globInput, options) {
|
|
2633
|
-
const [crawler,
|
|
2634
|
-
return crawler ? formatPaths(crawler.sync(),
|
|
2633
|
+
const [crawler, relative3] = getCrawler(globInput, options);
|
|
2634
|
+
return crawler ? formatPaths(crawler.sync(), relative3) : [];
|
|
2635
2635
|
}
|
|
2636
2636
|
exports2.convertPathToPattern = convertPathToPattern;
|
|
2637
2637
|
exports2.escapePath = escapePath;
|
|
@@ -2641,10 +2641,6 @@ var require_dist2 = __commonJS({
|
|
|
2641
2641
|
}
|
|
2642
2642
|
});
|
|
2643
2643
|
|
|
2644
|
-
// src/cli/main.ts
|
|
2645
|
-
var import_fs5 = require("fs");
|
|
2646
|
-
var import_path4 = require("path");
|
|
2647
|
-
|
|
2648
2644
|
// src/core/text.ts
|
|
2649
2645
|
var MAX_SUMMARY_BYTES = 1024 * 1024;
|
|
2650
2646
|
var MARKER = "\n\n\u2026(truncated by prodgator-report)";
|
|
@@ -2656,9 +2652,6 @@ function truncateUtf8(text, maxBytes) {
|
|
|
2656
2652
|
return { text: bytes.subarray(0, end).toString("utf8") + MARKER, truncated: true };
|
|
2657
2653
|
}
|
|
2658
2654
|
|
|
2659
|
-
// src/core/junit.ts
|
|
2660
|
-
var import_fs = require("fs");
|
|
2661
|
-
|
|
2662
2655
|
// node_modules/fast-xml-parser/src/util.js
|
|
2663
2656
|
var nameStartChar = ":A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
|
|
2664
2657
|
var nameChar = nameStartChar + "\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
|
|
@@ -5087,21 +5080,21 @@ var Matcher = class {
|
|
|
5087
5080
|
* @returns {string}
|
|
5088
5081
|
*/
|
|
5089
5082
|
toString(separator, includeNamespace = true) {
|
|
5090
|
-
const
|
|
5091
|
-
const isDefault =
|
|
5083
|
+
const sep3 = separator || this.separator;
|
|
5084
|
+
const isDefault = sep3 === this.separator && includeNamespace === true;
|
|
5092
5085
|
if (isDefault) {
|
|
5093
5086
|
if (this._pathStringCache !== null) {
|
|
5094
5087
|
return this._pathStringCache;
|
|
5095
5088
|
}
|
|
5096
5089
|
const result = this.path.map(
|
|
5097
5090
|
(n) => n.namespace ? `${n.namespace}:${n.tag}` : n.tag
|
|
5098
|
-
).join(
|
|
5091
|
+
).join(sep3);
|
|
5099
5092
|
this._pathStringCache = result;
|
|
5100
5093
|
return result;
|
|
5101
5094
|
}
|
|
5102
5095
|
return this.path.map(
|
|
5103
5096
|
(n) => includeNamespace && n.namespace ? `${n.namespace}:${n.tag}` : n.tag
|
|
5104
|
-
).join(
|
|
5097
|
+
).join(sep3);
|
|
5105
5098
|
}
|
|
5106
5099
|
/**
|
|
5107
5100
|
* Get path as array of tag names.
|
|
@@ -6778,6 +6771,103 @@ var XMLParser = class {
|
|
|
6778
6771
|
}
|
|
6779
6772
|
};
|
|
6780
6773
|
|
|
6774
|
+
// src/core/safe-path.ts
|
|
6775
|
+
var import_crypto = require("crypto");
|
|
6776
|
+
var import_fs = require("fs");
|
|
6777
|
+
var import_path = require("path");
|
|
6778
|
+
var O_NOFOLLOW = import_fs.constants.O_NOFOLLOW ?? 0;
|
|
6779
|
+
var outside = (rel) => !rel || rel === ".." || rel.startsWith(`..${import_path.sep}`) || rel.startsWith("../") || (0, import_path.isAbsolute)(rel) || /^[A-Za-z]:/.test(rel);
|
|
6780
|
+
var underGit = (rel) => rel.split(/[\\/]/).includes(".git");
|
|
6781
|
+
var refuse = (reason, message) => ({ ok: false, reason, message });
|
|
6782
|
+
function displayName(root, file) {
|
|
6783
|
+
const rel = (0, import_path.relative)((0, import_path.resolve)(root), (0, import_path.resolve)(root, file));
|
|
6784
|
+
return outside(rel) ? file : rel.split(import_path.sep).join("/");
|
|
6785
|
+
}
|
|
6786
|
+
async function checkWorkspacePath(root, name, opts = {}) {
|
|
6787
|
+
const display = opts.display ?? name;
|
|
6788
|
+
const absolute = (0, import_path.resolve)(root, name);
|
|
6789
|
+
const rel = (0, import_path.relative)((0, import_path.resolve)(root), absolute);
|
|
6790
|
+
if (outside(rel)) return refuse("outside", `${display} is outside the workspace`);
|
|
6791
|
+
const path = rel.split(import_path.sep).join("/");
|
|
6792
|
+
if (underGit(path)) return refuse("git", `${display} is inside a .git directory`);
|
|
6793
|
+
let real;
|
|
6794
|
+
let realRoot;
|
|
6795
|
+
try {
|
|
6796
|
+
[real, realRoot] = await Promise.all([import_fs.promises.realpath(absolute), import_fs.promises.realpath(root)]);
|
|
6797
|
+
} catch {
|
|
6798
|
+
return refuse("missing", `${display} was not found`);
|
|
6799
|
+
}
|
|
6800
|
+
const realRel = (0, import_path.relative)(realRoot, real);
|
|
6801
|
+
if (outside(realRel)) return refuse("symlink", `${display} resolves outside the workspace (symlink)`);
|
|
6802
|
+
if (underGit(realRel)) return refuse("git", `${display} resolves into a .git directory (symlink)`);
|
|
6803
|
+
if (opts.followLinks === false && realRel !== rel) return refuse("symlink", `${display} is a symlink`);
|
|
6804
|
+
return { ok: true, file: { absolute, path, real } };
|
|
6805
|
+
}
|
|
6806
|
+
async function openWorkspaceFile(root, name, opts = {}) {
|
|
6807
|
+
const display = opts.display ?? name;
|
|
6808
|
+
const checked = await checkWorkspacePath(root, name, opts);
|
|
6809
|
+
if (!checked.ok) return checked;
|
|
6810
|
+
const { file } = checked;
|
|
6811
|
+
let handle;
|
|
6812
|
+
try {
|
|
6813
|
+
handle = await import_fs.promises.open(file.real, import_fs.constants.O_RDONLY | O_NOFOLLOW);
|
|
6814
|
+
} catch (e) {
|
|
6815
|
+
const code = e.code;
|
|
6816
|
+
return code === "ENOENT" ? refuse("missing", `${display} was not found`) : refuse("changed", `${display} could not be opened safely`);
|
|
6817
|
+
}
|
|
6818
|
+
try {
|
|
6819
|
+
const st = await handle.stat();
|
|
6820
|
+
if (!st.isFile()) {
|
|
6821
|
+
await handle.close();
|
|
6822
|
+
return refuse("not-file", `${display} is not a file`);
|
|
6823
|
+
}
|
|
6824
|
+
if (!await sameFile(handle, st, file.real, await import_fs.promises.realpath(root))) {
|
|
6825
|
+
await handle.close();
|
|
6826
|
+
return refuse("changed", `${display} changed before it could be read (symlink)`);
|
|
6827
|
+
}
|
|
6828
|
+
return { ok: true, file, size: st.size, handle };
|
|
6829
|
+
} catch {
|
|
6830
|
+
await handle.close().catch(() => {
|
|
6831
|
+
});
|
|
6832
|
+
return refuse("changed", `${display} changed before it could be read (symlink)`);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
async function sameFile(handle, st, real, realRoot) {
|
|
6836
|
+
const viaProc = await import_fs.promises.realpath(`/proc/self/fd/${handle.fd}`).catch(() => null);
|
|
6837
|
+
if (viaProc !== null) {
|
|
6838
|
+
const rel = (0, import_path.relative)(realRoot, viaProc);
|
|
6839
|
+
return viaProc === real && !outside(rel) && !underGit(rel);
|
|
6840
|
+
}
|
|
6841
|
+
const again = await import_fs.promises.realpath(real).catch(() => null);
|
|
6842
|
+
if (again !== real) return false;
|
|
6843
|
+
const now = await import_fs.promises.stat(real).catch(() => null);
|
|
6844
|
+
return !!now && now.dev === st.dev && now.ino === st.ino;
|
|
6845
|
+
}
|
|
6846
|
+
async function readWorkspaceText(root, name, opts = {}) {
|
|
6847
|
+
const opened = await openWorkspaceFile(root, name, opts);
|
|
6848
|
+
if (!opened.ok) return opened;
|
|
6849
|
+
try {
|
|
6850
|
+
if (opts.maxBytes !== void 0 && opened.size > opts.maxBytes) return { ok: true, file: opened.file, size: opened.size, text: null };
|
|
6851
|
+
return { ok: true, file: opened.file, size: opened.size, text: await opened.handle.readFile("utf8") };
|
|
6852
|
+
} finally {
|
|
6853
|
+
await opened.handle.close();
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
async function sha256Handle(handle) {
|
|
6857
|
+
const hash = (0, import_crypto.createHash)("sha256");
|
|
6858
|
+
for await (const chunk of handle.createReadStream({ autoClose: false, start: 0 })) hash.update(chunk);
|
|
6859
|
+
return hash.digest("hex");
|
|
6860
|
+
}
|
|
6861
|
+
async function hashWorkspaceFile(root, name, opts = {}) {
|
|
6862
|
+
const opened = await openWorkspaceFile(root, name, opts);
|
|
6863
|
+
if (!opened.ok) return opened;
|
|
6864
|
+
try {
|
|
6865
|
+
return { ok: true, file: opened.file, size: opened.size, sha256: await sha256Handle(opened.handle) };
|
|
6866
|
+
} finally {
|
|
6867
|
+
await opened.handle.close();
|
|
6868
|
+
}
|
|
6869
|
+
}
|
|
6870
|
+
|
|
6781
6871
|
// src/core/junit.ts
|
|
6782
6872
|
var MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
6783
6873
|
var MAX_FILES = 500;
|
|
@@ -6822,12 +6912,17 @@ async function junitAttestation(patterns, opts) {
|
|
|
6822
6912
|
if (!patterns.length) return null;
|
|
6823
6913
|
const find = opts.glob;
|
|
6824
6914
|
const files = (await find(patterns)).slice(0, MAX_FILES);
|
|
6825
|
-
if (!files.length) return null;
|
|
6826
6915
|
const total = empty();
|
|
6916
|
+
let read = 0;
|
|
6827
6917
|
for (const f of files) {
|
|
6828
|
-
const
|
|
6829
|
-
if (
|
|
6830
|
-
|
|
6918
|
+
const r = await readWorkspaceText(opts.workspace, f, { maxBytes: MAX_FILE_BYTES, display: displayName(opts.workspace, f) });
|
|
6919
|
+
if (!r.ok) {
|
|
6920
|
+
if (r.reason !== "missing") opts.warn(`${r.message} and was not read`);
|
|
6921
|
+
continue;
|
|
6922
|
+
}
|
|
6923
|
+
read++;
|
|
6924
|
+
if (r.text === null) continue;
|
|
6925
|
+
const t = parseJUnitXml(r.text);
|
|
6831
6926
|
total.passed += t.passed;
|
|
6832
6927
|
total.failed += t.failed;
|
|
6833
6928
|
total.skipped += t.skipped;
|
|
@@ -6835,87 +6930,22 @@ async function junitAttestation(patterns, opts) {
|
|
|
6835
6930
|
total.durationSeconds = (total.durationSeconds ?? 0) + (t.durationSeconds ?? 0);
|
|
6836
6931
|
total.failures.push(...t.failures.slice(0, 50 - total.failures.length));
|
|
6837
6932
|
}
|
|
6933
|
+
if (!read) return null;
|
|
6838
6934
|
return { kind: "test-results", name: opts.name ?? "junit", data: { ...total } };
|
|
6839
6935
|
}
|
|
6840
6936
|
|
|
6841
6937
|
// src/core/attestations.ts
|
|
6842
|
-
var
|
|
6938
|
+
var import_crypto2 = require("crypto");
|
|
6843
6939
|
var import_path2 = require("path");
|
|
6844
|
-
|
|
6845
|
-
// src/core/artifacts.ts
|
|
6846
|
-
var import_crypto = require("crypto");
|
|
6847
|
-
var import_fs2 = require("fs");
|
|
6848
|
-
var import_path = require("path");
|
|
6849
|
-
var MAX_ARTIFACT_FILES = 50;
|
|
6850
|
-
var TYPES = {
|
|
6851
|
-
".zip": "application/zip",
|
|
6852
|
-
".gz": "application/gzip",
|
|
6853
|
-
".tgz": "application/gzip",
|
|
6854
|
-
".tar": "application/x-tar",
|
|
6855
|
-
".json": "application/json",
|
|
6856
|
-
".xml": "application/xml",
|
|
6857
|
-
".pdf": "application/pdf",
|
|
6858
|
-
".txt": "text/plain",
|
|
6859
|
-
".log": "text/plain",
|
|
6860
|
-
".info": "text/plain",
|
|
6861
|
-
".md": "text/markdown",
|
|
6862
|
-
".csv": "text/csv",
|
|
6863
|
-
".html": "text/html",
|
|
6864
|
-
".htm": "text/html",
|
|
6865
|
-
".png": "image/png",
|
|
6866
|
-
".jpg": "image/jpeg",
|
|
6867
|
-
".jpeg": "image/jpeg",
|
|
6868
|
-
".svg": "image/svg+xml"
|
|
6869
|
-
};
|
|
6870
|
-
var contentTypeFor = (path) => TYPES[(0, import_path.extname)(path).toLowerCase()] ?? "application/octet-stream";
|
|
6871
|
-
function sha256File(path) {
|
|
6872
|
-
return new Promise((resolveHash, reject) => {
|
|
6873
|
-
const hash = (0, import_crypto.createHash)("sha256");
|
|
6874
|
-
(0, import_fs2.createReadStream)(path).on("error", reject).on("data", (c) => hash.update(c)).on("end", () => resolveHash(hash.digest("hex")));
|
|
6875
|
-
});
|
|
6876
|
-
}
|
|
6877
|
-
var outside = (rel) => !rel || rel.startsWith("..") || (0, import_path.isAbsolute)(rel) || /^[A-Za-z]:/.test(rel);
|
|
6878
|
-
async function refusalFor(absolute, path, workspace) {
|
|
6879
|
-
if (path.split("/").includes(".git")) return `${path} is inside a .git directory and was not uploaded`;
|
|
6880
|
-
const [real, root] = await Promise.all([import_fs2.promises.realpath(absolute), import_fs2.promises.realpath(workspace)]);
|
|
6881
|
-
if (outside((0, import_path.relative)(root, real))) return `${path} resolves outside the workspace (symlink) and was not uploaded`;
|
|
6882
|
-
return null;
|
|
6883
|
-
}
|
|
6884
|
-
async function collectArtifacts(patterns, workspace, deps) {
|
|
6885
|
-
const warnings = [];
|
|
6886
|
-
if (!patterns.length) return { files: [], warnings };
|
|
6887
|
-
const find = deps.glob;
|
|
6888
|
-
const inside = [];
|
|
6889
|
-
for (const absolute of [...new Set(await find(patterns))]) {
|
|
6890
|
-
const rel = (0, import_path.relative)(workspace, absolute);
|
|
6891
|
-
if (outside(rel)) {
|
|
6892
|
-
warnings.push(`${absolute} is outside the workspace and was not uploaded`);
|
|
6893
|
-
continue;
|
|
6894
|
-
}
|
|
6895
|
-
const path = rel.split(import_path.sep).join("/");
|
|
6896
|
-
const refused = await refusalFor(absolute, path, workspace);
|
|
6897
|
-
if (refused) {
|
|
6898
|
-
warnings.push(refused);
|
|
6899
|
-
continue;
|
|
6900
|
-
}
|
|
6901
|
-
inside.push({ absolute, path });
|
|
6902
|
-
}
|
|
6903
|
-
inside.sort((a, b) => a.path.localeCompare(b.path));
|
|
6904
|
-
if (inside.length > MAX_ARTIFACT_FILES) warnings.push(`${inside.length} files matched; only the first ${MAX_ARTIFACT_FILES} are uploaded`);
|
|
6905
|
-
const files = [];
|
|
6906
|
-
for (const f of inside.slice(0, MAX_ARTIFACT_FILES)) {
|
|
6907
|
-
const stat = await import_fs2.promises.stat(f.absolute);
|
|
6908
|
-
if (!stat.isFile() || stat.size === 0) continue;
|
|
6909
|
-
files.push({ path: f.path, absolute: f.absolute, size: stat.size, sha256: await sha256File(f.absolute), contentType: contentTypeFor(f.path) });
|
|
6910
|
-
}
|
|
6911
|
-
return { files, warnings };
|
|
6912
|
-
}
|
|
6913
|
-
|
|
6914
|
-
// src/core/attestations.ts
|
|
6915
6940
|
var KINDS = ["test-results", "coverage", "sbom", "scan", "custom", "provenance"];
|
|
6916
|
-
function parseAttestationsInput(value, workspace) {
|
|
6941
|
+
async function parseAttestationsInput(value, workspace) {
|
|
6917
6942
|
if (!value) return [];
|
|
6918
|
-
|
|
6943
|
+
let text = value;
|
|
6944
|
+
if (!/^\s*[[{]/.test(value)) {
|
|
6945
|
+
const read = await readWorkspaceText(workspace, value.trim());
|
|
6946
|
+
if (!read.ok) throw new Error(`attestations file ${read.message}; it was not read`);
|
|
6947
|
+
text = read.text ?? "";
|
|
6948
|
+
}
|
|
6919
6949
|
let parsed;
|
|
6920
6950
|
try {
|
|
6921
6951
|
parsed = JSON.parse(text);
|
|
@@ -6931,6 +6961,7 @@ function parseAttestationsInput(value, workspace) {
|
|
|
6931
6961
|
});
|
|
6932
6962
|
}
|
|
6933
6963
|
var SCAN_FILE_FORMATS = ["sarif", "cyclonedx", "spdx", "grype-json", "trivy-json", "other"];
|
|
6964
|
+
var SCAN_MODE_WARNING = 'data.mode must be "full" or "diff"; ignored';
|
|
6934
6965
|
var MAX_SARIF_BYTES = 50 * 1024 * 1024;
|
|
6935
6966
|
function contentTypeForScanFile(format, path) {
|
|
6936
6967
|
if (format === "sarif") return "application/sarif+json";
|
|
@@ -6967,12 +6998,17 @@ function sarifCounts(json) {
|
|
|
6967
6998
|
}
|
|
6968
6999
|
return counts;
|
|
6969
7000
|
}
|
|
6970
|
-
async function resolveAttestationFiles(attestations, workspace
|
|
6971
|
-
const hash = deps.sha256File ?? sha256File;
|
|
7001
|
+
async function resolveAttestationFiles(attestations, workspace) {
|
|
6972
7002
|
const files = [];
|
|
6973
7003
|
const warnings = [];
|
|
6974
7004
|
const out = [];
|
|
6975
|
-
for (const
|
|
7005
|
+
for (const input of attestations) {
|
|
7006
|
+
let a = input;
|
|
7007
|
+
if (a.kind === "scan" && "mode" in a.data && a.data.mode !== "full" && a.data.mode !== "diff") {
|
|
7008
|
+
const { mode: _ignored, ...rest } = a.data;
|
|
7009
|
+
a = { ...a, data: rest };
|
|
7010
|
+
warnings.push(SCAN_MODE_WARNING);
|
|
7011
|
+
}
|
|
6976
7012
|
const file = a.file;
|
|
6977
7013
|
if (a.kind !== "scan" && a.kind !== "sbom" || typeof file !== "string" || !file.trim()) {
|
|
6978
7014
|
out.push(a);
|
|
@@ -6980,41 +7016,96 @@ async function resolveAttestationFiles(attestations, workspace, deps = {}) {
|
|
|
6980
7016
|
}
|
|
6981
7017
|
const format = a.format;
|
|
6982
7018
|
const scanFormat = typeof format === "string" && SCAN_FILE_FORMATS.includes(format) ? format : void 0;
|
|
6983
|
-
const
|
|
6984
|
-
const
|
|
6985
|
-
|
|
6986
|
-
|
|
7019
|
+
const wantCounts = a.kind === "scan" && scanFormat === "sarif" && a.data.critical === void 0 && a.data.high === void 0 && a.data.medium === void 0 && a.data.low === void 0;
|
|
7020
|
+
const notAttached = `and was not attached to "${a.name}"`;
|
|
7021
|
+
const opened = await openWorkspaceFile(workspace, file);
|
|
7022
|
+
if (!opened.ok) {
|
|
7023
|
+
const missing = opened.reason === "missing" || opened.reason === "not-file";
|
|
7024
|
+
warnings.push(missing ? `${file} was not found (or is empty) ${notAttached}` : `${opened.message} ${notAttached}`);
|
|
6987
7025
|
out.push(a);
|
|
6988
7026
|
continue;
|
|
6989
7027
|
}
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
}
|
|
7003
|
-
files.push({ path, absolute, size: stat.size, sha256: await hash(absolute), contentType: contentTypeForScanFile(scanFormat, path) });
|
|
7004
|
-
const data = { ...a.data, artifact: path };
|
|
7005
|
-
if (a.kind === "scan" && scanFormat) data.format = scanFormat;
|
|
7006
|
-
if (a.kind === "scan" && scanFormat === "sarif" && data.critical === void 0 && data.high === void 0 && data.medium === void 0 && data.low === void 0) {
|
|
7007
|
-
if (stat.size <= MAX_SARIF_BYTES) {
|
|
7008
|
-
Object.assign(data, sarifCounts((0, import_fs3.readFileSync)(absolute, "utf8")));
|
|
7028
|
+
let sha256;
|
|
7029
|
+
let counts = null;
|
|
7030
|
+
try {
|
|
7031
|
+
if (opened.size === 0) {
|
|
7032
|
+
warnings.push(`${file} was not found (or is empty) ${notAttached}`);
|
|
7033
|
+
out.push(a);
|
|
7034
|
+
continue;
|
|
7035
|
+
}
|
|
7036
|
+
if (wantCounts && opened.size <= MAX_SARIF_BYTES) {
|
|
7037
|
+
const bytes = await opened.handle.readFile();
|
|
7038
|
+
sha256 = (0, import_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
7039
|
+
counts = sarifCounts(bytes.toString("utf8"));
|
|
7009
7040
|
} else {
|
|
7010
|
-
|
|
7041
|
+
sha256 = await sha256Handle(opened.handle);
|
|
7011
7042
|
}
|
|
7043
|
+
} finally {
|
|
7044
|
+
await opened.handle.close();
|
|
7012
7045
|
}
|
|
7046
|
+
const { path, absolute } = opened.file;
|
|
7047
|
+
files.push({ path, absolute, workspace, size: opened.size, sha256, contentType: contentTypeForScanFile(scanFormat, path) });
|
|
7048
|
+
const data = { ...a.data, artifact: path };
|
|
7049
|
+
if (a.kind === "scan" && scanFormat) data.format = scanFormat;
|
|
7050
|
+
if (counts) Object.assign(data, counts);
|
|
7051
|
+
else if (wantCounts) warnings.push(`${file} is too large to compute SARIF counts from (over ${MAX_SARIF_BYTES} bytes); skipping`);
|
|
7013
7052
|
out.push({ kind: a.kind, name: a.name, status: a.status, data });
|
|
7014
7053
|
}
|
|
7015
7054
|
return { attestations: out, files, warnings };
|
|
7016
7055
|
}
|
|
7017
7056
|
|
|
7057
|
+
// src/core/artifacts.ts
|
|
7058
|
+
var import_path3 = require("path");
|
|
7059
|
+
var MAX_ARTIFACT_FILES = 50;
|
|
7060
|
+
var TYPES = {
|
|
7061
|
+
".zip": "application/zip",
|
|
7062
|
+
".gz": "application/gzip",
|
|
7063
|
+
".tgz": "application/gzip",
|
|
7064
|
+
".tar": "application/x-tar",
|
|
7065
|
+
".json": "application/json",
|
|
7066
|
+
".xml": "application/xml",
|
|
7067
|
+
".pdf": "application/pdf",
|
|
7068
|
+
".txt": "text/plain",
|
|
7069
|
+
".log": "text/plain",
|
|
7070
|
+
".info": "text/plain",
|
|
7071
|
+
".md": "text/markdown",
|
|
7072
|
+
".csv": "text/csv",
|
|
7073
|
+
".html": "text/html",
|
|
7074
|
+
".htm": "text/html",
|
|
7075
|
+
".png": "image/png",
|
|
7076
|
+
".jpg": "image/jpeg",
|
|
7077
|
+
".jpeg": "image/jpeg",
|
|
7078
|
+
".svg": "image/svg+xml"
|
|
7079
|
+
};
|
|
7080
|
+
var contentTypeFor = (path) => TYPES[(0, import_path3.extname)(path).toLowerCase()] ?? "application/octet-stream";
|
|
7081
|
+
async function collectArtifacts(patterns, workspace, deps) {
|
|
7082
|
+
const warnings = [];
|
|
7083
|
+
if (!patterns.length) return { files: [], warnings };
|
|
7084
|
+
const find = deps.glob;
|
|
7085
|
+
const inside = [];
|
|
7086
|
+
for (const absolute of [...new Set(await find(patterns))]) {
|
|
7087
|
+
const checked = await checkWorkspacePath(workspace, absolute, { display: displayName(workspace, absolute) });
|
|
7088
|
+
if (!checked.ok) {
|
|
7089
|
+
if (checked.reason !== "missing") warnings.push(`${checked.message} and was not uploaded`);
|
|
7090
|
+
continue;
|
|
7091
|
+
}
|
|
7092
|
+
inside.push({ absolute: checked.file.absolute, path: checked.file.path });
|
|
7093
|
+
}
|
|
7094
|
+
inside.sort((a, b) => a.path.localeCompare(b.path));
|
|
7095
|
+
if (inside.length > MAX_ARTIFACT_FILES) warnings.push(`${inside.length} files matched; only the first ${MAX_ARTIFACT_FILES} are uploaded`);
|
|
7096
|
+
const files = [];
|
|
7097
|
+
for (const f of inside.slice(0, MAX_ARTIFACT_FILES)) {
|
|
7098
|
+
const hashed = await hashWorkspaceFile(workspace, f.path);
|
|
7099
|
+
if (!hashed.ok) {
|
|
7100
|
+
if (hashed.reason !== "missing" && hashed.reason !== "not-file") warnings.push(`${hashed.message} and was not uploaded`);
|
|
7101
|
+
continue;
|
|
7102
|
+
}
|
|
7103
|
+
if (hashed.size === 0) continue;
|
|
7104
|
+
files.push({ path: f.path, absolute: f.absolute, workspace, size: hashed.size, sha256: hashed.sha256, contentType: contentTypeFor(f.path) });
|
|
7105
|
+
}
|
|
7106
|
+
return { files, warnings };
|
|
7107
|
+
}
|
|
7108
|
+
|
|
7018
7109
|
// src/core/request.ts
|
|
7019
7110
|
function buildIngestRequest(p) {
|
|
7020
7111
|
return {
|
|
@@ -7085,23 +7176,34 @@ var sendReport = (baseUrl, provider, req, audience, deps, azure) => postJson(`${
|
|
|
7085
7176
|
var completeReport = (baseUrl, provider, reportId, artifactIds, audience, deps, azure) => postJson(`${baseUrl}/api/ingest/${provider}/reports/${encodeURIComponent(reportId)}/complete`, azure ? { artifactIds, azure } : { artifactIds }, audience, deps);
|
|
7086
7177
|
|
|
7087
7178
|
// src/core/upload.ts
|
|
7088
|
-
var import_fs4 = require("fs");
|
|
7089
7179
|
var http = __toESM(require("http"));
|
|
7090
7180
|
var https = __toESM(require("https"));
|
|
7091
|
-
|
|
7181
|
+
var refused = (message) => Object.assign(new Error(message), { retryable: false });
|
|
7182
|
+
async function openForUpload(file) {
|
|
7183
|
+
const opened = await openWorkspaceFile(file.workspace, file.path);
|
|
7184
|
+
if (!opened.ok) throw refused(`${opened.message} and was not uploaded`);
|
|
7185
|
+
if (opened.size !== file.size) {
|
|
7186
|
+
await opened.handle.close();
|
|
7187
|
+
throw refused(`${file.path} changed since it was hashed and was not uploaded`);
|
|
7188
|
+
}
|
|
7189
|
+
return opened.handle;
|
|
7190
|
+
}
|
|
7191
|
+
async function putOnce(slot, file) {
|
|
7192
|
+
const handle = await openForUpload(file);
|
|
7193
|
+
const body = handle.createReadStream({ start: 0 });
|
|
7092
7194
|
return new Promise((resolve3, reject) => {
|
|
7093
7195
|
const url = new URL(slot.url);
|
|
7094
7196
|
const mod = url.protocol === "http:" ? http : https;
|
|
7095
7197
|
const headers = Object.fromEntries(Object.entries(slot.headers).filter(([k]) => !/^(proxy-)?authorization$|^cookie$/i.test(k)));
|
|
7096
7198
|
const req = mod.request(url, { method: "PUT", headers, timeout: 10 * 6e4 }, (res) => {
|
|
7097
|
-
let
|
|
7199
|
+
let body2 = "";
|
|
7098
7200
|
res.setEncoding("utf8");
|
|
7099
7201
|
res.on("data", (c) => {
|
|
7100
|
-
if (
|
|
7202
|
+
if (body2.length < 4e3) body2 += c;
|
|
7101
7203
|
});
|
|
7102
7204
|
res.on("end", () => {
|
|
7103
7205
|
if (res.statusCode && res.statusCode < 300) return resolve3();
|
|
7104
|
-
const code = /<Code>([^<]+)<\/Code>/.exec(
|
|
7206
|
+
const code = /<Code>([^<]+)<\/Code>/.exec(body2)?.[1] ?? `HTTP ${res.statusCode}`;
|
|
7105
7207
|
const err = new Error(`${file.path}: upload refused (${code})`);
|
|
7106
7208
|
err.retryable = (res.statusCode ?? 0) >= 500;
|
|
7107
7209
|
reject(err);
|
|
@@ -7116,8 +7218,8 @@ function putOnce(slot, file) {
|
|
|
7116
7218
|
if (e.retryable === void 0) e.retryable = true;
|
|
7117
7219
|
reject(e);
|
|
7118
7220
|
});
|
|
7119
|
-
|
|
7120
|
-
});
|
|
7221
|
+
body.on("error", reject).pipe(req);
|
|
7222
|
+
}).finally(() => body.destroy());
|
|
7121
7223
|
}
|
|
7122
7224
|
async function uploadFile(slot, file, attempts = 2) {
|
|
7123
7225
|
for (let i = 1; ; i++) {
|
|
@@ -7413,7 +7515,7 @@ function redactDeep(value, values) {
|
|
|
7413
7515
|
}
|
|
7414
7516
|
|
|
7415
7517
|
// src/cli/glob.ts
|
|
7416
|
-
var
|
|
7518
|
+
var import_path4 = require("path");
|
|
7417
7519
|
var { glob } = require_dist2();
|
|
7418
7520
|
function workspaceGlob(workspace, warn) {
|
|
7419
7521
|
return async (patterns) => {
|
|
@@ -7422,9 +7524,9 @@ function workspaceGlob(workspace, warn) {
|
|
|
7422
7524
|
for (const raw of patterns) {
|
|
7423
7525
|
const neg = raw.startsWith("!");
|
|
7424
7526
|
let p = neg ? raw.slice(1) : raw;
|
|
7425
|
-
if ((0,
|
|
7426
|
-
const rel = (0,
|
|
7427
|
-
if (!rel || rel.startsWith("..") || (0,
|
|
7527
|
+
if ((0, import_path4.isAbsolute)(p)) {
|
|
7528
|
+
const rel = (0, import_path4.relative)(workspace, p);
|
|
7529
|
+
if (!rel || rel.startsWith("..") || (0, import_path4.isAbsolute)(rel)) {
|
|
7428
7530
|
warn(`${raw} is outside the workspace and was skipped`);
|
|
7429
7531
|
continue;
|
|
7430
7532
|
}
|
|
@@ -7434,7 +7536,7 @@ function workspaceGlob(workspace, warn) {
|
|
|
7434
7536
|
}
|
|
7435
7537
|
if (!include.length) return [];
|
|
7436
7538
|
const found = await glob(include, { cwd: workspace, ignore, absolute: true, onlyFiles: true, dot: true, followSymbolicLinks: true });
|
|
7437
|
-
return found.map((f) => (0,
|
|
7539
|
+
return found.map((f) => (0, import_path4.resolve)(f)).sort();
|
|
7438
7540
|
};
|
|
7439
7541
|
}
|
|
7440
7542
|
|
|
@@ -7497,16 +7599,17 @@ var SKIPPED = {
|
|
|
7497
7599
|
monthly_quota: "your organization's monthly upload allowance is used up"
|
|
7498
7600
|
};
|
|
7499
7601
|
var DEFAULT_SUMMARY_FILE = "prodgator-summary.md";
|
|
7500
|
-
async function readSummary(workspace, summary, file,
|
|
7602
|
+
async function readSummary(workspace, summary, file, log) {
|
|
7501
7603
|
if (summary) return summary;
|
|
7502
7604
|
const path = file ?? DEFAULT_SUMMARY_FILE;
|
|
7503
|
-
const
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7605
|
+
const read = await readWorkspaceText(workspace, path);
|
|
7606
|
+
if (read.ok) return read.text;
|
|
7607
|
+
if (read.reason === "missing") {
|
|
7608
|
+
if (file) log.info(`No summary file at ${path}; sending the report without a summary.`);
|
|
7609
|
+
} else {
|
|
7610
|
+
log.warn(`${read.message}; sending the report without a summary.`);
|
|
7509
7611
|
}
|
|
7612
|
+
return null;
|
|
7510
7613
|
}
|
|
7511
7614
|
function mergeFiles(first, rest, warn) {
|
|
7512
7615
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -7520,7 +7623,7 @@ async function run(deps = {}) {
|
|
|
7520
7623
|
const argv = deps.argv ?? process.argv.slice(2);
|
|
7521
7624
|
const log = deps.log ?? { info: (m) => console.log(m), warn: (m) => console.log(`Warning: ${m}`), error: (m) => console.error(m) };
|
|
7522
7625
|
if (argv.includes("--version")) {
|
|
7523
|
-
log.info("3.
|
|
7626
|
+
log.info("3.19.1");
|
|
7524
7627
|
return 0;
|
|
7525
7628
|
}
|
|
7526
7629
|
let failOnError = false;
|
|
@@ -7538,7 +7641,7 @@ async function run(deps = {}) {
|
|
|
7538
7641
|
const problem = tokenProblem(token, opts.audience, ci.provider, variable, (deps.nowMs ?? Date.now)());
|
|
7539
7642
|
if (problem) throw new CliError(problem);
|
|
7540
7643
|
if (opts.gate) {
|
|
7541
|
-
const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"3.
|
|
7644
|
+
const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"3.19.1"} (${ci.provider}; gate)` };
|
|
7542
7645
|
return await runGate(ci, { prodgatorUrl: opts.prodgatorUrl, audience: opts.audience, timeoutMinutes: opts.gateTimeoutMinutes }, client2, {
|
|
7543
7646
|
sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
|
|
7544
7647
|
nowMs: deps.nowMs ?? Date.now,
|
|
@@ -7547,11 +7650,11 @@ async function run(deps = {}) {
|
|
|
7547
7650
|
}
|
|
7548
7651
|
const secrets = secretValues(env, [variable, ...opts.redactVariables]);
|
|
7549
7652
|
const glob2 = workspaceGlob(ci.workspace, (w) => log.warn(w));
|
|
7550
|
-
const rawSummary = await readSummary(ci.workspace, opts.summary, opts.summaryFile,
|
|
7653
|
+
const rawSummary = await readSummary(ci.workspace, opts.summary, opts.summaryFile, log);
|
|
7551
7654
|
const summary = rawSummary ? truncateUtf8(redactText(rawSummary, secrets), MAX_SUMMARY_BYTES).text : null;
|
|
7552
|
-
const scanned = await resolveAttestationFiles(parseAttestationsInput(opts.attestations, ci.workspace), ci.workspace);
|
|
7655
|
+
const scanned = await resolveAttestationFiles(await parseAttestationsInput(opts.attestations, ci.workspace), ci.workspace);
|
|
7553
7656
|
const attestations = redactDeep(scanned.attestations, secrets);
|
|
7554
|
-
const junit = await junitAttestation(opts.junit, { glob: glob2 });
|
|
7657
|
+
const junit = await junitAttestation(opts.junit, { glob: glob2, workspace: ci.workspace, warn: (w) => log.warn(w) });
|
|
7555
7658
|
if (junit) attestations.push(junit);
|
|
7556
7659
|
const globbed = await collectArtifacts(opts.artifacts, ci.workspace, { glob: glob2 });
|
|
7557
7660
|
[...scanned.warnings, ...globbed.warnings].forEach((w) => log.warn(w));
|
|
@@ -7561,7 +7664,7 @@ async function run(deps = {}) {
|
|
|
7561
7664
|
fetch: deps.fetch,
|
|
7562
7665
|
sleep: deps.sleep,
|
|
7563
7666
|
fixedToken: true,
|
|
7564
|
-
userAgent: `prodgator-report/${"3.
|
|
7667
|
+
userAgent: `prodgator-report/${"3.19.1"} (${ci.provider})`
|
|
7565
7668
|
};
|
|
7566
7669
|
const request = buildIngestRequest({ job: { name: opts.jobName ?? ci.jobName, matrix: opts.matrix, runner: ci.runner }, name: opts.name, summary, attestations, files });
|
|
7567
7670
|
const res = await sendReport(opts.prodgatorUrl, ci.provider, request, opts.audience, client, ci.azure);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prodgator/report",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.1",
|
|
4
4
|
"description": "Send CI job summaries, test results, attestations and artifacts to Prodgator, authenticated with the CI system's OIDC token",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.prodgator.io/pipelines/run-reports",
|