vouchington-tooling 0.0.19 → 0.0.20

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/index.d.mts CHANGED
@@ -8,7 +8,7 @@ export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAud
8
8
  export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mts';
9
9
  export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, VitestReportAttempt, VitestReportAttemptIdentity, } from './vitest-blob-manifest/index.mts';
10
10
  export { prepareVitestReports } from './vitest-blob-manifest/reports.mts';
11
- export type { PrepareVitestReportsOptions, SelectedVitestReport, VitestReportExpectation, } from './vitest-blob-manifest/reports.mts';
11
+ export type { PrepareVitestReportsOptions, RejectedVitestReportSource, SelectedVitestReport, VitestReportExpectation, VitestReportRejectionReason, } from './vitest-blob-manifest/reports.mts';
12
12
  export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mts';
13
13
  export type { InstallOptions, Lifecycle } from './pnpm-install/index.mts';
14
14
  export { flattenReleaseAgeSelectors, packageNameFromPnpmLockKey, pnpmLockPackageKeyMatchesSelector, validateReleaseAgeExemptionGroups, validateReleaseAgePolicy, } from './pnpm-install/index.mts';
@@ -0,0 +1,2 @@
1
+ export declare class VitestBlobBundleError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export class VitestBlobBundleError extends Error {
2
+ }
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { lstatSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
4
  import { VITEST_SUITE_PATTERN } from './constants.mjs';
5
+ import { VitestBlobBundleError } from './bundle-error.mjs';
5
6
  export * from './report-attempt.mjs';
6
7
  export { VITEST_SUITE_PATTERN } from './constants.mjs';
7
8
  export const VITEST_BLOB_MANIFEST_FILENAME = 'vitest-blob-manifest.json';
@@ -89,9 +90,7 @@ export function writeVitestBlobManifest(directory, identity) {
89
90
  try {
90
91
  unlinkSync(temporaryPath);
91
92
  }
92
- catch {
93
- // Successful rename removes the temporary path.
94
- }
93
+ catch { }
95
94
  }
96
95
  return manifestPath;
97
96
  }
@@ -105,11 +104,13 @@ export function vitestBlobBundlePaths(directory, suite) {
105
104
  return [manifestPath, reportPath];
106
105
  }
107
106
  export function inspectVitestBlobBundle(directory) {
108
- const entries = readdirSync(directory, { withFileTypes: true });
107
+ const entries = readdirSync(directory, { withFileTypes: true }), name = basename(directory);
109
108
  if (entries.length !== 2 || entries.some((entry) => !entry.isFile())) {
110
- throw new Error(`Vitest blob bundle ${basename(directory)} must contain exactly two files`);
109
+ throw new VitestBlobBundleError(`Vitest blob bundle ${name} must contain exactly two files`);
111
110
  }
112
111
  const manifestPath = join(directory, VITEST_BLOB_MANIFEST_FILENAME);
112
+ if (!entries.some((entry) => entry.name === VITEST_BLOB_MANIFEST_FILENAME))
113
+ throw new VitestBlobBundleError(`Invalid Vitest blob bundle ${name}`);
113
114
  assertRegularFile(manifestPath, 'Vitest blob manifest');
114
115
  const manifestBytes = readFileSync(manifestPath);
115
116
  let manifest;
@@ -117,20 +118,24 @@ export function inspectVitestBlobBundle(directory) {
117
118
  manifest = parseVitestBlobManifest(JSON.parse(manifestBytes.toString('utf8')));
118
119
  }
119
120
  catch (error) {
120
- throw new Error(`Invalid Vitest blob bundle ${basename(directory)}`, { cause: error });
121
+ throw new VitestBlobBundleError(`Invalid Vitest blob bundle ${name}`, { cause: error });
121
122
  }
122
123
  const reportPath = join(directory, manifest.report.filename);
124
+ if (!entries.some((entry) => entry.name === manifest.report.filename))
125
+ throw new VitestBlobBundleError(`Invalid Vitest blob bundle ${name}`);
123
126
  assertRegularFile(reportPath, 'Vitest blob report');
124
127
  const reportBytes = readFileSync(reportPath);
125
128
  if (reportBytes.byteLength !== manifest.report.byteLength ||
126
129
  sha256(reportBytes) !== manifest.report.sha256) {
127
- throw new Error(`Vitest blob report integrity check failed for ${manifest.suite}`);
130
+ throw new VitestBlobBundleError(`Vitest blob report integrity check failed for ${manifest.suite}`);
128
131
  }
129
132
  try {
130
133
  JSON.parse(reportBytes.toString('utf8'));
131
134
  }
132
135
  catch (error) {
133
- throw new Error(`Vitest blob report is not valid JSON for ${manifest.suite}`, { cause: error });
136
+ throw new VitestBlobBundleError(`Vitest blob report is not valid JSON for ${manifest.suite}`, {
137
+ cause: error,
138
+ });
134
139
  }
135
140
  return { directory, manifest, manifestBytes, reportBytes };
136
141
  }
@@ -0,0 +1,26 @@
1
+ import { type InspectedVitestBlobBundle } from './index.mts';
2
+ export type VitestReportSource = 'primary' | 'fallback';
3
+ export type VitestReportRejectionReason = 'root-not-directory' | 'invalid-archive' | 'unexpected-entry' | 'invalid-bundle' | 'identity-mismatch' | 'future-attempt' | 'intra-source-conflict' | 'unexpected-current-attempt-suite';
4
+ export interface RejectedVitestReportSource {
5
+ readonly source: VitestReportSource;
6
+ readonly reason: VitestReportRejectionReason;
7
+ }
8
+ export type Candidate = InspectedVitestBlobBundle & {
9
+ readonly source: VitestReportSource;
10
+ };
11
+ type SourceOptions = {
12
+ readonly repository: string;
13
+ readonly revision: string;
14
+ readonly run: {
15
+ readonly id: string;
16
+ readonly currentAttempt: number;
17
+ };
18
+ readonly expectedSuites: readonly {
19
+ readonly suite: string;
20
+ }[];
21
+ };
22
+ export declare function inspectVitestReportSource(root: string, source: VitestReportSource, options: SourceOptions): {
23
+ readonly candidates: readonly Candidate[];
24
+ readonly rejected?: RejectedVitestReportSource;
25
+ };
26
+ export {};
@@ -0,0 +1,88 @@
1
+ import { lstatSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { inspectVitestBlobBundle, VITEST_BLOB_MANIFEST_FILENAME, VITEST_SUITE_PATTERN, } from './index.mjs';
4
+ import { VitestBlobBundleError } from './bundle-error.mjs';
5
+ class SourceFailure extends Error {
6
+ reason;
7
+ constructor(reason) {
8
+ super(reason);
9
+ this.reason = reason;
10
+ }
11
+ }
12
+ function bundle(root, source) {
13
+ const entries = readdirSync(root, { withFileTypes: true });
14
+ if (entries.length === 0)
15
+ return [];
16
+ if (entries.some((entry) => entry.name === VITEST_BLOB_MANIFEST_FILENAME)) {
17
+ try {
18
+ return [{ ...inspectVitestBlobBundle(root), source }];
19
+ }
20
+ catch (error) {
21
+ if (error instanceof VitestBlobBundleError)
22
+ throw new SourceFailure('invalid-bundle');
23
+ throw error;
24
+ }
25
+ }
26
+ if (entries.some((entry) => entry.isFile() &&
27
+ entry.name.startsWith('.invalid-') &&
28
+ VITEST_SUITE_PATTERN.test(entry.name.slice('.invalid-'.length))))
29
+ throw new SourceFailure('invalid-archive');
30
+ if (entries.some((entry) => !entry.isDirectory()))
31
+ throw new SourceFailure('unexpected-entry');
32
+ try {
33
+ return entries
34
+ .toSorted((left, right) => left.name.localeCompare(right.name))
35
+ .map((entry) => ({ ...inspectVitestBlobBundle(join(root, entry.name)), source }));
36
+ }
37
+ catch (error) {
38
+ if (error instanceof VitestBlobBundleError)
39
+ throw new SourceFailure('invalid-bundle');
40
+ throw error;
41
+ }
42
+ }
43
+ function validate(candidates, options) {
44
+ const expected = new Set(options.expectedSuites.map((expectation) => expectation.suite));
45
+ const first = new Map();
46
+ for (const candidate of candidates) {
47
+ const { manifest } = candidate;
48
+ if (manifest.repository !== options.repository ||
49
+ manifest.revision !== options.revision ||
50
+ manifest.run.id !== options.run.id)
51
+ throw new SourceFailure('identity-mismatch');
52
+ if (manifest.run.attempt > options.run.currentAttempt)
53
+ throw new SourceFailure('future-attempt');
54
+ if (!expected.has(manifest.suite) && manifest.run.attempt === options.run.currentAttempt)
55
+ throw new SourceFailure('unexpected-current-attempt-suite');
56
+ const key = `${manifest.suite}\0${manifest.run.attempt}`, prior = first.get(key);
57
+ if (prior &&
58
+ (!prior.manifestBytes.equals(candidate.manifestBytes) ||
59
+ !prior.reportBytes.equals(candidate.reportBytes)))
60
+ throw new SourceFailure('intra-source-conflict');
61
+ first.set(key, prior ?? candidate);
62
+ }
63
+ }
64
+ export function inspectVitestReportSource(root, source, options) {
65
+ let metadata;
66
+ try {
67
+ metadata = lstatSync(root);
68
+ }
69
+ catch (error) {
70
+ if (error instanceof Error &&
71
+ 'code' in error &&
72
+ error.code === 'ENOENT')
73
+ return { candidates: [] };
74
+ throw error;
75
+ }
76
+ if (!metadata.isDirectory())
77
+ return { candidates: [], rejected: { source, reason: 'root-not-directory' } };
78
+ try {
79
+ const candidates = bundle(root, source);
80
+ validate(candidates, options);
81
+ return { candidates };
82
+ }
83
+ catch (error) {
84
+ if (error instanceof SourceFailure)
85
+ return { candidates: [], rejected: { source, reason: error.reason } };
86
+ throw error;
87
+ }
88
+ }
@@ -1,3 +1,4 @@
1
+ import { type RejectedVitestReportSource, type VitestReportSource } from './reports-source.mts';
1
2
  export interface PrepareVitestReportsOptions {
2
3
  readonly primaryDir: string;
3
4
  readonly fallbackDir: string;
@@ -17,12 +18,11 @@ export type VitestReportExpectation = {
17
18
  export interface SelectedVitestReport {
18
19
  readonly suite: string;
19
20
  readonly attempt: number;
20
- readonly sources: readonly ('primary' | 'fallback')[];
21
+ readonly sources: readonly VitestReportSource[];
21
22
  }
22
- /**
23
- * Validates untrusted blob bundles and atomically publishes one newest report per expected suite.
24
- * The caller owns artifact transport; this function deliberately has no network or CI-provider API.
25
- */
23
+ export type { RejectedVitestReportSource, VitestReportRejectionReason } from './reports-source.mts';
24
+ /** Validates untrusted blob bundles and atomically publishes one newest report per expected suite. */
26
25
  export declare function prepareVitestReports(options: PrepareVitestReportsOptions): {
27
26
  readonly selected: readonly SelectedVitestReport[];
27
+ readonly rejectedSources: readonly RejectedVitestReportSource[];
28
28
  };
@@ -1,90 +1,42 @@
1
- /* eslint-disable max-lines -- untrusted artifact discovery and publication form one security boundary. */
2
1
  import { randomUUID } from 'node:crypto';
3
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
4
3
  import { basename, dirname, join } from 'node:path';
5
- import { inspectVitestBlobBundle, VITEST_BLOB_MANIFEST_FILENAME, VITEST_SUITE_PATTERN, } from './index.mjs';
6
- function inspectSource(root, source) {
7
- if (!existsSync(root))
8
- return [];
9
- if (!lstatSync(root).isDirectory())
10
- throw new Error(`Vitest ${source} root must be a directory`);
11
- const entries = readdirSync(root, { withFileTypes: true });
12
- if (entries.length === 0)
13
- return [];
14
- const isFlattened = entries.some((entry) => entry.name === VITEST_BLOB_MANIFEST_FILENAME);
15
- if (isFlattened)
16
- return [{ ...inspectVitestBlobBundle(root), source }];
17
- const invalid = entries.find((entry) => {
18
- const suite = entry.name.startsWith('.invalid-') ? entry.name.slice('.invalid-'.length) : '';
19
- return entry.isFile() && VITEST_SUITE_PATTERN.test(suite);
20
- });
21
- if (invalid)
22
- throw new Error(`Vitest ${source} root contains an invalid archive marker`);
23
- const unexpected = entries.find((entry) => !entry.isDirectory());
24
- if (unexpected)
25
- throw new Error(`Vitest ${source} root has an unexpected entry`);
26
- return entries
27
- .toSorted((left, right) => left.name.localeCompare(right.name))
28
- .map((entry) => ({ ...inspectVitestBlobBundle(join(root, entry.name)), source }));
29
- }
4
+ import { VITEST_SUITE_PATTERN } from './index.mjs';
5
+ import { inspectVitestReportSource, } from './reports-source.mjs';
30
6
  function validateOptions(options) {
31
- if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(options.repository)) {
7
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(options.repository))
32
8
  throw new Error('Invalid Vitest repository');
33
- }
34
9
  if (!/^[0-9a-f]{40}$/.test(options.revision))
35
10
  throw new Error('Invalid Vitest revision');
36
- if (!/^[1-9][0-9]*$/.test(options.run.id) || !Number.isSafeInteger(options.run.currentAttempt)) {
37
- throw new Error('Invalid Vitest run');
38
- }
39
- if (options.run.currentAttempt < 1)
11
+ if (!/^[1-9][0-9]*$/.test(options.run.id) ||
12
+ !Number.isSafeInteger(options.run.currentAttempt) ||
13
+ options.run.currentAttempt < 1)
40
14
  throw new Error('Invalid Vitest run');
41
15
  for (const expectation of options.expectedSuites) {
42
- if (!VITEST_SUITE_PATTERN.test(expectation.suite) ||
43
- !Number.isSafeInteger(expectation.minimumAttempt)) {
16
+ if (!VITEST_SUITE_PATTERN.test(expectation.suite))
44
17
  throw new Error('Invalid expected Vitest suite');
45
- }
46
- if (expectation.minimumAttempt < 1 || expectation.minimumAttempt > options.run.currentAttempt) {
18
+ if (!Number.isSafeInteger(expectation.minimumAttempt) ||
19
+ expectation.minimumAttempt < 1 ||
20
+ expectation.minimumAttempt > options.run.currentAttempt)
47
21
  throw new Error('Invalid expected Vitest suite attempt');
48
- }
49
- }
50
- }
51
- function validateIdentity(candidate, options) {
52
- const { manifest } = candidate;
53
- if (manifest.repository !== options.repository ||
54
- manifest.revision !== options.revision ||
55
- manifest.run.id !== options.run.id) {
56
- throw new Error(`Vitest blob identity does not match this run for ${manifest.suite}`);
57
22
  }
58
- if (manifest.run.attempt > options.run.currentAttempt) {
59
- throw new Error(`Vitest blob attempt is from the future for ${manifest.suite}`);
60
- }
61
- }
62
- function identical(left, right) {
63
- return (left.manifestBytes.equals(right.manifestBytes) && left.reportBytes.equals(right.reportBytes));
23
+ if (new Set(options.expectedSuites.map((expectation) => expectation.suite)).size !==
24
+ options.expectedSuites.length)
25
+ throw new Error('Expected Vitest suites must be unique');
64
26
  }
65
- function rejectConflictingCopies(candidates) {
66
- const firstByIdentity = new Map();
67
- for (const candidate of candidates) {
68
- const key = `${candidate.manifest.suite}\0${candidate.manifest.run.attempt}`;
69
- const first = firstByIdentity.get(key);
70
- if (first && !identical(first, candidate)) {
71
- throw new Error(`Conflicting Vitest blobs for ${candidate.manifest.suite} attempt ${candidate.manifest.run.attempt}`);
72
- }
73
- firstByIdentity.set(key, first ?? candidate);
74
- }
27
+ function selectionError(message, rejected) {
28
+ const context = rejected.map(({ source, reason }) => `${source}=${reason}`).join(', ');
29
+ return new Error(context ? `${message}; rejected sources: ${context}` : message);
75
30
  }
76
- function selectCandidates(candidates, options) {
77
- const expected = new Map(options.expectedSuites.map((expectation) => [expectation.suite, expectation.minimumAttempt]));
78
- if (expected.size !== options.expectedSuites.length)
79
- throw new Error('Expected Vitest suites must be unique');
80
- for (const candidate of candidates)
81
- validateIdentity(candidate, options);
82
- rejectConflictingCopies(candidates);
31
+ function select(candidates, options, rejected) {
32
+ const first = new Map();
83
33
  for (const candidate of candidates) {
84
- if (!expected.has(candidate.manifest.suite) &&
85
- candidate.manifest.run.attempt === options.run.currentAttempt) {
86
- throw new Error(`Unexpected current-attempt Vitest suite: ${candidate.manifest.suite}`);
87
- }
34
+ const key = `${candidate.manifest.suite}\0${candidate.manifest.run.attempt}`, prior = first.get(key);
35
+ if (prior &&
36
+ (!prior.manifestBytes.equals(candidate.manifestBytes) ||
37
+ !prior.reportBytes.equals(candidate.reportBytes)))
38
+ throw selectionError(`Conflicting Vitest blobs for ${candidate.manifest.suite} attempt ${candidate.manifest.run.attempt}`, rejected);
39
+ first.set(key, prior ?? candidate);
88
40
  }
89
41
  return options.expectedSuites
90
42
  .toSorted((left, right) => left.suite.localeCompare(right.suite))
@@ -92,9 +44,9 @@ function selectCandidates(candidates, options) {
92
44
  const matches = candidates.filter((candidate) => candidate.manifest.suite === expectation.suite &&
93
45
  candidate.manifest.run.attempt >= expectation.minimumAttempt);
94
46
  if (matches.length === 0)
95
- throw new Error(`Missing expected Vitest suite: ${expectation.suite}`);
96
- const latestAttempt = Math.max(...matches.map((candidate) => candidate.manifest.run.attempt));
97
- const latest = matches.filter((candidate) => candidate.manifest.run.attempt === latestAttempt);
47
+ throw selectionError(`Missing expected Vitest suite: ${expectation.suite}`, rejected);
48
+ const attempt = Math.max(...matches.map((candidate) => candidate.manifest.run.attempt));
49
+ const latest = matches.filter((candidate) => candidate.manifest.run.attempt === attempt);
98
50
  return {
99
51
  candidate: latest[0],
100
52
  sources: [...new Set(latest.map((candidate) => candidate.source))].toSorted(),
@@ -104,16 +56,14 @@ function selectCandidates(candidates, options) {
104
56
  function replaceOutput(outputDir, selected) {
105
57
  const parent = dirname(outputDir);
106
58
  mkdirSync(parent, { recursive: true });
107
- const temporary = mkdtempSync(join(parent, `.${basename(outputDir)}-`));
108
- const backup = join(parent, `.${basename(outputDir)}-backup-${randomUUID()}`);
109
- let backedUp = false;
59
+ const temporary = mkdtempSync(join(parent, `.${basename(outputDir)}-`)), backup = join(parent, `.${basename(outputDir)}-backup-${randomUUID()}`);
60
+ let backedUp = false, published = false;
110
61
  try {
111
- for (const { candidate } of selected) {
62
+ for (const { candidate } of selected)
112
63
  writeFileSync(join(temporary, `${candidate.manifest.suite}.json`), candidate.reportBytes, {
113
64
  flag: 'wx',
114
65
  mode: 0o600,
115
66
  });
116
- }
117
67
  if (existsSync(outputDir)) {
118
68
  if (!lstatSync(outputDir).isDirectory())
119
69
  throw new Error('Vitest report output must be a directory');
@@ -122,33 +72,40 @@ function replaceOutput(outputDir, selected) {
122
72
  }
123
73
  try {
124
74
  renameSync(temporary, outputDir);
75
+ published = true;
125
76
  }
126
77
  catch (error) {
127
- /* v8 ignore start -- an OS-level rename failure restores the already-tested backup path */
128
- if (backedUp)
129
- renameSync(backup, outputDir);
130
- backedUp = false;
78
+ if (backedUp) {
79
+ try {
80
+ renameSync(backup, outputDir);
81
+ backedUp = false;
82
+ }
83
+ catch (restoreError) {
84
+ throw new AggregateError([error, restoreError], 'Vitest report output rollback failed');
85
+ }
86
+ }
131
87
  throw error;
132
- /* v8 ignore stop */
133
88
  }
134
89
  }
135
90
  finally {
136
- rmSync(temporary, { recursive: true, force: true });
137
- if (backedUp)
138
- rmSync(backup, { recursive: true, force: true });
91
+ try {
92
+ rmSync(temporary, { recursive: true, force: true });
93
+ }
94
+ finally {
95
+ if (backedUp && published)
96
+ rmSync(backup, { recursive: true, force: true });
97
+ }
139
98
  }
140
99
  }
141
- /**
142
- * Validates untrusted blob bundles and atomically publishes one newest report per expected suite.
143
- * The caller owns artifact transport; this function deliberately has no network or CI-provider API.
144
- */
100
+ /** Validates untrusted blob bundles and atomically publishes one newest report per expected suite. */
145
101
  export function prepareVitestReports(options) {
146
102
  validateOptions(options);
147
- const candidates = [
148
- ...inspectSource(options.primaryDir, 'primary'),
149
- ...inspectSource(options.fallbackDir, 'fallback'),
103
+ const inspected = [
104
+ inspectVitestReportSource(options.primaryDir, 'primary', options),
105
+ inspectVitestReportSource(options.fallbackDir, 'fallback', options),
150
106
  ];
151
- const selected = selectCandidates(candidates, options);
107
+ const rejectedSources = inspected.flatMap(({ rejected }) => (rejected ? [rejected] : []));
108
+ const selected = select(inspected.flatMap(({ candidates }) => candidates), options, rejectedSources);
152
109
  replaceOutput(options.outputDir, selected);
153
110
  return {
154
111
  selected: selected.map(({ candidate, sources }) => ({
@@ -156,5 +113,6 @@ export function prepareVitestReports(options) {
156
113
  attempt: candidate.manifest.run.attempt,
157
114
  sources,
158
115
  })),
116
+ rejectedSources,
159
117
  };
160
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {