vouchington-tooling 0.0.19 → 0.0.21

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/README.md CHANGED
@@ -95,7 +95,11 @@ import { runAstGrepRule } from 'vouchington-tooling/ast-grep-rule'
95
95
  import { parseReviewPayload, remapReviewComments } from 'vouchington-tooling/gha-review-payload'
96
96
  import { runPostReview } from 'vouchington-tooling/gha-post-review'
97
97
  import { nextPageUrlFromLinkHeader } from 'vouchington-tooling/http-link-pagination'
98
- import { cmdUpload, mintPresignedControl } from 'vouchington-tooling/coverage-transport'
98
+ import {
99
+ cmdUpload,
100
+ discoverDownloadControl,
101
+ mintPrefixUploadControl,
102
+ } from 'vouchington-tooling/coverage-transport'
99
103
  import { pruneDeployedRuntimeDeps } from 'vouchington-tooling/pnpm-deploy'
100
104
  import { parseDockerfileRuntimeImages } from 'vouchington-tooling/dockerfile-parse'
101
105
  import { checkSccComplexity } from 'vouchington-tooling/scc-complexity'
@@ -0,0 +1,42 @@
1
+ export interface PrefixPostTarget {
2
+ readonly url: string;
3
+ readonly fields: Readonly<Record<string, string>>;
4
+ readonly keyPrefix: string;
5
+ readonly maxObjectBytes: number;
6
+ }
7
+ export interface PrefixUploadTransportControl {
8
+ readonly version: 2;
9
+ readonly mode: 'prefix-upload';
10
+ readonly repository: string;
11
+ readonly revision: string;
12
+ readonly run: {
13
+ readonly id: string;
14
+ readonly controlAttempt: number;
15
+ };
16
+ readonly expiresAt: string;
17
+ readonly upload: PrefixPostTarget;
18
+ }
19
+ export interface DownloadedTransportObject {
20
+ readonly key: string;
21
+ readonly url: string;
22
+ readonly attempt: number;
23
+ readonly byteLength: number;
24
+ }
25
+ export interface DiscoveredDownloadTransportControl {
26
+ readonly version: 2;
27
+ readonly mode: 'discovered-download';
28
+ readonly repository: string;
29
+ readonly revision: string;
30
+ readonly run: {
31
+ readonly id: string;
32
+ readonly controlAttempt: number;
33
+ };
34
+ readonly expiresAt: string;
35
+ readonly coverage: Readonly<Record<string, {
36
+ readonly lcov: DownloadedTransportObject;
37
+ readonly manifest: DownloadedTransportObject;
38
+ }>>;
39
+ readonly blobs: Readonly<Record<string, DownloadedTransportObject>>;
40
+ }
41
+ export type TransportControlV2 = PrefixUploadTransportControl | DiscoveredDownloadTransportControl;
42
+ export declare function parseTransportControlV2(raw: Record<string, unknown>): TransportControlV2;
@@ -0,0 +1,117 @@
1
+ import { VITEST_SUITE_PATTERN } from '../vitest-blob-manifest/index.mjs';
2
+ import { DEFAULT_MAX_BODY_BYTES } from './constants.mjs';
3
+ import { assertPrefixTransportIdentity, parseTransportObjectKey, transportPrefix, } from './keys.mjs';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function exactKeys(value, keys) {
8
+ return Object.keys(value).toSorted().join('\0') === keys.toSorted().join('\0');
9
+ }
10
+ function validUrl(value) {
11
+ return typeof value === 'string' && URL.canParse(value) && /^https?:/i.test(value);
12
+ }
13
+ function identity(raw) {
14
+ if (!isRecord(raw.run) || !exactKeys(raw.run, ['id', 'controlAttempt']))
15
+ return null;
16
+ if (typeof raw.repository !== 'string' ||
17
+ typeof raw.revision !== 'string' ||
18
+ typeof raw.run.id !== 'string' ||
19
+ typeof raw.run.controlAttempt !== 'number')
20
+ return null;
21
+ const result = {
22
+ repository: raw.repository,
23
+ revision: raw.revision,
24
+ runId: raw.run.id,
25
+ controlAttempt: raw.run.controlAttempt,
26
+ };
27
+ try {
28
+ assertPrefixTransportIdentity(result);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ return result;
34
+ }
35
+ function parseObject(raw, expected, suite, kind) {
36
+ if (!isRecord(raw) ||
37
+ !exactKeys(raw, ['attempt', 'byteLength', 'key', 'url']) ||
38
+ !validUrl(raw.url) ||
39
+ typeof raw.key !== 'string' ||
40
+ typeof raw.attempt !== 'number' ||
41
+ typeof raw.byteLength !== 'number' ||
42
+ !Number.isSafeInteger(raw.attempt) ||
43
+ !Number.isSafeInteger(raw.byteLength) ||
44
+ raw.byteLength < 0 ||
45
+ raw.byteLength > DEFAULT_MAX_BODY_BYTES)
46
+ throw new Error('Discovered transport object is invalid');
47
+ const parsed = parseTransportObjectKey(raw.key, expected);
48
+ if (!parsed || parsed.suite !== suite || parsed.kind !== kind || parsed.attempt !== raw.attempt)
49
+ throw new Error('Discovered transport object key is invalid');
50
+ return raw;
51
+ }
52
+ function parseDownloadMap(raw, expected, kind) {
53
+ if (!isRecord(raw))
54
+ throw new Error('Discovered transport object map is invalid');
55
+ for (const [suite, value] of Object.entries(raw)) {
56
+ if (!VITEST_SUITE_PATTERN.test(suite))
57
+ throw new Error('Discovered transport suite is invalid');
58
+ if (kind === 'blob')
59
+ parseObject(value, expected, suite, 'blob');
60
+ else if (!isRecord(value) || !exactKeys(value, ['lcov', 'manifest']))
61
+ throw new Error('Discovered coverage pair is invalid');
62
+ else {
63
+ const lcov = parseObject(value.lcov, expected, suite, 'lcov');
64
+ const manifest = parseObject(value.manifest, expected, suite, 'manifest');
65
+ if (lcov.attempt !== manifest.attempt)
66
+ throw new Error('Discovered coverage pair attempts do not match');
67
+ }
68
+ }
69
+ return raw;
70
+ }
71
+ export function parseTransportControlV2(raw) {
72
+ const expected = identity(raw);
73
+ if (!expected ||
74
+ raw.version !== 2 ||
75
+ typeof raw.expiresAt !== 'string' ||
76
+ !Number.isFinite(Date.parse(raw.expiresAt)))
77
+ throw new Error('Coverage transport control has invalid identity fields');
78
+ if (raw.mode === 'prefix-upload') {
79
+ if (!exactKeys(raw, [
80
+ 'expiresAt',
81
+ 'mode',
82
+ 'repository',
83
+ 'revision',
84
+ 'run',
85
+ 'upload',
86
+ 'version',
87
+ ]) ||
88
+ !isRecord(raw.upload) ||
89
+ !exactKeys(raw.upload, ['fields', 'keyPrefix', 'maxObjectBytes', 'url']) ||
90
+ !validUrl(raw.upload.url) ||
91
+ typeof raw.upload.keyPrefix !== 'string' ||
92
+ raw.upload.keyPrefix !== `${transportPrefix(expected)}/` ||
93
+ typeof raw.upload.maxObjectBytes !== 'number' ||
94
+ !Number.isSafeInteger(raw.upload.maxObjectBytes) ||
95
+ raw.upload.maxObjectBytes < 1 ||
96
+ raw.upload.maxObjectBytes > DEFAULT_MAX_BODY_BYTES ||
97
+ !isRecord(raw.upload.fields) ||
98
+ Object.entries(raw.upload.fields).some(([key, value]) => key === 'key' || !key || typeof value !== 'string'))
99
+ throw new Error('Prefix upload coverage transport control is invalid');
100
+ return raw;
101
+ }
102
+ if (raw.mode !== 'discovered-download' ||
103
+ !exactKeys(raw, [
104
+ 'blobs',
105
+ 'coverage',
106
+ 'expiresAt',
107
+ 'mode',
108
+ 'repository',
109
+ 'revision',
110
+ 'run',
111
+ 'version',
112
+ ]))
113
+ throw new Error('Discovered download coverage transport control is invalid');
114
+ parseDownloadMap(raw.coverage, expected, 'coverage');
115
+ parseDownloadMap(raw.blobs, expected, 'blob');
116
+ return raw;
117
+ }
@@ -1,3 +1,4 @@
1
+ import { type TransportControlV2 } from './control-v2.mts';
1
2
  export interface PresignedCoverageUrls {
2
3
  lcovPut: string;
3
4
  lcovGet: string;
@@ -27,7 +28,7 @@ export interface FallbackOnlyTransportControl extends TransportControlBase {
27
28
  readonly mode: 'fallback-only';
28
29
  readonly reason: string;
29
30
  }
30
- export type TransportControl = PresignedTransportControl | FallbackOnlyTransportControl;
31
+ export type TransportControl = PresignedTransportControl | FallbackOnlyTransportControl | TransportControlV2;
31
32
  export interface ExpectedTransportIdentity {
32
33
  readonly repository: string;
33
34
  readonly revision: string;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { chmodSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { VITEST_SUITE_PATTERN } from '../vitest-blob-manifest/index.mjs';
4
+ import { parseTransportControlV2 } from './control-v2.mjs';
4
5
  function isRecord(value) {
5
6
  return typeof value === 'object' && value !== null && !Array.isArray(value);
6
7
  }
@@ -25,9 +26,13 @@ function assertUrlMap(raw, fields, label) {
25
26
  }
26
27
  }
27
28
  export function parseTransportControl(raw) {
28
- if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.run)) {
29
+ if (!isRecord(raw)) {
29
30
  throw new Error('Coverage transport control has an unsupported schema');
30
31
  }
32
+ if (raw.version === 2)
33
+ return parseTransportControlV2(raw);
34
+ if (raw.version !== 1 || !isRecord(raw.run))
35
+ throw new Error('Coverage transport control has an unsupported schema');
31
36
  if (typeof raw.repository !== 'string' ||
32
37
  !raw.repository ||
33
38
  typeof raw.revision !== 'string' ||
@@ -105,7 +110,7 @@ export function readTransportControl(path, expected) {
105
110
  control.run.controlAttempt > expected.currentAttempt)) {
106
111
  throw new Error('Coverage transport control identity does not match this run');
107
112
  }
108
- if (control.mode === 'presigned' && Date.parse(control.expiresAt) <= Date.now()) {
113
+ if (control.mode !== 'fallback-only' && Date.parse(control.expiresAt) <= Date.now()) {
109
114
  throw new Error('Coverage transport control has expired');
110
115
  }
111
116
  return control;
@@ -0,0 +1,17 @@
1
+ import type { DiscoveredDownloadTransportControl, PrefixUploadTransportControl } from './control-v2.mts';
2
+ import { type MintPrefixUploadOptions } from './prefix.mts';
3
+ export declare const MAX_DISCOVERED_TRANSPORT_OBJECTS = 1024;
4
+ export interface ObjectGetSigner {
5
+ signGet(key: string, ttlSeconds: number): Promise<string>;
6
+ }
7
+ export interface ListedTransportObject {
8
+ readonly key: string;
9
+ readonly byteLength: number;
10
+ }
11
+ export interface TransportObjectLister {
12
+ list(prefix: string, continuationToken?: string): Promise<{
13
+ readonly objects: readonly ListedTransportObject[];
14
+ readonly continuationToken?: string;
15
+ }>;
16
+ }
17
+ export declare function discoverDownloadControl(source: PrefixUploadTransportControl, lister: TransportObjectLister, signer: ObjectGetSigner, options?: MintPrefixUploadOptions): Promise<DiscoveredDownloadTransportControl>;
@@ -0,0 +1,99 @@
1
+ import { DEFAULT_MAX_BODY_BYTES } from './constants.mjs';
2
+ import { parseTransportControl } from './control.mjs';
3
+ import { parseTransportObjectKey, } from './keys.mjs';
4
+ import { DEFAULT_TRANSPORT_TTL_SECONDS, transportExpiresAt, } from './prefix.mjs';
5
+ export const MAX_DISCOVERED_TRANSPORT_OBJECTS = 1024;
6
+ function identity(control) {
7
+ return {
8
+ repository: control.repository,
9
+ revision: control.revision,
10
+ runId: control.run.id,
11
+ controlAttempt: control.run.controlAttempt,
12
+ };
13
+ }
14
+ async function listCandidates(identity, lister) {
15
+ const result = [];
16
+ const tokens = new Set();
17
+ const keys = new Set();
18
+ let continuationToken;
19
+ do {
20
+ const page = await lister.list(`coverage-transport/${identity.repository}/${identity.runId}/${identity.revision}/`, continuationToken);
21
+ if (!Array.isArray(page.objects) ||
22
+ page.objects.length + result.length > MAX_DISCOVERED_TRANSPORT_OBJECTS)
23
+ throw new Error('Coverage transport discovery exceeds object limit');
24
+ for (const object of page.objects) {
25
+ if (!object ||
26
+ typeof object.key !== 'string' ||
27
+ !Number.isSafeInteger(object.byteLength) ||
28
+ object.byteLength < 0 ||
29
+ object.byteLength > DEFAULT_MAX_BODY_BYTES)
30
+ throw new Error('Coverage transport discovery object is invalid');
31
+ if (keys.has(object.key))
32
+ throw new Error('Coverage transport discovery has duplicate object keys');
33
+ keys.add(object.key);
34
+ const parsed = parseTransportObjectKey(object.key, identity);
35
+ if (!parsed)
36
+ throw new Error('Coverage transport discovery key is invalid');
37
+ result.push({ object, parsed });
38
+ }
39
+ if (page.continuationToken !== undefined && typeof page.continuationToken !== 'string')
40
+ throw new Error('Coverage transport discovery continuation token is invalid');
41
+ continuationToken = page.continuationToken;
42
+ if (continuationToken) {
43
+ if (tokens.has(continuationToken))
44
+ throw new Error('Coverage transport discovery pagination is cyclic');
45
+ tokens.add(continuationToken);
46
+ }
47
+ } while (continuationToken);
48
+ return result;
49
+ }
50
+ export async function discoverDownloadControl(source, lister, signer, options = {}) {
51
+ const value = identity(source);
52
+ const expiresAt = transportExpiresAt(options);
53
+ const candidates = await listCandidates(value, lister);
54
+ const suites = new Map();
55
+ for (const candidate of candidates) {
56
+ const entries = suites.get(candidate.parsed.suite);
57
+ if (entries)
58
+ entries.push(candidate);
59
+ else
60
+ suites.set(candidate.parsed.suite, [candidate]);
61
+ }
62
+ const coverage = {};
63
+ const blobs = {};
64
+ const object = async (candidate) => ({
65
+ key: candidate.object.key,
66
+ attempt: candidate.parsed.attempt,
67
+ byteLength: candidate.object.byteLength,
68
+ url: await signer.signGet(candidate.object.key, options.ttlSeconds ?? DEFAULT_TRANSPORT_TTL_SECONDS),
69
+ });
70
+ await Promise.all([...suites].map(async ([suite, entries]) => {
71
+ const attempts = [...new Set(entries.map((entry) => entry.parsed.attempt))].toSorted((a, b) => b - a);
72
+ const pair = attempts
73
+ .map((attempt) => entries.filter((entry) => entry.parsed.attempt === attempt))
74
+ .map((entries) => ({
75
+ lcov: entries.find((entry) => entry.parsed.kind === 'lcov'),
76
+ manifest: entries.find((entry) => entry.parsed.kind === 'manifest'),
77
+ }))
78
+ .find((pair) => pair.lcov && pair.manifest);
79
+ if (pair?.lcov && pair.manifest) {
80
+ const [lcov, manifest] = await Promise.all([object(pair.lcov), object(pair.manifest)]);
81
+ coverage[suite] = { lcov, manifest };
82
+ }
83
+ const blob = attempts
84
+ .map((attempt) => entries.find((entry) => entry.parsed.attempt === attempt && entry.parsed.kind === 'blob'))
85
+ .find(Boolean);
86
+ if (blob)
87
+ blobs[suite] = await object(blob);
88
+ }));
89
+ return parseTransportControl({
90
+ version: 2,
91
+ mode: 'discovered-download',
92
+ repository: value.repository,
93
+ revision: value.revision,
94
+ run: { id: value.runId, controlAttempt: value.controlAttempt },
95
+ expiresAt,
96
+ coverage,
97
+ blobs,
98
+ });
99
+ }
@@ -4,3 +4,4 @@ export declare function coveragePresignFailureLog(error: unknown): string;
4
4
  export declare function logTransport(options: RequestOptions, line: string): void;
5
5
  export declare function fetchPut(url: string, body: Buffer, options?: RequestOptions): Promise<boolean>;
6
6
  export declare function fetchGet(url: string, options?: RequestOptions): Promise<Buffer | null>;
7
+ export declare function fetchPost(url: string, fields: Readonly<Record<string, string>>, key: string, body: Buffer, options?: RequestOptions): Promise<boolean>;
@@ -57,7 +57,7 @@ async function request(method, url, body, options) {
57
57
  try {
58
58
  const response = await fetch(url, {
59
59
  method,
60
- ...(body ? { body: new Uint8Array(body) } : {}),
60
+ ...(body ? { body: typeof body === 'function' ? body() : new Uint8Array(body) } : {}),
61
61
  signal: controller.signal,
62
62
  });
63
63
  // A missing presigned object is terminal. Let fetchGet yield null or fetchPut yield false
@@ -97,3 +97,14 @@ export async function fetchGet(url, options = {}) {
97
97
  throw new Error('[coverage-transport] GET exhausted');
98
98
  return response.body;
99
99
  }
100
+ export async function fetchPost(url, fields, key, body, options = {}) {
101
+ const response = await request('POST', url, () => {
102
+ const form = new FormData();
103
+ for (const [name, value] of Object.entries(fields))
104
+ form.set(name, value);
105
+ form.set('key', key);
106
+ form.set('file', new Blob([new Uint8Array(body).slice().buffer]));
107
+ return form;
108
+ }, options);
109
+ return response?.ok === true;
110
+ }
@@ -1,6 +1,10 @@
1
1
  export { DEFAULT_COVERAGE_MANIFEST_FILENAME, cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, } from './lib.mts';
2
2
  export { parseTransportControl, readTransportControl, writeTransportControl, type ExpectedTransportIdentity, type FallbackOnlyTransportControl, type PresignedBlobUrls, type PresignedCoverageUrls, type PresignedTransportControl, type RequestOptions, type TransportControl, } from './control.mts';
3
- export { coveragePresignFailureLog, fetchGet, fetchPut, logTransport, redactTransportLog, } from './http.mts';
3
+ export { type DiscoveredDownloadTransportControl, type DownloadedTransportObject, type PrefixPostTarget, type PrefixUploadTransportControl, } from './control-v2.mts';
4
+ export { coveragePresignFailureLog, fetchGet, fetchPost, fetchPut, logTransport, redactTransportLog, } from './http.mts';
4
5
  export { assertCoverageTransportBlobOutcome, assertCoverageTransportOutcome, isBlobPrimaryState, isStepOutcome, writeUploadOutcomeOutput, type AppendOutput, type BlobPrimaryState, type StepOutcome, } from './outcome.mts';
5
6
  export { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mts';
6
7
  export { mintPresignedControl, transportObjectKeys, type MintPresignedControlOptions, type ObjectSigner, type PresignIdentity, } from './presign.mts';
8
+ export { discoverDownloadControl, MAX_DISCOVERED_TRANSPORT_OBJECTS, type ListedTransportObject, type ObjectGetSigner, type TransportObjectLister, } from './discovery.mts';
9
+ export { mintPrefixUploadControl, type MintPrefixUploadOptions, type PrefixPostSigner, } from './prefix.mts';
10
+ export { parseTransportObjectKey, transportObjectKeysV2, transportPrefix, type PrefixTransportIdentity, type TransportObjectKey, type TransportObjectKind, } from './keys.mts';
@@ -1,6 +1,10 @@
1
1
  export { DEFAULT_COVERAGE_MANIFEST_FILENAME, cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, } from './lib.mjs';
2
2
  export { parseTransportControl, readTransportControl, writeTransportControl, } from './control.mjs';
3
- export { coveragePresignFailureLog, fetchGet, fetchPut, logTransport, redactTransportLog, } from './http.mjs';
3
+ export {} from './control-v2.mjs';
4
+ export { coveragePresignFailureLog, fetchGet, fetchPost, fetchPut, logTransport, redactTransportLog, } from './http.mjs';
4
5
  export { assertCoverageTransportBlobOutcome, assertCoverageTransportOutcome, isBlobPrimaryState, isStepOutcome, writeUploadOutcomeOutput, } from './outcome.mjs';
5
6
  export { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mjs';
6
7
  export { mintPresignedControl, transportObjectKeys, } from './presign.mjs';
8
+ export { discoverDownloadControl, MAX_DISCOVERED_TRANSPORT_OBJECTS, } from './discovery.mjs';
9
+ export { mintPrefixUploadControl, } from './prefix.mjs';
10
+ export { parseTransportObjectKey, transportObjectKeysV2, transportPrefix, } from './keys.mjs';
@@ -0,0 +1,16 @@
1
+ export interface PrefixTransportIdentity {
2
+ readonly repository: string;
3
+ readonly revision: string;
4
+ readonly runId: string;
5
+ readonly controlAttempt: number;
6
+ }
7
+ export type TransportObjectKind = 'lcov' | 'manifest' | 'blob';
8
+ export interface TransportObjectKey {
9
+ readonly attempt: number;
10
+ readonly suite: string;
11
+ readonly kind: TransportObjectKind;
12
+ }
13
+ export declare function assertPrefixTransportIdentity(identity: PrefixTransportIdentity): void;
14
+ export declare function transportPrefix(identity: PrefixTransportIdentity, attempt?: number): string;
15
+ export declare function transportObjectKeysV2(identity: PrefixTransportIdentity, suite: string, attempt?: number): Readonly<Record<TransportObjectKind, string>>;
16
+ export declare function parseTransportObjectKey(key: string, identity: PrefixTransportIdentity): TransportObjectKey | null;
@@ -0,0 +1,44 @@
1
+ import { VITEST_SUITE_PATTERN } from '../vitest-blob-manifest/index.mjs';
2
+ export function assertPrefixTransportIdentity(identity) {
3
+ if (identity.repository.includes('..') ||
4
+ !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(identity.repository) ||
5
+ !/^[0-9a-f]{40}$/.test(identity.revision) ||
6
+ !/^[1-9][0-9]*$/.test(identity.runId) ||
7
+ !Number.isSafeInteger(identity.controlAttempt) ||
8
+ identity.controlAttempt < 1) {
9
+ throw new Error('Coverage transport key identity is invalid');
10
+ }
11
+ }
12
+ export function transportPrefix(identity, attempt = identity.controlAttempt) {
13
+ assertPrefixTransportIdentity(identity);
14
+ if (!Number.isSafeInteger(attempt) || attempt < 1 || attempt > identity.controlAttempt) {
15
+ throw new Error('Coverage transport attempt is invalid');
16
+ }
17
+ return `coverage-transport/${identity.repository}/${identity.runId}/${identity.revision}/attempt-${attempt}`;
18
+ }
19
+ export function transportObjectKeysV2(identity, suite, attempt = identity.controlAttempt) {
20
+ if (!VITEST_SUITE_PATTERN.test(suite))
21
+ throw new Error('Coverage transport suite is invalid');
22
+ const prefix = transportPrefix(identity, attempt);
23
+ return {
24
+ lcov: `${prefix}/coverage/${suite}/lcov.info`,
25
+ manifest: `${prefix}/coverage/${suite}/coverage-manifest.json`,
26
+ blob: `${prefix}/blobs/${suite}.tar.gz`,
27
+ };
28
+ }
29
+ export function parseTransportObjectKey(key, identity) {
30
+ const root = `coverage-transport/${identity.repository}/${identity.runId}/${identity.revision}/`;
31
+ if (!key.startsWith(root) || key.includes('..') || key.includes('\\'))
32
+ return null;
33
+ const match = /^attempt-([1-9][0-9]*)\/(?:coverage\/([a-z0-9]+(?:-[a-z0-9]+)*)\/(lcov\.info|coverage-manifest\.json)|blobs\/([a-z0-9]+(?:-[a-z0-9]+)*)\.tar\.gz)$/.exec(key.slice(root.length));
34
+ if (!match)
35
+ return null;
36
+ const attempt = Number(match[1]);
37
+ if (!Number.isSafeInteger(attempt) || attempt > identity.controlAttempt)
38
+ return null;
39
+ const coverageSuite = match[2];
40
+ if (coverageSuite) {
41
+ return { attempt, suite: coverageSuite, kind: match[3] === 'lcov.info' ? 'lcov' : 'manifest' };
42
+ }
43
+ return { attempt, suite: match[4], kind: 'blob' };
44
+ }
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { DEFAULT_COVERAGE_MANIFEST_FILENAME, DEFAULT_MAX_BODY_BYTES, assertCoverageManifestFilename, } from './constants.mjs';
5
5
  import { readTransportControl, } from './control.mjs';
6
6
  import { fetchGet, fetchPut, logTransport } from './http.mjs';
7
+ import { downloadPrefixBlobs, downloadPrefixCoverage, uploadPrefixTransport, } from './prefix-transfer.mjs';
7
8
  import { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mjs';
8
9
  export { DEFAULT_COVERAGE_MANIFEST_FILENAME } from './constants.mjs';
9
10
  export async function cmdUpload(controlPath, suite, options) {
@@ -15,6 +16,11 @@ export async function cmdUpload(controlPath, suite, options) {
15
16
  const cwd = options.cwd ?? process.cwd();
16
17
  const manifestFilename = options.coverageManifestFilename ?? DEFAULT_COVERAGE_MANIFEST_FILENAME;
17
18
  assertCoverageManifestFilename(manifestFilename);
19
+ if (control.mode === 'prefix-upload') {
20
+ return uploadPrefixTransport(control, suite, cwd, options.expectedIdentity, manifestFilename, options);
21
+ }
22
+ if (control.mode === 'discovered-download')
23
+ throw new Error('Coverage transport download control cannot upload');
18
24
  const coverageUrls = control.coverage[suite];
19
25
  const lcovPath = join(cwd, 'coverage', 'lcov.info');
20
26
  const manifestPath = join(cwd, 'coverage', manifestFilename);
@@ -58,6 +64,11 @@ export async function cmdDownloadCoverage(controlPath, destinationRoot, options)
58
64
  }
59
65
  const manifestFilename = options.coverageManifestFilename ?? DEFAULT_COVERAGE_MANIFEST_FILENAME;
60
66
  assertCoverageManifestFilename(manifestFilename);
67
+ if (control.mode === 'prefix-upload')
68
+ throw new Error('Coverage transport upload control cannot download');
69
+ if (control.mode === 'discovered-download') {
70
+ return downloadPrefixCoverage(control, destinationRoot, manifestFilename, options);
71
+ }
61
72
  await Promise.all(Object.entries(control.coverage).map(async ([suite, urls]) => {
62
73
  const [lcov, manifest] = await Promise.all([
63
74
  fetchGet(urls.lcovGet, options),
@@ -80,5 +91,9 @@ export async function cmdDownloadVitestBlobs(controlPath, destinationRoot, optio
80
91
  logTransport(options, '[coverage-transport] S3 unavailable; artifact fallback required');
81
92
  return;
82
93
  }
94
+ if (control.mode === 'prefix-upload')
95
+ throw new Error('Coverage transport upload control cannot download');
96
+ if (control.mode === 'discovered-download')
97
+ return downloadPrefixBlobs(control, destinationRoot, options);
83
98
  await downloadVitestBlobBundles(control.blobs, destinationRoot, options);
84
99
  }
@@ -0,0 +1,8 @@
1
+ import type { DiscoveredDownloadTransportControl, PrefixUploadTransportControl } from './control-v2.mts';
2
+ import type { ExpectedTransportIdentity, RequestOptions } from './control.mts';
3
+ export declare function uploadPrefixTransport(control: PrefixUploadTransportControl, suite: string, cwd: string, expectedIdentity: ExpectedTransportIdentity, manifestFilename: string, options: RequestOptions): Promise<{
4
+ coverage: boolean;
5
+ blob: boolean;
6
+ }>;
7
+ export declare function downloadPrefixCoverage(control: DiscoveredDownloadTransportControl, destinationRoot: string, manifestFilename: string, options: RequestOptions): Promise<void>;
8
+ export declare function downloadPrefixBlobs(control: DiscoveredDownloadTransportControl, destinationRoot: string, options: RequestOptions): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { DEFAULT_COVERAGE_MANIFEST_FILENAME } from './constants.mjs';
5
+ import { fetchGet, fetchPost, logTransport } from './http.mjs';
6
+ import { transportObjectKeysV2 } from './keys.mjs';
7
+ import { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mjs';
8
+ function identity(control) {
9
+ return {
10
+ repository: control.repository,
11
+ revision: control.revision,
12
+ runId: control.run.id,
13
+ controlAttempt: control.run.controlAttempt,
14
+ };
15
+ }
16
+ export async function uploadPrefixTransport(control, suite, cwd, expectedIdentity, manifestFilename, options) {
17
+ if (control.run.controlAttempt !== expectedIdentity.currentAttempt)
18
+ throw new Error('Prefix transport control attempt does not match the producer attempt');
19
+ if (manifestFilename !== DEFAULT_COVERAGE_MANIFEST_FILENAME)
20
+ throw new Error('Prefix transport requires the default coverage manifest filename');
21
+ const keys = transportObjectKeysV2(identity(control), suite);
22
+ const maxObjectBytes = Math.min(control.upload.maxObjectBytes, options.maxBodyBytes ?? control.upload.maxObjectBytes);
23
+ const lcovPath = join(cwd, 'coverage', 'lcov.info');
24
+ const manifestPath = join(cwd, 'coverage', manifestFilename);
25
+ const lcov = existsSync(lcovPath) && existsSync(manifestPath) ? await readFile(lcovPath) : null;
26
+ const manifest = lcov === null ? null : await readFile(manifestPath);
27
+ const storedLcov = lcov !== null && lcov.byteLength <= maxObjectBytes
28
+ ? await fetchPost(control.upload.url, control.upload.fields, keys.lcov, lcov, options)
29
+ : false;
30
+ const coverage = storedLcov && manifest !== null && manifest.byteLength <= maxObjectBytes
31
+ ? await fetchPost(control.upload.url, control.upload.fields, keys.manifest, manifest, options)
32
+ : false;
33
+ if (lcov !== null)
34
+ logTransport(options, coverage
35
+ ? `[coverage-transport] Uploaded coverage pair for ${suite}`
36
+ : `[coverage-transport] Coverage pair upload failed for ${suite}`);
37
+ const blobData = packVitestBlobBundle(cwd, suite, expectedIdentity, options);
38
+ const blob = Boolean(blobData &&
39
+ blobData.byteLength <= maxObjectBytes &&
40
+ (await fetchPost(control.upload.url, control.upload.fields, keys.blob, blobData, options)));
41
+ if (blob)
42
+ logTransport(options, `[coverage-transport] Uploaded vitest blob for ${suite}`);
43
+ else if (blobData)
44
+ logTransport(options, `[coverage-transport] Vitest blob upload failed for ${suite}`);
45
+ return { coverage, blob };
46
+ }
47
+ export async function downloadPrefixCoverage(control, destinationRoot, manifestFilename, options) {
48
+ if (manifestFilename !== DEFAULT_COVERAGE_MANIFEST_FILENAME)
49
+ throw new Error('Prefix transport requires the default coverage manifest filename');
50
+ await Promise.all(Object.entries(control.coverage).map(async ([suite, pair]) => {
51
+ const [lcov, manifest] = await Promise.all([
52
+ fetchGet(pair.lcov.url, options),
53
+ fetchGet(pair.manifest.url, options),
54
+ ]);
55
+ if (!lcov || !manifest)
56
+ return logTransport(options, `[coverage-transport] Skipped incomplete coverage pair for ${suite}`);
57
+ const destination = join(destinationRoot, `coverage-${suite}`);
58
+ mkdirSync(destination, { recursive: true });
59
+ writeFileSync(join(destination, 'lcov.info'), lcov);
60
+ writeFileSync(join(destination, manifestFilename), manifest, { mode: 0o600 });
61
+ logTransport(options, `[coverage-transport] Downloaded coverage pair for ${suite}`);
62
+ }));
63
+ }
64
+ export async function downloadPrefixBlobs(control, destinationRoot, options) {
65
+ await downloadVitestBlobBundles(Object.fromEntries(Object.entries(control.blobs).map(([suite, object]) => [
66
+ suite,
67
+ { get: object.url, put: 'unused' },
68
+ ])), destinationRoot, options);
69
+ }
@@ -0,0 +1,13 @@
1
+ import type { PrefixPostTarget, PrefixUploadTransportControl } from './control-v2.mts';
2
+ import { type PrefixTransportIdentity } from './keys.mts';
3
+ export interface PrefixPostSigner {
4
+ signPost(keyPrefix: string, ttlSeconds: number, maxObjectBytes: number): Promise<PrefixPostTarget>;
5
+ }
6
+ export interface MintPrefixUploadOptions {
7
+ readonly ttlSeconds?: number;
8
+ readonly maxObjectBytes?: number;
9
+ readonly now?: () => Date;
10
+ }
11
+ export declare const DEFAULT_TRANSPORT_TTL_SECONDS = 14400;
12
+ export declare function transportExpiresAt(options: MintPrefixUploadOptions): string;
13
+ export declare function mintPrefixUploadControl(value: PrefixTransportIdentity, signer: PrefixPostSigner, options?: MintPrefixUploadOptions): Promise<PrefixUploadTransportControl>;
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_MAX_BODY_BYTES } from './constants.mjs';
2
+ import { parseTransportControl } from './control.mjs';
3
+ import { transportPrefix } from './keys.mjs';
4
+ export const DEFAULT_TRANSPORT_TTL_SECONDS = 14_400;
5
+ export function transportExpiresAt(options) {
6
+ const ttl = options.ttlSeconds ?? DEFAULT_TRANSPORT_TTL_SECONDS;
7
+ if (!Number.isSafeInteger(ttl) || ttl < 1)
8
+ throw new Error('Coverage transport TTL is invalid');
9
+ return new Date((options.now?.() ?? new Date()).getTime() + ttl * 1000).toISOString();
10
+ }
11
+ export async function mintPrefixUploadControl(value, signer, options = {}) {
12
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_TRANSPORT_TTL_SECONDS;
13
+ const maxObjectBytes = options.maxObjectBytes ?? DEFAULT_MAX_BODY_BYTES;
14
+ if (!Number.isSafeInteger(maxObjectBytes) ||
15
+ maxObjectBytes < 1 ||
16
+ maxObjectBytes > DEFAULT_MAX_BODY_BYTES)
17
+ throw new Error('Coverage transport size limit is invalid');
18
+ const upload = await signer.signPost(`${transportPrefix(value)}/`, ttlSeconds, maxObjectBytes);
19
+ if (upload.maxObjectBytes > maxObjectBytes)
20
+ throw new Error('Coverage transport signer size limit is invalid');
21
+ return parseTransportControl({
22
+ version: 2,
23
+ mode: 'prefix-upload',
24
+ repository: value.repository,
25
+ revision: value.revision,
26
+ run: { id: value.runId, controlAttempt: value.controlAttempt },
27
+ expiresAt: transportExpiresAt(options),
28
+ upload,
29
+ });
30
+ }
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.21",
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": {