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.
- package/README.md +24 -0
- package/dist/ci-local/index.d.mts +5 -0
- package/dist/ci-local/index.mjs +79 -0
- package/dist/ci-local/parse.d.mts +3 -0
- package/dist/ci-local/parse.mjs +43 -0
- package/dist/ci-local/types.d.mts +41 -0
- package/dist/ci-local/types.mjs +1 -0
- package/dist/cli/commands/nuget-central-version.d.mts +1 -0
- package/dist/cli/commands/nuget-central-version.mjs +11 -0
- package/dist/cli/commands/swift-semantic-equal.d.mts +1 -0
- package/dist/cli/commands/swift-semantic-equal.mjs +4 -0
- package/dist/cli/index.mjs +20 -0
- package/dist/cli/parse.d.mts +7 -1
- package/dist/cli/parse.mjs +15 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +26 -0
- package/dist/dockerfile-parse/index.d.mts +4 -0
- package/dist/dockerfile-parse/index.mjs +2 -0
- package/dist/dockerfile-parse/prewarm-ports.d.mts +12 -0
- package/dist/dockerfile-parse/prewarm-ports.mjs +59 -0
- package/dist/dockerfile-parse/runtime-cmd.d.mts +11 -0
- package/dist/dockerfile-parse/runtime-cmd.mjs +142 -0
- package/dist/dockerfile-parse/stages.d.mts +7 -0
- package/dist/dockerfile-parse/stages.mjs +18 -0
- package/dist/gha-pr-checkpoint/index.d.mts +58 -0
- package/dist/gha-pr-checkpoint/index.mjs +100 -0
- package/dist/gha-rate-limit/index.d.mts +15 -0
- package/dist/gha-rate-limit/index.mjs +69 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +11 -0
- package/dist/nuget-central-version/cli.d.mts +1 -0
- package/dist/nuget-central-version/cli.mjs +11 -0
- package/dist/nuget-central-version/index.d.mts +1 -0
- package/dist/nuget-central-version/index.mjs +74 -0
- package/dist/pnpm-deploy/index.d.mts +6 -0
- package/dist/pnpm-deploy/index.mjs +3 -0
- package/dist/pnpm-deploy/normalize.d.mts +15 -0
- package/dist/pnpm-deploy/normalize.mjs +64 -0
- package/dist/pnpm-deploy/prune.d.mts +14 -0
- package/dist/pnpm-deploy/prune.mjs +48 -0
- package/dist/pnpm-deploy/restore.d.mts +31 -0
- package/dist/pnpm-deploy/restore.mjs +109 -0
- package/dist/scc-complexity/index.d.mts +19 -0
- package/dist/scc-complexity/index.mjs +91 -0
- package/dist/swift-resolved-pin-delta/index.d.mts +18 -0
- package/dist/swift-resolved-pin-delta/index.mjs +63 -0
- package/dist/swift-semantic-equal/cli.d.mts +2 -0
- package/dist/swift-semantic-equal/cli.mjs +26 -0
- package/dist/swift-semantic-equal/index.d.mts +1 -0
- package/dist/swift-semantic-equal/index.mjs +1 -0
- package/dist/swift-semantic-equal/normalize.d.mts +1 -0
- package/dist/swift-semantic-equal/normalize.mjs +108 -0
- package/dist/swift-source-offset/index.d.mts +2 -0
- package/dist/swift-source-offset/index.mjs +68 -0
- package/dist/workspace-gates/dependabot.d.mts +3 -0
- package/dist/workspace-gates/dependabot.mjs +26 -0
- package/dist/workspace-gates/exclude-registry.d.mts +3 -0
- package/dist/workspace-gates/exclude-registry.mjs +30 -0
- package/dist/workspace-gates/first-party-graph.d.mts +4 -0
- package/dist/workspace-gates/first-party-graph.mjs +87 -0
- package/dist/workspace-gates/index.d.mts +2 -0
- package/dist/workspace-gates/index.mjs +1 -0
- package/dist/workspace-gates/options.d.mts +27 -0
- package/dist/workspace-gates/options.mjs +15 -0
- package/dist/workspace-gates/policy.d.mts +5 -0
- package/dist/workspace-gates/policy.mjs +27 -0
- package/dist/workspace-gates/test-helpers.d.mts +21 -0
- package/dist/workspace-gates/test-helpers.mjs +69 -0
- package/dist/workspace-gates/yaml-loader.d.mts +2 -0
- package/dist/workspace-gates/yaml-loader.mjs +28 -0
- package/dist/workspace-glob.d.mts +1 -0
- package/dist/workspace-glob.mjs +46 -0
- package/package.json +59 -2
- package/scripts/gha/check-cache-size.sh +38 -0
- package/scripts/gha/clean-workspace.sh +227 -0
- package/scripts/gha/ghcr-package-retention.sh +65 -0
- package/scripts/gha/install-github-release.sh +180 -0
- package/scripts/gha/install-playwright-chromium-arm64.sh +84 -0
- package/scripts/gha/lint-links.sh +92 -0
- package/scripts/gha/load-runner-env.sh +75 -0
- package/scripts/gha/make-shard-matrix.sh +30 -0
- package/scripts/gha/materialize-pr-context.sh +123 -0
- package/scripts/gha/run-with-timeout.sh +119 -0
- package/scripts/gha/wait-for-apt-locks.sh +60 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { expandWorkspaceGlob } from '../workspace-glob.mjs';
|
|
4
|
+
function packagePath(packageName) {
|
|
5
|
+
return path.join(...packageName.split('/'));
|
|
6
|
+
}
|
|
7
|
+
function linkTarget(fromDir, toPath) {
|
|
8
|
+
return path.relative(fromDir, toPath);
|
|
9
|
+
}
|
|
10
|
+
function readWorkspacePackageNames(backendDir) {
|
|
11
|
+
const backendPackage = JSON.parse(fs.readFileSync(path.join(backendDir, 'package.json'), 'utf8'));
|
|
12
|
+
const workspaces = Array.isArray(backendPackage.workspaces) ? backendPackage.workspaces : [];
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (const workspace of workspaces.flatMap((pattern) => expandWorkspaceGlob(backendDir, pattern))) {
|
|
15
|
+
const packageJson = path.join(workspace, 'package.json');
|
|
16
|
+
if (!fs.existsSync(packageJson))
|
|
17
|
+
continue;
|
|
18
|
+
const { name } = JSON.parse(fs.readFileSync(packageJson, 'utf8'));
|
|
19
|
+
if (name)
|
|
20
|
+
names.add(name);
|
|
21
|
+
}
|
|
22
|
+
return names;
|
|
23
|
+
}
|
|
24
|
+
function mergeSibling(backlink, storeModules, sibling) {
|
|
25
|
+
const siblingLink = path.join(backlink, sibling);
|
|
26
|
+
const source = path.join(storeModules, sibling);
|
|
27
|
+
const existing = fs.lstatSync(siblingLink, { throwIfNoEntry: false });
|
|
28
|
+
if (!existing) {
|
|
29
|
+
fs.symlinkSync(linkTarget(backlink, source), siblingLink, 'dir');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (existing.isSymbolicLink() || !existing.isDirectory())
|
|
33
|
+
return;
|
|
34
|
+
const sourceStats = fs.statSync(source, { throwIfNoEntry: false });
|
|
35
|
+
if (!sourceStats?.isDirectory())
|
|
36
|
+
return;
|
|
37
|
+
for (const child of fs.readdirSync(source))
|
|
38
|
+
mergeSibling(siblingLink, source, child);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Node's --experimental-strip-types refuses to load TypeScript whose real path
|
|
42
|
+
* contains a node_modules segment, but pnpm deploy places workspace packages
|
|
43
|
+
* (shipped as TypeScript source) inside the virtual store at
|
|
44
|
+
* node_modules/.pnpm/<store-dir>/node_modules/<name>.
|
|
45
|
+
*
|
|
46
|
+
* Relocate every workspace-package copy outside node_modules while preserving
|
|
47
|
+
* pnpm's resolution topology, without enumerating dependencies:
|
|
48
|
+
*
|
|
49
|
+
* - move the content dir to workspace-packages/<store-dir>/<name>
|
|
50
|
+
* - leave a relative symlink at the original virtual-store location, so every
|
|
51
|
+
* pnpm-created link (top-level node_modules entries, sibling links from other
|
|
52
|
+
* store dirs) keeps resolving through it
|
|
53
|
+
* - add a node_modules backlink inside the moved dir pointing at the package's
|
|
54
|
+
* original virtual-store sibling directory, so the package's own imports
|
|
55
|
+
* resolve exactly as pnpm laid them out
|
|
56
|
+
*/
|
|
57
|
+
export function restoreDeployedWorkspacePackages({ backendDir, prodDir, workspaceRoot, }) {
|
|
58
|
+
if (!prodDir)
|
|
59
|
+
throw new Error('prodDir is required');
|
|
60
|
+
if (!backendDir)
|
|
61
|
+
throw new Error('backendDir is required');
|
|
62
|
+
const resolvedWorkspaceRoot = workspaceRoot ?? path.join(prodDir, 'workspace-packages');
|
|
63
|
+
const workspacePackageNames = readWorkspacePackageNames(backendDir);
|
|
64
|
+
const pnpmRoot = path.join(prodDir, 'node_modules', '.pnpm');
|
|
65
|
+
if (!fs.existsSync(pnpmRoot))
|
|
66
|
+
return;
|
|
67
|
+
for (const storeEntry of fs.readdirSync(pnpmRoot, { withFileTypes: true })) {
|
|
68
|
+
if (!storeEntry.isDirectory() || storeEntry.name === 'node_modules')
|
|
69
|
+
continue;
|
|
70
|
+
const storeModules = path.join(pnpmRoot, storeEntry.name, 'node_modules');
|
|
71
|
+
for (const name of workspacePackageNames) {
|
|
72
|
+
const contentDir = path.join(storeModules, packagePath(name));
|
|
73
|
+
const stats = fs.lstatSync(contentDir, { throwIfNoEntry: false });
|
|
74
|
+
if (!stats?.isDirectory())
|
|
75
|
+
continue;
|
|
76
|
+
const relocated = path.join(resolvedWorkspaceRoot, storeEntry.name, packagePath(name));
|
|
77
|
+
fs.rmSync(relocated, { recursive: true, force: true });
|
|
78
|
+
fs.mkdirSync(path.dirname(relocated), { recursive: true });
|
|
79
|
+
// copy+delete, not rename: renaming a directory out of a lower Docker
|
|
80
|
+
// overlayfs layer fails with EXDEV
|
|
81
|
+
fs.cpSync(contentDir, relocated, { recursive: true, verbatimSymlinks: true });
|
|
82
|
+
fs.rmSync(contentDir, { recursive: true, force: true });
|
|
83
|
+
fs.symlinkSync(linkTarget(path.dirname(contentDir), relocated), contentDir, 'dir');
|
|
84
|
+
const backlink = path.join(relocated, 'node_modules');
|
|
85
|
+
const backlinkStats = fs.lstatSync(backlink, { throwIfNoEntry: false });
|
|
86
|
+
if (!backlinkStats) {
|
|
87
|
+
fs.symlinkSync(linkTarget(relocated, storeModules), backlink, 'dir');
|
|
88
|
+
}
|
|
89
|
+
else if (backlinkStats.isDirectory()) {
|
|
90
|
+
// pnpm deploy leaves a real node_modules (containing .bin) inside some
|
|
91
|
+
// injected packages; merge sibling links into it instead of replacing it
|
|
92
|
+
for (const sibling of fs.readdirSync(storeModules)) {
|
|
93
|
+
mergeSibling(backlink, storeModules, sibling);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function runRestoreDeployedWorkspacePackagesCli({ args, env, isMain, restore = restoreDeployedWorkspacePackages, }) {
|
|
100
|
+
if (!isMain)
|
|
101
|
+
return;
|
|
102
|
+
const prodDir = args[0] ?? env.PROD_DIR;
|
|
103
|
+
const backendDir = args[1] ?? env.BACKEND_DIR;
|
|
104
|
+
if (!prodDir)
|
|
105
|
+
throw new Error('PROD_DIR or a directory argument is required');
|
|
106
|
+
if (!backendDir)
|
|
107
|
+
throw new Error('BACKEND_DIR or a backend directory argument is required');
|
|
108
|
+
restore({ prodDir, backendDir });
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SharedContext } from '../shared-context/index.mts';
|
|
2
|
+
export declare const SCC_COMPLEXITY_LIMIT = 50;
|
|
3
|
+
export interface SccComplexityOptions {
|
|
4
|
+
limit?: number;
|
|
5
|
+
includeExt?: string;
|
|
6
|
+
excludeDir?: string;
|
|
7
|
+
notMatch?: string;
|
|
8
|
+
tmpdirPrefix?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SccComplexityViolation {
|
|
12
|
+
file: string;
|
|
13
|
+
complexity: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildSccArgs(options?: SccComplexityOptions): string[];
|
|
16
|
+
export declare function checkSccComplexity(ctx: SharedContext, options?: SccComplexityOptions, runScc?: (outputPath: string) => Promise<string>): Promise<{
|
|
17
|
+
errors: string[];
|
|
18
|
+
}>;
|
|
19
|
+
export declare function parseSccComplexityViolations(json: string, trackedFileSet: ReadonlySet<string>, limit?: number): SccComplexityViolation[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export const SCC_COMPLEXITY_LIMIT = 50;
|
|
8
|
+
const DEFAULT_INCLUDE_EXT = 'js,mts,jsx,ts,tsx';
|
|
9
|
+
const DEFAULT_EXCLUDE_DIR = '.git,fixtures,__tests__,test-helpers';
|
|
10
|
+
const DEFAULT_NOT_MATCH = String.raw `\.(test|spec)\.`;
|
|
11
|
+
const DEFAULT_TMPDIR_PREFIX = 'scc-complexity-';
|
|
12
|
+
export function buildSccArgs(options = {}) {
|
|
13
|
+
return [
|
|
14
|
+
'--format',
|
|
15
|
+
'json',
|
|
16
|
+
'--include-ext',
|
|
17
|
+
options.includeExt ?? DEFAULT_INCLUDE_EXT,
|
|
18
|
+
'--by-file',
|
|
19
|
+
'--sort',
|
|
20
|
+
'complexity',
|
|
21
|
+
'--exclude-dir',
|
|
22
|
+
options.excludeDir ?? DEFAULT_EXCLUDE_DIR,
|
|
23
|
+
'--not-match',
|
|
24
|
+
options.notMatch ?? DEFAULT_NOT_MATCH,
|
|
25
|
+
'--no-cocomo',
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
export async function checkSccComplexity(ctx, options = {}, runScc) {
|
|
29
|
+
if (!ctx.isInsideGitRepo) {
|
|
30
|
+
return { errors: [`::error::${ctx.repoRoot} is not inside a git repository`] };
|
|
31
|
+
}
|
|
32
|
+
const dir = await mkdtemp(join(tmpdir(), options.tmpdirPrefix ?? DEFAULT_TMPDIR_PREFIX));
|
|
33
|
+
const outputPath = join(dir, 'scc.json');
|
|
34
|
+
const limit = options.limit ?? SCC_COMPLEXITY_LIMIT;
|
|
35
|
+
try {
|
|
36
|
+
const resolve = runScc ?? ((path) => runSccJson(ctx.repoRoot, path, options));
|
|
37
|
+
const json = await resolve(outputPath);
|
|
38
|
+
const violations = parseSccComplexityViolations(json, ctx.trackedFileSet, limit);
|
|
39
|
+
return { errors: violations.map((violation) => formatViolation(violation, limit)) };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
return { errors: [`::error::scc-complexity failed: ${message}`] };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await rm(dir, { force: true, recursive: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function parseSccComplexityViolations(json, trackedFileSet, limit = SCC_COMPLEXITY_LIMIT) {
|
|
50
|
+
const parsed = JSON.parse(json);
|
|
51
|
+
if (!Array.isArray(parsed))
|
|
52
|
+
throw new Error('scc JSON output must be an array');
|
|
53
|
+
const violations = [];
|
|
54
|
+
for (const language of parsed) {
|
|
55
|
+
if (!Array.isArray(language.Files))
|
|
56
|
+
continue;
|
|
57
|
+
for (const file of language.Files) {
|
|
58
|
+
if (typeof file.Location !== 'string')
|
|
59
|
+
continue;
|
|
60
|
+
if (!trackedFileSet.has(file.Location))
|
|
61
|
+
continue;
|
|
62
|
+
if (typeof file.Complexity !== 'number')
|
|
63
|
+
continue;
|
|
64
|
+
if (file.Complexity <= limit)
|
|
65
|
+
continue;
|
|
66
|
+
violations.push({ file: file.Location, complexity: file.Complexity });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return violations.toSorted((a, b) => b.complexity - a.complexity || a.file.localeCompare(b.file));
|
|
70
|
+
}
|
|
71
|
+
async function runSccJson(repoRoot, outputPath, options) {
|
|
72
|
+
try {
|
|
73
|
+
await execFileAsync(options.command ?? 'scc', [...buildSccArgs(options), '--output', outputPath], {
|
|
74
|
+
cwd: repoRoot,
|
|
75
|
+
maxBuffer: 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (isNodeSystemError(error) && error.code === 'ENOENT') {
|
|
80
|
+
throw new Error('scc executable not found; install with mise install', { cause: error });
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return readFile(outputPath, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
function formatViolation({ file, complexity }, limit) {
|
|
87
|
+
return `::error file=${file}::${file}: scc complexity ${complexity} exceeds ${limit}; simplify or split this file`;
|
|
88
|
+
}
|
|
89
|
+
function isNodeSystemError(error) {
|
|
90
|
+
return error instanceof Error && 'code' in error;
|
|
91
|
+
}
|
|
@@ -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,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,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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import picomatch from 'picomatch';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function checkDependabotCrosslist(dependabot, errors, options) {
|
|
6
|
+
const updates = Array.isArray(dependabot['updates']) ? dependabot['updates'] : [];
|
|
7
|
+
const npmUpdate = updates.find((entry) => isRecord(entry) && entry['package-ecosystem'] === 'npm' && entry['directory'] === '/');
|
|
8
|
+
if (!npmUpdate)
|
|
9
|
+
return;
|
|
10
|
+
const cooldown = npmUpdate['cooldown'];
|
|
11
|
+
const rawExclude = isRecord(cooldown) && Array.isArray(cooldown['exclude'])
|
|
12
|
+
? cooldown['exclude']
|
|
13
|
+
: [];
|
|
14
|
+
for (const entry of rawExclude) {
|
|
15
|
+
if (typeof entry !== 'string') {
|
|
16
|
+
errors.push(`::error file=${options.dependabotPath}::${options.dependabotPath}: npm cooldown.exclude entry ${JSON.stringify(entry)} must be a string glob pattern`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const patterns = rawExclude.filter((entry) => typeof entry === 'string');
|
|
20
|
+
const matchers = patterns.map((pattern) => picomatch(pattern));
|
|
21
|
+
for (const name of options.firstPartyNames) {
|
|
22
|
+
if (!matchers.some((match) => match(name))) {
|
|
23
|
+
errors.push(`::error file=${options.dependabotPath}::${options.dependabotPath}: "${name}" is in ${options.workspaceYamlPath} minimumReleaseAgeExclude but not covered by npm cooldown.exclude — add it or a matching glob`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function checkExcludeRegistry(workspace, errors, options) {
|
|
2
|
+
const raw = workspace['minimumReleaseAgeExclude'];
|
|
3
|
+
const yamlEntries = Array.isArray(raw)
|
|
4
|
+
? raw.filter((entry) => typeof entry === 'string')
|
|
5
|
+
: [];
|
|
6
|
+
const yamlNames = new Set();
|
|
7
|
+
for (const name of yamlEntries) {
|
|
8
|
+
if (yamlNames.has(name)) {
|
|
9
|
+
errors.push(`::error file=${options.workspaceYamlPath}::${options.workspaceYamlPath}: minimumReleaseAgeExclude duplicates "${name}"`);
|
|
10
|
+
}
|
|
11
|
+
yamlNames.add(name);
|
|
12
|
+
}
|
|
13
|
+
const registryNames = new Set(options.firstPartyNames);
|
|
14
|
+
const temporarySelectors = new Set(options.temporarySelectors);
|
|
15
|
+
for (const name of yamlNames) {
|
|
16
|
+
if (!registryNames.has(name) && !temporarySelectors.has(name)) {
|
|
17
|
+
errors.push(`::error file=${options.workspaceYamlPath}::${options.workspaceYamlPath}: minimumReleaseAgeExclude contains "${name}" which is not in a release-age exemption registry`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const name of registryNames) {
|
|
21
|
+
if (!yamlNames.has(name)) {
|
|
22
|
+
errors.push(`::error file=${options.workspaceYamlPath}::${options.workspaceYamlPath}: first-party package "${name}" is in the ${options.firstPartyRegistryLabel} but missing from minimumReleaseAgeExclude`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const selector of temporarySelectors) {
|
|
26
|
+
if (!yamlNames.has(selector)) {
|
|
27
|
+
errors.push(`::error file=${options.workspaceYamlPath}::${options.workspaceYamlPath}: temporary release-age exemption "${selector}" is in the ${options.firstPartyRegistryLabel} but missing from minimumReleaseAgeExclude`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SharedContext } from '../shared-context/index.mts';
|
|
2
|
+
import type { ResolvedWorkspaceGatesOptions } from './options.mts';
|
|
3
|
+
export declare function checkActiveFirstPartyGraph(ctx: SharedContext, errors: string[], options: ResolvedWorkspaceGatesOptions): Promise<void>;
|
|
4
|
+
export declare function checkTemporaryReleaseAgeSelectorsInLockfile(ctx: SharedContext, errors: string[], temporarySelectors: readonly string[], lockfilePath: string): Promise<void>;
|