vouchington-tooling 0.0.15 → 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.
- package/README.md +12 -0
- package/dist/ci-local/index.d.mts +5 -0
- package/dist/ci-local/index.mjs +79 -0
- package/dist/ci-local/parse.d.mts +3 -0
- package/dist/ci-local/parse.mjs +43 -0
- package/dist/ci-local/types.d.mts +41 -0
- package/dist/ci-local/types.mjs +1 -0
- package/dist/cli/index.mjs +5 -0
- package/dist/cli/parse.d.mts +1 -1
- package/dist/cli/parse.mjs +5 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +10 -0
- package/dist/dockerfile-parse/index.d.mts +4 -0
- package/dist/dockerfile-parse/index.mjs +2 -0
- package/dist/dockerfile-parse/prewarm-ports.d.mts +12 -0
- package/dist/dockerfile-parse/prewarm-ports.mjs +59 -0
- package/dist/dockerfile-parse/runtime-cmd.d.mts +11 -0
- package/dist/dockerfile-parse/runtime-cmd.mjs +142 -0
- package/dist/dockerfile-parse/stages.d.mts +7 -0
- package/dist/dockerfile-parse/stages.mjs +18 -0
- package/dist/gha-pr-checkpoint/index.d.mts +58 -0
- package/dist/gha-pr-checkpoint/index.mjs +100 -0
- package/dist/gha-rate-limit/index.d.mts +15 -0
- package/dist/gha-rate-limit/index.mjs +69 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +7 -0
- package/dist/pnpm-deploy/index.d.mts +6 -0
- package/dist/pnpm-deploy/index.mjs +3 -0
- package/dist/pnpm-deploy/normalize.d.mts +15 -0
- package/dist/pnpm-deploy/normalize.mjs +64 -0
- package/dist/pnpm-deploy/prune.d.mts +14 -0
- package/dist/pnpm-deploy/prune.mjs +48 -0
- package/dist/pnpm-deploy/restore.d.mts +31 -0
- package/dist/pnpm-deploy/restore.mjs +109 -0
- package/dist/scc-complexity/index.d.mts +19 -0
- package/dist/scc-complexity/index.mjs +91 -0
- package/dist/workspace-gates/dependabot.d.mts +3 -0
- package/dist/workspace-gates/dependabot.mjs +26 -0
- package/dist/workspace-gates/exclude-registry.d.mts +3 -0
- package/dist/workspace-gates/exclude-registry.mjs +30 -0
- package/dist/workspace-gates/first-party-graph.d.mts +4 -0
- package/dist/workspace-gates/first-party-graph.mjs +87 -0
- package/dist/workspace-gates/index.d.mts +2 -0
- package/dist/workspace-gates/index.mjs +1 -0
- package/dist/workspace-gates/options.d.mts +27 -0
- package/dist/workspace-gates/options.mjs +15 -0
- package/dist/workspace-gates/policy.d.mts +5 -0
- package/dist/workspace-gates/policy.mjs +27 -0
- package/dist/workspace-gates/test-helpers.d.mts +21 -0
- package/dist/workspace-gates/test-helpers.mjs +69 -0
- package/dist/workspace-gates/yaml-loader.d.mts +2 -0
- package/dist/workspace-gates/yaml-loader.mjs +28 -0
- package/dist/workspace-glob.d.mts +1 -0
- package/dist/workspace-glob.mjs +46 -0
- package/package.json +39 -2
- package/scripts/gha/check-cache-size.sh +38 -0
- package/scripts/gha/clean-workspace.sh +227 -0
- package/scripts/gha/install-github-release.sh +136 -0
- package/scripts/gha/load-runner-env.sh +75 -0
- package/scripts/gha/make-shard-matrix.sh +30 -0
package/README.md
CHANGED
|
@@ -28,6 +28,11 @@ vouchington gha-artifacts-cleanup run --run-id 123 --keep-pattern 'plan-*' --del
|
|
|
28
28
|
vouchington http-origin --field cdn_origin https://images.example.com
|
|
29
29
|
vouchington vitest-blob-manifest <suite> [reports-directory]
|
|
30
30
|
vouchington pnpm-install --runner-lifecycle persistent --install-scripts true
|
|
31
|
+
vouchington check-cache-size /tmp/cache 1048576 node-modules
|
|
32
|
+
vouchington make-shard-matrix 4
|
|
33
|
+
vouchington load-runner-env
|
|
34
|
+
vouchington clean-workspace
|
|
35
|
+
vouchington install-github-release --repo lycheeverse/lychee --version 0.24.2 --asset 'lychee-{platform}.tar.gz' --bin lychee
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
Host-lock environment:
|
|
@@ -80,6 +85,13 @@ import { runAstGrepRule } from 'vouchington-tooling/ast-grep-rule'
|
|
|
80
85
|
import { parseReviewPayload, remapReviewComments } from 'vouchington-tooling/gha-review-payload'
|
|
81
86
|
import { nextPageUrlFromLinkHeader } from 'vouchington-tooling/http-link-pagination'
|
|
82
87
|
import { cmdUpload, mintPresignedControl } from 'vouchington-tooling/coverage-transport'
|
|
88
|
+
import { pruneDeployedRuntimeDeps } from 'vouchington-tooling/pnpm-deploy'
|
|
89
|
+
import { parseDockerfileRuntimeImages } from 'vouchington-tooling/dockerfile-parse'
|
|
90
|
+
import { checkSccComplexity } from 'vouchington-tooling/scc-complexity'
|
|
91
|
+
import { runCiLocal } from 'vouchington-tooling/ci-local'
|
|
92
|
+
import { rateLimitDelay } from 'vouchington-tooling/gha-rate-limit'
|
|
93
|
+
import { parseCheckpoint } from 'vouchington-tooling/gha-pr-checkpoint'
|
|
94
|
+
import { checkWorkspaceGatesPolicy } from 'vouchington-tooling/workspace-gates'
|
|
83
95
|
```
|
|
84
96
|
|
|
85
97
|
The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CiLocalTarget, RunCiLocalOptions } from './types.mts';
|
|
2
|
+
export type { CiLocalCommand, CiLocalSpawn, CiLocalSpawnOptions, CiLocalSpawnResult, CiLocalTarget, LineWriter, ParsedCiLocalArgs, RunCiLocalOptions, } from './types.mts';
|
|
3
|
+
export { parseCiLocalArgs } from './parse.mts';
|
|
4
|
+
export declare function assertWorkflowCommandDrift(targets: Record<string, CiLocalTarget>, rootDir?: string): void;
|
|
5
|
+
export declare function runCiLocal({ args, targets, cwd, spawn, stdout, stderr, usage, }: RunCiLocalOptions): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseCiLocalArgs } from './parse.mjs';
|
|
5
|
+
export { parseCiLocalArgs } from './parse.mjs';
|
|
6
|
+
const DEFAULT_USAGE = 'Usage: ci-local [--list] | <target> [--dry-run]';
|
|
7
|
+
export function assertWorkflowCommandDrift(targets, rootDir = process.cwd()) {
|
|
8
|
+
for (const target of Object.values(targets)) {
|
|
9
|
+
for (const command of target.commands) {
|
|
10
|
+
if (!command.source)
|
|
11
|
+
continue;
|
|
12
|
+
const { workflow: sourceWorkflow, contains } = command.source;
|
|
13
|
+
const workflow = readFileSync(path.join(rootDir, sourceWorkflow), 'utf8');
|
|
14
|
+
const expectedCommands = Array.isArray(contains) ? contains : [contains];
|
|
15
|
+
if (expectedCommands.every((expectedCommand) => workflow.includes(expectedCommand)))
|
|
16
|
+
continue;
|
|
17
|
+
throw new Error(`ci-local drift: ${command.description} command was not found in ${command.source.workflow}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function defaultSpawn(command, args, options) {
|
|
22
|
+
const result = spawnSync(command, [...args], options);
|
|
23
|
+
return { error: result.error, status: result.status };
|
|
24
|
+
}
|
|
25
|
+
function writeLine(stream, line) {
|
|
26
|
+
stream.write(`${line}\n`);
|
|
27
|
+
}
|
|
28
|
+
function runCommand(command, dryRun, cwd, spawn, stdout) {
|
|
29
|
+
const envPreview = Object.keys(command.env ?? {})
|
|
30
|
+
.filter((key) => command.env?.[key] !== undefined)
|
|
31
|
+
.join(',');
|
|
32
|
+
const renderedCommand = envPreview
|
|
33
|
+
? `env ${envPreview} bash -c ${JSON.stringify(command.command)}`
|
|
34
|
+
: command.command;
|
|
35
|
+
writeLine(stdout, `\n$ ${renderedCommand}`);
|
|
36
|
+
if (dryRun)
|
|
37
|
+
return 0;
|
|
38
|
+
const result = spawn('/bin/bash', ['-c', command.command], {
|
|
39
|
+
cwd,
|
|
40
|
+
env: {
|
|
41
|
+
...process.env,
|
|
42
|
+
...command.env,
|
|
43
|
+
},
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
if (result.error)
|
|
47
|
+
throw result.error;
|
|
48
|
+
return result.status ?? 1;
|
|
49
|
+
}
|
|
50
|
+
export function runCiLocal({ args, targets, cwd = process.cwd(), spawn = defaultSpawn, stdout = process.stdout, stderr = process.stderr, usage = DEFAULT_USAGE, }) {
|
|
51
|
+
try {
|
|
52
|
+
const targetNames = Object.keys(targets);
|
|
53
|
+
const parsed = parseCiLocalArgs(args, targetNames);
|
|
54
|
+
if (parsed.help) {
|
|
55
|
+
writeLine(stdout, usage);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (parsed.list || !parsed.target) {
|
|
59
|
+
for (const [name, target] of Object.entries(targets)) {
|
|
60
|
+
writeLine(stdout, `${name} - ${target.description}`);
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const target = targets[parsed.target];
|
|
65
|
+
assertWorkflowCommandDrift({ [parsed.target]: target }, cwd);
|
|
66
|
+
writeLine(stdout, `${parsed.target}: ${target.description}`);
|
|
67
|
+
for (const command of target.commands) {
|
|
68
|
+
const status = runCommand(command, parsed.dryRun, cwd, spawn, stdout);
|
|
69
|
+
if (status !== 0)
|
|
70
|
+
return status;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
stderr.write(`${message}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as nodeUtil from 'node:util';
|
|
2
|
+
export function parseCiLocalArgs(args, targetNames, parseArgs = nodeUtil.parseArgs) {
|
|
3
|
+
const helpArgs = args.filter((arg) => arg === '-h' || arg === '--help');
|
|
4
|
+
const help = helpArgs.length === 1 && args.length === 1;
|
|
5
|
+
if (helpArgs.length > 0 && !help)
|
|
6
|
+
throw new Error('help must be used by itself');
|
|
7
|
+
if (args.filter((arg) => arg === '--dry-run').length > 1 ||
|
|
8
|
+
args.filter((arg) => arg === '--list').length > 1) {
|
|
9
|
+
throw new Error('Options may only be specified once.');
|
|
10
|
+
}
|
|
11
|
+
let dryRun;
|
|
12
|
+
let list;
|
|
13
|
+
let positionals;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = parseArgs({
|
|
16
|
+
args: [...args],
|
|
17
|
+
options: {
|
|
18
|
+
'dry-run': { type: 'boolean' },
|
|
19
|
+
help: { short: 'h', type: 'boolean' },
|
|
20
|
+
list: { type: 'boolean' },
|
|
21
|
+
},
|
|
22
|
+
strict: true,
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
});
|
|
25
|
+
dryRun = parsed.values['dry-run'] ?? false;
|
|
26
|
+
list = parsed.values.list ?? false;
|
|
27
|
+
positionals = parsed.positionals;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(error instanceof Error ? error.message : String(error), { cause: error });
|
|
31
|
+
}
|
|
32
|
+
if (positionals.length > 1)
|
|
33
|
+
throw new Error('Only one target may be specified.');
|
|
34
|
+
if (list && (dryRun || positionals.length > 0))
|
|
35
|
+
throw new Error('--list must be used by itself.');
|
|
36
|
+
const target = positionals[0];
|
|
37
|
+
if (dryRun && !target)
|
|
38
|
+
throw new Error('--dry-run requires a target.');
|
|
39
|
+
if (target && !targetNames.includes(target)) {
|
|
40
|
+
throw new Error(`Unknown target "${target}". Valid targets: ${targetNames.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
return target ? { dryRun, help, list, target } : { dryRun, help, list };
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface CiLocalCommand {
|
|
2
|
+
command: string;
|
|
3
|
+
description: string;
|
|
4
|
+
env?: Record<string, string | undefined>;
|
|
5
|
+
source?: {
|
|
6
|
+
workflow: string;
|
|
7
|
+
contains: string | string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface CiLocalTarget {
|
|
11
|
+
description: string;
|
|
12
|
+
commands: CiLocalCommand[];
|
|
13
|
+
}
|
|
14
|
+
export interface ParsedCiLocalArgs {
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
help: boolean;
|
|
17
|
+
list: boolean;
|
|
18
|
+
target?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CiLocalSpawnResult {
|
|
21
|
+
error?: Error | undefined;
|
|
22
|
+
status: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface CiLocalSpawnOptions {
|
|
25
|
+
cwd: string;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
stdio: 'inherit';
|
|
28
|
+
}
|
|
29
|
+
export type CiLocalSpawn = (command: string, args: readonly string[], options: CiLocalSpawnOptions) => CiLocalSpawnResult;
|
|
30
|
+
export interface LineWriter {
|
|
31
|
+
write(chunk: string): unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface RunCiLocalOptions {
|
|
34
|
+
args: readonly string[];
|
|
35
|
+
targets: Record<string, CiLocalTarget>;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
spawn?: CiLocalSpawn;
|
|
38
|
+
stdout?: LineWriter;
|
|
39
|
+
stderr?: LineWriter;
|
|
40
|
+
usage?: string;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli/index.mjs
CHANGED
|
@@ -34,6 +34,11 @@ const SCRIPT_PATHS = {
|
|
|
34
34
|
path: 'scripts/gha/diagnose-port-collision.sh',
|
|
35
35
|
},
|
|
36
36
|
'prepare-trivy-db': { command: 'bash', path: 'scripts/gha/prepare-trivy-db.sh' },
|
|
37
|
+
'check-cache-size': { command: 'bash', path: 'scripts/gha/check-cache-size.sh' },
|
|
38
|
+
'make-shard-matrix': { command: 'bash', path: 'scripts/gha/make-shard-matrix.sh' },
|
|
39
|
+
'load-runner-env': { command: 'bash', path: 'scripts/gha/load-runner-env.sh' },
|
|
40
|
+
'clean-workspace': { command: 'bash', path: 'scripts/gha/clean-workspace.sh' },
|
|
41
|
+
'install-github-release': { command: 'bash', path: 'scripts/gha/install-github-release.sh' },
|
|
37
42
|
};
|
|
38
43
|
export function runCli(argv = process.argv) {
|
|
39
44
|
const parsed = parseCli(argv);
|
package/dist/cli/parse.d.mts
CHANGED
|
@@ -29,5 +29,5 @@ export type ParsedCli = {
|
|
|
29
29
|
field: string;
|
|
30
30
|
value: string;
|
|
31
31
|
} | ParsedGhaRuntimeAudit | ParsedGhaArtifactsCleanup;
|
|
32
|
-
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db';
|
|
32
|
+
export type ScriptCommand = 'gha-output' | 'gha-needs-results' | 'download-with-diagnostics' | 'host-pressure-diagnostics' | 'allocate-browser-safe-ports' | 'diagnose-port-collision' | 'prepare-trivy-db' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release';
|
|
33
33
|
export declare function parseCli(argv: readonly string[]): ParsedCli;
|
package/dist/cli/parse.mjs
CHANGED
|
@@ -8,6 +8,11 @@ const SCRIPT_COMMANDS = new Set([
|
|
|
8
8
|
'allocate-browser-safe-ports',
|
|
9
9
|
'diagnose-port-collision',
|
|
10
10
|
'prepare-trivy-db',
|
|
11
|
+
'check-cache-size',
|
|
12
|
+
'make-shard-matrix',
|
|
13
|
+
'load-runner-env',
|
|
14
|
+
'clean-workspace',
|
|
15
|
+
'install-github-release',
|
|
11
16
|
]);
|
|
12
17
|
export function parseCli(argv) {
|
|
13
18
|
const args = argv.slice(2);
|
package/dist/cli/usage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\n";
|
|
1
|
+
export declare const USAGE = "Usage: vouchington <command> [options]\n\nCommands:\n runner-port-policy Print or validate a runner port policy\n with-host-lock Run a command under a host-wide lock\n gha-runtime-audit Audit successful GitHub Actions job runtimes\n gha-output Write a collision-safe multiline GITHUB_OUTPUT record\n gha-needs-results Fail if required GitHub Actions job results failed\n download-with-diagnostics Download a URL and report HTTP status on failure\n host-pressure-diagnostics Print a bounded host memory/OOM/PSI snapshot\n allocate-browser-safe-ports Allocate Fetch-safe localhost ports\n diagnose-port-collision Capture bounded localhost port diagnostics\n prepare-trivy-db Download the Trivy vulnerability database\n gha-artifacts-cleanup Delete classified GitHub Actions artifacts\n http-origin Validate an optional HTTP(S) origin\n vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file\n pnpm-install Install a pnpm workspace with retry and release-age fail-fast\n check-cache-size Measure a path and decide whether to save a GHA cache\n make-shard-matrix Emit a [1..N] GitHub Actions shard matrix\n load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards\n clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate\n install-github-release Download a checksum-verified GitHub Release binary\n\nOptions:\n -h, --help Show this help\n -v, --version Print the package version\n\nrunner-port-policy\n (no args) Print the shipped policy as JSON\n --file <path> Validate and print a policy file\n --reserved <port> Print true if the port is reserved\n\nwith-host-lock\n --name <family>\n [--slots <n>]\n --timeout-seconds <n>\n [--command-timeout-seconds <n>]\n [--failure-diagnostics <absolute-script>]\n [--on-acquire-timeout fail|run-unlocked]\n -- <command> [args...]\n\ngha-runtime-audit\n [--repository owner/name] Default GITHUB_REPOSITORY\n [--branch main]\n --pr-workflow <name|/regex/> Repeatable\n --push-workflow <name|/regex/> Repeatable\n\ngha-output <name>\ngha-needs-results [label]\ndownload-with-diagnostics <url> <destination> [-- curl-args...]\nhost-pressure-diagnostics\nallocate-browser-safe-ports [count] [--policy path] [--forbidden-ports path]\ndiagnose-port-collision [--ports \"2200 2216\"] [--output-dir PATH]\nprepare-trivy-db\ngha-artifacts-cleanup run --run-id <id> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\ngha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--delete-pattern glob] [--patterns-file json]\nhttp-origin [--field NAME] [value]\nvitest-blob-manifest <suite> [reports-directory]\npnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false\ncheck-cache-size <path> <max-bytes> <label>\nmake-shard-matrix <total>\nload-runner-env\nclean-workspace\ninstall-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name\n";
|
|
2
2
|
export declare function printUsage(stream?: NodeJS.WritableStream): void;
|
package/dist/cli/usage.mjs
CHANGED
|
@@ -15,6 +15,11 @@ Commands:
|
|
|
15
15
|
http-origin Validate an optional HTTP(S) origin
|
|
16
16
|
vitest-blob-manifest Stamp a vitest-blob-manifest:v1 identity file
|
|
17
17
|
pnpm-install Install a pnpm workspace with retry and release-age fail-fast
|
|
18
|
+
check-cache-size Measure a path and decide whether to save a GHA cache
|
|
19
|
+
make-shard-matrix Emit a [1..N] GitHub Actions shard matrix
|
|
20
|
+
load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards
|
|
21
|
+
clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate
|
|
22
|
+
install-github-release Download a checksum-verified GitHub Release binary
|
|
18
23
|
|
|
19
24
|
Options:
|
|
20
25
|
-h, --help Show this help
|
|
@@ -52,6 +57,11 @@ gha-artifacts-cleanup sweep --older-than-hours <n> [--keep-pattern glob] [--dele
|
|
|
52
57
|
http-origin [--field NAME] [value]
|
|
53
58
|
vitest-blob-manifest <suite> [reports-directory]
|
|
54
59
|
pnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false
|
|
60
|
+
check-cache-size <path> <max-bytes> <label>
|
|
61
|
+
make-shard-matrix <total>
|
|
62
|
+
load-runner-env
|
|
63
|
+
clean-workspace
|
|
64
|
+
install-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name
|
|
55
65
|
`;
|
|
56
66
|
export function printUsage(stream = process.stdout) {
|
|
57
67
|
stream.write(USAGE);
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export declare const CHECKPOINT_MARKER = "pr-checkpoint:v1";
|
|
2
|
+
export type CheckpointStatus = 'queued' | 'running' | 'awaiting_verification' | 'failed' | 'deadline' | 'unresumable' | 'complete';
|
|
3
|
+
export type Checkpoint = {
|
|
4
|
+
marker: string;
|
|
5
|
+
repository: string;
|
|
6
|
+
pr: number;
|
|
7
|
+
/** Absent on checkpoints posted before trigger ids were recorded; keep optional so resume can still parse them. */
|
|
8
|
+
triggerCommentId?: number;
|
|
9
|
+
headRef: string;
|
|
10
|
+
startSha: string;
|
|
11
|
+
sessionStartSha: string;
|
|
12
|
+
runId: string;
|
|
13
|
+
runUrl: string;
|
|
14
|
+
actor: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
/** Absent from v1 checkpoints created before session URLs were exposed. */
|
|
17
|
+
sessionUrl?: string;
|
|
18
|
+
resumeSourceRunId: string;
|
|
19
|
+
status: CheckpointStatus;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
};
|
|
23
|
+
export type GitHubComment = {
|
|
24
|
+
id: number;
|
|
25
|
+
user?: {
|
|
26
|
+
login?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
};
|
|
29
|
+
performed_via_github_app?: {
|
|
30
|
+
slug?: string;
|
|
31
|
+
} | null;
|
|
32
|
+
body?: string;
|
|
33
|
+
created_at?: string;
|
|
34
|
+
};
|
|
35
|
+
export interface CheckpointCodecOptions {
|
|
36
|
+
marker?: string;
|
|
37
|
+
sessionIdPattern?: RegExp;
|
|
38
|
+
}
|
|
39
|
+
export interface TrustedCheckpointActor {
|
|
40
|
+
actor: string;
|
|
41
|
+
userType?: string;
|
|
42
|
+
appSlug?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
|
|
46
|
+
* so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
|
|
47
|
+
* authored by the expected App identity.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isTrustedCheckpointComment(comment: GitHubComment, context: TrustedCheckpointActor): boolean;
|
|
50
|
+
export declare function renderCheckpoint(checkpoint: Checkpoint, options?: {
|
|
51
|
+
marker?: string;
|
|
52
|
+
}): string;
|
|
53
|
+
export declare function parseCheckpoint(body: string, options?: CheckpointCodecOptions): Checkpoint | undefined;
|
|
54
|
+
export declare function validateCheckpoint(value: unknown, options?: CheckpointCodecOptions): Checkpoint | undefined;
|
|
55
|
+
export declare function sortedCheckpointCandidates(comments: GitHubComment[], options?: CheckpointCodecOptions): {
|
|
56
|
+
comment: GitHubComment;
|
|
57
|
+
checkpoint: Checkpoint;
|
|
58
|
+
}[];
|