vouchington-tooling 0.0.3 → 0.0.5
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 +12 -0
- package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.d.mts +6 -0
- package/dist/cli/commands/allocate-browser-safe-ports.slot-fixtures.test-helpers.mjs +103 -0
- package/dist/cli/commands/gha-runtime-audit.d.mts +3 -0
- package/dist/cli/commands/gha-runtime-audit.mjs +23 -0
- package/dist/cli/commands/github-output.test-helpers.d.mts +1 -0
- package/dist/cli/commands/github-output.test-helpers.mjs +32 -0
- package/dist/cli/commands/pnpm-install.d.mts +1 -0
- package/dist/cli/commands/pnpm-install.mjs +27 -0
- package/dist/cli/commands/spawn-script.d.mts +5 -0
- package/dist/cli/commands/spawn-script.mjs +7 -0
- package/dist/cli/commands/vitest-blob-manifest.d.mts +1 -0
- package/dist/cli/commands/vitest-blob-manifest.mjs +11 -0
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +43 -2
- package/dist/cli/parse-gha-runtime-audit.d.mts +13 -0
- package/dist/cli/parse-gha-runtime-audit.mjs +44 -0
- package/dist/cli/parse.d.mts +13 -1
- package/dist/cli/parse.mjs +17 -0
- package/dist/cli/script-path.d.mts +1 -0
- package/dist/cli/script-path.mjs +4 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +22 -0
- package/dist/gha-runtime-audit/audit.d.mts +3 -0
- package/dist/gha-runtime-audit/audit.mjs +106 -0
- package/dist/gha-runtime-audit/index.d.mts +5 -0
- package/dist/gha-runtime-audit/index.mjs +3 -0
- package/dist/gha-runtime-audit/index.test-helpers.d.mts +14 -0
- package/dist/gha-runtime-audit/index.test-helpers.mjs +63 -0
- package/dist/gha-runtime-audit/model.d.mts +60 -0
- package/dist/gha-runtime-audit/model.mjs +83 -0
- package/dist/gha-runtime-audit/results.d.mts +15 -0
- package/dist/gha-runtime-audit/results.mjs +48 -0
- package/dist/gha-runtime-audit/scope.d.mts +29 -0
- package/dist/gha-runtime-audit/scope.mjs +39 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +4 -0
- package/dist/pnpm-install/exec.d.mts +8 -0
- package/dist/pnpm-install/exec.mjs +88 -0
- package/dist/pnpm-install/index.d.mts +5 -0
- package/dist/pnpm-install/index.mjs +4 -0
- package/dist/pnpm-install/metadata.d.mts +5 -0
- package/dist/pnpm-install/metadata.mjs +91 -0
- package/dist/pnpm-install/pnpm-install-fixture.test-helpers.d.mts +22 -0
- package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +174 -0
- package/dist/pnpm-install/process.d.mts +11 -0
- package/dist/pnpm-install/process.mjs +81 -0
- package/dist/pnpm-install/release-age.d.mts +9 -0
- package/dist/pnpm-install/release-age.mjs +59 -0
- package/dist/pnpm-install/runner.d.mts +2 -0
- package/dist/pnpm-install/runner.mjs +106 -0
- package/dist/pnpm-install/support.d.mts +30 -0
- package/dist/pnpm-install/support.mjs +143 -0
- package/dist/shared-context/fake-git.test-helpers.d.mts +11 -0
- package/dist/shared-context/fake-git.test-helpers.mjs +59 -0
- package/dist/shared-context/index.d.mts +26 -0
- package/dist/shared-context/index.mjs +89 -0
- package/dist/vitest-blob-manifest/cli.d.mts +2 -0
- package/dist/vitest-blob-manifest/cli.mjs +33 -0
- package/dist/vitest-blob-manifest/index.d.mts +37 -0
- package/dist/vitest-blob-manifest/index.mjs +134 -0
- package/package.json +21 -1
- package/scripts/allocate-browser-safe-ports.py +661 -0
- package/scripts/fetch-forbidden-ports.json +7 -0
- package/scripts/gha/check-needs-results.sh +19 -0
- package/scripts/gha/download-with-diagnostics.sh +52 -0
- package/scripts/gha/host-pressure-diagnostics.sh +179 -0
- package/scripts/gha/write-github-multiline-output.sh +60 -0
- package/scripts/runner-port-policy.json +7 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const exampleAuditOptions = {
|
|
2
|
+
workflows: [
|
|
3
|
+
{ name: 'CI', event: 'pull_request' },
|
|
4
|
+
{ name: /^Main CI \(.+\)$/, event: 'push' },
|
|
5
|
+
],
|
|
6
|
+
};
|
|
7
|
+
export function makeJob(runId, name, seconds, queueMinutes = 0, conclusion = 'success') {
|
|
8
|
+
const started = new Date(Date.UTC(2026, 0, runId, 0, queueMinutes));
|
|
9
|
+
return {
|
|
10
|
+
id: runId * 10,
|
|
11
|
+
name,
|
|
12
|
+
started_at: started.toISOString(),
|
|
13
|
+
completed_at: new Date(started.getTime() + seconds * 1000).toISOString(),
|
|
14
|
+
conclusion,
|
|
15
|
+
html_url: `https://github.test/jobs/${runId}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function makeExecutor(runs, jobsByRun) {
|
|
19
|
+
return async (request) => {
|
|
20
|
+
const { endpoint } = request;
|
|
21
|
+
if (endpoint.includes('/actions/workflows?')) {
|
|
22
|
+
return {
|
|
23
|
+
workflows: endpoint.includes('page=1')
|
|
24
|
+
? [
|
|
25
|
+
{ id: 1, name: 'CI', state: 'active' },
|
|
26
|
+
{ id: 2, name: 'Main CI (web)', state: 'active' },
|
|
27
|
+
{ id: 3, name: 'Other', state: 'active' },
|
|
28
|
+
]
|
|
29
|
+
: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (endpoint.includes('/actions/workflows/1/runs?')) {
|
|
33
|
+
return {
|
|
34
|
+
workflow_runs: endpoint.includes('page=1')
|
|
35
|
+
? runs.filter((run) => run.name === 'CI')
|
|
36
|
+
: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (endpoint.includes('/actions/workflows/2/runs?')) {
|
|
40
|
+
return {
|
|
41
|
+
workflow_runs: endpoint.includes('page=1')
|
|
42
|
+
? runs.filter((run) => run.name === 'Main CI (web)')
|
|
43
|
+
: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const match = endpoint.match(/\/actions\/runs\/(\d+)\/jobs/);
|
|
47
|
+
if (!match)
|
|
48
|
+
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
|
49
|
+
return { jobs: jobsByRun[Number(match[1])] };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function makeRun(id, name, event, branch = 'main', conclusion = 'success', createdAt = new Date(Date.UTC(2026, 0, id)).toISOString(), headBranch = branch) {
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
name,
|
|
56
|
+
event,
|
|
57
|
+
conclusion,
|
|
58
|
+
created_at: createdAt,
|
|
59
|
+
html_url: `https://github.test/runs/${id}`,
|
|
60
|
+
head_branch: headBranch,
|
|
61
|
+
pull_requests: event === 'pull_request' ? [{ base: { ref: branch } }] : [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface GhApiRequest {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
jq: string;
|
|
4
|
+
}
|
|
5
|
+
export type GhApiExecutor = (request: GhApiRequest) => Promise<unknown>;
|
|
6
|
+
export interface WorkflowRecord {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
state: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RunRecord {
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
event: string;
|
|
15
|
+
conclusion: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
url: string;
|
|
18
|
+
headBranch: string | null;
|
|
19
|
+
pullRequestBaseBranches: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface RuntimeSample {
|
|
22
|
+
runId: number;
|
|
23
|
+
runUrl: string;
|
|
24
|
+
jobId: number;
|
|
25
|
+
jobUrl: string;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
completedAt: string;
|
|
28
|
+
durationSeconds: number;
|
|
29
|
+
}
|
|
30
|
+
export type RuntimeViolationReason = 'sample-at-or-above-hard-ceiling' | 'five-sample-median-above-threshold';
|
|
31
|
+
export interface RuntimeJobResult {
|
|
32
|
+
key: string;
|
|
33
|
+
workflow: string;
|
|
34
|
+
job: string;
|
|
35
|
+
sampleCount: number;
|
|
36
|
+
medianSeconds: number | null;
|
|
37
|
+
maximumSeconds: number;
|
|
38
|
+
reasons: RuntimeViolationReason[];
|
|
39
|
+
samples: RuntimeSample[];
|
|
40
|
+
}
|
|
41
|
+
export interface RuntimeAuditResult {
|
|
42
|
+
scope: {
|
|
43
|
+
repository: string;
|
|
44
|
+
sampleLimit: number;
|
|
45
|
+
recentCompletedRunHorizon: number;
|
|
46
|
+
medianThresholdSeconds: number;
|
|
47
|
+
hardCeilingSeconds: number;
|
|
48
|
+
branch: string;
|
|
49
|
+
};
|
|
50
|
+
jobs: RuntimeJobResult[];
|
|
51
|
+
violations: RuntimeJobResult[];
|
|
52
|
+
}
|
|
53
|
+
export declare function parseObject(value: unknown, detail: string): Record<string, unknown>;
|
|
54
|
+
export declare function parseArray(value: unknown, detail: string): unknown[];
|
|
55
|
+
export declare function parseRun(value: unknown): RunRecord;
|
|
56
|
+
export declare function parseWorkflow(value: unknown): WorkflowRecord;
|
|
57
|
+
export declare function parseSample(value: unknown, run: RunRecord): {
|
|
58
|
+
name: string;
|
|
59
|
+
sample: RuntimeSample;
|
|
60
|
+
} | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function malformed(detail) {
|
|
2
|
+
throw new Error(`Malformed GitHub API data: ${detail}`);
|
|
3
|
+
}
|
|
4
|
+
export function parseObject(value, detail) {
|
|
5
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
6
|
+
malformed(detail);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function parseString(value, detail) {
|
|
10
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
11
|
+
malformed(detail);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function parseNumber(value, detail) {
|
|
15
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0)
|
|
16
|
+
malformed(detail);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
export function parseArray(value, detail) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
malformed(detail);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function parseTimestamp(value, detail) {
|
|
25
|
+
const timestamp = parseString(value, `${detail} must be a non-empty string`);
|
|
26
|
+
if (!Number.isFinite(Date.parse(timestamp)))
|
|
27
|
+
malformed(`${detail} must be an ISO timestamp`);
|
|
28
|
+
return timestamp;
|
|
29
|
+
}
|
|
30
|
+
export function parseRun(value) {
|
|
31
|
+
const run = parseObject(value, 'workflow run must be an object');
|
|
32
|
+
const pullRequests = parseArray(run['pull_requests'], 'workflow run pull_requests must be an array');
|
|
33
|
+
const pullRequestBaseBranches = [];
|
|
34
|
+
for (const [index, entry] of pullRequests.entries()) {
|
|
35
|
+
const pullRequest = parseObject(entry, `pull_requests[${index}] must be an object`);
|
|
36
|
+
const base = parseObject(pullRequest['base'], `pull_requests[${index}].base must be an object`);
|
|
37
|
+
pullRequestBaseBranches.push(parseString(base['ref'], `pull_requests[${index}].base.ref must be a string`));
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
id: parseNumber(run['id'], 'workflow run id must be a positive integer'),
|
|
41
|
+
name: parseString(run['name'], 'workflow run name must be a non-empty string'),
|
|
42
|
+
event: parseString(run['event'], 'workflow run event must be a non-empty string'),
|
|
43
|
+
conclusion: parseString(run['conclusion'], 'workflow run conclusion must be a non-empty string'),
|
|
44
|
+
createdAt: parseTimestamp(run['created_at'], 'workflow run created_at'),
|
|
45
|
+
url: parseString(run['html_url'], 'workflow run html_url must be a non-empty string'),
|
|
46
|
+
headBranch: run['head_branch'] === null
|
|
47
|
+
? null
|
|
48
|
+
: parseString(run['head_branch'], 'workflow run head_branch'),
|
|
49
|
+
pullRequestBaseBranches,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function parseWorkflow(value) {
|
|
53
|
+
const workflow = parseObject(value, 'workflow must be an object');
|
|
54
|
+
return {
|
|
55
|
+
id: parseNumber(workflow['id'], 'workflow id must be a positive integer'),
|
|
56
|
+
name: parseString(workflow['name'], 'workflow name must be a non-empty string'),
|
|
57
|
+
state: parseString(workflow['state'], 'workflow state must be a non-empty string'),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function parseSample(value, run) {
|
|
61
|
+
const job = parseObject(value, `job in run ${run.id} must be an object`);
|
|
62
|
+
const conclusion = parseString(job['conclusion'], `job in run ${run.id} conclusion must be a string`);
|
|
63
|
+
if (conclusion !== 'success')
|
|
64
|
+
return null;
|
|
65
|
+
const startedAt = parseTimestamp(job['started_at'], `job in run ${run.id} started_at`);
|
|
66
|
+
const completedAt = parseTimestamp(job['completed_at'], `job in run ${run.id} completed_at`);
|
|
67
|
+
const durationSeconds = (Date.parse(completedAt) - Date.parse(startedAt)) / 1000;
|
|
68
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) {
|
|
69
|
+
malformed(`job in run ${run.id} has an invalid execution interval`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name: parseString(job['name'], `job in run ${run.id} name must be a non-empty string`),
|
|
73
|
+
sample: {
|
|
74
|
+
runId: run.id,
|
|
75
|
+
runUrl: run.url,
|
|
76
|
+
jobId: parseNumber(job['id'], `job in run ${run.id} id must be a positive integer`),
|
|
77
|
+
jobUrl: parseString(job['html_url'], `job in run ${run.id} html_url must be a string`),
|
|
78
|
+
startedAt,
|
|
79
|
+
completedAt,
|
|
80
|
+
durationSeconds,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RuntimeJobResult, RuntimeSample } from './model.mts';
|
|
2
|
+
export interface RuntimeSamplesByJob {
|
|
3
|
+
workflow: string;
|
|
4
|
+
job: string;
|
|
5
|
+
samples: RuntimeSample[];
|
|
6
|
+
}
|
|
7
|
+
export type RuntimeThresholds = {
|
|
8
|
+
medianThresholdSeconds: number;
|
|
9
|
+
hardCeilingSeconds: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function violationOrder(a: RuntimeJobResult, b: RuntimeJobResult): number;
|
|
12
|
+
export declare function buildRuntimeResults(samplesByKey: ReadonlyMap<string, RuntimeSamplesByJob>, thresholds: RuntimeThresholds): {
|
|
13
|
+
jobs: RuntimeJobResult[];
|
|
14
|
+
violations: RuntimeJobResult[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function summarize(entry, thresholds) {
|
|
2
|
+
const durations = [];
|
|
3
|
+
for (const sample of entry.samples)
|
|
4
|
+
durations.push(sample.durationSeconds);
|
|
5
|
+
durations.sort((a, b) => a - b);
|
|
6
|
+
const medianSeconds = durations.length === 5 ? durations[2] : null;
|
|
7
|
+
const reasons = [];
|
|
8
|
+
if (durations.some((duration) => duration >= thresholds.hardCeilingSeconds)) {
|
|
9
|
+
reasons.push('sample-at-or-above-hard-ceiling');
|
|
10
|
+
}
|
|
11
|
+
if (medianSeconds !== null && medianSeconds > thresholds.medianThresholdSeconds) {
|
|
12
|
+
reasons.push('five-sample-median-above-threshold');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
key: `${entry.workflow} / ${entry.job}`,
|
|
16
|
+
workflow: entry.workflow,
|
|
17
|
+
job: entry.job,
|
|
18
|
+
sampleCount: entry.samples.length,
|
|
19
|
+
medianSeconds,
|
|
20
|
+
maximumSeconds: Math.max(...durations),
|
|
21
|
+
reasons,
|
|
22
|
+
samples: entry.samples,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function violationOrder(a, b) {
|
|
26
|
+
const aHard = a.reasons.includes('sample-at-or-above-hard-ceiling');
|
|
27
|
+
const bHard = b.reasons.includes('sample-at-or-above-hard-ceiling');
|
|
28
|
+
if (aHard && !bHard)
|
|
29
|
+
return -1;
|
|
30
|
+
if (!aHard && bHard)
|
|
31
|
+
return 1;
|
|
32
|
+
const aScore = aHard ? a.maximumSeconds : a.medianSeconds;
|
|
33
|
+
const bScore = bHard ? b.maximumSeconds : b.medianSeconds;
|
|
34
|
+
return bScore - aScore || a.key.localeCompare(b.key);
|
|
35
|
+
}
|
|
36
|
+
export function buildRuntimeResults(samplesByKey, thresholds) {
|
|
37
|
+
const jobs = [];
|
|
38
|
+
for (const entry of samplesByKey.values())
|
|
39
|
+
jobs.push(summarize(entry, thresholds));
|
|
40
|
+
jobs.sort((a, b) => a.key.localeCompare(b.key));
|
|
41
|
+
const violations = [];
|
|
42
|
+
for (const job of jobs) {
|
|
43
|
+
if (job.reasons.length > 0)
|
|
44
|
+
violations.push(job);
|
|
45
|
+
}
|
|
46
|
+
violations.sort(violationOrder);
|
|
47
|
+
return { jobs, violations };
|
|
48
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RunRecord, WorkflowRecord } from './model.mts';
|
|
2
|
+
export type WorkflowNameMatch = string | RegExp;
|
|
3
|
+
export type RuntimeAuditWorkflowFilter = {
|
|
4
|
+
name: WorkflowNameMatch;
|
|
5
|
+
event: 'pull_request' | 'push';
|
|
6
|
+
};
|
|
7
|
+
export type RuntimeAuditOptions = {
|
|
8
|
+
repository: string;
|
|
9
|
+
workflows: RuntimeAuditWorkflowFilter[];
|
|
10
|
+
branch?: string;
|
|
11
|
+
sampleLimit?: number;
|
|
12
|
+
recentCompletedRunHorizon?: number;
|
|
13
|
+
medianThresholdSeconds?: number;
|
|
14
|
+
hardCeilingSeconds?: number;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedRuntimeAuditOptions = {
|
|
17
|
+
repository: string;
|
|
18
|
+
workflows: RuntimeAuditWorkflowFilter[];
|
|
19
|
+
branch: string;
|
|
20
|
+
sampleLimit: number;
|
|
21
|
+
recentCompletedRunHorizon: number;
|
|
22
|
+
medianThresholdSeconds: number;
|
|
23
|
+
hardCeilingSeconds: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function parseWorkflowNameMatch(value: string): WorkflowNameMatch;
|
|
26
|
+
export declare function resolveRuntimeAuditOptions(options: RuntimeAuditOptions): ResolvedRuntimeAuditOptions;
|
|
27
|
+
export declare function matchingWorkflowFilter(name: string, filters: readonly RuntimeAuditWorkflowFilter[]): RuntimeAuditWorkflowFilter | undefined;
|
|
28
|
+
export declare function isSelectedWorkflow(workflow: WorkflowRecord, filters: readonly RuntimeAuditWorkflowFilter[]): boolean;
|
|
29
|
+
export declare function isRunInScope(run: RunRecord, filter: RuntimeAuditWorkflowFilter, branch: string): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function matchesWorkflowName(name, match) {
|
|
2
|
+
return typeof match === 'string' ? name === match : match.test(name);
|
|
3
|
+
}
|
|
4
|
+
export function parseWorkflowNameMatch(value) {
|
|
5
|
+
if (value.length >= 2 && value.startsWith('/') && value.endsWith('/')) {
|
|
6
|
+
return new RegExp(value.slice(1, -1));
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function resolveRuntimeAuditOptions(options) {
|
|
11
|
+
if (!/^[^/]+\/[^/]+$/.test(options.repository)) {
|
|
12
|
+
throw new Error('Repository must be owner/name');
|
|
13
|
+
}
|
|
14
|
+
if (options.workflows.length === 0) {
|
|
15
|
+
throw new Error('At least one workflow filter is required');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
repository: options.repository,
|
|
19
|
+
workflows: options.workflows,
|
|
20
|
+
branch: options.branch ?? 'main',
|
|
21
|
+
sampleLimit: options.sampleLimit ?? 5,
|
|
22
|
+
recentCompletedRunHorizon: options.recentCompletedRunHorizon ?? 10,
|
|
23
|
+
medianThresholdSeconds: options.medianThresholdSeconds ?? 360,
|
|
24
|
+
hardCeilingSeconds: options.hardCeilingSeconds ?? 600,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function matchingWorkflowFilter(name, filters) {
|
|
28
|
+
return filters.find((filter) => matchesWorkflowName(name, filter.name));
|
|
29
|
+
}
|
|
30
|
+
export function isSelectedWorkflow(workflow, filters) {
|
|
31
|
+
return workflow.state === 'active' && matchingWorkflowFilter(workflow.name, filters) !== undefined;
|
|
32
|
+
}
|
|
33
|
+
export function isRunInScope(run, filter, branch) {
|
|
34
|
+
if (run.event !== filter.event)
|
|
35
|
+
return false;
|
|
36
|
+
if (filter.event === 'pull_request')
|
|
37
|
+
return run.pullRequestBaseBranches.includes(branch);
|
|
38
|
+
return run.headBranch === branch;
|
|
39
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -3,3 +3,11 @@ export type { EphemeralListenerOptions, RunnerPortPolicy } from './runner-port-p
|
|
|
3
3
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mts';
|
|
4
4
|
export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreateTableMetadata, SqlDropIndexMetadata, SqlIndexParam, SqlMigrationConstraintMetadata, } from './sql-ast/index.mts';
|
|
5
5
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
|
|
6
|
+
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mts';
|
|
7
|
+
export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
|
|
8
|
+
export { createVitestBlobManifest, inspectVitestBlobBundle, parseVitestBlobManifest, serializeVitestBlobManifest, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, } from './vitest-blob-manifest/index.mts';
|
|
9
|
+
export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, } from './vitest-blob-manifest/index.mts';
|
|
10
|
+
export { formatReleaseAgeFailure, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mts';
|
|
11
|
+
export type { InstallOptions, Lifecycle } from './pnpm-install/index.mts';
|
|
12
|
+
export { buildContextFromTrackedFiles, buildSharedContext, gitEnv, runNamedChecks, } from './shared-context/index.mts';
|
|
13
|
+
export type { NamedCheck, SharedContext } from './shared-context/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
|
|
2
2
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
|
|
3
3
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
|
|
4
|
+
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
|
|
5
|
+
export { createVitestBlobManifest, inspectVitestBlobBundle, parseVitestBlobManifest, serializeVitestBlobManifest, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, } from './vitest-blob-manifest/index.mjs';
|
|
6
|
+
export { formatReleaseAgeFailure, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mjs';
|
|
7
|
+
export { buildContextFromTrackedFiles, buildSharedContext, gitEnv, runNamedChecks, } from './shared-context/index.mjs';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CommandResult, InstallOptions } from './support.mts';
|
|
2
|
+
/**
|
|
3
|
+
* Spawn one `pnpm` attempt. Both stdout and stderr are always captured (so a terminal failure
|
|
4
|
+
* like a release-age violation can be classified from `errorOutput`) and, unless `capture` is
|
|
5
|
+
* set, forwarded live to the parent process's streams — `capture` is for structured calls (e.g.
|
|
6
|
+
* `pnpm m ls --json`) whose stdout the caller parses rather than displays.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runPnpm(args: string[], options: InstallOptions, capture?: boolean): Promise<CommandResult>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
// oxfmt-ignore
|
|
3
|
+
import { INSTALL_TERMINATION_FAILED, installExitCode, startInstallHeartbeat, terminateSafeProcessGroup } from './process.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* Spawn one `pnpm` attempt. Both stdout and stderr are always captured (so a terminal failure
|
|
6
|
+
* like a release-age violation can be classified from `errorOutput`) and, unless `capture` is
|
|
7
|
+
* set, forwarded live to the parent process's streams — `capture` is for structured calls (e.g.
|
|
8
|
+
* `pnpm m ls --json`) whose stdout the caller parses rather than displays.
|
|
9
|
+
*/
|
|
10
|
+
export function runPnpm(args, options, capture = false) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const errorOutput = [];
|
|
13
|
+
const output = [];
|
|
14
|
+
// oxfmt-ignore
|
|
15
|
+
const result = (code) => ({ code, errorOutput: Buffer.concat(errorOutput).toString(), output: Buffer.concat(output).toString() });
|
|
16
|
+
let completed = false;
|
|
17
|
+
const complete = (result) => {
|
|
18
|
+
if (completed)
|
|
19
|
+
return;
|
|
20
|
+
completed = true;
|
|
21
|
+
resolve(result);
|
|
22
|
+
};
|
|
23
|
+
let child;
|
|
24
|
+
try {
|
|
25
|
+
child = spawn('pnpm', args, {
|
|
26
|
+
detached: true,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
console.error(`pnpm failed to spawn: ${message}`);
|
|
33
|
+
complete({ code: 1, errorOutput: message, output: '' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
child.stdout?.on('data', (data) => {
|
|
37
|
+
output.push(data);
|
|
38
|
+
if (!capture)
|
|
39
|
+
process.stdout.write(data);
|
|
40
|
+
});
|
|
41
|
+
child.stderr?.on('data', (data) => {
|
|
42
|
+
errorOutput.push(data);
|
|
43
|
+
if (!capture)
|
|
44
|
+
process.stderr.write(data);
|
|
45
|
+
});
|
|
46
|
+
child.once('error', (error) => {
|
|
47
|
+
console.error(`pnpm failed to start: ${error.message}`);
|
|
48
|
+
complete({ code: 1, errorOutput: error.message, output: '' });
|
|
49
|
+
});
|
|
50
|
+
const heartbeat = startInstallHeartbeat();
|
|
51
|
+
let termination;
|
|
52
|
+
// Benign race: if the timeout callback and the 'close' handler both fire, `complete`'s dedup
|
|
53
|
+
// guard lets only the first resolve. `timedOut` is only read by the winner, so whichever wins
|
|
54
|
+
// sees the correct value for its own case (true when the timeout won, false when close won).
|
|
55
|
+
let timedOut = false;
|
|
56
|
+
const terminate = () => (termination ??= terminateSafeProcessGroup(child.pid));
|
|
57
|
+
const timeout = options.commandTimeoutSeconds === 0
|
|
58
|
+
? undefined
|
|
59
|
+
: setTimeout(() => {
|
|
60
|
+
/* v8 ignore next -- close can win the race and already settle */
|
|
61
|
+
if (completed)
|
|
62
|
+
return;
|
|
63
|
+
timedOut = true;
|
|
64
|
+
void terminate()
|
|
65
|
+
.then((stopped) => complete(result(stopped ? 1 : INSTALL_TERMINATION_FAILED)))
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
console.error(`pnpm install termination failed: ${String(error)}`);
|
|
68
|
+
complete(result(INSTALL_TERMINATION_FAILED));
|
|
69
|
+
})
|
|
70
|
+
.finally(() => {
|
|
71
|
+
clearInterval(heartbeat);
|
|
72
|
+
});
|
|
73
|
+
}, options.commandTimeoutSeconds * 1000);
|
|
74
|
+
timeout?.unref();
|
|
75
|
+
child.once('close', async (code) => {
|
|
76
|
+
try {
|
|
77
|
+
await termination;
|
|
78
|
+
complete(result(installExitCode(code, timedOut)));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
clearInterval(heartbeat);
|
|
82
|
+
// oxlint-disable-next-line promise/no-multiple-resolved -- oxlint reports the multi-path settlement at this finally line; the sole `resolve` call site is first-winner-guarded by `completed` inside `complete`
|
|
83
|
+
if (timeout)
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { runInstallLifecycle } from './runner.mts';
|
|
2
|
+
export { parseInstallOptions } from './support.mts';
|
|
3
|
+
export type { InstallOptions, Lifecycle } from './support.mts';
|
|
4
|
+
export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolations, } from './release-age.mts';
|
|
5
|
+
export { INSTALL_TERMINATION_FAILED } from './process.mts';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { runInstallLifecycle } from './runner.mjs';
|
|
2
|
+
export { parseInstallOptions } from './support.mjs';
|
|
3
|
+
export { formatReleaseAgeFailure, isReleaseAgeViolation, parseReleaseAgeViolations, } from './release-age.mjs';
|
|
4
|
+
export { INSTALL_TERMINATION_FAILED } from './process.mjs';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CaptureCommand } from './support.mts';
|
|
2
|
+
export declare function persistentMetadataFingerprint(runCapture: CaptureCommand, installScripts: boolean): Promise<string>;
|
|
3
|
+
export declare function persistentMetadataMatches(fingerprint: string): Promise<boolean>;
|
|
4
|
+
export declare function persistentDependencyTreeIsCold(): Promise<boolean>;
|
|
5
|
+
export declare function writePersistentMetadataStamp(fingerprint: string): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { listWorkspaces, reportGlibcVersionRuntime } from './support.mjs';
|
|
5
|
+
function persistentMetadataStampPath() {
|
|
6
|
+
return path.join(process.cwd(), 'node_modules', '.pnpm-install-metadata-health.json');
|
|
7
|
+
}
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
function addFingerprintInput(hash, label, value) {
|
|
12
|
+
hash.update(`${label.length}:${label}${value.length}:${value}`);
|
|
13
|
+
}
|
|
14
|
+
async function optionalFile(pathname) {
|
|
15
|
+
try {
|
|
16
|
+
return await readFile(pathname, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return '';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function runtimePlatformIdentity() {
|
|
25
|
+
const report = process.report?.getReport();
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
arch: process.arch,
|
|
28
|
+
glibc: reportGlibcVersionRuntime(report),
|
|
29
|
+
modules: process.versions.modules,
|
|
30
|
+
node: process.version,
|
|
31
|
+
npmConfigArch: process.env.npm_config_arch ?? '',
|
|
32
|
+
npmConfigLibc: process.env.npm_config_libc ?? '',
|
|
33
|
+
npmConfigPlatform: process.env.npm_config_platform ?? '',
|
|
34
|
+
platform: process.platform,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export async function persistentMetadataFingerprint(runCapture, installScripts) {
|
|
38
|
+
const workspaces = (await listWorkspaces(runCapture)).toSorted((left, right) => left.path.localeCompare(right.path));
|
|
39
|
+
const pnpmVersion = await runCapture(['--version']);
|
|
40
|
+
if (pnpmVersion.code !== 0)
|
|
41
|
+
fail(`pnpm --version failed: ${pnpmVersion.errorOutput?.trim() || pnpmVersion.output.trim() || 'unknown error'}`);
|
|
42
|
+
const hash = createHash('sha256');
|
|
43
|
+
addFingerprintInput(hash, 'runtime', runtimePlatformIdentity());
|
|
44
|
+
addFingerprintInput(hash, 'pnpm', pnpmVersion.output.trim());
|
|
45
|
+
addFingerprintInput(hash, 'installScripts', String(installScripts));
|
|
46
|
+
for (const filename of [
|
|
47
|
+
'pnpm-lock.yaml',
|
|
48
|
+
'pnpm-workspace.yaml',
|
|
49
|
+
'.npmrc',
|
|
50
|
+
'.pnpmfile.cjs',
|
|
51
|
+
'.pnpmfile.mjs',
|
|
52
|
+
])
|
|
53
|
+
addFingerprintInput(hash, filename, await optionalFile(path.join(process.cwd(), filename)));
|
|
54
|
+
for (const workspace of workspaces)
|
|
55
|
+
addFingerprintInput(hash, path.relative(process.cwd(), path.join(workspace.path, 'package.json')), await readFile(path.join(workspace.path, 'package.json'), 'utf8'));
|
|
56
|
+
return hash.digest('hex');
|
|
57
|
+
}
|
|
58
|
+
export async function persistentMetadataMatches(fingerprint) {
|
|
59
|
+
try {
|
|
60
|
+
const stamp = JSON.parse(await readFile(persistentMetadataStampPath(), 'utf8'));
|
|
61
|
+
return stamp.version === 1 && stamp.fingerprint === fingerprint;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function persistentDependencyTreeIsCold() {
|
|
68
|
+
try {
|
|
69
|
+
await stat(path.dirname(persistentMetadataStampPath()));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === 'ENOENT')
|
|
74
|
+
return true;
|
|
75
|
+
/* v8 ignore next -- non-ENOENT stat failures are host-specific */
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function writePersistentMetadataStamp(fingerprint) {
|
|
80
|
+
const stampPath = persistentMetadataStampPath();
|
|
81
|
+
const directory = path.dirname(stampPath);
|
|
82
|
+
const temporary = `${stampPath}.${process.pid}.tmp`;
|
|
83
|
+
await mkdir(directory, { recursive: true });
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(temporary, `${JSON.stringify({ fingerprint, version: 1 })}\n`);
|
|
86
|
+
await rename(temporary, stampPath);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await rm(temporary, { force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type FixtureOptions = {
|
|
2
|
+
commandTimeoutSeconds?: number;
|
|
3
|
+
installScripts?: boolean;
|
|
4
|
+
lifecycle?: 'ephemeral' | 'ephemeral-full' | 'persistent';
|
|
5
|
+
maxAttempts?: number;
|
|
6
|
+
selectors?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function makeFixture(): Promise<{
|
|
9
|
+
consumer: string;
|
|
10
|
+
dependency: string;
|
|
11
|
+
dependencyLink: string;
|
|
12
|
+
env: NodeJS.ProcessEnv;
|
|
13
|
+
pnpmLog: string;
|
|
14
|
+
root: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function runInstaller(fixture: Awaited<ReturnType<typeof makeFixture>>, options?: FixtureOptions): Promise<{
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function installCalls(fixture: Awaited<ReturnType<typeof makeFixture>>): Promise<string[]>;
|
|
22
|
+
export declare function resetInstallCalls(fixture: Awaited<ReturnType<typeof makeFixture>>): Promise<void>;
|