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 +1 -1
- package/dist/vitest-blob-manifest/bundle-error.d.mts +2 -0
- package/dist/vitest-blob-manifest/bundle-error.mjs +2 -0
- package/dist/vitest-blob-manifest/index.mjs +13 -8
- package/dist/vitest-blob-manifest/reports-source.d.mts +26 -0
- package/dist/vitest-blob-manifest/reports-source.mjs +88 -0
- package/dist/vitest-blob-manifest/reports.d.mts +5 -5
- package/dist/vitest-blob-manifest/reports.mjs +55 -97
- package/package.json +1 -1
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';
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
21
|
+
readonly sources: readonly VitestReportSource[];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
3
|
import { basename, dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
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) ||
|
|
37
|
-
|
|
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
|
-
|
|
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 (
|
|
59
|
-
|
|
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
|
|
66
|
-
const
|
|
67
|
-
|
|
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
|
|
77
|
-
const
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
96
|
-
const
|
|
97
|
-
const latest = matches.filter((candidate) => candidate.manifest.run.attempt ===
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
103
|
+
const inspected = [
|
|
104
|
+
inspectVitestReportSource(options.primaryDir, 'primary', options),
|
|
105
|
+
inspectVitestReportSource(options.fallbackDir, 'fallback', options),
|
|
150
106
|
];
|
|
151
|
-
const
|
|
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.
|
|
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": {
|