vouchington-tooling 0.0.16 → 0.0.18

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 (52) hide show
  1. package/README.md +17 -2
  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/post-review.d.mts +1 -0
  5. package/dist/cli/commands/post-review.mjs +4 -0
  6. package/dist/cli/commands/stage-review-payload.d.mts +1 -0
  7. package/dist/cli/commands/stage-review-payload.mjs +4 -0
  8. package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
  9. package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
  10. package/dist/cli/index.mjs +21 -0
  11. package/dist/cli/parse.d.mts +13 -1
  12. package/dist/cli/parse.mjs +14 -0
  13. package/dist/cli/usage.d.mts +1 -1
  14. package/dist/cli/usage.mjs +21 -1
  15. package/dist/gha-post-review/claude-token.d.mts +25 -0
  16. package/dist/gha-post-review/claude-token.mjs +107 -0
  17. package/dist/gha-post-review/cli.d.mts +1 -0
  18. package/dist/gha-post-review/cli.mjs +22 -0
  19. package/dist/gha-post-review/github.d.mts +20 -0
  20. package/dist/gha-post-review/github.mjs +77 -0
  21. package/dist/gha-post-review/index.d.mts +9 -0
  22. package/dist/gha-post-review/index.mjs +5 -0
  23. package/dist/gha-post-review/post.d.mts +18 -0
  24. package/dist/gha-post-review/post.mjs +27 -0
  25. package/dist/gha-post-review/token.d.mts +8 -0
  26. package/dist/gha-post-review/token.mjs +20 -0
  27. package/dist/gha-review-payload/cli.d.mts +1 -0
  28. package/dist/gha-review-payload/cli.mjs +24 -0
  29. package/dist/index.d.mts +7 -0
  30. package/dist/index.mjs +5 -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/swift-resolved-pin-delta/index.d.mts +18 -0
  36. package/dist/swift-resolved-pin-delta/index.mjs +63 -0
  37. package/dist/swift-semantic-equal/cli.d.mts +2 -0
  38. package/dist/swift-semantic-equal/cli.mjs +26 -0
  39. package/dist/swift-semantic-equal/index.d.mts +1 -0
  40. package/dist/swift-semantic-equal/index.mjs +1 -0
  41. package/dist/swift-semantic-equal/normalize.d.mts +1 -0
  42. package/dist/swift-semantic-equal/normalize.mjs +108 -0
  43. package/dist/swift-source-offset/index.d.mts +2 -0
  44. package/dist/swift-source-offset/index.mjs +68 -0
  45. package/package.json +26 -1
  46. package/scripts/gha/ghcr-package-retention.sh +65 -0
  47. package/scripts/gha/install-github-release.sh +57 -13
  48. package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
  49. package/scripts/gha/lint-links.sh +92 -0
  50. package/scripts/gha/materialize-pr-context.sh +123 -0
  51. package/scripts/gha/run-with-timeout.sh +119 -0
  52. package/scripts/gha/wait-for-apt-locks.sh +60 -0
package/dist/index.d.mts CHANGED
@@ -33,6 +33,8 @@ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mts'
33
33
  export type { AstGrepRuleInvocation, RunAstGrepRuleOptions } from './ast-grep-rule/index.mts';
34
34
  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.mts';
35
35
  export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, ReviewComment, ReviewFile, ReviewSide, SanitizedReview, } from './gha-review-payload/index.mts';
36
+ export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, PostReviewError, requireEnv, resolveReviewPostToken, revokeClaudeAppToken, runPostReview, runPostReviewCli, withClaudeAppToken, } from './gha-post-review/index.mts';
37
+ export type { ClaudeTokenIo, PostResult, PostReviewIo, PullFile, ReviewPostToken, } from './gha-post-review/index.mts';
36
38
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
37
39
  export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mts';
38
40
  export type { ExpectedTransportIdentity, ObjectSigner, PresignIdentity, TransportControl, } from './coverage-transport/index.mts';
@@ -49,3 +51,8 @@ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderC
49
51
  export type { Checkpoint, CheckpointCodecOptions, GitHubComment, } from './gha-pr-checkpoint/index.mts';
50
52
  export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mts';
51
53
  export type { WorkspaceGatesOptions } from './workspace-gates/index.mts';
54
+ export { validateNugetUpdate } from './nuget-central-version/index.mts';
55
+ export { normalizeSwiftSource } from './swift-semantic-equal/index.mts';
56
+ export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mts';
57
+ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mts';
58
+ export type { ResolvedDocument, ResolvedPin, ValidateResolvedPinDeltaOptions, } from './swift-resolved-pin-delta/index.mts';
package/dist/index.mjs CHANGED
@@ -19,6 +19,7 @@ export { escapeSpreadsheetFormula, parseCsvRows, streamCsvRows, stripCsvBom } fr
19
19
  export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, ResponseBodyTooLargeError, } from './http-body/index.mjs';
20
20
  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
+ export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, PostReviewError, requireEnv, resolveReviewPostToken, revokeClaudeAppToken, runPostReview, runPostReviewCli, withClaudeAppToken, } from './gha-post-review/index.mjs';
22
23
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
23
24
  export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
24
25
  export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
@@ -28,3 +29,7 @@ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-l
28
29
  export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
29
30
  export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
30
31
  export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mjs';
32
+ export { validateNugetUpdate } from './nuget-central-version/index.mjs';
33
+ export { normalizeSwiftSource } from './swift-semantic-equal/index.mjs';
34
+ export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mjs';
35
+ 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.18",
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": {
@@ -125,6 +125,11 @@
125
125
  "import": "./dist/gha-review-payload/index.mjs",
126
126
  "default": "./dist/gha-review-payload/index.mjs"
127
127
  },
128
+ "./gha-post-review": {
129
+ "types": "./dist/gha-post-review/index.d.mts",
130
+ "import": "./dist/gha-post-review/index.mjs",
131
+ "default": "./dist/gha-post-review/index.mjs"
132
+ },
128
133
  "./http-link-pagination": {
129
134
  "types": "./dist/http-link-pagination/index.d.mts",
130
135
  "import": "./dist/http-link-pagination/index.mjs",
@@ -170,6 +175,26 @@
170
175
  "import": "./dist/workspace-gates/index.mjs",
171
176
  "default": "./dist/workspace-gates/index.mjs"
172
177
  },
178
+ "./nuget-central-version": {
179
+ "types": "./dist/nuget-central-version/index.d.mts",
180
+ "import": "./dist/nuget-central-version/index.mjs",
181
+ "default": "./dist/nuget-central-version/index.mjs"
182
+ },
183
+ "./swift-semantic-equal": {
184
+ "types": "./dist/swift-semantic-equal/index.d.mts",
185
+ "import": "./dist/swift-semantic-equal/index.mjs",
186
+ "default": "./dist/swift-semantic-equal/index.mjs"
187
+ },
188
+ "./swift-source-offset": {
189
+ "types": "./dist/swift-source-offset/index.d.mts",
190
+ "import": "./dist/swift-source-offset/index.mjs",
191
+ "default": "./dist/swift-source-offset/index.mjs"
192
+ },
193
+ "./swift-resolved-pin-delta": {
194
+ "types": "./dist/swift-resolved-pin-delta/index.d.mts",
195
+ "import": "./dist/swift-resolved-pin-delta/index.mjs",
196
+ "default": "./dist/swift-resolved-pin-delta/index.mjs"
197
+ },
173
198
  "./package.json": "./package.json"
174
199
  },
175
200
  "publishConfig": {
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # Keep the most recent KEEP_MIN non-protected GHCR package versions, plus any
3
+ # version whose tags match the protected prefix/contains rules.
4
+ set -euo pipefail
5
+
6
+ KEEP_MIN="${KEEP_MIN:-10}"
7
+ OWNER_TYPE="${GHCR_OWNER_TYPE:-user}"
8
+ OWNER="${GHCR_OWNER:-}"
9
+ PROTECTED_PREFIX="${PROTECTED_TAG_PREFIX:-latest}"
10
+ PROTECTED_CONTAINS="${PROTECTED_TAG_CONTAINS:-buildcache}"
11
+
12
+ case "$KEEP_MIN" in
13
+ *[!0-9]* | '')
14
+ echo "ghcr-package-retention: KEEP_MIN must be a non-negative integer" >&2
15
+ exit 2
16
+ ;;
17
+ esac
18
+
19
+ case "$OWNER_TYPE" in
20
+ user) package_root="user/packages/container" ;;
21
+ org)
22
+ [ -n "$OWNER" ] || {
23
+ echo "ghcr-package-retention: GHCR_OWNER is required when GHCR_OWNER_TYPE=org" >&2
24
+ exit 2
25
+ }
26
+ package_root="orgs/${OWNER}/packages/container"
27
+ ;;
28
+ *)
29
+ echo "ghcr-package-retention: GHCR_OWNER_TYPE must be user or org" >&2
30
+ exit 2
31
+ ;;
32
+ esac
33
+
34
+ if [ "$#" -eq 0 ]; then
35
+ echo "usage: ghcr-package-retention.sh <url-encoded-package>..." >&2
36
+ exit 2
37
+ fi
38
+
39
+ for pkg in "$@"; do
40
+ echo "=== Processing $pkg ==="
41
+ count=0
42
+ while IFS= read -r version_json; do
43
+ version_id=$(printf '%s' "$version_json" | jq -r '.id')
44
+ tags=$(printf '%s' "$version_json" | jq -r '[.metadata.container.tags[]?]')
45
+
46
+ protected=$(printf '%s' "$tags" |
47
+ jq --arg prefix "$PROTECTED_PREFIX" --arg contains "$PROTECTED_CONTAINS" \
48
+ 'map(select(startswith($prefix) or contains($contains))) | length')
49
+ if [ "$protected" -gt 0 ]; then
50
+ printf ' keep (protected tag): version %s\n' "$version_id"
51
+ continue
52
+ fi
53
+
54
+ count=$((count + 1))
55
+ if [ "$count" -le "$KEEP_MIN" ]; then
56
+ printf ' keep (recent %d/%d): version %s\n' "$count" "$KEEP_MIN" "$version_id"
57
+ continue
58
+ fi
59
+
60
+ printf ' delete: version %s\n' "$version_id"
61
+ gh api -X DELETE "${package_root}/$pkg/versions/$version_id" || true
62
+ done < <(gh api \
63
+ "${package_root}/$pkg/versions?per_page=100" \
64
+ --jq '.[] | @json')
65
+ done