vouchington-tooling 0.0.16 → 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 (34) hide show
  1. package/README.md +12 -0
  2. package/dist/cli/commands/nuget-central-version.d.mts +1 -0
  3. package/dist/cli/commands/nuget-central-version.mjs +11 -0
  4. package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
  5. package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
  6. package/dist/cli/index.mjs +15 -0
  7. package/dist/cli/parse.d.mts +7 -1
  8. package/dist/cli/parse.mjs +10 -0
  9. package/dist/cli/usage.d.mts +1 -1
  10. package/dist/cli/usage.mjs +17 -1
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.mjs +4 -0
  13. package/dist/nuget-central-version/cli.d.mts +1 -0
  14. package/dist/nuget-central-version/cli.mjs +11 -0
  15. package/dist/nuget-central-version/index.d.mts +1 -0
  16. package/dist/nuget-central-version/index.mjs +74 -0
  17. package/dist/swift-resolved-pin-delta/index.d.mts +18 -0
  18. package/dist/swift-resolved-pin-delta/index.mjs +63 -0
  19. package/dist/swift-semantic-equal/cli.d.mts +2 -0
  20. package/dist/swift-semantic-equal/cli.mjs +26 -0
  21. package/dist/swift-semantic-equal/index.d.mts +1 -0
  22. package/dist/swift-semantic-equal/index.mjs +1 -0
  23. package/dist/swift-semantic-equal/normalize.d.mts +1 -0
  24. package/dist/swift-semantic-equal/normalize.mjs +108 -0
  25. package/dist/swift-source-offset/index.d.mts +2 -0
  26. package/dist/swift-source-offset/index.mjs +68 -0
  27. package/package.json +21 -1
  28. package/scripts/gha/ghcr-package-retention.sh +65 -0
  29. package/scripts/gha/install-github-release.sh +57 -13
  30. package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
  31. package/scripts/gha/lint-links.sh +92 -0
  32. package/scripts/gha/materialize-pr-context.sh +123 -0
  33. package/scripts/gha/run-with-timeout.sh +119 -0
  34. package/scripts/gha/wait-for-apt-locks.sh +60 -0
package/README.md CHANGED
@@ -33,6 +33,14 @@ vouchington make-shard-matrix 4
33
33
  vouchington load-runner-env
34
34
  vouchington clean-workspace
35
35
  vouchington install-github-release --repo lycheeverse/lychee --version 0.24.2 --asset 'lychee-{platform}.tar.gz' --bin lychee
36
+ vouchington run-with-timeout 120 10 docker push example
37
+ vouchington lint-links --offline
38
+ vouchington materialize-pr-context
39
+ vouchington wait-for-apt-locks
40
+ vouchington install-playwright-chromium-arm64
41
+ vouchington ghcr-package-retention example%2Fapi
42
+ vouchington nuget-central-version trusted.props candidate.props metadata.json out.props
43
+ vouchington swift-semantic-equal BASE HEAD App.swift
36
44
  ```
37
45
 
38
46
  Host-lock environment:
@@ -92,6 +100,10 @@ import { runCiLocal } from 'vouchington-tooling/ci-local'
92
100
  import { rateLimitDelay } from 'vouchington-tooling/gha-rate-limit'
93
101
  import { parseCheckpoint } from 'vouchington-tooling/gha-pr-checkpoint'
94
102
  import { checkWorkspaceGatesPolicy } from 'vouchington-tooling/workspace-gates'
103
+ import { validateNugetUpdate } from 'vouchington-tooling/nuget-central-version'
104
+ import { normalizeSwiftSource } from 'vouchington-tooling/swift-semantic-equal'
105
+ import { parseUniqueSwiftBinaryTargetChecksum } from 'vouchington-tooling/swift-source-offset'
106
+ import { validateResolvedPinDelta } from 'vouchington-tooling/swift-resolved-pin-delta'
95
107
  ```
96
108
 
97
109
  The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
@@ -0,0 +1 @@
1
+ export declare function runNugetCentralVersionCommand(args: readonly string[]): number;
@@ -0,0 +1,11 @@
1
+ import { runNugetCentralVersionCli } from '../../nuget-central-version/cli.mjs';
2
+ export function runNugetCentralVersionCommand(args) {
3
+ try {
4
+ runNugetCentralVersionCli(args);
5
+ return 0;
6
+ }
7
+ catch (error) {
8
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
9
+ return 1;
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export declare function runSwiftSemanticEqualCommand(args: readonly string[]): number;
@@ -0,0 +1,4 @@
1
+ import { runSwiftSemanticEqualCli } from '../../swift-semantic-equal/cli.mjs';
2
+ export function runSwiftSemanticEqualCommand(args) {
3
+ return runSwiftSemanticEqualCli(args);
4
+ }
@@ -9,6 +9,8 @@ import { runHttpOrigin } from './commands/http-origin.mjs';
9
9
  import { runPnpmInstallCli } from './commands/pnpm-install.mjs';
10
10
  import { runRunnerPortPolicy } from './commands/runner-port-policy.mjs';
11
11
  import { runScript } from './commands/spawn-script.mjs';
12
+ import { runNugetCentralVersionCommand } from './commands/nuget-central-version.mjs';
13
+ import { runSwiftSemanticEqualCommand } from './commands/swift-semantic-equal.mjs';
12
14
  import { runVitestBlobManifestCommand } from './commands/vitest-blob-manifest.mjs';
13
15
  import { runWithHostLock } from './commands/with-host-lock.mjs';
14
16
  import { parseCli } from './parse.mjs';
@@ -39,6 +41,15 @@ const SCRIPT_PATHS = {
39
41
  'load-runner-env': { command: 'bash', path: 'scripts/gha/load-runner-env.sh' },
40
42
  'clean-workspace': { command: 'bash', path: 'scripts/gha/clean-workspace.sh' },
41
43
  'install-github-release': { command: 'bash', path: 'scripts/gha/install-github-release.sh' },
44
+ 'run-with-timeout': { command: 'bash', path: 'scripts/gha/run-with-timeout.sh' },
45
+ 'lint-links': { command: 'bash', path: 'scripts/gha/lint-links.sh' },
46
+ 'materialize-pr-context': { command: 'bash', path: 'scripts/gha/materialize-pr-context.sh' },
47
+ 'wait-for-apt-locks': { command: 'bash', path: 'scripts/gha/wait-for-apt-locks.sh' },
48
+ 'install-playwright-chromium-arm64': {
49
+ command: 'bash',
50
+ path: 'scripts/gha/install-playwright-chromium-arm64.sh',
51
+ },
52
+ 'ghcr-package-retention': { command: 'bash', path: 'scripts/gha/ghcr-package-retention.sh' },
42
53
  };
43
54
  export function runCli(argv = process.argv) {
44
55
  const parsed = parseCli(argv);
@@ -67,6 +78,10 @@ export function runCli(argv = process.argv) {
67
78
  return runPnpmInstallCli(parsed.args);
68
79
  case 'vitest-blob-manifest':
69
80
  return runVitestBlobManifestCommand(parsed.args);
81
+ case 'nuget-central-version':
82
+ return runNugetCentralVersionCommand(parsed.args);
83
+ case 'swift-semantic-equal':
84
+ return runSwiftSemanticEqualCommand(parsed.args);
70
85
  case 'http-origin':
71
86
  return runHttpOrigin(parsed.field, parsed.value);
72
87
  case 'gha-artifacts-cleanup':
@@ -24,10 +24,16 @@ export type ParsedCli = {
24
24
  } | {
25
25
  kind: 'vitest-blob-manifest';
26
26
  args: string[];
27
+ } | {
28
+ kind: 'nuget-central-version';
29
+ args: string[];
30
+ } | {
31
+ kind: 'swift-semantic-equal';
32
+ args: string[];
27
33
  } | {
28
34
  kind: 'http-origin';
29
35
  field: string;
30
36
  value: string;
31
37
  } | 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' | 'check-cache-size' | 'make-shard-matrix' | 'load-runner-env' | 'clean-workspace' | 'install-github-release';
38
+ 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' | 'run-with-timeout' | 'lint-links' | 'materialize-pr-context' | 'wait-for-apt-locks' | 'install-playwright-chromium-arm64' | 'ghcr-package-retention';
33
39
  export declare function parseCli(argv: readonly string[]): ParsedCli;
@@ -13,6 +13,12 @@ const SCRIPT_COMMANDS = new Set([
13
13
  'load-runner-env',
14
14
  'clean-workspace',
15
15
  'install-github-release',
16
+ 'run-with-timeout',
17
+ 'lint-links',
18
+ 'materialize-pr-context',
19
+ 'wait-for-apt-locks',
20
+ 'install-playwright-chromium-arm64',
21
+ 'ghcr-package-retention',
16
22
  ]);
17
23
  export function parseCli(argv) {
18
24
  const args = argv.slice(2);
@@ -31,6 +37,10 @@ export function parseCli(argv) {
31
37
  return { kind: 'pnpm-install', args: rest };
32
38
  if (command === 'vitest-blob-manifest')
33
39
  return { kind: 'vitest-blob-manifest', args: rest };
40
+ if (command === 'nuget-central-version')
41
+ return { kind: 'nuget-central-version', args: rest };
42
+ if (command === 'swift-semantic-equal')
43
+ return { kind: 'swift-semantic-equal', args: rest };
34
44
  if (command === 'http-origin')
35
45
  return parseHttpOrigin(rest);
36
46
  if (command === 'gha-artifacts-cleanup')
@@ -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 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";
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 run-with-timeout Run a command with GNU timeout or a Perl fallback\n lint-links Two-pass lychee: internal links fail, external warn\n materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl\n wait-for-apt-locks Wait until apt/dpkg lock files are free\n install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json\n ghcr-package-retention Delete old GHCR package versions past KEEP_MIN\n nuget-central-version Validate a Directory.Packages.props PackageVersion delta\n swift-semantic-equal Compare Swift sources ignoring comments and whitespace\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 [--tag-prefix PREFIX] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]\nrun-with-timeout <timeout-seconds> <kill-after-seconds> <command...>\nlint-links [--offline] [--config PATH] [--glob PATTERN] [files...]\nmaterialize-pr-context\nwait-for-apt-locks\ninstall-playwright-chromium-arm64 [name:archive...]\nghcr-package-retention <url-encoded-package>...\nnuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>\nswift-semantic-equal <base> <head> <file.swift>\n";
2
2
  export declare function printUsage(stream?: NodeJS.WritableStream): void;
@@ -20,6 +20,14 @@ Commands:
20
20
  load-runner-env Overlay a runner env file onto GITHUB_ENV with injection guards
21
21
  clean-workspace Reset a persistent-runner workspace with a fork-PR trust gate
22
22
  install-github-release Download a checksum-verified GitHub Release binary
23
+ run-with-timeout Run a command with GNU timeout or a Perl fallback
24
+ lint-links Two-pass lychee: internal links fail, external warn
25
+ materialize-pr-context Dump PR title/body/files/diff/comments and #N crawl
26
+ wait-for-apt-locks Wait until apt/dpkg lock files are free
27
+ install-playwright-chromium-arm64 Install Playwright Chromium from browsers.json
28
+ ghcr-package-retention Delete old GHCR package versions past KEEP_MIN
29
+ nuget-central-version Validate a Directory.Packages.props PackageVersion delta
30
+ swift-semantic-equal Compare Swift sources ignoring comments and whitespace
23
31
 
24
32
  Options:
25
33
  -h, --help Show this help
@@ -61,7 +69,15 @@ check-cache-size <path> <max-bytes> <label>
61
69
  make-shard-matrix <total>
62
70
  load-runner-env
63
71
  clean-workspace
64
- install-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name
72
+ install-github-release --repo owner/name --version X --asset 'name-{platform}.tar.gz' --bin name [--tag-prefix PREFIX] [--no-checksum] [--checksums-asset NAME] [--version-flag FLAG] [--bin-dir DIR]
73
+ run-with-timeout <timeout-seconds> <kill-after-seconds> <command...>
74
+ lint-links [--offline] [--config PATH] [--glob PATTERN] [files...]
75
+ materialize-pr-context
76
+ wait-for-apt-locks
77
+ install-playwright-chromium-arm64 [name:archive...]
78
+ ghcr-package-retention <url-encoded-package>...
79
+ nuget-central-version <trusted-props> <candidate-props> <metadata-json> <output-props>
80
+ swift-semantic-equal <base> <head> <file.swift>
65
81
  `;
66
82
  export function printUsage(stream = process.stdout) {
67
83
  stream.write(USAGE);
package/dist/index.d.mts CHANGED
@@ -49,3 +49,8 @@ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderC
49
49
  export type { Checkpoint, CheckpointCodecOptions, GitHubComment, } from './gha-pr-checkpoint/index.mts';
50
50
  export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mts';
51
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
@@ -28,3 +28,7 @@ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-l
28
28
  export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
29
29
  export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
30
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,18 @@
1
+ export type ResolvedPin = {
2
+ identity?: string;
3
+ kind?: string;
4
+ location?: string;
5
+ state?: {
6
+ revision?: string;
7
+ version?: string;
8
+ };
9
+ };
10
+ export type ResolvedDocument = {
11
+ originHash?: string;
12
+ pins?: ResolvedPin[];
13
+ version?: number;
14
+ };
15
+ export type ValidateResolvedPinDeltaOptions = {
16
+ requiredIdentity?: string;
17
+ };
18
+ export declare function validateResolvedPinDelta(trustedResolved: ResolvedDocument, candidateResolved: ResolvedDocument, options?: ValidateResolvedPinDeltaOptions): void;
@@ -0,0 +1,63 @@
1
+ function isExactSemver(value) {
2
+ const match = value?.match(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
3
+ return Boolean(match &&
4
+ !match.groups?.prerelease
5
+ ?.split('.')
6
+ .some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier[0] === '0'));
7
+ }
8
+ function exactPins(resolved) {
9
+ if (!Array.isArray(resolved.pins))
10
+ throw new Error('Package.resolved must contain pins');
11
+ const pins = new Map();
12
+ for (const pin of resolved.pins) {
13
+ const state = pin.state;
14
+ if (!pin.identity?.match(/^[a-z0-9][a-z0-9.-]*$/) ||
15
+ pin.kind !== 'remoteSourceControl' ||
16
+ !pin.location?.match(/^https:\/\/[^\s]+$/) ||
17
+ !state?.revision?.match(/^[a-f0-9]{40}$/) ||
18
+ !isExactSemver(state.version) ||
19
+ Object.keys(pin).toSorted().join(',') !== 'identity,kind,location,state' ||
20
+ Object.keys(state).toSorted().join(',') !== 'revision,version') {
21
+ throw new Error('Package.resolved pins must remain fully specified exact pins');
22
+ }
23
+ if (pins.has(pin.identity)) {
24
+ throw new Error('Package.resolved must contain unique pin identities');
25
+ }
26
+ pins.set(pin.identity, pin);
27
+ }
28
+ return pins;
29
+ }
30
+ function canonicalPin(pin) {
31
+ return JSON.stringify({
32
+ identity: pin.identity,
33
+ kind: pin.kind,
34
+ location: pin.location,
35
+ state: { revision: pin.state.revision, version: pin.state.version },
36
+ });
37
+ }
38
+ export function validateResolvedPinDelta(trustedResolved, candidateResolved, options = {}) {
39
+ const requiredIdentity = options.requiredIdentity ?? 'skip';
40
+ if (trustedResolved.version !== 3 ||
41
+ candidateResolved.version !== 3 ||
42
+ !candidateResolved.originHash?.match(/^[a-f0-9]{64}$/)) {
43
+ throw new Error('Package.resolved must remain a version 3 lock with an origin hash');
44
+ }
45
+ const trustedPins = exactPins(trustedResolved);
46
+ const candidatePins = exactPins(candidateResolved);
47
+ const trustedRequired = trustedPins.get(requiredIdentity);
48
+ const candidateRequired = candidatePins.get(requiredIdentity);
49
+ if (!trustedRequired ||
50
+ !candidateRequired ||
51
+ trustedRequired.kind !== candidateRequired.kind ||
52
+ trustedRequired.location !== candidateRequired.location) {
53
+ throw new Error(`Package.resolved must keep the trusted ${requiredIdentity} source`);
54
+ }
55
+ for (const [identity, trustedPin] of trustedPins) {
56
+ if (identity === requiredIdentity)
57
+ continue;
58
+ const candidatePin = candidatePins.get(identity);
59
+ if (!candidatePin || canonicalPin(candidatePin) !== canonicalPin(trustedPin)) {
60
+ throw new Error(`Package.resolved must keep every trusted non-${requiredIdentity} pin unchanged`);
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,2 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ export declare function runSwiftSemanticEqualCli(args: readonly string[], gitShow?: typeof execFileSync): number;
@@ -0,0 +1,26 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { normalizeSwiftSource } from './normalize.mjs';
3
+ export function runSwiftSemanticEqualCli(args, gitShow = execFileSync) {
4
+ const [base, head, file] = args;
5
+ if (!base || !head || !file || args.length !== 3 || !file.endsWith('.swift')) {
6
+ return 1;
7
+ }
8
+ try {
9
+ const baseSource = gitShow('git', ['show', `${base}:${file}`], {
10
+ encoding: 'utf8',
11
+ maxBuffer: 64 * 1024 * 1024,
12
+ stdio: ['ignore', 'pipe', 'ignore'],
13
+ });
14
+ const headSource = gitShow('git', ['show', `${head}:${file}`], {
15
+ encoding: 'utf8',
16
+ maxBuffer: 64 * 1024 * 1024,
17
+ stdio: ['ignore', 'pipe', 'ignore'],
18
+ });
19
+ return normalizeSwiftSource(String(baseSource)) === normalizeSwiftSource(String(headSource))
20
+ ? 0
21
+ : 1;
22
+ }
23
+ catch {
24
+ return 1;
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export { normalizeSwiftSource } from './normalize.mts';
@@ -0,0 +1 @@
1
+ export { normalizeSwiftSource } from './normalize.mjs';
@@ -0,0 +1 @@
1
+ export declare function normalizeSwiftSource(source: string): string;
@@ -0,0 +1,108 @@
1
+ export function normalizeSwiftSource(source) {
2
+ let normalized = '';
3
+ let index = 0;
4
+ while (index < source.length) {
5
+ const char = source[index];
6
+ if (char === undefined || /\s/u.test(char)) {
7
+ index += 1;
8
+ continue;
9
+ }
10
+ if (source.startsWith('//', index)) {
11
+ index = skipLineComment(source, index);
12
+ continue;
13
+ }
14
+ if (source.startsWith('/*', index)) {
15
+ index = skipBlockComment(source, index);
16
+ continue;
17
+ }
18
+ const hashCount = countLeadingHashes(source, index);
19
+ const afterHashes = source[index + hashCount];
20
+ if (afterHashes === '"') {
21
+ const literal = readSwiftStringLiteral(source, index, hashCount);
22
+ if (literal) {
23
+ normalized += literal.text;
24
+ index = literal.end;
25
+ continue;
26
+ }
27
+ }
28
+ if (afterHashes === '/') {
29
+ const literal = readSwiftRegexLiteral(source, index, hashCount, normalized);
30
+ if (literal) {
31
+ normalized += literal.text;
32
+ index = literal.end;
33
+ continue;
34
+ }
35
+ }
36
+ normalized += char;
37
+ index += 1;
38
+ }
39
+ return normalized;
40
+ }
41
+ function countLeadingHashes(source, start) {
42
+ let count = 0;
43
+ while (source[start + count] === '#')
44
+ count += 1;
45
+ return count;
46
+ }
47
+ function skipLineComment(source, start) {
48
+ const end = source.indexOf('\n', start + 2);
49
+ return end === -1 ? source.length : end + 1;
50
+ }
51
+ function skipBlockComment(source, start) {
52
+ let depth = 1;
53
+ let index = start + 2;
54
+ while (index < source.length && depth > 0) {
55
+ if (source.startsWith('/*', index)) {
56
+ depth += 1;
57
+ index += 2;
58
+ }
59
+ else if (source.startsWith('*/', index)) {
60
+ depth -= 1;
61
+ index += 2;
62
+ }
63
+ else {
64
+ index += 1;
65
+ }
66
+ }
67
+ return index;
68
+ }
69
+ function readSwiftStringLiteral(source, start, hashCount) {
70
+ const openingQuote = start + hashCount;
71
+ const isTripleQuoted = source.startsWith('"""', openingQuote);
72
+ const openerLength = hashCount + (isTripleQuoted ? 3 : 1);
73
+ const closer = `${isTripleQuoted ? '"""' : '"'}${'#'.repeat(hashCount)}`;
74
+ let index = start + openerLength;
75
+ while (index < source.length) {
76
+ if (!isTripleQuoted && hashCount === 0 && source[index] === '\\') {
77
+ index += 2;
78
+ continue;
79
+ }
80
+ if (source.startsWith(closer, index)) {
81
+ return { end: index + closer.length, text: source.slice(start, index + closer.length) };
82
+ }
83
+ index += 1;
84
+ }
85
+ return null;
86
+ }
87
+ function readSwiftRegexLiteral(source, start, hashCount, normalizedPrefix) {
88
+ if (hashCount === 0 && !canStartBareRegex(normalizedPrefix))
89
+ return null;
90
+ const closer = `/${'#'.repeat(hashCount)}`;
91
+ let index = start + hashCount + 1;
92
+ while (index < source.length) {
93
+ if (hashCount === 0 && source[index] === '\\') {
94
+ index += 2;
95
+ continue;
96
+ }
97
+ if (source.startsWith(closer, index)) {
98
+ return { end: index + closer.length, text: source.slice(start, index + closer.length) };
99
+ }
100
+ index += 1;
101
+ }
102
+ return null;
103
+ }
104
+ function canStartBareRegex(normalizedPrefix) {
105
+ if (normalizedPrefix.length === 0)
106
+ return true;
107
+ return /[({[=,:;!&|?]/u.test(normalizedPrefix.at(-1));
108
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isSwiftCodeOffset(source: string, offset: number): boolean;
2
+ export declare function parseUniqueSwiftBinaryTargetChecksum(source: string, targetName: string, expectedUrl: string): string | undefined;
@@ -0,0 +1,68 @@
1
+ function escapeRegExp(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+ export function isSwiftCodeOffset(source, offset) {
5
+ let blockCommentDepth = 0;
6
+ let escaped = false;
7
+ let inLineComment = false;
8
+ let inMultilineString = false;
9
+ let inString = false;
10
+ for (let index = 0; index < offset; index += 1) {
11
+ const character = source[index];
12
+ const pair = source.slice(index, index + 2);
13
+ const triple = source.slice(index, index + 3);
14
+ if (inLineComment) {
15
+ if (character === '\n')
16
+ inLineComment = false;
17
+ continue;
18
+ }
19
+ if (blockCommentDepth > 0) {
20
+ if (pair === '/*') {
21
+ blockCommentDepth += 1;
22
+ index += 1;
23
+ }
24
+ else if (pair === '*/') {
25
+ blockCommentDepth -= 1;
26
+ index += 1;
27
+ }
28
+ continue;
29
+ }
30
+ if (inMultilineString) {
31
+ if (triple === '"""') {
32
+ inMultilineString = false;
33
+ index += 2;
34
+ }
35
+ continue;
36
+ }
37
+ if (inString) {
38
+ if (escaped)
39
+ escaped = false;
40
+ else if (character === '\\')
41
+ escaped = true;
42
+ else if (character === '"')
43
+ inString = false;
44
+ continue;
45
+ }
46
+ if (pair === '//') {
47
+ inLineComment = true;
48
+ index += 1;
49
+ }
50
+ else if (pair === '/*') {
51
+ blockCommentDepth = 1;
52
+ index += 1;
53
+ }
54
+ else if (triple === '"""') {
55
+ inMultilineString = true;
56
+ index += 2;
57
+ }
58
+ else if (character === '"') {
59
+ inString = true;
60
+ }
61
+ }
62
+ return !inLineComment && blockCommentDepth === 0 && !inMultilineString && !inString;
63
+ }
64
+ export function parseUniqueSwiftBinaryTargetChecksum(source, targetName, expectedUrl) {
65
+ const expression = new RegExp(`^\\s*package\\.targets\\s*\\+=\\s*\\[\\s*\\.binaryTarget\\(\\s*name:\\s*"${escapeRegExp(targetName)}"\\s*,\\s*url:\\s*"${escapeRegExp(expectedUrl)}"\\s*,\\s*checksum:\\s*"(?<checksum>[a-f0-9]{64})"\\s*\\)\\s*\\]\\s*$`, 'gm');
66
+ const matches = [...source.matchAll(expression)].filter((match) => match.index !== undefined && isSwiftCodeOffset(source, match.index));
67
+ return matches.length === 1 ? matches[0]?.groups?.checksum : undefined;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -170,6 +170,26 @@
170
170
  "import": "./dist/workspace-gates/index.mjs",
171
171
  "default": "./dist/workspace-gates/index.mjs"
172
172
  },
173
+ "./nuget-central-version": {
174
+ "types": "./dist/nuget-central-version/index.d.mts",
175
+ "import": "./dist/nuget-central-version/index.mjs",
176
+ "default": "./dist/nuget-central-version/index.mjs"
177
+ },
178
+ "./swift-semantic-equal": {
179
+ "types": "./dist/swift-semantic-equal/index.d.mts",
180
+ "import": "./dist/swift-semantic-equal/index.mjs",
181
+ "default": "./dist/swift-semantic-equal/index.mjs"
182
+ },
183
+ "./swift-source-offset": {
184
+ "types": "./dist/swift-source-offset/index.d.mts",
185
+ "import": "./dist/swift-source-offset/index.mjs",
186
+ "default": "./dist/swift-source-offset/index.mjs"
187
+ },
188
+ "./swift-resolved-pin-delta": {
189
+ "types": "./dist/swift-resolved-pin-delta/index.d.mts",
190
+ "import": "./dist/swift-resolved-pin-delta/index.mjs",
191
+ "default": "./dist/swift-resolved-pin-delta/index.mjs"
192
+ },
173
193
  "./package.json": "./package.json"
174
194
  },
175
195
  "publishConfig": {