vouchington-tooling 0.0.13 → 0.0.15

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 (60) hide show
  1. package/README.md +17 -2
  2. package/dist/ast-grep-rule/index.d.mts +13 -0
  3. package/dist/ast-grep-rule/index.mjs +43 -0
  4. package/dist/coverage-transport/constants.d.mts +3 -0
  5. package/dist/coverage-transport/constants.mjs +11 -0
  6. package/dist/coverage-transport/control.d.mts +47 -0
  7. package/dist/coverage-transport/control.mjs +112 -0
  8. package/dist/coverage-transport/http.d.mts +6 -0
  9. package/dist/coverage-transport/http.mjs +99 -0
  10. package/dist/coverage-transport/index.d.mts +6 -0
  11. package/dist/coverage-transport/index.mjs +6 -0
  12. package/dist/coverage-transport/lib.d.mts +17 -0
  13. package/dist/coverage-transport/lib.mjs +84 -0
  14. package/dist/coverage-transport/outcome.d.mts +29 -0
  15. package/dist/coverage-transport/outcome.mjs +70 -0
  16. package/dist/coverage-transport/presign.d.mts +22 -0
  17. package/dist/coverage-transport/presign.mjs +51 -0
  18. package/dist/coverage-transport/vitest-blob-transport.d.mts +5 -0
  19. package/dist/coverage-transport/vitest-blob-transport.mjs +129 -0
  20. package/dist/csv/index.d.mts +5 -0
  21. package/dist/csv/index.mjs +31 -0
  22. package/dist/gha-review-payload/diff.d.mts +30 -0
  23. package/dist/gha-review-payload/diff.mjs +121 -0
  24. package/dist/gha-review-payload/file.d.mts +7 -0
  25. package/dist/gha-review-payload/file.mjs +62 -0
  26. package/dist/gha-review-payload/index.d.mts +7 -0
  27. package/dist/gha-review-payload/index.mjs +4 -0
  28. package/dist/gha-review-payload/payload.d.mts +30 -0
  29. package/dist/gha-review-payload/payload.mjs +111 -0
  30. package/dist/gha-review-payload/remap.d.mts +13 -0
  31. package/dist/gha-review-payload/remap.mjs +89 -0
  32. package/dist/http-body/index.d.mts +18 -0
  33. package/dist/http-body/index.mjs +86 -0
  34. package/dist/http-link-pagination/index.d.mts +19 -0
  35. package/dist/http-link-pagination/index.mjs +149 -0
  36. package/dist/index.d.mts +18 -2
  37. package/dist/index.mjs +11 -1
  38. package/dist/pnpm-install/index.d.mts +2 -0
  39. package/dist/pnpm-install/index.mjs +1 -0
  40. package/dist/pnpm-install/release-age-policy-types.d.mts +34 -0
  41. package/dist/pnpm-install/release-age-policy-types.mjs +1 -0
  42. package/dist/pnpm-install/release-age-policy.d.mts +31 -0
  43. package/dist/pnpm-install/release-age-policy.mjs +136 -0
  44. package/dist/transient-retry/attempts.d.mts +5 -0
  45. package/dist/transient-retry/attempts.mjs +8 -0
  46. package/dist/transient-retry/decision-evaluator.d.mts +20 -0
  47. package/dist/transient-retry/decision-evaluator.mjs +62 -0
  48. package/dist/transient-retry/index.d.mts +4 -0
  49. package/dist/transient-retry/index.mjs +2 -0
  50. package/dist/transient-retry/types.d.mts +39 -0
  51. package/dist/transient-retry/types.mjs +1 -0
  52. package/dist/vitest-blob-manifest/constants.d.mts +1 -0
  53. package/dist/vitest-blob-manifest/constants.mjs +1 -0
  54. package/dist/vitest-blob-manifest/index.d.mts +2 -1
  55. package/dist/vitest-blob-manifest/index.mjs +3 -1
  56. package/dist/vitest-blob-manifest/report-attempt.d.mts +24 -0
  57. package/dist/vitest-blob-manifest/report-attempt.mjs +127 -0
  58. package/dist/vitest-blob-manifest/reports.d.mts +28 -0
  59. package/dist/vitest-blob-manifest/reports.mjs +160 -0
  60. package/package.json +43 -1
package/README.md CHANGED
@@ -50,8 +50,12 @@ import {
50
50
  import { initSqlAst, extractCreateTableMetadata } from 'vouchington-tooling/sql-ast'
51
51
  import { splitSqlStatements, stripSqlComments } from 'vouchington-tooling/sql-scanner'
52
52
  import { auditCiJobRuntime } from 'vouchington-tooling/gha-runtime-audit'
53
- import { writeVitestBlobManifest } from 'vouchington-tooling/vitest-blob-manifest'
54
- import { runInstallLifecycle } from 'vouchington-tooling/pnpm-install'
53
+ import {
54
+ readVitestReportAttempts,
55
+ writeVitestBlobManifest,
56
+ } from 'vouchington-tooling/vitest-blob-manifest'
57
+ import { prepareVitestReports } from 'vouchington-tooling/vitest-reports'
58
+ import { runInstallLifecycle, validateReleaseAgePolicy } from 'vouchington-tooling/pnpm-install'
55
59
  import {
56
60
  buildSharedContext,
57
61
  installFakeGit,
@@ -69,4 +73,15 @@ import {
69
73
  renderSchemaMarkdown,
70
74
  } from 'vouchington-tooling/pg-schema-snapshot'
71
75
  import { buildOpenApiDocument, writeOpenApi } from 'vouchington-tooling/openapi-document'
76
+ import { decide, deriveRetryAttempt } from 'vouchington-tooling/transient-retry'
77
+ import { parseCsvRows, streamCsvRows } from 'vouchington-tooling/csv'
78
+ import { readResponseBody } from 'vouchington-tooling/http-body'
79
+ import { runAstGrepRule } from 'vouchington-tooling/ast-grep-rule'
80
+ import { parseReviewPayload, remapReviewComments } from 'vouchington-tooling/gha-review-payload'
81
+ import { nextPageUrlFromLinkHeader } from 'vouchington-tooling/http-link-pagination'
82
+ import { cmdUpload, mintPresignedControl } from 'vouchington-tooling/coverage-transport'
72
83
  ```
84
+
85
+ The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
86
+ boundaries. They do not include provider credentials, product policy, network transport, or
87
+ repository-specific package names; consumers supply those through their own adapters.
@@ -0,0 +1,13 @@
1
+ export interface AstGrepRuleInvocation {
2
+ readonly ruleId: string;
3
+ readonly passthrough: readonly string[];
4
+ }
5
+ export interface RunAstGrepRuleOptions {
6
+ readonly args: readonly string[];
7
+ readonly cwd?: string;
8
+ readonly rulesDirectory?: string;
9
+ readonly executable?: string;
10
+ readonly defaultScanArguments?: readonly string[];
11
+ }
12
+ export declare function parseAstGrepRuleArgs(args: readonly string[]): AstGrepRuleInvocation;
13
+ export declare function runAstGrepRule(options: RunAstGrepRuleOptions): Promise<number>;
@@ -0,0 +1,43 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ export function parseAstGrepRuleArgs(args) {
5
+ const normalized = args[0] === '--' ? args.slice(1) : args;
6
+ const [ruleId, ...passthrough] = normalized;
7
+ if (!ruleId)
8
+ throw new Error('Expected an AST-grep rule id');
9
+ if (ruleId.startsWith('-') || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(ruleId)) {
10
+ throw new Error(`Invalid AST-grep rule id: ${ruleId}`);
11
+ }
12
+ return { ruleId, passthrough };
13
+ }
14
+ function containedRulePath(rulesDirectory, ruleId) {
15
+ const root = realpathSync(rulesDirectory);
16
+ const path = realpathSync(join(root, `${ruleId}.yml`));
17
+ if (resolve(path).startsWith(`${resolve(root)}/`))
18
+ return path;
19
+ throw new Error(`AST-grep rule resolves outside the rules directory: ${ruleId}`);
20
+ }
21
+ export function runAstGrepRule(options) {
22
+ const cwd = options.cwd ?? process.cwd();
23
+ const rulesDirectory = resolve(cwd, options.rulesDirectory ?? 'ast-grep-rules');
24
+ const { ruleId, passthrough } = parseAstGrepRuleArgs(options.args);
25
+ const unresolvedRulePath = join(rulesDirectory, `${ruleId}.yml`);
26
+ if (!existsSync(unresolvedRulePath))
27
+ throw new Error(`Unknown AST-grep rule id: ${ruleId}`);
28
+ const rulePath = containedRulePath(rulesDirectory, ruleId);
29
+ const executable = resolve(cwd, options.executable ?? join('node_modules', '@ast-grep', 'cli', 'ast-grep'));
30
+ const defaults = options.defaultScanArguments ?? [
31
+ '--no-ignore',
32
+ 'hidden',
33
+ '--off=unused-suppression',
34
+ ];
35
+ return new Promise((resolveResult, reject) => {
36
+ const child = spawn(executable, ['scan', ...defaults, '--rule', rulePath, ...passthrough], {
37
+ cwd,
38
+ stdio: 'inherit',
39
+ });
40
+ child.once('error', reject);
41
+ child.once('close', (code) => resolveResult(code ?? 1));
42
+ });
43
+ }
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_COVERAGE_MANIFEST_FILENAME = "coverage-manifest.json";
2
+ export declare const DEFAULT_MAX_BODY_BYTES: number;
3
+ export declare function assertCoverageManifestFilename(filename: string): void;
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_COVERAGE_MANIFEST_FILENAME = 'coverage-manifest.json';
2
+ export const DEFAULT_MAX_BODY_BYTES = 32 * 1024 * 1024;
3
+ const MANIFEST_FILENAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.json$/;
4
+ export function assertCoverageManifestFilename(filename) {
5
+ if (filename.includes('/') ||
6
+ filename.includes('\\') ||
7
+ filename.includes('..') ||
8
+ !MANIFEST_FILENAME_PATTERN.test(filename)) {
9
+ throw new Error('Coverage manifest filename is invalid');
10
+ }
11
+ }
@@ -0,0 +1,47 @@
1
+ export interface PresignedCoverageUrls {
2
+ lcovPut: string;
3
+ lcovGet: string;
4
+ manifestPut: string;
5
+ manifestGet: string;
6
+ }
7
+ export interface PresignedBlobUrls {
8
+ put: string;
9
+ get: string;
10
+ }
11
+ interface TransportControlBase {
12
+ readonly version: 1;
13
+ readonly repository: string;
14
+ readonly revision: string;
15
+ readonly run: {
16
+ readonly id: string;
17
+ readonly controlAttempt: number;
18
+ };
19
+ }
20
+ export interface PresignedTransportControl extends TransportControlBase {
21
+ readonly mode: 'presigned';
22
+ readonly expiresAt: string;
23
+ readonly coverage: Record<string, PresignedCoverageUrls>;
24
+ readonly blobs: Record<string, PresignedBlobUrls>;
25
+ }
26
+ export interface FallbackOnlyTransportControl extends TransportControlBase {
27
+ readonly mode: 'fallback-only';
28
+ readonly reason: string;
29
+ }
30
+ export type TransportControl = PresignedTransportControl | FallbackOnlyTransportControl;
31
+ export interface ExpectedTransportIdentity {
32
+ readonly repository: string;
33
+ readonly revision: string;
34
+ readonly runId: string;
35
+ readonly currentAttempt: number;
36
+ }
37
+ export interface RequestOptions {
38
+ readonly retryDelayMs?: number;
39
+ readonly timeoutMs?: number;
40
+ readonly maxBodyBytes?: number;
41
+ readonly maxMemberBytes?: number;
42
+ readonly log?: (line: string) => void;
43
+ }
44
+ export declare function parseTransportControl(raw: unknown): TransportControl;
45
+ export declare function writeTransportControl(path: string, control: TransportControl): void;
46
+ export declare function readTransportControl(path: string, expected?: ExpectedTransportIdentity): TransportControl;
47
+ export {};
@@ -0,0 +1,112 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { VITEST_SUITE_PATTERN } from '../vitest-blob-manifest/index.mjs';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function positiveInteger(value) {
8
+ return Number.isSafeInteger(value) && Number(value) > 0;
9
+ }
10
+ function assertUrlMap(raw, fields, label) {
11
+ if (!isRecord(raw))
12
+ throw new Error(`${label} URL map must be an object`);
13
+ for (const [suite, urls] of Object.entries(raw)) {
14
+ if (!VITEST_SUITE_PATTERN.test(suite) ||
15
+ !isRecord(urls) ||
16
+ Object.keys(urls).toSorted().join('\0') !== fields.toSorted().join('\0')) {
17
+ throw new Error(`${label} URL map has an invalid entry`);
18
+ }
19
+ for (const field of fields) {
20
+ const value = urls[field];
21
+ if (typeof value !== 'string' || !URL.canParse(value) || !/^https?:/i.test(value)) {
22
+ throw new Error(`${label} URL map has an invalid URL`);
23
+ }
24
+ }
25
+ }
26
+ }
27
+ export function parseTransportControl(raw) {
28
+ if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.run)) {
29
+ throw new Error('Coverage transport control has an unsupported schema');
30
+ }
31
+ if (typeof raw.repository !== 'string' ||
32
+ !raw.repository ||
33
+ typeof raw.revision !== 'string' ||
34
+ !/^[0-9a-f]{40}$/.test(raw.revision) ||
35
+ typeof raw.run.id !== 'string' ||
36
+ !/^[1-9][0-9]*$/.test(raw.run.id) ||
37
+ !positiveInteger(raw.run.controlAttempt)) {
38
+ throw new Error('Coverage transport control has invalid identity fields');
39
+ }
40
+ if (Object.keys(raw.run).toSorted().join('\0') !== ['controlAttempt', 'id'].join('\0')) {
41
+ throw new Error('Coverage transport control run schema is invalid');
42
+ }
43
+ if (raw.mode === 'fallback-only') {
44
+ const fallbackKeys = ['mode', 'reason', 'repository', 'revision', 'run', 'version'];
45
+ if (Object.keys(raw).toSorted().join('\0') !== fallbackKeys.toSorted().join('\0') ||
46
+ typeof raw.reason !== 'string' ||
47
+ !raw.reason) {
48
+ throw new Error('Fallback-only coverage transport control is invalid');
49
+ }
50
+ return raw;
51
+ }
52
+ const presignedKeys = [
53
+ 'blobs',
54
+ 'coverage',
55
+ 'expiresAt',
56
+ 'mode',
57
+ 'repository',
58
+ 'revision',
59
+ 'run',
60
+ 'version',
61
+ ];
62
+ if (raw.mode !== 'presigned' ||
63
+ Object.keys(raw).toSorted().join('\0') !== presignedKeys.toSorted().join('\0') ||
64
+ typeof raw.expiresAt !== 'string' ||
65
+ !Number.isFinite(Date.parse(raw.expiresAt))) {
66
+ throw new Error('Presigned coverage transport control is invalid');
67
+ }
68
+ assertUrlMap(raw.coverage, ['lcovGet', 'lcovPut', 'manifestGet', 'manifestPut'], 'Coverage');
69
+ assertUrlMap(raw.blobs, ['get', 'put'], 'Blob');
70
+ return raw;
71
+ }
72
+ export function writeTransportControl(path, control) {
73
+ const validated = parseTransportControl(control);
74
+ const temporary = `${path}.${randomUUID()}.tmp`;
75
+ try {
76
+ writeFileSync(temporary, `${JSON.stringify(validated, null, 2)}\n`, {
77
+ flag: 'wx',
78
+ mode: 0o600,
79
+ });
80
+ chmodSync(temporary, 0o600);
81
+ renameSync(temporary, path);
82
+ }
83
+ catch (error) {
84
+ try {
85
+ unlinkSync(temporary);
86
+ }
87
+ catch {
88
+ // The write/rename failure is the actionable error.
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ export function readTransportControl(path, expected) {
94
+ if ((statSync(path).mode & 0o777) !== 0o600) {
95
+ throw new Error('Coverage transport control file must have mode 0600');
96
+ }
97
+ const control = parseTransportControl(JSON.parse(readFileSync(path, 'utf8')));
98
+ if (expected && !positiveInteger(expected.currentAttempt)) {
99
+ throw new Error('Coverage transport control identity does not match this run');
100
+ }
101
+ if (expected &&
102
+ (control.repository !== expected.repository ||
103
+ control.revision !== expected.revision ||
104
+ control.run.id !== expected.runId ||
105
+ control.run.controlAttempt > expected.currentAttempt)) {
106
+ throw new Error('Coverage transport control identity does not match this run');
107
+ }
108
+ if (control.mode === 'presigned' && Date.parse(control.expiresAt) <= Date.now()) {
109
+ throw new Error('Coverage transport control has expired');
110
+ }
111
+ return control;
112
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequestOptions } from './control.mts';
2
+ export declare function redactTransportLog(value: unknown): string;
3
+ export declare function coveragePresignFailureLog(error: unknown): string;
4
+ export declare function logTransport(options: RequestOptions, line: string): void;
5
+ export declare function fetchPut(url: string, body: Buffer, options?: RequestOptions): Promise<boolean>;
6
+ export declare function fetchGet(url: string, options?: RequestOptions): Promise<Buffer | null>;
@@ -0,0 +1,99 @@
1
+ import { DEFAULT_MAX_BODY_BYTES } from './constants.mjs';
2
+ const FETCH_TIMEOUT_MS = 30_000;
3
+ const FETCH_ATTEMPTS = 2;
4
+ const DEFAULT_RETRY_DELAY_MS = 1000;
5
+ function formatTransportError(value, seen) {
6
+ if (typeof value === 'object' && value !== null) {
7
+ if (seen.has(value))
8
+ return '[circular]';
9
+ seen.add(value);
10
+ }
11
+ if (value instanceof Error) {
12
+ const cause = value.cause !== undefined ? `; cause: ${formatTransportError(value.cause, seen)}` : '';
13
+ return `${value.name}: ${value.message}${cause}`;
14
+ }
15
+ return String(value);
16
+ }
17
+ export function redactTransportLog(value) {
18
+ return formatTransportError(value, new WeakSet()).replaceAll(/https?:\/\/[^\s]+/gi, '[redacted-url]');
19
+ }
20
+ export function coveragePresignFailureLog(error) {
21
+ return `[coverage-transport] presign failed: ${redactTransportLog(error)}; artifact fallback required`;
22
+ }
23
+ export function logTransport(options, line) {
24
+ ;
25
+ (options.log ?? ((message) => process.stderr.write(`${message}\n`)))(redactTransportLog(line));
26
+ }
27
+ async function delay(milliseconds) {
28
+ if (milliseconds <= 0)
29
+ return;
30
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
31
+ }
32
+ async function cancelBody(response) {
33
+ await response.body?.cancel();
34
+ }
35
+ async function readLimitedBody(response, maxBytes) {
36
+ if (response.body === null)
37
+ return Buffer.alloc(0);
38
+ const reader = response.body.getReader();
39
+ const chunks = [];
40
+ let total = 0;
41
+ for (;;) {
42
+ const { done, value } = await reader.read();
43
+ if (done)
44
+ return Buffer.concat(chunks);
45
+ total += value.byteLength;
46
+ if (total > maxBytes) {
47
+ await reader.cancel();
48
+ throw new Error('[coverage-transport] GET body exceeds size limit');
49
+ }
50
+ chunks.push(value);
51
+ }
52
+ }
53
+ async function request(method, url, body, options) {
54
+ for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt += 1) {
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? FETCH_TIMEOUT_MS);
57
+ try {
58
+ const response = await fetch(url, {
59
+ method,
60
+ ...(body ? { body: new Uint8Array(body) } : {}),
61
+ signal: controller.signal,
62
+ });
63
+ // A missing presigned object is terminal. Let fetchGet yield null or fetchPut yield false
64
+ // without retrying the same URL or emitting a misleading transport-error diagnostic.
65
+ if (response.ok || response.status === 404) {
66
+ const payload = method === 'GET' && response.ok
67
+ ? await readLimitedBody(response, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES)
68
+ : undefined;
69
+ if (payload === undefined)
70
+ await cancelBody(response);
71
+ return { ok: response.ok, status: response.status, body: payload };
72
+ }
73
+ await cancelBody(response);
74
+ logTransport(options, `[coverage-transport] ${method} failed: HTTP ${response.status}`);
75
+ }
76
+ catch (error) {
77
+ logTransport(options, `[coverage-transport] ${method} error: ${redactTransportLog(error)}`);
78
+ }
79
+ finally {
80
+ clearTimeout(timer);
81
+ }
82
+ if (attempt < FETCH_ATTEMPTS) {
83
+ await delay(options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ export async function fetchPut(url, body, options = {}) {
89
+ const response = await request('PUT', url, body, options);
90
+ return response?.ok === true;
91
+ }
92
+ export async function fetchGet(url, options = {}) {
93
+ const response = await request('GET', url, undefined, options);
94
+ if (response?.status === 404)
95
+ return null;
96
+ if (!response?.ok)
97
+ throw new Error('[coverage-transport] GET exhausted');
98
+ return response.body;
99
+ }
@@ -0,0 +1,6 @@
1
+ export { DEFAULT_COVERAGE_MANIFEST_FILENAME, cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, } from './lib.mts';
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';
4
+ export { assertCoverageTransportBlobOutcome, assertCoverageTransportOutcome, isBlobPrimaryState, isStepOutcome, writeUploadOutcomeOutput, type AppendOutput, type BlobPrimaryState, type StepOutcome, } from './outcome.mts';
5
+ export { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mts';
6
+ export { mintPresignedControl, transportObjectKeys, type MintPresignedControlOptions, type ObjectSigner, type PresignIdentity, } from './presign.mts';
@@ -0,0 +1,6 @@
1
+ export { DEFAULT_COVERAGE_MANIFEST_FILENAME, cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, } from './lib.mjs';
2
+ export { parseTransportControl, readTransportControl, writeTransportControl, } from './control.mjs';
3
+ export { coveragePresignFailureLog, fetchGet, fetchPut, logTransport, redactTransportLog, } from './http.mjs';
4
+ export { assertCoverageTransportBlobOutcome, assertCoverageTransportOutcome, isBlobPrimaryState, isStepOutcome, writeUploadOutcomeOutput, } from './outcome.mjs';
5
+ export { downloadVitestBlobBundles, packVitestBlobBundle } from './vitest-blob-transport.mjs';
6
+ export { mintPresignedControl, transportObjectKeys, } from './presign.mjs';
@@ -0,0 +1,17 @@
1
+ import { type ExpectedTransportIdentity, type RequestOptions } from './control.mts';
2
+ export { DEFAULT_COVERAGE_MANIFEST_FILENAME } from './constants.mts';
3
+ interface UploadOptions extends RequestOptions {
4
+ readonly cwd?: string;
5
+ readonly expectedIdentity: ExpectedTransportIdentity;
6
+ readonly coverageManifestFilename?: string;
7
+ }
8
+ interface DownloadOptions extends RequestOptions {
9
+ readonly expectedIdentity: ExpectedTransportIdentity;
10
+ readonly coverageManifestFilename?: string;
11
+ }
12
+ export declare function cmdUpload(controlPath: string, suite: string, options: UploadOptions): Promise<{
13
+ coverage: boolean;
14
+ blob: boolean;
15
+ }>;
16
+ export declare function cmdDownloadCoverage(controlPath: string, destinationRoot: string, options: DownloadOptions): Promise<void>;
17
+ export declare function cmdDownloadVitestBlobs(controlPath: string, destinationRoot: string, options: DownloadOptions): Promise<void>;
@@ -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>;