@prodgator/report 3.19.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.
Files changed (2) hide show
  1. package/dist/cli.js +257 -161
  2. 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 constants = require_constants();
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
- } = constants;
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 : constants.DEFAULT_MAX_EXTGLOB_RECURSION;
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 = constants.globChars(opts.windows);
901
- const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_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
- } = constants.globChars(opts.windows);
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 constants = require_constants();
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 = 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 fs5 = __toESM2(require("fs"));
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 || fs5
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 fs5 = require("fs");
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 || fs5.readdir,
2611
- readdirSync: opts.fs.readdirSync || fs5.readdirSync,
2612
- realpath: opts.fs.realpath || fs5.realpath,
2613
- realpathSync: opts.fs.realpathSync || fs5.realpathSync,
2614
- stat: opts.fs.stat || fs5.stat,
2615
- statSync: opts.fs.statSync || fs5.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, relative4] = getCrawler(globInput, options);
2630
- return crawler ? formatPaths(await crawler.withPromise(), relative4) : [];
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, relative4] = getCrawler(globInput, options);
2634
- return crawler ? formatPaths(crawler.sync(), relative4) : [];
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 sep4 = separator || this.separator;
5091
- const isDefault = sep4 === this.separator && includeNamespace === true;
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(sep4);
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(sep4);
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 stat = await import_fs.promises.stat(f);
6829
- if (stat.size > MAX_FILE_BYTES) continue;
6830
- const t = parseJUnitXml(await import_fs.promises.readFile(f, "utf8"));
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 import_fs3 = require("fs");
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
- const text = /^\s*[[{]/.test(value) ? value : (0, import_fs3.readFileSync)((0, import_path2.resolve)(workspace, value.trim()), "utf8");
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);
@@ -6968,8 +6998,7 @@ function sarifCounts(json) {
6968
6998
  }
6969
6999
  return counts;
6970
7000
  }
6971
- async function resolveAttestationFiles(attestations, workspace, deps = {}) {
6972
- const hash = deps.sha256File ?? sha256File;
7001
+ async function resolveAttestationFiles(attestations, workspace) {
6973
7002
  const files = [];
6974
7003
  const warnings = [];
6975
7004
  const out = [];
@@ -6987,41 +7016,96 @@ async function resolveAttestationFiles(attestations, workspace, deps = {}) {
6987
7016
  }
6988
7017
  const format = a.format;
6989
7018
  const scanFormat = typeof format === "string" && SCAN_FILE_FORMATS.includes(format) ? format : void 0;
6990
- const absolute = (0, import_path2.resolve)(workspace, file);
6991
- const rel = (0, import_path2.relative)(workspace, absolute);
6992
- if (!rel || rel.startsWith("..") || /^[A-Za-z]:/.test(rel)) {
6993
- warnings.push(`${file} is outside the workspace and was not attached to "${a.name}"`);
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}`);
6994
7025
  out.push(a);
6995
7026
  continue;
6996
7027
  }
6997
- const path = rel.split(import_path2.sep).join("/");
6998
- const stat = await import_fs3.promises.stat(absolute).catch(() => null);
6999
- if (!stat?.isFile() || stat.size === 0) {
7000
- warnings.push(`${file} was not found (or is empty) and was not attached to "${a.name}"`);
7001
- out.push(a);
7002
- continue;
7003
- }
7004
- const refused = await refusalFor(absolute, path, workspace);
7005
- if (refused) {
7006
- warnings.push(refused);
7007
- out.push(a);
7008
- continue;
7009
- }
7010
- files.push({ path, absolute, size: stat.size, sha256: await hash(absolute), contentType: contentTypeForScanFile(scanFormat, path) });
7011
- const data = { ...a.data, artifact: path };
7012
- if (a.kind === "scan" && scanFormat) data.format = scanFormat;
7013
- if (a.kind === "scan" && scanFormat === "sarif" && data.critical === void 0 && data.high === void 0 && data.medium === void 0 && data.low === void 0) {
7014
- if (stat.size <= MAX_SARIF_BYTES) {
7015
- 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"));
7016
7040
  } else {
7017
- warnings.push(`${file} is too large to compute SARIF counts from (over ${MAX_SARIF_BYTES} bytes); skipping`);
7041
+ sha256 = await sha256Handle(opened.handle);
7018
7042
  }
7043
+ } finally {
7044
+ await opened.handle.close();
7019
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`);
7020
7052
  out.push({ kind: a.kind, name: a.name, status: a.status, data });
7021
7053
  }
7022
7054
  return { attestations: out, files, warnings };
7023
7055
  }
7024
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
+
7025
7109
  // src/core/request.ts
7026
7110
  function buildIngestRequest(p) {
7027
7111
  return {
@@ -7092,23 +7176,34 @@ var sendReport = (baseUrl, provider, req, audience, deps, azure) => postJson(`${
7092
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);
7093
7177
 
7094
7178
  // src/core/upload.ts
7095
- var import_fs4 = require("fs");
7096
7179
  var http = __toESM(require("http"));
7097
7180
  var https = __toESM(require("https"));
7098
- function putOnce(slot, file) {
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 });
7099
7194
  return new Promise((resolve3, reject) => {
7100
7195
  const url = new URL(slot.url);
7101
7196
  const mod = url.protocol === "http:" ? http : https;
7102
7197
  const headers = Object.fromEntries(Object.entries(slot.headers).filter(([k]) => !/^(proxy-)?authorization$|^cookie$/i.test(k)));
7103
7198
  const req = mod.request(url, { method: "PUT", headers, timeout: 10 * 6e4 }, (res) => {
7104
- let body = "";
7199
+ let body2 = "";
7105
7200
  res.setEncoding("utf8");
7106
7201
  res.on("data", (c) => {
7107
- if (body.length < 4e3) body += c;
7202
+ if (body2.length < 4e3) body2 += c;
7108
7203
  });
7109
7204
  res.on("end", () => {
7110
7205
  if (res.statusCode && res.statusCode < 300) return resolve3();
7111
- const code = /<Code>([^<]+)<\/Code>/.exec(body)?.[1] ?? `HTTP ${res.statusCode}`;
7206
+ const code = /<Code>([^<]+)<\/Code>/.exec(body2)?.[1] ?? `HTTP ${res.statusCode}`;
7112
7207
  const err = new Error(`${file.path}: upload refused (${code})`);
7113
7208
  err.retryable = (res.statusCode ?? 0) >= 500;
7114
7209
  reject(err);
@@ -7123,8 +7218,8 @@ function putOnce(slot, file) {
7123
7218
  if (e.retryable === void 0) e.retryable = true;
7124
7219
  reject(e);
7125
7220
  });
7126
- (0, import_fs4.createReadStream)(file.absolute).on("error", reject).pipe(req);
7127
- });
7221
+ body.on("error", reject).pipe(req);
7222
+ }).finally(() => body.destroy());
7128
7223
  }
7129
7224
  async function uploadFile(slot, file, attempts = 2) {
7130
7225
  for (let i = 1; ; i++) {
@@ -7420,7 +7515,7 @@ function redactDeep(value, values) {
7420
7515
  }
7421
7516
 
7422
7517
  // src/cli/glob.ts
7423
- var import_path3 = require("path");
7518
+ var import_path4 = require("path");
7424
7519
  var { glob } = require_dist2();
7425
7520
  function workspaceGlob(workspace, warn) {
7426
7521
  return async (patterns) => {
@@ -7429,9 +7524,9 @@ function workspaceGlob(workspace, warn) {
7429
7524
  for (const raw of patterns) {
7430
7525
  const neg = raw.startsWith("!");
7431
7526
  let p = neg ? raw.slice(1) : raw;
7432
- if ((0, import_path3.isAbsolute)(p)) {
7433
- const rel = (0, import_path3.relative)(workspace, p);
7434
- if (!rel || rel.startsWith("..") || (0, import_path3.isAbsolute)(rel)) {
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)) {
7435
7530
  warn(`${raw} is outside the workspace and was skipped`);
7436
7531
  continue;
7437
7532
  }
@@ -7441,7 +7536,7 @@ function workspaceGlob(workspace, warn) {
7441
7536
  }
7442
7537
  if (!include.length) return [];
7443
7538
  const found = await glob(include, { cwd: workspace, ignore, absolute: true, onlyFiles: true, dot: true, followSymbolicLinks: true });
7444
- return found.map((f) => (0, import_path3.resolve)(f)).sort();
7539
+ return found.map((f) => (0, import_path4.resolve)(f)).sort();
7445
7540
  };
7446
7541
  }
7447
7542
 
@@ -7504,16 +7599,17 @@ var SKIPPED = {
7504
7599
  monthly_quota: "your organization's monthly upload allowance is used up"
7505
7600
  };
7506
7601
  var DEFAULT_SUMMARY_FILE = "prodgator-summary.md";
7507
- async function readSummary(workspace, summary, file, info) {
7602
+ async function readSummary(workspace, summary, file, log) {
7508
7603
  if (summary) return summary;
7509
7604
  const path = file ?? DEFAULT_SUMMARY_FILE;
7510
- const absolute = (0, import_path4.isAbsolute)(path) ? path : (0, import_path4.join)(workspace, path);
7511
- try {
7512
- return await import_fs5.promises.readFile(absolute, "utf8");
7513
- } catch {
7514
- if (file) info(`No summary file at ${path}; sending the report without a summary.`);
7515
- return null;
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.`);
7516
7611
  }
7612
+ return null;
7517
7613
  }
7518
7614
  function mergeFiles(first, rest, warn) {
7519
7615
  const byPath = /* @__PURE__ */ new Map();
@@ -7527,7 +7623,7 @@ async function run(deps = {}) {
7527
7623
  const argv = deps.argv ?? process.argv.slice(2);
7528
7624
  const log = deps.log ?? { info: (m) => console.log(m), warn: (m) => console.log(`Warning: ${m}`), error: (m) => console.error(m) };
7529
7625
  if (argv.includes("--version")) {
7530
- log.info("3.19.0");
7626
+ log.info("3.19.1");
7531
7627
  return 0;
7532
7628
  }
7533
7629
  let failOnError = false;
@@ -7545,7 +7641,7 @@ async function run(deps = {}) {
7545
7641
  const problem = tokenProblem(token, opts.audience, ci.provider, variable, (deps.nowMs ?? Date.now)());
7546
7642
  if (problem) throw new CliError(problem);
7547
7643
  if (opts.gate) {
7548
- const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"3.19.0"} (${ci.provider}; gate)` };
7644
+ const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"3.19.1"} (${ci.provider}; gate)` };
7549
7645
  return await runGate(ci, { prodgatorUrl: opts.prodgatorUrl, audience: opts.audience, timeoutMinutes: opts.gateTimeoutMinutes }, client2, {
7550
7646
  sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
7551
7647
  nowMs: deps.nowMs ?? Date.now,
@@ -7554,11 +7650,11 @@ async function run(deps = {}) {
7554
7650
  }
7555
7651
  const secrets = secretValues(env, [variable, ...opts.redactVariables]);
7556
7652
  const glob2 = workspaceGlob(ci.workspace, (w) => log.warn(w));
7557
- const rawSummary = await readSummary(ci.workspace, opts.summary, opts.summaryFile, (m) => log.info(m));
7653
+ const rawSummary = await readSummary(ci.workspace, opts.summary, opts.summaryFile, log);
7558
7654
  const summary = rawSummary ? truncateUtf8(redactText(rawSummary, secrets), MAX_SUMMARY_BYTES).text : null;
7559
- const scanned = await resolveAttestationFiles(parseAttestationsInput(opts.attestations, ci.workspace), ci.workspace);
7655
+ const scanned = await resolveAttestationFiles(await parseAttestationsInput(opts.attestations, ci.workspace), ci.workspace);
7560
7656
  const attestations = redactDeep(scanned.attestations, secrets);
7561
- 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) });
7562
7658
  if (junit) attestations.push(junit);
7563
7659
  const globbed = await collectArtifacts(opts.artifacts, ci.workspace, { glob: glob2 });
7564
7660
  [...scanned.warnings, ...globbed.warnings].forEach((w) => log.warn(w));
@@ -7568,7 +7664,7 @@ async function run(deps = {}) {
7568
7664
  fetch: deps.fetch,
7569
7665
  sleep: deps.sleep,
7570
7666
  fixedToken: true,
7571
- userAgent: `prodgator-report/${"3.19.0"} (${ci.provider})`
7667
+ userAgent: `prodgator-report/${"3.19.1"} (${ci.provider})`
7572
7668
  };
7573
7669
  const request = buildIngestRequest({ job: { name: opts.jobName ?? ci.jobName, matrix: opts.matrix, runner: ci.runner }, name: opts.name, summary, attestations, files });
7574
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.19.0",
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",