vouchington-tooling 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +13 -0
  2. package/dist/ci-local/index.d.mts +5 -0
  3. package/dist/ci-local/index.mjs +79 -0
  4. package/dist/ci-local/parse.d.mts +3 -0
  5. package/dist/ci-local/parse.mjs +43 -0
  6. package/dist/ci-local/types.d.mts +41 -0
  7. package/dist/ci-local/types.mjs +1 -0
  8. package/dist/cli/index.mjs +5 -0
  9. package/dist/cli/parse.d.mts +1 -1
  10. package/dist/cli/parse.mjs +5 -0
  11. package/dist/cli/usage.d.mts +1 -1
  12. package/dist/cli/usage.mjs +10 -0
  13. package/dist/coverage-transport/constants.d.mts +3 -0
  14. package/dist/coverage-transport/constants.mjs +11 -0
  15. package/dist/coverage-transport/control.d.mts +47 -0
  16. package/dist/coverage-transport/control.mjs +112 -0
  17. package/dist/coverage-transport/http.d.mts +6 -0
  18. package/dist/coverage-transport/http.mjs +99 -0
  19. package/dist/coverage-transport/index.d.mts +6 -0
  20. package/dist/coverage-transport/index.mjs +6 -0
  21. package/dist/coverage-transport/lib.d.mts +17 -0
  22. package/dist/coverage-transport/lib.mjs +84 -0
  23. package/dist/coverage-transport/outcome.d.mts +29 -0
  24. package/dist/coverage-transport/outcome.mjs +70 -0
  25. package/dist/coverage-transport/presign.d.mts +22 -0
  26. package/dist/coverage-transport/presign.mjs +51 -0
  27. package/dist/coverage-transport/vitest-blob-transport.d.mts +5 -0
  28. package/dist/coverage-transport/vitest-blob-transport.mjs +129 -0
  29. package/dist/dockerfile-parse/index.d.mts +4 -0
  30. package/dist/dockerfile-parse/index.mjs +2 -0
  31. package/dist/dockerfile-parse/prewarm-ports.d.mts +12 -0
  32. package/dist/dockerfile-parse/prewarm-ports.mjs +59 -0
  33. package/dist/dockerfile-parse/runtime-cmd.d.mts +11 -0
  34. package/dist/dockerfile-parse/runtime-cmd.mjs +142 -0
  35. package/dist/dockerfile-parse/stages.d.mts +7 -0
  36. package/dist/dockerfile-parse/stages.mjs +18 -0
  37. package/dist/gha-pr-checkpoint/index.d.mts +58 -0
  38. package/dist/gha-pr-checkpoint/index.mjs +100 -0
  39. package/dist/gha-rate-limit/index.d.mts +15 -0
  40. package/dist/gha-rate-limit/index.mjs +69 -0
  41. package/dist/index.d.mts +15 -0
  42. package/dist/index.mjs +8 -0
  43. package/dist/pnpm-deploy/index.d.mts +6 -0
  44. package/dist/pnpm-deploy/index.mjs +3 -0
  45. package/dist/pnpm-deploy/normalize.d.mts +15 -0
  46. package/dist/pnpm-deploy/normalize.mjs +64 -0
  47. package/dist/pnpm-deploy/prune.d.mts +14 -0
  48. package/dist/pnpm-deploy/prune.mjs +48 -0
  49. package/dist/pnpm-deploy/restore.d.mts +31 -0
  50. package/dist/pnpm-deploy/restore.mjs +109 -0
  51. package/dist/scc-complexity/index.d.mts +19 -0
  52. package/dist/scc-complexity/index.mjs +91 -0
  53. package/dist/transient-retry/decision-evaluator.d.mts +2 -0
  54. package/dist/transient-retry/decision-evaluator.mjs +6 -0
  55. package/dist/transient-retry/types.d.mts +3 -3
  56. package/dist/workspace-gates/dependabot.d.mts +3 -0
  57. package/dist/workspace-gates/dependabot.mjs +26 -0
  58. package/dist/workspace-gates/exclude-registry.d.mts +3 -0
  59. package/dist/workspace-gates/exclude-registry.mjs +30 -0
  60. package/dist/workspace-gates/first-party-graph.d.mts +4 -0
  61. package/dist/workspace-gates/first-party-graph.mjs +87 -0
  62. package/dist/workspace-gates/index.d.mts +2 -0
  63. package/dist/workspace-gates/index.mjs +1 -0
  64. package/dist/workspace-gates/options.d.mts +27 -0
  65. package/dist/workspace-gates/options.mjs +15 -0
  66. package/dist/workspace-gates/policy.d.mts +5 -0
  67. package/dist/workspace-gates/policy.mjs +27 -0
  68. package/dist/workspace-gates/test-helpers.d.mts +21 -0
  69. package/dist/workspace-gates/test-helpers.mjs +69 -0
  70. package/dist/workspace-gates/yaml-loader.d.mts +2 -0
  71. package/dist/workspace-gates/yaml-loader.mjs +28 -0
  72. package/dist/workspace-glob.d.mts +1 -0
  73. package/dist/workspace-glob.mjs +46 -0
  74. package/package.json +44 -2
  75. package/scripts/gha/check-cache-size.sh +38 -0
  76. package/scripts/gha/clean-workspace.sh +227 -0
  77. package/scripts/gha/install-github-release.sh +136 -0
  78. package/scripts/gha/load-runner-env.sh +75 -0
  79. package/scripts/gha/make-shard-matrix.sh +30 -0
@@ -0,0 +1,84 @@
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, DEFAULT_MAX_BODY_BYTES, assertCoverageManifestFilename, } from './constants.mjs';
5
+ import { readTransportControl, } from './control.mjs';
6
+ import { fetchGet, fetchPut, logTransport } from './http.mjs';
7
+ import { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mjs';
8
+ export { DEFAULT_COVERAGE_MANIFEST_FILENAME } from './constants.mjs';
9
+ export async function cmdUpload(controlPath, suite, options) {
10
+ const control = readTransportControl(controlPath, options.expectedIdentity);
11
+ if (control.mode === 'fallback-only') {
12
+ logTransport(options, `[coverage-transport] S3 unavailable for ${suite}; artifact fallback required`);
13
+ return { coverage: false, blob: false };
14
+ }
15
+ const cwd = options.cwd ?? process.cwd();
16
+ const manifestFilename = options.coverageManifestFilename ?? DEFAULT_COVERAGE_MANIFEST_FILENAME;
17
+ assertCoverageManifestFilename(manifestFilename);
18
+ const coverageUrls = control.coverage[suite];
19
+ const lcovPath = join(cwd, 'coverage', 'lcov.info');
20
+ const manifestPath = join(cwd, 'coverage', manifestFilename);
21
+ const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
22
+ const lcov = coverageUrls && existsSync(lcovPath) && existsSync(manifestPath)
23
+ ? await readFile(lcovPath)
24
+ : null;
25
+ const manifest = lcov === null ? null : await readFile(manifestPath);
26
+ const lcovStored = coverageUrls &&
27
+ lcov !== null &&
28
+ manifest !== null &&
29
+ lcov.byteLength <= maxBytes &&
30
+ manifest.byteLength <= maxBytes
31
+ ? await fetchPut(coverageUrls.lcovPut, lcov, options)
32
+ : false;
33
+ const coverage = lcovStored && coverageUrls && manifest !== null && manifest.byteLength <= maxBytes
34
+ ? await fetchPut(coverageUrls.manifestPut, manifest, options)
35
+ : false;
36
+ if (coverageUrls && existsSync(lcovPath) && existsSync(manifestPath)) {
37
+ logTransport(options, coverage
38
+ ? `[coverage-transport] Uploaded coverage pair for ${suite}`
39
+ : `[coverage-transport] Coverage pair upload failed for ${suite}`);
40
+ }
41
+ const blobUrls = control.blobs[suite];
42
+ const blobData = blobUrls
43
+ ? packVitestBlobBundle(cwd, suite, options.expectedIdentity, options)
44
+ : null;
45
+ const blob = Boolean(blobUrls && blobData && (await fetchPut(blobUrls.put, blobData, options)));
46
+ if (blob)
47
+ logTransport(options, `[coverage-transport] Uploaded vitest blob for ${suite}`);
48
+ else if (blobUrls && blobData) {
49
+ logTransport(options, `[coverage-transport] Vitest blob upload failed for ${suite}`);
50
+ }
51
+ return { coverage, blob };
52
+ }
53
+ export async function cmdDownloadCoverage(controlPath, destinationRoot, options) {
54
+ const control = readTransportControl(controlPath, options.expectedIdentity);
55
+ if (control.mode === 'fallback-only') {
56
+ logTransport(options, '[coverage-transport] S3 unavailable; artifact fallback required');
57
+ return;
58
+ }
59
+ const manifestFilename = options.coverageManifestFilename ?? DEFAULT_COVERAGE_MANIFEST_FILENAME;
60
+ assertCoverageManifestFilename(manifestFilename);
61
+ await Promise.all(Object.entries(control.coverage).map(async ([suite, urls]) => {
62
+ const [lcov, manifest] = await Promise.all([
63
+ fetchGet(urls.lcovGet, options),
64
+ fetchGet(urls.manifestGet, options),
65
+ ]);
66
+ if (!lcov || !manifest) {
67
+ logTransport(options, `[coverage-transport] Skipped incomplete coverage pair for ${suite}`);
68
+ return;
69
+ }
70
+ const destination = join(destinationRoot, `coverage-${suite}`);
71
+ mkdirSync(destination, { recursive: true });
72
+ writeFileSync(join(destination, 'lcov.info'), lcov);
73
+ writeFileSync(join(destination, manifestFilename), manifest, { mode: 0o600 });
74
+ logTransport(options, `[coverage-transport] Downloaded coverage pair for ${suite}`);
75
+ }));
76
+ }
77
+ export async function cmdDownloadVitestBlobs(controlPath, destinationRoot, options) {
78
+ const control = readTransportControl(controlPath, options.expectedIdentity);
79
+ if (control.mode === 'fallback-only') {
80
+ logTransport(options, '[coverage-transport] S3 unavailable; artifact fallback required');
81
+ return;
82
+ }
83
+ await downloadVitestBlobBundles(control.blobs, destinationRoot, options);
84
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Transport exhaustion guards for a coverage pair and a Vitest blob, plus the
3
+ * `$GITHUB_OUTPUT` writer producers use when the upload exit code is not the blob signal.
4
+ */
5
+ export type StepOutcome = 'success' | 'failure' | 'cancelled' | 'skipped';
6
+ /**
7
+ * CLI argv values arrive as plain strings (or `undefined` when a positional arg was omitted); this
8
+ * narrows to `StepOutcome` so a caller can validate a step-outcome argument without an unchecked
9
+ * `as` cast.
10
+ */
11
+ export declare function isStepOutcome(value: string | undefined): value is StepOutcome;
12
+ export declare function assertCoverageTransportOutcome(suite: string, primary: StepOutcome, artifactAttempt1: StepOutcome, artifactAttempt2: StepOutcome, emit?: (line: string) => void): boolean;
13
+ export type BlobPrimaryState = 'true' | 'false' | 'skipped';
14
+ export declare function isBlobPrimaryState(value: string | undefined): value is BlobPrimaryState;
15
+ /**
16
+ * Sibling to `assertCoverageTransportOutcome` for the Vitest blob. `true`/`false` are the S3
17
+ * upload step's `blob` output; `skipped` means that step never ran. GitHub fallback is always
18
+ * attempted when enabled — workflows must not gate it on `blob != 'true'`.
19
+ */
20
+ export declare function assertCoverageTransportBlobOutcome(suite: string, primaryPersisted: BlobPrimaryState, artifactAttempt1: StepOutcome, artifactAttempt2: StepOutcome, emit?: (line: string) => void): boolean;
21
+ export type AppendOutput = (path: string, data: string) => void;
22
+ /**
23
+ * Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting. GitHub-fallback blob upload
24
+ * must always be attempted when enabled; do not gate it on this signal. The upload subcommand's
25
+ * exit code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
26
+ */
27
+ export declare function writeUploadOutcomeOutput(outcome: {
28
+ readonly blob: boolean;
29
+ }, githubOutputPath: string | undefined, appendOutput?: AppendOutput): void;
@@ -0,0 +1,70 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ const STEP_OUTCOMES = new Set([
3
+ 'success',
4
+ 'failure',
5
+ 'cancelled',
6
+ 'skipped',
7
+ ]);
8
+ /**
9
+ * CLI argv values arrive as plain strings (or `undefined` when a positional arg was omitted); this
10
+ * narrows to `StepOutcome` so a caller can validate a step-outcome argument without an unchecked
11
+ * `as` cast.
12
+ */
13
+ export function isStepOutcome(value) {
14
+ return value !== undefined && STEP_OUTCOMES.has(value);
15
+ }
16
+ export function assertCoverageTransportOutcome(suite, primary, artifactAttempt1, artifactAttempt2, emit = (line) => process.stderr.write(`${line}\n`)) {
17
+ const primarySucceeded = primary === 'success';
18
+ const artifactSucceeded = artifactAttempt1 === 'success' || artifactAttempt2 === 'success';
19
+ if (primarySucceeded && artifactSucceeded)
20
+ return true;
21
+ if (primarySucceeded) {
22
+ emit(`::warning::Coverage persisted only to S3 for suite=${suite}; GitHub artifact fallback is degraded.`);
23
+ return true;
24
+ }
25
+ if (artifactSucceeded) {
26
+ emit(`::warning::Coverage persisted only to GitHub artifacts for suite=${suite}; S3 primary is degraded.`);
27
+ return true;
28
+ }
29
+ emit(`::error::COVERAGE_TRANSPORT_EXHAUSTED suite=${suite} Neither S3 nor GitHub artifacts persisted the coverage pair.`);
30
+ return false;
31
+ }
32
+ const BLOB_PRIMARY_STATES = new Set([
33
+ 'true',
34
+ 'false',
35
+ 'skipped',
36
+ ]);
37
+ export function isBlobPrimaryState(value) {
38
+ return value !== undefined && BLOB_PRIMARY_STATES.has(value);
39
+ }
40
+ /**
41
+ * Sibling to `assertCoverageTransportOutcome` for the Vitest blob. `true`/`false` are the S3
42
+ * upload step's `blob` output; `skipped` means that step never ran. GitHub fallback is always
43
+ * attempted when enabled — workflows must not gate it on `blob != 'true'`.
44
+ */
45
+ export function assertCoverageTransportBlobOutcome(suite, primaryPersisted, artifactAttempt1, artifactAttempt2, emit = (line) => process.stderr.write(`${line}\n`)) {
46
+ const artifactSucceeded = artifactAttempt1 === 'success' || artifactAttempt2 === 'success';
47
+ if (primaryPersisted === 'true') {
48
+ if (!artifactSucceeded) {
49
+ emit(`::warning::Vitest blob persisted only to S3 for suite=${suite}; GitHub artifact fallback is degraded.`);
50
+ }
51
+ return true;
52
+ }
53
+ if (artifactSucceeded) {
54
+ if (primaryPersisted === 'false') {
55
+ emit(`::warning::Vitest blob persisted only to GitHub artifacts for suite=${suite}; S3 primary is degraded.`);
56
+ }
57
+ return true;
58
+ }
59
+ emit(`::error::COVERAGE_TRANSPORT_BLOB_EXHAUSTED suite=${suite} Neither S3 nor GitHub artifacts persisted the vitest blob.`);
60
+ return false;
61
+ }
62
+ /**
63
+ * Writes `blob=true|false` to `$GITHUB_OUTPUT` for outcome reporting. GitHub-fallback blob upload
64
+ * must always be attempted when enabled; do not gate it on this signal. The upload subcommand's
65
+ * exit code tracks the coverage pair, not the blob. A no-op outside CI (`githubOutputPath` unset).
66
+ */
67
+ export function writeUploadOutcomeOutput(outcome, githubOutputPath, appendOutput = appendFileSync) {
68
+ if (githubOutputPath)
69
+ appendOutput(githubOutputPath, `blob=${String(outcome.blob)}\n`);
70
+ }
@@ -0,0 +1,22 @@
1
+ import type { PresignedTransportControl } from './control.mts';
2
+ export interface PresignIdentity {
3
+ readonly repository: string;
4
+ readonly revision: string;
5
+ readonly runId: string;
6
+ readonly controlAttempt: number;
7
+ }
8
+ export interface ObjectSigner {
9
+ signPut(key: string, ttlSeconds: number): Promise<string>;
10
+ signGet(key: string, ttlSeconds: number): Promise<string>;
11
+ }
12
+ export interface MintPresignedControlOptions {
13
+ readonly ttlSeconds?: number;
14
+ readonly now?: () => Date;
15
+ readonly manifestFilename?: string;
16
+ }
17
+ export declare function transportObjectKeys(repository: string, runId: string, controlAttempt: number, suite: string, manifestFilename?: string): {
18
+ readonly lcov: string;
19
+ readonly manifest: string;
20
+ readonly blob: string;
21
+ };
22
+ export declare function mintPresignedControl(identity: PresignIdentity, coverageSuites: readonly string[], blobSuites: readonly string[], signer: ObjectSigner, options?: MintPresignedControlOptions): Promise<PresignedTransportControl>;
@@ -0,0 +1,51 @@
1
+ import { assertCoverageManifestFilename, DEFAULT_COVERAGE_MANIFEST_FILENAME } from './constants.mjs';
2
+ const DEFAULT_PRESIGN_TTL_SECONDS = 14_400;
3
+ export function transportObjectKeys(repository, runId, controlAttempt, suite, manifestFilename = DEFAULT_COVERAGE_MANIFEST_FILENAME) {
4
+ assertCoverageManifestFilename(manifestFilename);
5
+ if (repository.includes('..') ||
6
+ !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) ||
7
+ !/^[1-9][0-9]*$/.test(runId) ||
8
+ !Number.isSafeInteger(controlAttempt) ||
9
+ controlAttempt < 1 ||
10
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(suite)) {
11
+ throw new Error('Coverage transport key identity is invalid');
12
+ }
13
+ const prefix = `coverage-transport/${repository}/${runId}/${controlAttempt}`;
14
+ return {
15
+ lcov: `${prefix}/coverage/${suite}/lcov.info`,
16
+ manifest: `${prefix}/coverage/${suite}/${manifestFilename}`,
17
+ blob: `${prefix}/blobs/${suite}.tar.gz`,
18
+ };
19
+ }
20
+ export async function mintPresignedControl(identity, coverageSuites, blobSuites, signer, options = {}) {
21
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS;
22
+ const expiresAt = new Date((options.now?.() ?? new Date()).getTime() + ttlSeconds * 1000);
23
+ const manifestFilename = options.manifestFilename ?? DEFAULT_COVERAGE_MANIFEST_FILENAME;
24
+ const control = {
25
+ version: 1,
26
+ mode: 'presigned',
27
+ repository: identity.repository,
28
+ revision: identity.revision,
29
+ run: { id: identity.runId, controlAttempt: identity.controlAttempt },
30
+ expiresAt: expiresAt.toISOString(),
31
+ coverage: {},
32
+ blobs: {},
33
+ };
34
+ for (const suite of coverageSuites) {
35
+ const keys = transportObjectKeys(identity.repository, identity.runId, identity.controlAttempt, suite, manifestFilename);
36
+ control.coverage[suite] = {
37
+ lcovPut: await signer.signPut(keys.lcov, ttlSeconds),
38
+ lcovGet: await signer.signGet(keys.lcov, ttlSeconds),
39
+ manifestPut: await signer.signPut(keys.manifest, ttlSeconds),
40
+ manifestGet: await signer.signGet(keys.manifest, ttlSeconds),
41
+ };
42
+ }
43
+ for (const suite of blobSuites) {
44
+ const key = transportObjectKeys(identity.repository, identity.runId, identity.controlAttempt, suite, manifestFilename).blob;
45
+ control.blobs[suite] = {
46
+ put: await signer.signPut(key, ttlSeconds),
47
+ get: await signer.signGet(key, ttlSeconds),
48
+ };
49
+ }
50
+ return control;
51
+ }
@@ -0,0 +1,5 @@
1
+ import type { ExpectedTransportIdentity, PresignedBlobUrls, RequestOptions } from './control.mts';
2
+ export declare function packVitestBlobBundle(cwd: string, suite: string, identity: ExpectedTransportIdentity, options: RequestOptions): Buffer | null;
3
+ export declare function tarVerboseMemberSize(line: string): number;
4
+ export declare function assertTarMemberSizes(verboseLines: readonly string[], maxMemberBytes?: number): void;
5
+ export declare function downloadVitestBlobBundles(blobs: Readonly<Record<string, PresignedBlobUrls>>, destinationRoot: string, options: RequestOptions): Promise<void>;
@@ -0,0 +1,129 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { fetchGet, logTransport, redactTransportLog } from './http.mjs';
6
+ import { inspectVitestBlobBundle, VITEST_BLOB_MANIFEST_FILENAME, VITEST_SUITE_PATTERN, vitestBlobBundlePaths, writeVitestBlobManifest, } from '../vitest-blob-manifest/index.mjs';
7
+ function archiveWorkspace(suite) {
8
+ assertVitestSuite(suite);
9
+ const root = mkdtempSync(join(tmpdir(), `ct-blob-${suite}-`));
10
+ chmodSync(root, 0o700);
11
+ return { root, archive: join(root, 'bundle.tar.gz') };
12
+ }
13
+ function assertVitestSuite(suite) {
14
+ if (!VITEST_SUITE_PATTERN.test(suite))
15
+ throw new Error('Invalid Vitest suite');
16
+ }
17
+ export function packVitestBlobBundle(cwd, suite, identity, options) {
18
+ const directory = join(cwd, '.vitest-reports');
19
+ const { root, archive } = archiveWorkspace(suite);
20
+ try {
21
+ writeVitestBlobManifest(directory, {
22
+ suite,
23
+ repository: identity.repository,
24
+ revision: identity.revision,
25
+ runId: identity.runId,
26
+ runAttempt: identity.currentAttempt,
27
+ });
28
+ const paths = vitestBlobBundlePaths(directory, suite);
29
+ const limit = options.maxMemberBytes ?? MAX_VITEST_BLOB_MEMBER_BYTES;
30
+ if (paths.some((path) => statSync(path).size > limit)) {
31
+ throw new Error('Vitest blob member exceeds size limit');
32
+ }
33
+ const names = paths.map((path) => basename(path));
34
+ execFileSync('tar', ['czf', archive, '-C', directory, ...names], {
35
+ stdio: ['ignore', 'ignore', 'inherit'],
36
+ });
37
+ return readFileSync(archive);
38
+ }
39
+ catch (error) {
40
+ logTransport(options, `[coverage-transport] vitest blob pack failed: ${redactTransportLog(error)}`);
41
+ return null;
42
+ }
43
+ finally {
44
+ rmSync(root, { recursive: true, force: true });
45
+ }
46
+ }
47
+ export function tarVerboseMemberSize(line) {
48
+ const match = line.match(/\s(\d+)\s+\d{4}-\d{2}-\d{2}(?:\s|$)/) ??
49
+ line.match(/\s(\d+)\s+[A-Z][a-z]{2}\s+\d{1,2}\s/);
50
+ const size = match === null ? Number.NaN : Number(match[1]);
51
+ if (!Number.isSafeInteger(size) || size < 0) {
52
+ throw new Error('Vitest blob archive listing is malformed');
53
+ }
54
+ return size;
55
+ }
56
+ const MAX_VITEST_BLOB_MEMBER_BYTES = 32 * 1024 * 1024;
57
+ export function assertTarMemberSizes(verboseLines, maxMemberBytes = MAX_VITEST_BLOB_MEMBER_BYTES) {
58
+ if (verboseLines.some((line) => tarVerboseMemberSize(line) > maxMemberBytes)) {
59
+ throw new Error('Vitest blob archive exceeds the member size limit');
60
+ }
61
+ }
62
+ function validateArchive(archive, suite, maxMemberBytes) {
63
+ const entries = execFileSync('tar', ['tzf', archive], { encoding: 'utf8' })
64
+ .split('\n')
65
+ .filter(Boolean);
66
+ const expected = [VITEST_BLOB_MANIFEST_FILENAME, `${suite}.json`].toSorted();
67
+ if (entries.length !== 2 || entries.toSorted().join('\0') !== expected.join('\0')) {
68
+ throw new Error(`Vitest blob archive for ${suite} has unexpected entries`);
69
+ }
70
+ const verbose = execFileSync('tar', ['tvzf', archive], { encoding: 'utf8' })
71
+ .split('\n')
72
+ .filter(Boolean);
73
+ if (verbose.length !== 2 || verbose.some((line) => !line.startsWith('-'))) {
74
+ throw new Error(`Vitest blob archive for ${suite} must contain regular files`);
75
+ }
76
+ assertTarMemberSizes(verbose, maxMemberBytes);
77
+ return entries;
78
+ }
79
+ function extractValidatedBundle(archive, destinationRoot, suite, maxMemberBytes) {
80
+ const entries = validateArchive(archive, suite, maxMemberBytes);
81
+ mkdirSync(destinationRoot, { recursive: true });
82
+ const temporary = mkdtempSync(join(destinationRoot, `.vitest-blob-${suite}-`));
83
+ try {
84
+ execFileSync('tar', ['xzf', archive, '-C', temporary, ...entries], {
85
+ stdio: ['ignore', 'ignore', 'inherit'],
86
+ });
87
+ for (const entry of entries)
88
+ chmodSync(join(temporary, entry), 0o600);
89
+ inspectVitestBlobBundle(temporary);
90
+ const destination = join(destinationRoot, `vitest-blob-${suite}`);
91
+ rmSync(destination, { recursive: true, force: true });
92
+ renameSync(temporary, destination);
93
+ }
94
+ finally {
95
+ rmSync(temporary, { recursive: true, force: true });
96
+ }
97
+ }
98
+ export async function downloadVitestBlobBundles(blobs, destinationRoot, options) {
99
+ mkdirSync(destinationRoot, { recursive: true });
100
+ await Promise.all(Object.entries(blobs).map(async ([suite, urls]) => {
101
+ assertVitestSuite(suite);
102
+ const data = await fetchGet(urls.get, options);
103
+ if (!data) {
104
+ logTransport(options, `[coverage-transport] No vitest blob available for ${suite}`);
105
+ return;
106
+ }
107
+ const { root, archive } = archiveWorkspace(suite);
108
+ const invalidMarker = join(destinationRoot, `.invalid-${suite}`);
109
+ try {
110
+ writeFileSync(archive, data, { flag: 'wx', mode: 0o600 });
111
+ extractValidatedBundle(archive, destinationRoot, suite, options.maxMemberBytes ?? MAX_VITEST_BLOB_MEMBER_BYTES);
112
+ rmSync(invalidMarker, { recursive: true, force: true });
113
+ logTransport(options, `[coverage-transport] Downloaded vitest blob for ${suite}`);
114
+ }
115
+ catch (error) {
116
+ try {
117
+ rmSync(invalidMarker, { force: true });
118
+ writeFileSync(invalidMarker, 'invalid archive\n', { flag: 'wx', mode: 0o600 });
119
+ }
120
+ catch {
121
+ // The original archive failure is more actionable than a diagnostic-write failure.
122
+ }
123
+ throw error;
124
+ }
125
+ finally {
126
+ rmSync(root, { recursive: true, force: true });
127
+ }
128
+ }));
129
+ }
@@ -0,0 +1,4 @@
1
+ export { parseDockerfileRuntimeImages } from './runtime-cmd.mts';
2
+ export type { DockerfileRuntimeImage, ParseDockerfileRuntimeImagesOptions } from './runtime-cmd.mts';
3
+ export { parseDockerfilePrewarmStages } from './prewarm-ports.mts';
4
+ export type { DockerfilePrewarmStage, ParseDockerfilePrewarmOptions } from './prewarm-ports.mts';
@@ -0,0 +1,2 @@
1
+ export { parseDockerfileRuntimeImages } from './runtime-cmd.mjs';
2
+ export { parseDockerfilePrewarmStages } from './prewarm-ports.mjs';
@@ -0,0 +1,12 @@
1
+ export interface DockerfilePrewarmStage {
2
+ stage: string;
3
+ copySource: string | undefined;
4
+ monitoredPort: number | undefined;
5
+ envPort: number | undefined;
6
+ }
7
+ export interface ParseDockerfilePrewarmOptions {
8
+ copySourcePrefix?: string;
9
+ prewarmBinary?: string;
10
+ prewarmPortEnv?: string;
11
+ }
12
+ export declare function parseDockerfilePrewarmStages(dockerfile: string, { copySourcePrefix, prewarmBinary, prewarmPortEnv, }?: ParseDockerfilePrewarmOptions): DockerfilePrewarmStage[];
@@ -0,0 +1,59 @@
1
+ import { Copy, DockerfileParser, Run } from 'dockerfile-ast';
2
+ import { collectStages } from './stages.mjs';
3
+ export function parseDockerfilePrewarmStages(dockerfile, { copySourcePrefix = '/prod/', prewarmBinary = 'node-prewarm', prewarmPortEnv = 'NODE_PREWARM_PORT', } = {}) {
4
+ const parser = DockerfileParser.parse(dockerfile);
5
+ const stages = collectStages(parser);
6
+ const prewarmStages = [];
7
+ for (const { stage, instructions } of stages) {
8
+ const prewarmRun = instructions.find((instruction) => instruction instanceof Run && isPrewarmRun(instruction, prewarmBinary));
9
+ if (!prewarmRun)
10
+ continue;
11
+ const args = runArguments(prewarmRun);
12
+ prewarmStages.push({
13
+ stage,
14
+ copySource: matchCopySource(instructions, copySourcePrefix),
15
+ monitoredPort: matchMonitoredPort(args),
16
+ envPort: matchEnvPort(args, prewarmPortEnv),
17
+ });
18
+ }
19
+ return prewarmStages;
20
+ }
21
+ function runArguments(instruction) {
22
+ return instruction.getArguments().map((argument) => argument.getValue());
23
+ }
24
+ function isPrewarmRun(instruction, prewarmBinary) {
25
+ return runArguments(instruction).some((argument) => argument === prewarmBinary || argument.startsWith(`${prewarmBinary}@`));
26
+ }
27
+ function matchCopySource(instructions, copySourcePrefix) {
28
+ for (const instruction of instructions) {
29
+ if (!(instruction instanceof Copy))
30
+ continue;
31
+ if (!instruction.getFlags().some((flag) => flag.getName() === 'from'))
32
+ continue;
33
+ const args = instruction.getArguments();
34
+ if (args.at(-1)?.getValue() !== './' && args.at(-1)?.getValue() !== '.')
35
+ continue;
36
+ const sourcePath = args[0]?.getValue();
37
+ if (sourcePath?.startsWith(copySourcePrefix))
38
+ return sourcePath;
39
+ }
40
+ return undefined;
41
+ }
42
+ function matchMonitoredPort(args) {
43
+ const index = args.indexOf('--port');
44
+ if (index === -1)
45
+ return undefined;
46
+ const value = args[index + 1];
47
+ return value !== undefined && /^\d+$/.test(value) ? Number(value) : undefined;
48
+ }
49
+ function matchEnvPort(args, prewarmPortEnv) {
50
+ const prefix = `${prewarmPortEnv}=`;
51
+ for (const argument of args) {
52
+ if (!argument.startsWith(prefix))
53
+ continue;
54
+ const value = argument.slice(prefix.length);
55
+ if (/^\d+$/.test(value))
56
+ return Number(value);
57
+ }
58
+ return undefined;
59
+ }
@@ -0,0 +1,11 @@
1
+ export interface DockerfileRuntimeImage {
2
+ stage: string;
3
+ pnpmFilter: string;
4
+ cmdPath: string;
5
+ workspaceDir: string;
6
+ }
7
+ export interface ParseDockerfileRuntimeImagesOptions {
8
+ monorepoRoot: string;
9
+ copySourcePrefix?: string;
10
+ }
11
+ export declare function parseDockerfileRuntimeImages(dockerfile: string, { monorepoRoot, copySourcePrefix }: ParseDockerfileRuntimeImagesOptions): DockerfileRuntimeImage[];
@@ -0,0 +1,142 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Cmd, Copy, DockerfileParser, Run } from 'dockerfile-ast';
4
+ import { expandWorkspaceGlob } from '../workspace-glob.mjs';
5
+ import { collectStages } from './stages.mjs';
6
+ const CMD_SCRIPT = /\.(?:[cm]?[jt]s|tsx)$/u;
7
+ export function parseDockerfileRuntimeImages(dockerfile, { monorepoRoot, copySourcePrefix = '/prod/' }) {
8
+ const parser = DockerfileParser.parse(dockerfile);
9
+ const stages = collectStages(parser);
10
+ const targetToFilter = collectDeployTargets(stages);
11
+ const images = [];
12
+ for (const { stage, instructions } of stages) {
13
+ const target = matchCopyTarget(instructions, copySourcePrefix);
14
+ if (!target)
15
+ continue;
16
+ const pnpmFilter = targetToFilter.get(deployKey(target.fromStage, target.sourcePath));
17
+ if (!pnpmFilter)
18
+ continue;
19
+ const cmdPath = matchCmdScriptPath(instructions);
20
+ if (!cmdPath)
21
+ continue;
22
+ images.push({
23
+ stage,
24
+ pnpmFilter,
25
+ cmdPath,
26
+ workspaceDir: findWorkspaceDirByPackageName(monorepoRoot, pnpmFilter),
27
+ });
28
+ }
29
+ return images;
30
+ }
31
+ function deployKey(stage, target) {
32
+ return `${stage}\0${target}`;
33
+ }
34
+ function collectDeployTargets(stages) {
35
+ const targetToFilter = new Map();
36
+ for (const { stage, instructions } of stages) {
37
+ for (const instruction of instructions) {
38
+ if (!(instruction instanceof Run))
39
+ continue;
40
+ const runText = instruction
41
+ .getArguments()
42
+ .map((argument) => argument.getValue())
43
+ .join(' ');
44
+ for (const { filter, target } of parsePnpmDeploys(runText)) {
45
+ targetToFilter.set(deployKey(stage, target), filter);
46
+ }
47
+ }
48
+ }
49
+ return targetToFilter;
50
+ }
51
+ function parsePnpmDeploys(runText) {
52
+ const found = [];
53
+ for (const command of runText.split(/\s*(?:&&|;)\s*/)) {
54
+ const parsed = parsePnpmDeploy(command);
55
+ if (parsed)
56
+ found.push(parsed);
57
+ }
58
+ return found;
59
+ }
60
+ function parsePnpmDeploy(command) {
61
+ const tokens = command.split(/\s+/).filter(Boolean);
62
+ const pnpm = tokens.indexOf('pnpm');
63
+ if (pnpm === -1)
64
+ return undefined;
65
+ const deploy = tokens.indexOf('deploy', pnpm + 1);
66
+ if (deploy === -1)
67
+ return undefined;
68
+ const filter = flagValue(tokens, pnpm, tokens.length, 'filter');
69
+ const prodTarget = flagValue(tokens, deploy + 1, tokens.length, 'prod');
70
+ const positional = tokens[deploy + 1];
71
+ const target = prodTarget ??
72
+ (tokens.slice(pnpm, deploy).includes('--prod') && positional?.startsWith('/')
73
+ ? positional
74
+ : undefined);
75
+ return filter && target ? { filter, target } : undefined;
76
+ }
77
+ function flagValue(tokens, start, end, name) {
78
+ const flag = `--${name}`;
79
+ for (let index = start; index < end; index += 1) {
80
+ const token = tokens[index];
81
+ if (token === flag)
82
+ return tokens[index + 1];
83
+ if (token?.startsWith(`${flag}=`))
84
+ return token.slice(flag.length + 1);
85
+ }
86
+ return undefined;
87
+ }
88
+ function matchCopyTarget(instructions, copySourcePrefix) {
89
+ for (const instruction of instructions) {
90
+ if (!(instruction instanceof Copy))
91
+ continue;
92
+ const fromStage = instruction
93
+ .getFlags()
94
+ .find((flag) => flag.getName() === 'from')
95
+ ?.getValue();
96
+ if (!fromStage)
97
+ continue;
98
+ const args = instruction.getArguments();
99
+ if (args.at(-1)?.getValue() !== './' && args.at(-1)?.getValue() !== '.')
100
+ continue;
101
+ const sourcePath = args[0]?.getValue();
102
+ if (sourcePath?.startsWith(copySourcePrefix))
103
+ return { fromStage, sourcePath };
104
+ }
105
+ return undefined;
106
+ }
107
+ function matchCmdScriptPath(instructions) {
108
+ const cmds = instructions.filter((instruction) => instruction instanceof Cmd);
109
+ if (cmds.length === 0)
110
+ return undefined;
111
+ const parts = cmds
112
+ .at(-1)
113
+ .getJSONStrings()
114
+ ?.map((argument) => argument.getJSONValue());
115
+ return parts?.find((part) => CMD_SCRIPT.test(part));
116
+ }
117
+ function findWorkspaceDirByPackageName(monorepoRoot, packageName) {
118
+ if (packageName.startsWith('.') || packageName.startsWith('/')) {
119
+ const dir = path.resolve(monorepoRoot, packageName);
120
+ if (existsSync(path.join(dir, 'package.json')))
121
+ return dir;
122
+ throw new Error(`Workspace package not found for filter: ${packageName}`);
123
+ }
124
+ const monorepoPackage = JSON.parse(readFileSync(path.join(monorepoRoot, 'package.json'), 'utf8'));
125
+ const workspaces = Array.isArray(monorepoPackage.workspaces)
126
+ ? monorepoPackage.workspaces
127
+ : [];
128
+ for (const pattern of workspaces) {
129
+ for (const dir of expandWorkspaceGlob(monorepoRoot, pattern)) {
130
+ if (matchesPackage(dir, packageName))
131
+ return dir;
132
+ }
133
+ }
134
+ throw new Error(`Workspace package not found for filter: ${packageName}`);
135
+ }
136
+ function matchesPackage(dir, packageName) {
137
+ const pkgPath = path.join(dir, 'package.json');
138
+ if (!existsSync(pkgPath))
139
+ return false;
140
+ const { name } = JSON.parse(readFileSync(pkgPath, 'utf8'));
141
+ return name === packageName;
142
+ }
@@ -0,0 +1,7 @@
1
+ import { Cmd, Copy, From, Run, type Dockerfile } from 'dockerfile-ast';
2
+ export type StageInstruction = From | Copy | Cmd | Run;
3
+ export interface ParsedStage {
4
+ stage: string;
5
+ instructions: StageInstruction[];
6
+ }
7
+ export declare function collectStages(parser: Dockerfile): ParsedStage[];
@@ -0,0 +1,18 @@
1
+ import { Cmd, Copy, From, Run } from 'dockerfile-ast';
2
+ export function collectStages(parser) {
3
+ const stages = [];
4
+ let currentStage;
5
+ for (const instruction of parser.getInstructions()) {
6
+ if (instruction instanceof From) {
7
+ const stage = instruction.getBuildStage() ?? String(stages.length);
8
+ currentStage = { stage, instructions: [instruction] };
9
+ stages.push(currentStage);
10
+ continue;
11
+ }
12
+ if (currentStage &&
13
+ (instruction instanceof Copy || instruction instanceof Cmd || instruction instanceof Run)) {
14
+ currentStage.instructions.push(instruction);
15
+ }
16
+ }
17
+ return stages;
18
+ }