vouchington-tooling 0.0.15 → 0.0.17

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 (84) hide show
  1. package/README.md +24 -0
  2. package/dist/ci-local/index.d.mts +5 -0
  3. package/dist/ci-local/index.mjs +79 -0
  4. package/dist/ci-local/parse.d.mts +3 -0
  5. package/dist/ci-local/parse.mjs +43 -0
  6. package/dist/ci-local/types.d.mts +41 -0
  7. package/dist/ci-local/types.mjs +1 -0
  8. package/dist/cli/commands/nuget-central-version.d.mts +1 -0
  9. package/dist/cli/commands/nuget-central-version.mjs +11 -0
  10. package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
  11. package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
  12. package/dist/cli/index.mjs +20 -0
  13. package/dist/cli/parse.d.mts +7 -1
  14. package/dist/cli/parse.mjs +15 -0
  15. package/dist/cli/usage.d.mts +1 -1
  16. package/dist/cli/usage.mjs +26 -0
  17. package/dist/dockerfile-parse/index.d.mts +4 -0
  18. package/dist/dockerfile-parse/index.mjs +2 -0
  19. package/dist/dockerfile-parse/prewarm-ports.d.mts +12 -0
  20. package/dist/dockerfile-parse/prewarm-ports.mjs +59 -0
  21. package/dist/dockerfile-parse/runtime-cmd.d.mts +11 -0
  22. package/dist/dockerfile-parse/runtime-cmd.mjs +142 -0
  23. package/dist/dockerfile-parse/stages.d.mts +7 -0
  24. package/dist/dockerfile-parse/stages.mjs +18 -0
  25. package/dist/gha-pr-checkpoint/index.d.mts +58 -0
  26. package/dist/gha-pr-checkpoint/index.mjs +100 -0
  27. package/dist/gha-rate-limit/index.d.mts +15 -0
  28. package/dist/gha-rate-limit/index.mjs +69 -0
  29. package/dist/index.d.mts +18 -0
  30. package/dist/index.mjs +11 -0
  31. package/dist/nuget-central-version/cli.d.mts +1 -0
  32. package/dist/nuget-central-version/cli.mjs +11 -0
  33. package/dist/nuget-central-version/index.d.mts +1 -0
  34. package/dist/nuget-central-version/index.mjs +74 -0
  35. package/dist/pnpm-deploy/index.d.mts +6 -0
  36. package/dist/pnpm-deploy/index.mjs +3 -0
  37. package/dist/pnpm-deploy/normalize.d.mts +15 -0
  38. package/dist/pnpm-deploy/normalize.mjs +64 -0
  39. package/dist/pnpm-deploy/prune.d.mts +14 -0
  40. package/dist/pnpm-deploy/prune.mjs +48 -0
  41. package/dist/pnpm-deploy/restore.d.mts +31 -0
  42. package/dist/pnpm-deploy/restore.mjs +109 -0
  43. package/dist/scc-complexity/index.d.mts +19 -0
  44. package/dist/scc-complexity/index.mjs +91 -0
  45. package/dist/swift-resolved-pin-delta/index.d.mts +18 -0
  46. package/dist/swift-resolved-pin-delta/index.mjs +63 -0
  47. package/dist/swift-semantic-equal/cli.d.mts +2 -0
  48. package/dist/swift-semantic-equal/cli.mjs +26 -0
  49. package/dist/swift-semantic-equal/index.d.mts +1 -0
  50. package/dist/swift-semantic-equal/index.mjs +1 -0
  51. package/dist/swift-semantic-equal/normalize.d.mts +1 -0
  52. package/dist/swift-semantic-equal/normalize.mjs +108 -0
  53. package/dist/swift-source-offset/index.d.mts +2 -0
  54. package/dist/swift-source-offset/index.mjs +68 -0
  55. package/dist/workspace-gates/dependabot.d.mts +3 -0
  56. package/dist/workspace-gates/dependabot.mjs +26 -0
  57. package/dist/workspace-gates/exclude-registry.d.mts +3 -0
  58. package/dist/workspace-gates/exclude-registry.mjs +30 -0
  59. package/dist/workspace-gates/first-party-graph.d.mts +4 -0
  60. package/dist/workspace-gates/first-party-graph.mjs +87 -0
  61. package/dist/workspace-gates/index.d.mts +2 -0
  62. package/dist/workspace-gates/index.mjs +1 -0
  63. package/dist/workspace-gates/options.d.mts +27 -0
  64. package/dist/workspace-gates/options.mjs +15 -0
  65. package/dist/workspace-gates/policy.d.mts +5 -0
  66. package/dist/workspace-gates/policy.mjs +27 -0
  67. package/dist/workspace-gates/test-helpers.d.mts +21 -0
  68. package/dist/workspace-gates/test-helpers.mjs +69 -0
  69. package/dist/workspace-gates/yaml-loader.d.mts +2 -0
  70. package/dist/workspace-gates/yaml-loader.mjs +28 -0
  71. package/dist/workspace-glob.d.mts +1 -0
  72. package/dist/workspace-glob.mjs +46 -0
  73. package/package.json +59 -2
  74. package/scripts/gha/check-cache-size.sh +38 -0
  75. package/scripts/gha/clean-workspace.sh +227 -0
  76. package/scripts/gha/ghcr-package-retention.sh +65 -0
  77. package/scripts/gha/install-github-release.sh +180 -0
  78. package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
  79. package/scripts/gha/lint-links.sh +92 -0
  80. package/scripts/gha/load-runner-env.sh +75 -0
  81. package/scripts/gha/make-shard-matrix.sh +30 -0
  82. package/scripts/gha/materialize-pr-context.sh +123 -0
  83. package/scripts/gha/run-with-timeout.sh +119 -0
  84. package/scripts/gha/wait-for-apt-locks.sh +60 -0
@@ -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
+ }[];
@@ -0,0 +1,100 @@
1
+ export const CHECKPOINT_MARKER = 'pr-checkpoint:v1';
2
+ /**
3
+ * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
4
+ * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
5
+ * authored by the expected App identity.
6
+ */
7
+ export function isTrustedCheckpointComment(comment, context) {
8
+ return (comment.user?.login === context.actor &&
9
+ comment.user?.type === (context.userType ?? 'Bot') &&
10
+ comment.performed_via_github_app?.slug === (context.appSlug ?? 'github-actions'));
11
+ }
12
+ export function renderCheckpoint(checkpoint, options = {}) {
13
+ const marker = options.marker ?? checkpoint.marker;
14
+ const payload = Buffer.from(JSON.stringify({ ...checkpoint, marker }), 'utf8').toString('base64url');
15
+ const session = checkpoint.sessionUrl
16
+ ? `[${checkpoint.sessionId}](${checkpoint.sessionUrl})`
17
+ : `\`${checkpoint.sessionId || 'pending'}\``;
18
+ return [
19
+ `👀 [View the automation workflow run](${checkpoint.runUrl}).`,
20
+ '',
21
+ `Status: \`${checkpoint.status}\` · Session: ${session}`,
22
+ '',
23
+ `<!-- ${marker} ${payload} -->`,
24
+ ].join('\n');
25
+ }
26
+ export function parseCheckpoint(body, options = {}) {
27
+ const marker = options.marker ?? CHECKPOINT_MARKER;
28
+ const match = body.match(new RegExp(`<!-- ${escapeRegExp(marker)} ([A-Za-z0-9_-]+) -->`, 'u'));
29
+ if (!match?.[1])
30
+ return undefined;
31
+ try {
32
+ return validateCheckpoint(JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8')), options);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function validateCheckpoint(value, options = {}) {
39
+ if (!value || typeof value !== 'object')
40
+ return undefined;
41
+ const checkpoint = value;
42
+ const marker = options.marker ?? CHECKPOINT_MARKER;
43
+ if (checkpoint.marker !== marker ||
44
+ !Number.isInteger(checkpoint.pr) ||
45
+ (checkpoint.triggerCommentId !== undefined && !Number.isInteger(checkpoint.triggerCommentId)) ||
46
+ typeof checkpoint.startSha !== 'string' ||
47
+ !/^[0-9a-f]{40}$/u.test(checkpoint.startSha) ||
48
+ typeof checkpoint.sessionStartSha !== 'string' ||
49
+ !/^[0-9a-f]{40}$/u.test(checkpoint.sessionStartSha) ||
50
+ typeof checkpoint.runId !== 'string' ||
51
+ !/^[0-9]+$/u.test(checkpoint.runId) ||
52
+ typeof checkpoint.resumeSourceRunId !== 'string' ||
53
+ (checkpoint.resumeSourceRunId !== '' && !/^[0-9]+$/u.test(checkpoint.resumeSourceRunId)) ||
54
+ typeof checkpoint.repository !== 'string' ||
55
+ typeof checkpoint.headRef !== 'string' ||
56
+ typeof checkpoint.runUrl !== 'string' ||
57
+ typeof checkpoint.actor !== 'string' ||
58
+ typeof checkpoint.sessionId !== 'string' ||
59
+ typeof checkpoint.createdAt !== 'string' ||
60
+ !Number.isFinite(Date.parse(checkpoint.createdAt)) ||
61
+ typeof checkpoint.updatedAt !== 'string' ||
62
+ !Number.isFinite(Date.parse(checkpoint.updatedAt)) ||
63
+ typeof checkpoint.status !== 'string' ||
64
+ ![
65
+ 'queued',
66
+ 'running',
67
+ 'awaiting_verification',
68
+ 'failed',
69
+ 'deadline',
70
+ 'unresumable',
71
+ 'complete',
72
+ ].includes(checkpoint.status)) {
73
+ return undefined;
74
+ }
75
+ if (checkpoint.sessionId &&
76
+ options.sessionIdPattern &&
77
+ !matchesPattern(options.sessionIdPattern, checkpoint.sessionId)) {
78
+ return undefined;
79
+ }
80
+ if (checkpoint.sessionUrl !== undefined &&
81
+ (!URL.canParse(checkpoint.sessionUrl) || !checkpoint.sessionUrl.startsWith('https:'))) {
82
+ return undefined;
83
+ }
84
+ return checkpoint;
85
+ }
86
+ export function sortedCheckpointCandidates(comments, options = {}) {
87
+ return comments
88
+ .map((comment) => ({ comment, checkpoint: parseCheckpoint(comment.body ?? '', options) }))
89
+ .filter((candidate) => candidate.checkpoint !== undefined)
90
+ .toSorted((left, right) => {
91
+ const timestampOrder = String(right.comment.created_at ?? '').localeCompare(String(left.comment.created_at ?? ''));
92
+ return timestampOrder || right.comment.id - left.comment.id;
93
+ });
94
+ }
95
+ function matchesPattern(pattern, value) {
96
+ return new RegExp(pattern.source, pattern.flags.replaceAll(/[gy]/g, '')).test(value);
97
+ }
98
+ function escapeRegExp(value) {
99
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ }
@@ -0,0 +1,15 @@
1
+ export declare const MAX_RATE_LIMIT_WAIT_MS = 120000;
2
+ export declare class GitHubRateLimitError extends Error {
3
+ readonly code: string;
4
+ readonly httpStatus?: number | undefined;
5
+ readonly retryAfter?: string | undefined;
6
+ readonly rateLimitRemaining?: string | undefined;
7
+ readonly rateLimitReset?: string | undefined;
8
+ constructor(code: string, message: string, httpStatus?: number, retryAfter?: string, rateLimitRemaining?: string, rateLimitReset?: string);
9
+ }
10
+ export declare function isRetryableCancellationError(error: unknown): boolean;
11
+ export declare function rateLimitDelay(error: GitHubRateLimitError): number;
12
+ export declare function isRateLimited(error: unknown): error is GitHubRateLimitError;
13
+ export declare function reserveRateLimitDelay(used: {
14
+ milliseconds: number;
15
+ }, delay: number): void;
@@ -0,0 +1,69 @@
1
+ const MAX_DELAY_MS = 60_000;
2
+ export const MAX_RATE_LIMIT_WAIT_MS = 120_000;
3
+ export class GitHubRateLimitError extends Error {
4
+ code;
5
+ httpStatus;
6
+ retryAfter;
7
+ rateLimitRemaining;
8
+ rateLimitReset;
9
+ constructor(code, message, httpStatus, retryAfter, rateLimitRemaining, rateLimitReset) {
10
+ super(message);
11
+ this.code = code;
12
+ this.httpStatus = httpStatus;
13
+ this.retryAfter = retryAfter;
14
+ this.rateLimitRemaining = rateLimitRemaining;
15
+ this.rateLimitReset = rateLimitReset;
16
+ this.name = 'GitHubRateLimitError';
17
+ }
18
+ }
19
+ function invalid() {
20
+ throw new GitHubRateLimitError('INVALID_RESPONSE', 'GitHub rate-limit response is invalid');
21
+ }
22
+ function rateLimit403(error) {
23
+ return (error.httpStatus === 403 && (error.retryAfter !== undefined || error.rateLimitRemaining === '0'));
24
+ }
25
+ export function isRetryableCancellationError(error) {
26
+ return (error instanceof GitHubRateLimitError &&
27
+ (error.code === 'REQUEST_FAILED' ||
28
+ (error.code === 'HTTP_STATUS' &&
29
+ (error.httpStatus === 429 ||
30
+ rateLimit403(error) ||
31
+ ((error.httpStatus ?? 0) >= 500 && (error.httpStatus ?? 0) <= 599)))));
32
+ }
33
+ export function rateLimitDelay(error) {
34
+ if (error.retryAfter !== undefined) {
35
+ if (!/^\d+$/u.test(error.retryAfter))
36
+ invalid();
37
+ const milliseconds = Number(error.retryAfter) * 1000;
38
+ if (!Number.isSafeInteger(milliseconds) || milliseconds > MAX_DELAY_MS)
39
+ invalid();
40
+ return milliseconds;
41
+ }
42
+ if (error.rateLimitRemaining !== undefined && !/^\d+$/u.test(error.rateLimitRemaining))
43
+ invalid();
44
+ if (error.rateLimitRemaining !== '0') {
45
+ if (error.httpStatus !== 429)
46
+ invalid();
47
+ return MAX_DELAY_MS;
48
+ }
49
+ if (!error.rateLimitReset || !/^\d+$/u.test(error.rateLimitReset))
50
+ invalid();
51
+ const reset = Number(error.rateLimitReset);
52
+ if (!Number.isSafeInteger(reset))
53
+ invalid();
54
+ const milliseconds = Math.max(0, reset * 1000 - Date.now());
55
+ if (milliseconds > MAX_DELAY_MS)
56
+ invalid();
57
+ return milliseconds;
58
+ }
59
+ export function isRateLimited(error) {
60
+ return (error instanceof GitHubRateLimitError &&
61
+ error.code === 'HTTP_STATUS' &&
62
+ (error.httpStatus === 429 || rateLimit403(error)));
63
+ }
64
+ export function reserveRateLimitDelay(used, delay) {
65
+ if (used.milliseconds + delay > MAX_RATE_LIMIT_WAIT_MS) {
66
+ throw new GitHubRateLimitError('RATE_LIMIT_WAIT_EXHAUSTED', 'GitHub rate-limit wait budget is exhausted');
67
+ }
68
+ used.milliseconds += delay;
69
+ }
package/dist/index.d.mts CHANGED
@@ -36,3 +36,21 @@ export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, R
36
36
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
37
37
  export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mts';
38
38
  export type { ExpectedTransportIdentity, ObjectSigner, PresignIdentity, TransportControl, } from './coverage-transport/index.mts';
39
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mts';
40
+ export type { NormalizeDeployedLayerResult, PruneResult, RestoreWorkspacePackagesOptions, } from './pnpm-deploy/index.mts';
41
+ export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mts';
42
+ export type { DockerfilePrewarmStage, DockerfileRuntimeImage, ParseDockerfilePrewarmOptions, ParseDockerfileRuntimeImagesOptions, } from './dockerfile-parse/index.mts';
43
+ export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mts';
44
+ export type { SccComplexityOptions, SccComplexityViolation } from './scc-complexity/index.mts';
45
+ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mts';
46
+ export type { CiLocalCommand, CiLocalSpawn, CiLocalSpawnOptions, CiLocalSpawnResult, CiLocalTarget, RunCiLocalOptions, } from './ci-local/index.mts';
47
+ export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mts';
48
+ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mts';
49
+ export type { Checkpoint, CheckpointCodecOptions, GitHubComment, } from './gha-pr-checkpoint/index.mts';
50
+ export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mts';
51
+ export type { WorkspaceGatesOptions } from './workspace-gates/index.mts';
52
+ export { validateNugetUpdate } from './nuget-central-version/index.mts';
53
+ export { normalizeSwiftSource } from './swift-semantic-equal/index.mts';
54
+ export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mts';
55
+ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mts';
56
+ export type { ResolvedDocument, ResolvedPin, ValidateResolvedPinDeltaOptions, } from './swift-resolved-pin-delta/index.mts';
package/dist/index.mjs CHANGED
@@ -21,3 +21,14 @@ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mjs'
21
21
  export { bodyOnlyReviewFallback, indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mjs';
22
22
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
23
23
  export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
24
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
25
+ export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mjs';
26
+ export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mjs';
27
+ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mjs';
28
+ export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
29
+ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
30
+ export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mjs';
31
+ export { validateNugetUpdate } from './nuget-central-version/index.mjs';
32
+ export { normalizeSwiftSource } from './swift-semantic-equal/index.mjs';
33
+ export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mjs';
34
+ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mjs';
@@ -0,0 +1 @@
1
+ export declare function runNugetCentralVersionCli(args: readonly string[]): void;
@@ -0,0 +1,11 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { validateNugetUpdate } from './index.mjs';
3
+ export function runNugetCentralVersionCli(args) {
4
+ const [trustedPath, candidatePath, metadataPath, outputPath] = args;
5
+ if (!trustedPath || !candidatePath || !metadataPath || !outputPath || args.length !== 4) {
6
+ throw new Error('Usage: vouchington nuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>');
7
+ }
8
+ const candidateSource = readFileSync(candidatePath, 'utf8');
9
+ validateNugetUpdate(readFileSync(trustedPath, 'utf8'), candidateSource, readFileSync(metadataPath, 'utf8'));
10
+ writeFileSync(outputPath, candidateSource);
11
+ }
@@ -0,0 +1 @@
1
+ export declare function validateNugetUpdate(trustedSource: string, candidateSource: string, metadataSource: string): string[];
@@ -0,0 +1,74 @@
1
+ const packageVersionPattern = /<PackageVersion\s+Include="(?<name>[^"]+)"\s+Version="(?<version>[^"]+)"\s*\/>/g;
2
+ const literalNugetVersionPattern = /^\d+(?:\.\d+){1,3}(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/;
3
+ function parseVersions(source) {
4
+ const versions = new Map();
5
+ for (const match of source.matchAll(packageVersionPattern)) {
6
+ const name = match.groups?.name;
7
+ const version = match.groups?.version;
8
+ if (!name || !version || versions.has(name)) {
9
+ throw new Error('Directory.Packages.props must contain unique PackageVersion entries');
10
+ }
11
+ versions.set(name, { literal: match[0], version });
12
+ }
13
+ if (versions.size === 0)
14
+ throw new Error('Directory.Packages.props has no PackageVersion entries');
15
+ return versions;
16
+ }
17
+ export function validateNugetUpdate(trustedSource, candidateSource, metadataSource) {
18
+ const trusted = parseVersions(trustedSource);
19
+ const candidate = parseVersions(candidateSource);
20
+ if (candidate.size !== trusted.size) {
21
+ throw new Error('Dependabot may not add or remove central package declarations');
22
+ }
23
+ const changed = [];
24
+ let expected = trustedSource;
25
+ for (const [name, trustedEntry] of trusted) {
26
+ const candidateEntry = candidate.get(name);
27
+ if (!candidateEntry)
28
+ throw new Error(`Dependabot removed central package ${name}`);
29
+ if (candidateEntry.version === trustedEntry.version)
30
+ continue;
31
+ if (!literalNugetVersionPattern.test(trustedEntry.version)) {
32
+ throw new Error(`Dependabot may not change MSBuild-managed version for ${name}`);
33
+ }
34
+ if (!literalNugetVersionPattern.test(candidateEntry.version)) {
35
+ throw new Error(`Dependabot proposed a non-literal NuGet version for ${name}`);
36
+ }
37
+ changed.push(name);
38
+ expected = expected.replace(trustedEntry.literal, trustedEntry.literal.replace(`Version="${trustedEntry.version}"`, `Version="${candidateEntry.version}"`));
39
+ }
40
+ if (changed.length === 0)
41
+ throw new Error('Dependabot NuGet update changed no package versions');
42
+ if (candidateSource !== expected) {
43
+ throw new Error('Dependabot may change only literal central PackageVersion values');
44
+ }
45
+ const metadata = JSON.parse(metadataSource);
46
+ if (!Array.isArray(metadata))
47
+ throw new Error('Dependabot NuGet metadata must be an array');
48
+ const metadataByName = new Map();
49
+ for (const update of metadata) {
50
+ const name = update.dependencyName?.toLowerCase();
51
+ if (!name || !update.prevVersion || !update.newVersion || metadataByName.has(name)) {
52
+ throw new Error('Dependabot NuGet metadata must identify every changed package exactly once');
53
+ }
54
+ metadataByName.set(name, update);
55
+ }
56
+ const metadataNames = [...metadataByName.keys()].toSorted();
57
+ const changedNames = changed.map((name) => name.toLowerCase()).toSorted();
58
+ if (metadataNames.length !== changedNames.length ||
59
+ new Set(metadataNames).size !== metadataNames.length) {
60
+ throw new Error('Dependabot NuGet metadata must identify every changed package exactly once');
61
+ }
62
+ if (!metadataNames.every((name, index) => name === changedNames[index])) {
63
+ throw new Error('Dependabot NuGet metadata does not match the changed central packages');
64
+ }
65
+ for (const name of changed) {
66
+ const update = metadataByName.get(name.toLowerCase());
67
+ const trustedVersion = trusted.get(name)?.version;
68
+ const candidateVersion = candidate.get(name)?.version;
69
+ if (update?.prevVersion !== trustedVersion || update?.newVersion !== candidateVersion) {
70
+ throw new Error(`Dependabot NuGet metadata versions do not match ${name}`);
71
+ }
72
+ }
73
+ return changed.toSorted();
74
+ }
@@ -0,0 +1,6 @@
1
+ export { pruneDeployedRuntimeDeps, runPruneDeployedRuntimeDepsCli } from './prune.mts';
2
+ export type { PruneResult } from './prune.mts';
3
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, runNormalizeDeployedLayerCli, } from './normalize.mts';
4
+ export type { NormalizeDeployedLayerResult } from './normalize.mts';
5
+ export { restoreDeployedWorkspacePackages, runRestoreDeployedWorkspacePackagesCli, } from './restore.mts';
6
+ export type { RestoreWorkspacePackagesOptions } from './restore.mts';
@@ -0,0 +1,3 @@
1
+ export { pruneDeployedRuntimeDeps, runPruneDeployedRuntimeDepsCli } from './prune.mjs';
2
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, runNormalizeDeployedLayerCli, } from './normalize.mjs';
3
+ export { restoreDeployedWorkspacePackages, runRestoreDeployedWorkspacePackagesCli, } from './restore.mjs';
@@ -0,0 +1,15 @@
1
+ export declare const EPOCH_PRUNED_AT = "Thu, 01 Jan 1970 00:00:00 GMT";
2
+ export interface NormalizeDeployedLayerResult {
3
+ prunedAtPinned: boolean;
4
+ entriesTouched: number;
5
+ }
6
+ export declare function normalizeDeployedLayer(prodDir: string): NormalizeDeployedLayerResult;
7
+ interface NormalizeCliOptions {
8
+ args: readonly string[];
9
+ env: NodeJS.ProcessEnv;
10
+ isMain: boolean;
11
+ normalize?: typeof normalizeDeployedLayer;
12
+ stdout: (message: string) => void;
13
+ }
14
+ export declare function runNormalizeDeployedLayerCli({ args, env, isMain, normalize, stdout, }: NormalizeCliOptions): void;
15
+ export {};
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const EPOCH_PRUNED_AT = 'Thu, 01 Jan 1970 00:00:00 GMT';
4
+ export function normalizeDeployedLayer(prodDir) {
5
+ return {
6
+ prunedAtPinned: pinPrunedAt(prodDir),
7
+ entriesTouched: clampTreeMtimes(prodDir),
8
+ };
9
+ }
10
+ function pinPrunedAt(prodDir) {
11
+ const modulesPath = path.join(prodDir, 'node_modules', '.modules.yaml');
12
+ if (!fs.existsSync(modulesPath)) {
13
+ throw new Error(`${modulesPath} is missing; expected pnpm deploy to write it`);
14
+ }
15
+ // pnpm 11.13.1 writeModulesManifest serializes this file with JSON.stringify
16
+ // despite the .yaml name. A pnpm bump that switches to YAML must update this parse.
17
+ const parsed = JSON.parse(fs.readFileSync(modulesPath, 'utf8'));
18
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
19
+ throw new Error(`${modulesPath} is not a JSON object`);
20
+ }
21
+ const manifest = parsed;
22
+ manifest.prunedAt = EPOCH_PRUNED_AT;
23
+ fs.writeFileSync(modulesPath, `${JSON.stringify(manifest, null, 2)}\n`);
24
+ return true;
25
+ }
26
+ function clampTreeMtimes(root) {
27
+ let entriesTouched = 0;
28
+ function visit(entryPath, isDirectory) {
29
+ if (isDirectory) {
30
+ for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
31
+ visit(path.join(entryPath, entry.name), entry.isDirectory());
32
+ }
33
+ }
34
+ else {
35
+ const stats = fs.lstatSync(entryPath);
36
+ if (stats.isFile() && stats.nlink > 1) {
37
+ const copyPath = `${entryPath}.${process.pid}.copy`;
38
+ fs.copyFileSync(entryPath, copyPath);
39
+ fs.renameSync(copyPath, entryPath);
40
+ }
41
+ }
42
+ fs.lutimesSync(entryPath, 0, 0);
43
+ entriesTouched += 1;
44
+ }
45
+ visit(root, fs.lstatSync(root).isDirectory());
46
+ return entriesTouched;
47
+ }
48
+ function resolveCliProdDirs(args, env) {
49
+ if (args.length > 0)
50
+ return [...args];
51
+ const prodDir = env.PROD_DIR;
52
+ if (!prodDir)
53
+ throw new Error('PROD_DIR or a directory argument is required');
54
+ return [prodDir];
55
+ }
56
+ export function runNormalizeDeployedLayerCli({ args, env, isMain, normalize = normalizeDeployedLayer, stdout, }) {
57
+ if (!isMain)
58
+ return;
59
+ for (const prodDir of resolveCliProdDirs(args, env)) {
60
+ const result = normalize(prodDir);
61
+ const prunedAt = result.prunedAtPinned ? 'prunedAt pinned' : 'prunedAt absent';
62
+ stdout(`Normalized deployed layer at ${prodDir} (${prunedAt}, ${result.entriesTouched} entries)`);
63
+ }
64
+ }
@@ -0,0 +1,14 @@
1
+ export interface PruneResult {
2
+ bytesRemoved: number;
3
+ filesRemoved: number;
4
+ }
5
+ export declare function pruneDeployedRuntimeDeps(prodDir: string): PruneResult;
6
+ interface PruneCliOptions {
7
+ args: readonly string[];
8
+ env: NodeJS.ProcessEnv;
9
+ isMain: boolean;
10
+ prune?: typeof pruneDeployedRuntimeDeps;
11
+ stdout: (message: string) => void;
12
+ }
13
+ export declare function runPruneDeployedRuntimeDepsCli({ args, env, isMain, prune, stdout, }: PruneCliOptions): void;
14
+ export {};
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const DECLARATION_SUFFIXES = ['.d.ts', '.d.mts', '.d.cts', '.d.ts.map', '.d.mts.map', '.d.cts.map'];
4
+ function shouldPruneRuntimeDependencyFile(filePath) {
5
+ return (DECLARATION_SUFFIXES.some((suffix) => filePath.endsWith(suffix)) ||
6
+ filePath.endsWith('.tsbuildinfo'));
7
+ }
8
+ function pruneDirectory(dir) {
9
+ const result = { bytesRemoved: 0, filesRemoved: 0 };
10
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11
+ const entryPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ const child = pruneDirectory(entryPath);
14
+ result.bytesRemoved += child.bytesRemoved;
15
+ result.filesRemoved += child.filesRemoved;
16
+ continue;
17
+ }
18
+ if (!entry.isFile() || !shouldPruneRuntimeDependencyFile(entryPath))
19
+ continue;
20
+ const { size } = fs.statSync(entryPath);
21
+ fs.rmSync(entryPath, { force: true });
22
+ result.bytesRemoved += size;
23
+ result.filesRemoved += 1;
24
+ }
25
+ return result;
26
+ }
27
+ export function pruneDeployedRuntimeDeps(prodDir) {
28
+ const pnpmStore = path.join(prodDir, 'node_modules', '.pnpm');
29
+ if (!fs.existsSync(pnpmStore))
30
+ return { bytesRemoved: 0, filesRemoved: 0 };
31
+ return pruneDirectory(pnpmStore);
32
+ }
33
+ function resolveCliProdDirs(args, env) {
34
+ if (args.length > 0)
35
+ return [...args];
36
+ const prodDir = env.PROD_DIR;
37
+ if (!prodDir)
38
+ throw new Error('PROD_DIR or a directory argument is required');
39
+ return [prodDir];
40
+ }
41
+ export function runPruneDeployedRuntimeDepsCli({ args, env, isMain, prune = pruneDeployedRuntimeDeps, stdout, }) {
42
+ if (!isMain)
43
+ return;
44
+ for (const prodDir of resolveCliProdDirs(args, env)) {
45
+ const result = prune(prodDir);
46
+ stdout(`Pruned ${result.filesRemoved} runtime dependency artifact(s) from ${prodDir} (${result.bytesRemoved} bytes)`);
47
+ }
48
+ }
@@ -0,0 +1,31 @@
1
+ export interface RestoreWorkspacePackagesOptions {
2
+ backendDir?: string;
3
+ prodDir: string;
4
+ workspaceRoot?: string;
5
+ }
6
+ /**
7
+ * Node's --experimental-strip-types refuses to load TypeScript whose real path
8
+ * contains a node_modules segment, but pnpm deploy places workspace packages
9
+ * (shipped as TypeScript source) inside the virtual store at
10
+ * node_modules/.pnpm/<store-dir>/node_modules/<name>.
11
+ *
12
+ * Relocate every workspace-package copy outside node_modules while preserving
13
+ * pnpm's resolution topology, without enumerating dependencies:
14
+ *
15
+ * - move the content dir to workspace-packages/<store-dir>/<name>
16
+ * - leave a relative symlink at the original virtual-store location, so every
17
+ * pnpm-created link (top-level node_modules entries, sibling links from other
18
+ * store dirs) keeps resolving through it
19
+ * - add a node_modules backlink inside the moved dir pointing at the package's
20
+ * original virtual-store sibling directory, so the package's own imports
21
+ * resolve exactly as pnpm laid them out
22
+ */
23
+ export declare function restoreDeployedWorkspacePackages({ backendDir, prodDir, workspaceRoot, }: RestoreWorkspacePackagesOptions): void;
24
+ interface RestoreCliOptions {
25
+ args: readonly string[];
26
+ env: NodeJS.ProcessEnv;
27
+ isMain: boolean;
28
+ restore?: typeof restoreDeployedWorkspacePackages;
29
+ }
30
+ export declare function runRestoreDeployedWorkspacePackagesCli({ args, env, isMain, restore, }: RestoreCliOptions): void;
31
+ export {};