vouchington-tooling 0.3.5 → 0.4.0
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 +19 -5
- package/dist/cli/commands/prepare-vitest-reports.d.mts +2 -0
- package/dist/cli/commands/prepare-vitest-reports.mjs +11 -0
- package/dist/cli/commands/vitest-report-attempt.d.mts +2 -0
- package/dist/cli/commands/vitest-report-attempt.mjs +11 -0
- package/dist/cli/index.mjs +6 -0
- package/dist/cli/parse.d.mts +6 -0
- package/dist/cli/parse.mjs +4 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +4 -0
- package/dist/gha-claude-post-review/index.d.mts +3 -0
- package/dist/gha-claude-post-review/index.mjs +2 -0
- package/dist/gha-post-review/github.d.mts +7 -0
- package/dist/gha-post-review/github.mjs +20 -5
- package/dist/gha-post-review/index.d.mts +5 -1
- package/dist/gha-post-review/index.mjs +4 -1
- package/dist/gha-post-review/token.d.mts +1 -0
- package/dist/gha-post-review/token.mjs +1 -0
- package/dist/gha-workspace-policy/docker-workspace-user.d.mts +2 -0
- package/dist/gha-workspace-policy/docker-workspace-user.mjs +208 -0
- package/dist/gha-workspace-policy/index.d.mts +8 -0
- package/dist/gha-workspace-policy/index.mjs +17 -0
- package/dist/gha-workspace-policy/shared.d.mts +6 -0
- package/dist/gha-workspace-policy/shared.mjs +54 -0
- package/dist/gha-workspace-policy/sparse-checkout.d.mts +2 -0
- package/dist/gha-workspace-policy/sparse-checkout.mjs +28 -0
- package/dist/index.d.mts +8 -2
- package/dist/index.mjs +4 -1
- package/dist/pnpm-install/install-operations.d.mts +1 -0
- package/dist/pnpm-install/install-operations.mjs +26 -6
- package/dist/pnpm-install/native-health.d.mts +2 -0
- package/dist/pnpm-install/native-health.mjs +22 -6
- package/dist/pnpm-install/pending-builds.d.mts +1 -0
- package/dist/pnpm-install/pending-builds.mjs +17 -0
- package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +6 -0
- package/dist/pnpm-install/pnpm-install-fixture.test-helpers.d.mts +1 -0
- package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +17 -0
- package/dist/pnpm-install/runner.mjs +21 -5
- package/dist/pnpm-install/support.d.mts +1 -0
- package/dist/pnpm-install/support.mjs +3 -3
- package/dist/vitest-blob-manifest/cli.mjs +2 -4
- package/dist/vitest-blob-manifest/report-attempt-cli.d.mts +2 -0
- package/dist/vitest-blob-manifest/report-attempt-cli.mjs +32 -0
- package/dist/vitest-blob-manifest/reports-cli.d.mts +2 -0
- package/dist/vitest-blob-manifest/reports-cli.mjs +55 -0
- package/dist/vitest-blob-manifest/run-attempt.d.mts +2 -0
- package/dist/vitest-blob-manifest/run-attempt.mjs +11 -0
- package/dist/workspace-gates/manifest-version-assertions.d.mts +3 -0
- package/dist/workspace-gates/manifest-version-assertions.mjs +100 -0
- package/dist/workspace-gates/manifest-version-parser.d.mts +6 -0
- package/dist/workspace-gates/manifest-version-parser.mjs +104 -0
- package/dist/workspace-gates/manifest-version-patterns.d.mts +9 -0
- package/dist/workspace-gates/manifest-version-patterns.mjs +21 -0
- package/dist/workspace-gates/policy.mjs +2 -0
- package/package.json +13 -3
- package/scripts/gha/clean-workspace.sh +48 -2
- package/skills/dependabot/SKILL.md +111 -0
- package/skills/github-actions-authoring/SKILL.md +10 -0
- package/skills/github-actions-checklist/SKILL.md +29 -2
- package/skills/manifest.json +24 -17
- package/skills/review-ci-logs/SKILL.md +8 -2
- package/skills/static-analysis-checklist/SKILL.md +8 -5
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { visitRunSteps } from './shared.mjs';
|
|
2
|
+
const SPARSE_CHECKOUT_KEYS = ['sparse-checkout', 'sparse-checkout-cone-mode'];
|
|
3
|
+
const SPARSE_CHECKOUT_COMMANDS = [
|
|
4
|
+
/\bgit\b[^\n;&|]*\bsparse-checkout\s+(?:init|set|add|reapply)\b/iu,
|
|
5
|
+
/\bgit\b[^\n;&|]*\bconfig\b[^\n;&|]*\bcore\.sparseCheckout(?:Cone)?(?:\s*=\s*|\s+)(?:"|')?(?:true|yes|on|1)(?:"|')?(?=\s|;|$)/iu,
|
|
6
|
+
];
|
|
7
|
+
export function checkNoSparseCheckoutDocument(file, document, kind, errors) {
|
|
8
|
+
visitRunSteps(document, kind === 'action', (scope, index, step) => {
|
|
9
|
+
const run = step.run;
|
|
10
|
+
const normalizedRun = typeof run === 'string' ? run.replace(/\\\r?\n\s*/gu, ' ') : '';
|
|
11
|
+
if (SPARSE_CHECKOUT_COMMANDS.some((pattern) => pattern.test(normalizedRun))) {
|
|
12
|
+
errors.push(`::error file=${file}::${file}: ${scope} step ${index} enables sparse checkout. ` +
|
|
13
|
+
'Use a full checkout; commands that disable or unset sparse-checkout state remain allowed.');
|
|
14
|
+
}
|
|
15
|
+
if (typeof step.uses !== 'string' || !step.uses.startsWith('actions/checkout@'))
|
|
16
|
+
return;
|
|
17
|
+
const withValue = step.with;
|
|
18
|
+
if (!withValue || typeof withValue !== 'object')
|
|
19
|
+
return;
|
|
20
|
+
for (const key of SPARSE_CHECKOUT_KEYS) {
|
|
21
|
+
if (!(key in withValue))
|
|
22
|
+
continue;
|
|
23
|
+
errors.push(`::error file=${file}::${file}: ${scope} step ${index} passes "${key}" to ` +
|
|
24
|
+
'actions/checkout. Persistent runners reuse workspace directories, so leaked ' +
|
|
25
|
+
'sparse-checkout state can silently narrow a later checkout. Check out the full tree.');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,8 @@ export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreat
|
|
|
15
15
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
|
|
16
16
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mts';
|
|
17
17
|
export type { GhApiExecutor, RuntimeAuditOptions, RuntimeAuditResult, RuntimeAuditWorkflowFilter, RuntimeJobResult, RuntimeSample, } from './gha-runtime-audit/index.mts';
|
|
18
|
+
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mts';
|
|
19
|
+
export type { GhaWorkspacePolicyOptions } from './gha-workspace-policy/index.mts';
|
|
18
20
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mts';
|
|
19
21
|
export type { InspectedVitestBlobBundle, VitestBlobIdentity, VitestBlobManifest, VitestReportAttempt, VitestReportAttemptIdentity, } from './vitest-blob-manifest/index.mts';
|
|
20
22
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mts';
|
|
@@ -45,8 +47,12 @@ export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mts'
|
|
|
45
47
|
export type { AstGrepRuleInvocation, RunAstGrepRuleOptions } from './ast-grep-rule/index.mts';
|
|
46
48
|
export { 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';
|
|
47
49
|
export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, ReviewComment, ReviewFile, ReviewSide, SanitizedReview, } from './gha-review-payload/index.mts';
|
|
48
|
-
export {
|
|
49
|
-
export type {
|
|
50
|
+
export { PostReviewError, requireEnv, runPostReview, runPostReviewCli, postReviewWithTokenFromEnv, } from './gha-post-review/index.mts';
|
|
51
|
+
export type { PostResult, PostReviewIo, PullFile } from './gha-post-review/index.mts';
|
|
52
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
53
|
+
export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, resolveReviewPostToken, revokeClaudeAppToken, withClaudeAppToken, } from './gha-post-review/index.mts';
|
|
54
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
55
|
+
export type { ClaudeTokenIo, ReviewPostToken } from './gha-post-review/index.mts';
|
|
50
56
|
export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
|
|
51
57
|
export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mts';
|
|
52
58
|
export type { ExpectedTransportIdentity, ObjectSigner, PresignIdentity, TransportControl, } from './coverage-transport/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOn
|
|
|
8
8
|
export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
|
|
9
9
|
export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
|
|
10
10
|
export { auditCiJobRuntime, parseWorkflowNameMatch } from './gha-runtime-audit/index.mjs';
|
|
11
|
+
export { checkGhaWorkspacePolicy } from './gha-workspace-policy/index.mjs';
|
|
11
12
|
export { createVitestBlobManifest, createVitestReportAttempt, inspectVitestBlobBundle, parseVitestBlobManifest, parseVitestReportAttempt, readVitestReportAttempts, serializeVitestBlobManifest, serializeVitestReportAttempt, VITEST_BLOB_MANIFEST_FILENAME, VITEST_BLOB_MANIFEST_VERSION, VITEST_REPORT_ATTEMPT_PREFIX, VITEST_REPORT_ATTEMPT_VERSION, vitestBlobBundlePaths, writeVitestBlobManifest, writeVitestReportAttempt, } from './vitest-blob-manifest/index.mjs';
|
|
12
13
|
export { prepareVitestReports } from './vitest-blob-manifest/reports.mjs';
|
|
13
14
|
export { findWorkspaceLinkMismatches, formatReleaseAgeFailure, INSTALL_TERMINATION_FAILED, isReleaseAgeViolation, parseInstallOptions, parseReleaseAgeViolations, runInstallLifecycle, } from './pnpm-install/index.mjs';
|
|
@@ -25,7 +26,9 @@ export { escapeSpreadsheetFormula, parseCsvRows, streamCsvRows, stripCsvBom } fr
|
|
|
25
26
|
export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, ResponseBodyTooLargeError, } from './http-body/index.mjs';
|
|
26
27
|
export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mjs';
|
|
27
28
|
export { 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';
|
|
28
|
-
export {
|
|
29
|
+
export { PostReviewError, requireEnv, runPostReview, runPostReviewCli, postReviewWithTokenFromEnv, } from './gha-post-review/index.mjs';
|
|
30
|
+
/** @deprecated Import Claude helpers from vouchington-tooling/gha-claude-post-review. */
|
|
31
|
+
export { CLAUDE_OIDC_AUDIENCE, createActionsClaudeTokenIo, mintClaudeAppToken, resolveReviewPostToken, revokeClaudeAppToken, withClaudeAppToken, } from './gha-post-review/index.mjs';
|
|
29
32
|
export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
|
|
30
33
|
export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
|
|
31
34
|
export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
|
|
@@ -2,3 +2,4 @@ import { type CommandResult, type InstallOptions } from './support.mts';
|
|
|
2
2
|
export declare function withScriptPolicy(args: string[], installScripts: boolean): string[];
|
|
3
3
|
export declare function install(args: string[], options: InstallOptions, label: string): Promise<void>;
|
|
4
4
|
export declare function reconcileOrFail(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>): Promise<void>;
|
|
5
|
+
export declare function repairIsolatedNativeMismatch(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, mismatchedNativePaths: string[]): Promise<boolean>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { scheduler } from 'node:timers/promises';
|
|
2
2
|
import { runPnpm } from './exec.mjs';
|
|
3
|
+
import { nativeBinariesMatchRuntime, repairedNativeBinariesMatchRuntime } from './native-health.mjs';
|
|
4
|
+
import { buildLedgersAllowNativeRepair } from './pending-builds.mjs';
|
|
3
5
|
import { INSTALL_TERMINATION_FAILED } from './process.mjs';
|
|
4
6
|
import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
|
|
5
|
-
import {
|
|
7
|
+
import { findWorkspaceLinkMismatches, forcedInstallArgs, logWorkspaceLinkMismatches, } from './support.mjs';
|
|
6
8
|
function fail(message) {
|
|
7
9
|
throw new Error(message);
|
|
8
10
|
}
|
|
@@ -27,12 +29,30 @@ export async function install(args, options, label) {
|
|
|
27
29
|
fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
|
|
28
30
|
}
|
|
29
31
|
export async function reconcileOrFail(options, runCapture) {
|
|
30
|
-
|
|
31
|
-
await install(
|
|
32
|
-
await
|
|
33
|
-
|
|
32
|
+
await install([...forcedInstallArgs, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
|
|
33
|
+
await install(withScriptPolicy(forcedInstallArgs, options.installScripts), options, 'strict persistent reconciliation');
|
|
34
|
+
await verifyInstallHealth(runCapture, 'persistent reconciliation');
|
|
35
|
+
}
|
|
36
|
+
async function verifyInstallHealth(runCapture, phase, repairedNativePaths = []) {
|
|
37
|
+
const [nativesMatch, remaining] = await Promise.all([
|
|
38
|
+
repairedNativePaths.length > 0
|
|
39
|
+
? repairedNativeBinariesMatchRuntime(repairedNativePaths)
|
|
40
|
+
: nativeBinariesMatchRuntime(),
|
|
41
|
+
findWorkspaceLinkMismatches(runCapture),
|
|
42
|
+
]);
|
|
43
|
+
if (!nativesMatch)
|
|
44
|
+
fail(`${phase} completed with mismatched native binaries`);
|
|
34
45
|
if (remaining.length > 0) {
|
|
35
46
|
logWorkspaceLinkMismatches(remaining);
|
|
36
|
-
fail(
|
|
47
|
+
fail(`${phase} completed with invalid workspace links`);
|
|
37
48
|
}
|
|
38
49
|
}
|
|
50
|
+
export async function repairIsolatedNativeMismatch(options, runCapture, mismatchedNativePaths) {
|
|
51
|
+
if (!(await buildLedgersAllowNativeRepair()))
|
|
52
|
+
return false;
|
|
53
|
+
if ((await findWorkspaceLinkMismatches(runCapture)).length > 0)
|
|
54
|
+
return false;
|
|
55
|
+
await install(withScriptPolicy(forcedInstallArgs, options.installScripts), options, 'native health reconciliation');
|
|
56
|
+
await verifyInstallHealth(runCapture, 'native health reconciliation', mismatchedNativePaths);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type NativeFamily = 'elf' | 'macho' | 'pe';
|
|
2
2
|
export declare function nativeFamilyFromMagic(buffer: Buffer): NativeFamily | undefined;
|
|
3
3
|
export declare function expectedNativeFamily(platform?: NodeJS.Platform): NativeFamily | undefined;
|
|
4
|
+
export declare function mismatchedNativeBinaries(root?: string, platform?: NodeJS.Platform): Promise<string[]>;
|
|
4
5
|
export declare function nativeBinariesMatchRuntime(root?: string, platform?: NodeJS.Platform): Promise<boolean>;
|
|
6
|
+
export declare function repairedNativeBinariesMatchRuntime(paths: string[], platform?: NodeJS.Platform): Promise<boolean>;
|
|
@@ -58,27 +58,43 @@ async function searchRoot(nodeModules) {
|
|
|
58
58
|
return nodeModules;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
export async function
|
|
61
|
+
export async function mismatchedNativeBinaries(root = process.cwd(), platform = process.platform) {
|
|
62
62
|
const expected = expectedNativeFamily(platform);
|
|
63
63
|
if (expected === undefined)
|
|
64
|
-
return
|
|
64
|
+
return [];
|
|
65
65
|
const nodeModules = path.join(root, 'node_modules');
|
|
66
66
|
try {
|
|
67
67
|
const info = await stat(nodeModules);
|
|
68
68
|
if (!info.isDirectory())
|
|
69
|
-
return
|
|
69
|
+
return [];
|
|
70
70
|
}
|
|
71
71
|
catch {
|
|
72
|
-
return
|
|
72
|
+
return [];
|
|
73
73
|
}
|
|
74
74
|
const cwd = await searchRoot(nodeModules);
|
|
75
|
+
const mismatches = [];
|
|
75
76
|
for await (const relative of glob('**/*.{node,bin}', { cwd })) {
|
|
76
|
-
const
|
|
77
|
+
const pathname = path.join(cwd, relative);
|
|
78
|
+
const magic = await readMagic(pathname);
|
|
77
79
|
if (magic === undefined)
|
|
78
80
|
continue;
|
|
79
81
|
const family = nativeFamilyFromMagic(magic);
|
|
80
82
|
if (family !== undefined && family !== expected)
|
|
83
|
+
mismatches.push(pathname);
|
|
84
|
+
}
|
|
85
|
+
return mismatches;
|
|
86
|
+
}
|
|
87
|
+
export async function nativeBinariesMatchRuntime(root = process.cwd(), platform = process.platform) {
|
|
88
|
+
return (await mismatchedNativeBinaries(root, platform)).length === 0;
|
|
89
|
+
}
|
|
90
|
+
export async function repairedNativeBinariesMatchRuntime(paths, platform = process.platform) {
|
|
91
|
+
const expected = expectedNativeFamily(platform);
|
|
92
|
+
if (expected === undefined)
|
|
93
|
+
return true;
|
|
94
|
+
for (const pathname of paths) {
|
|
95
|
+
const magic = await readMagic(pathname);
|
|
96
|
+
if (magic === undefined || nativeFamilyFromMagic(magic) !== expected)
|
|
81
97
|
return false;
|
|
82
98
|
}
|
|
83
|
-
return
|
|
99
|
+
return nativeBinariesMatchRuntime();
|
|
84
100
|
}
|
|
@@ -12,6 +12,7 @@ export type PendingBuildDelta = {
|
|
|
12
12
|
workspaceIds: string[];
|
|
13
13
|
};
|
|
14
14
|
export declare function pendingBuilds(): Promise<PendingBuilds>;
|
|
15
|
+
export declare function buildLedgersAllowNativeRepair(): Promise<boolean>;
|
|
15
16
|
export declare function validDependencyBuildIds(ids: [string, ...string[]]): Promise<[string, ...string[]] | undefined>;
|
|
16
17
|
export declare function clearPendingDependencyBuilds(ids: string[]): Promise<boolean>;
|
|
17
18
|
export declare function pendingBuildDelta(before: PendingBuilds, after: PendingBuilds): Promise<PendingBuildDelta>;
|
|
@@ -15,6 +15,23 @@ export async function pendingBuilds() {
|
|
|
15
15
|
return { kind: 'unknown' };
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
export async function buildLedgersAllowNativeRepair() {
|
|
19
|
+
try {
|
|
20
|
+
const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
|
|
21
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
22
|
+
return false;
|
|
23
|
+
const record = value;
|
|
24
|
+
const ignored = record.ignoredBuilds;
|
|
25
|
+
const pending = record.pendingBuilds ?? [];
|
|
26
|
+
return (Array.isArray(ignored) &&
|
|
27
|
+
ignored.length === 0 &&
|
|
28
|
+
Array.isArray(pending) &&
|
|
29
|
+
pending.length === 0);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
18
35
|
async function lockfileDependencyIds() {
|
|
19
36
|
try {
|
|
20
37
|
const lockfile = parse(await readFile(path.join(process.cwd(), 'pnpm-lock.yaml'), 'utf8'));
|
|
@@ -41,6 +41,12 @@ case " $* " in
|
|
|
41
41
|
if [ "\${PNPM_REBUILD_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
|
|
42
42
|
;;
|
|
43
43
|
*' --force '*)
|
|
44
|
+
if [ "\${PNPM_DELETE_NATIVE:-0}" = 1 ]; then
|
|
45
|
+
rm -f "$PNPM_NATIVE_ADDON"
|
|
46
|
+
elif [ "\${PNPM_REPAIR_NATIVE:-0}" = 1 ]; then
|
|
47
|
+
cp "$PNPM_NATIVE_REPLACEMENT" "$PNPM_NATIVE_ADDON"
|
|
48
|
+
fi
|
|
49
|
+
if [ "\${PNPM_FORCE_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
|
|
44
50
|
if [ "\${PNPM_REPAIR_LINK:-0}" = 1 ]; then
|
|
45
51
|
mkdir -p "$(dirname "$PNPM_DEPENDENCY_LINK")"
|
|
46
52
|
rm -f "$PNPM_DEPENDENCY_LINK"
|
|
@@ -14,6 +14,7 @@ export declare function makeFixture(): Promise<{
|
|
|
14
14
|
root: string;
|
|
15
15
|
summary: string;
|
|
16
16
|
}>;
|
|
17
|
+
export declare function configureNativeRepair(fixture: Awaited<ReturnType<typeof makeFixture>>, addon: string): Promise<void>;
|
|
17
18
|
export declare function runInstaller(fixture: Awaited<ReturnType<typeof makeFixture>>, options?: FixtureOptions): Promise<{
|
|
18
19
|
stdout: string;
|
|
19
20
|
stderr: string;
|
|
@@ -45,9 +45,14 @@ export async function makeFixture() {
|
|
|
45
45
|
PNPM_CALLS: join(root, 'pnpm.calls'),
|
|
46
46
|
PNPM_DEPENDENCY: dependency,
|
|
47
47
|
PNPM_DEPENDENCY_LINK: dependencyLink,
|
|
48
|
+
PNPM_DELETE_NATIVE: '0',
|
|
48
49
|
PNPM_LOG: pnpmLog,
|
|
49
50
|
PNPM_NODE_MODULES: join(root, 'node_modules'),
|
|
51
|
+
PNPM_NATIVE_ADDON: '',
|
|
52
|
+
PNPM_NATIVE_REPLACEMENT: '',
|
|
50
53
|
PNPM_PENDING_BUILDS: '',
|
|
54
|
+
PNPM_FORCE_BREAK_LINK: '0',
|
|
55
|
+
PNPM_REPAIR_NATIVE: '0',
|
|
51
56
|
PNPM_REPAIR_LINK: '0',
|
|
52
57
|
PNPM_REBUILD_BREAK_LINK: '0',
|
|
53
58
|
PNPM_WORKSPACES_JSON: JSON.stringify(workspaces),
|
|
@@ -62,6 +67,18 @@ export async function makeFixture() {
|
|
|
62
67
|
summary,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
70
|
+
export async function configureNativeRepair(fixture, addon) {
|
|
71
|
+
const replacement = join(fixture.root, 'native-replacement.node');
|
|
72
|
+
const magic = process.platform === 'darwin'
|
|
73
|
+
? Buffer.from([0xcf, 0xfa, 0xed, 0xfe])
|
|
74
|
+
: process.platform === 'win32'
|
|
75
|
+
? Buffer.from([0x4d, 0x5a])
|
|
76
|
+
: Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
|
|
77
|
+
await writeFile(replacement, magic);
|
|
78
|
+
fixture.env.PNPM_NATIVE_ADDON = addon;
|
|
79
|
+
fixture.env.PNPM_NATIVE_REPLACEMENT = replacement;
|
|
80
|
+
fixture.env.PNPM_REPAIR_NATIVE = '1';
|
|
81
|
+
}
|
|
65
82
|
export async function runInstaller(fixture, options = {}) {
|
|
66
83
|
const lifecycle = options.lifecycle ?? 'persistent';
|
|
67
84
|
const installScripts = options.installScripts ?? true;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { persistentDependencyTreeIsCold, persistentMetadataFingerprintV4, persistentMetadataStatusV4, writePersistentMetadataStampV4, } from './metadata.mjs';
|
|
2
2
|
import { runPnpm } from './exec.mjs';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { mismatchedNativeBinaries } from './native-health.mjs';
|
|
4
|
+
// oxfmt-ignore
|
|
5
|
+
import { clearPendingDependencyBuilds, pendingBuildDelta, pendingBuilds, validDependencyBuildIds } from './pending-builds.mjs';
|
|
6
|
+
// oxfmt-ignore
|
|
7
|
+
import { install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
|
|
8
|
+
// oxfmt-ignore
|
|
9
|
+
import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches } from './support.mjs';
|
|
7
10
|
import { persistentInstallTransition, persistentProvenanceDiagnostic } from './transition.mjs';
|
|
8
11
|
// oxfmt-ignore
|
|
9
12
|
const fail = (message) => { throw new Error(message); };
|
|
@@ -13,7 +16,20 @@ async function persistent(options) {
|
|
|
13
16
|
const runCapture = (args) => runPnpm(args, options, true);
|
|
14
17
|
const fingerprint = await persistentMetadataFingerprintV4(runCapture);
|
|
15
18
|
const provenance = await persistentMetadataStatusV4(fingerprint);
|
|
16
|
-
const
|
|
19
|
+
const mismatchedNatives = await mismatchedNativeBinaries();
|
|
20
|
+
const nativesMatch = mismatchedNatives.length === 0;
|
|
21
|
+
const repairedNativeMismatch = !nativesMatch &&
|
|
22
|
+
provenance.kind === 'matching' &&
|
|
23
|
+
(await repairIsolatedNativeMismatch(options, runCapture, mismatchedNatives));
|
|
24
|
+
if (repairedNativeMismatch) {
|
|
25
|
+
console.warn('persistent optional native binaries do not match this runtime; reconciled');
|
|
26
|
+
console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
|
|
27
|
+
action: 'reconcile',
|
|
28
|
+
reason: 'native-health-mismatch',
|
|
29
|
+
}));
|
|
30
|
+
await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
|
|
31
|
+
return 'persistent native health reconciled';
|
|
32
|
+
}
|
|
17
33
|
const provisionalTransition = persistentInstallTransition(provenance, options.installScripts);
|
|
18
34
|
let transition = nativesMatch
|
|
19
35
|
? provisionalTransition
|
|
@@ -23,6 +23,7 @@ export type Workspace = {
|
|
|
23
23
|
path: string;
|
|
24
24
|
};
|
|
25
25
|
export declare const baseInstallArgs: string[];
|
|
26
|
+
export declare const forcedInstallArgs: string[];
|
|
26
27
|
export declare function parseInstallOptions(argv: string[]): InstallOptions;
|
|
27
28
|
export declare function listWorkspaces(runCapture: CaptureCommand): Promise<Workspace[]>;
|
|
28
29
|
export declare function findWorkspaceLinkMismatches(runCapture: CaptureCommand): Promise<WorkspaceLinkMismatch[]>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
'install',
|
|
5
|
-
'--frozen-lockfile',
|
|
3
|
+
const commonInstallArgs = [
|
|
6
4
|
'--prefer-offline',
|
|
7
5
|
'--prod=false',
|
|
8
6
|
'--config.disallow-workspace-cycles=false',
|
|
9
7
|
];
|
|
8
|
+
export const baseInstallArgs = ['install', '--frozen-lockfile', ...commonInstallArgs];
|
|
9
|
+
export const forcedInstallArgs = ['install', '--frozen-lockfile', '--force', ...commonInstallArgs];
|
|
10
10
|
const usage = 'usage: vouchington pnpm-install --runner-lifecycle persistent|ephemeral|ephemeral-full --install-scripts true|false [--ephemeral-workspaces <newline-separated selectors>] [--command-timeout-seconds <nonnegative integer>] [--max-attempts <positive integer>]';
|
|
11
11
|
const MAX_COMMAND_TIMEOUT_SECONDS = 3600;
|
|
12
12
|
const MAX_ATTEMPTS = 10;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { writeVitestBlobManifest } from './index.mjs';
|
|
5
|
+
import { parseGitHubRunAttempt } from './run-attempt.mjs';
|
|
5
6
|
function failRepository() {
|
|
6
7
|
throw new Error('GITHUB_REPOSITORY is required');
|
|
7
8
|
}
|
|
@@ -15,10 +16,7 @@ export function runVitestBlobManifestCli(args, env = process.env, revision = exe
|
|
|
15
16
|
if (!suite || extra.length > 0 || !runId || !rawAttempt) {
|
|
16
17
|
throw new Error('Usage: vouchington vitest-blob-manifest <suite> [reports-directory]');
|
|
17
18
|
}
|
|
18
|
-
const runAttempt =
|
|
19
|
-
if (!Number.isSafeInteger(runAttempt) || runAttempt < 1) {
|
|
20
|
-
throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
|
|
21
|
-
}
|
|
19
|
+
const runAttempt = parseGitHubRunAttempt(rawAttempt);
|
|
22
20
|
writeVitestBlobManifest(directory, {
|
|
23
21
|
suite,
|
|
24
22
|
repository: env.GITHUB_REPOSITORY || failRepository(),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readVitestReportAttempts, writeVitestReportAttempt, } from './report-attempt.mjs';
|
|
2
|
+
import { parseGitHubRunAttempt } from './run-attempt.mjs';
|
|
3
|
+
function required(env, name) {
|
|
4
|
+
const value = env[name];
|
|
5
|
+
if (!value)
|
|
6
|
+
throw new Error(`${name} is required`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function identity(env) {
|
|
10
|
+
return {
|
|
11
|
+
repository: required(env, 'GITHUB_REPOSITORY'),
|
|
12
|
+
revision: required(env, 'GITHUB_SHA'),
|
|
13
|
+
runId: required(env, 'GITHUB_RUN_ID'),
|
|
14
|
+
attempt: parseGitHubRunAttempt(env.GITHUB_RUN_ATTEMPT),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Writes or reads authenticated-for-run Vitest report-attempt markers. */
|
|
18
|
+
export function runVitestReportAttemptCli(args, env = process.env, log = (line) => process.stdout.write(`${line}\n`)) {
|
|
19
|
+
const [command, path, suite, ...extra] = args;
|
|
20
|
+
if (extra.length > 0 || !path || (command !== 'write' && command !== 'read'))
|
|
21
|
+
throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
|
|
22
|
+
const current = identity(env);
|
|
23
|
+
if (command === 'write') {
|
|
24
|
+
if (!suite)
|
|
25
|
+
throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
|
|
26
|
+
writeVitestReportAttempt(path, suite, current);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (suite)
|
|
30
|
+
throw new Error('Usage: vouchington vitest-report-attempt <write DIRECTORY SUITE|read ROOT>');
|
|
31
|
+
log(JSON.stringify(readVitestReportAttempts(path, current)));
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { VITEST_SUITE_PATTERN } from './constants.mjs';
|
|
2
|
+
import { prepareVitestReports } from './reports.mjs';
|
|
3
|
+
import { parseGitHubRunAttempt } from './run-attempt.mjs';
|
|
4
|
+
function required(env, name) {
|
|
5
|
+
const value = env[name];
|
|
6
|
+
if (!value)
|
|
7
|
+
throw new Error(`${name} is required`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function parseContext(raw, attempt) {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
13
|
+
throw new Error('Vitest report expectation context must be an object');
|
|
14
|
+
const context = parsed;
|
|
15
|
+
if (Object.keys(context).toSorted().join('\0') !== ['attempt', 'suites', 'version'].join('\0') ||
|
|
16
|
+
context.version !== 'vitest-report-expectations:v2' ||
|
|
17
|
+
context.attempt !== attempt ||
|
|
18
|
+
!Array.isArray(context.suites) ||
|
|
19
|
+
context.suites.some((expectation) => typeof expectation !== 'object' ||
|
|
20
|
+
expectation === null ||
|
|
21
|
+
Array.isArray(expectation) ||
|
|
22
|
+
Object.keys(expectation).toSorted().join('\0') !== ['minimumAttempt', 'suite'].join('\0') ||
|
|
23
|
+
typeof expectation.suite !== 'string' ||
|
|
24
|
+
!VITEST_SUITE_PATTERN.test(expectation.suite) ||
|
|
25
|
+
!Number.isSafeInteger(expectation.minimumAttempt) ||
|
|
26
|
+
Number(expectation.minimumAttempt) < 1 ||
|
|
27
|
+
Number(expectation.minimumAttempt) > attempt))
|
|
28
|
+
throw new Error('Vitest report expectation context has an invalid schema');
|
|
29
|
+
const typed = context;
|
|
30
|
+
const suites = typed.suites.map((expectation) => expectation.suite);
|
|
31
|
+
if (new Set(suites).size !== suites.length || suites.join('\0') !== suites.toSorted().join('\0'))
|
|
32
|
+
throw new Error('Vitest report expectation suites must be unique and sorted');
|
|
33
|
+
return typed;
|
|
34
|
+
}
|
|
35
|
+
/** Validates GitHub run context, then prepares the selected report JSON files. */
|
|
36
|
+
export function runPrepareVitestReportsCli(args, env = process.env, log = (line) => process.stdout.write(`${line}\n`)) {
|
|
37
|
+
const [primaryDir = './vitest-blob-primary', fallbackDir = './vitest-blob-fallback', outputDir = './vitest-blob-reports/merge-input', ...extra] = args;
|
|
38
|
+
if (extra.length > 0)
|
|
39
|
+
throw new Error('Expected at most three Vitest report directories');
|
|
40
|
+
const currentAttempt = parseGitHubRunAttempt(env.GITHUB_RUN_ATTEMPT);
|
|
41
|
+
const result = prepareVitestReports({
|
|
42
|
+
primaryDir,
|
|
43
|
+
fallbackDir,
|
|
44
|
+
outputDir,
|
|
45
|
+
expectedSuites: parseContext(required(env, 'VITEST_REPORT_EXPECTATIONS'), currentAttempt)
|
|
46
|
+
.suites,
|
|
47
|
+
repository: required(env, 'GITHUB_REPOSITORY'),
|
|
48
|
+
revision: required(env, 'GITHUB_SHA'),
|
|
49
|
+
run: { id: required(env, 'GITHUB_RUN_ID'), currentAttempt },
|
|
50
|
+
});
|
|
51
|
+
for (const rejected of result.rejectedSources)
|
|
52
|
+
log(`::warning::Rejected Vitest ${rejected.source} report source: ${rejected.reason}`);
|
|
53
|
+
for (const selected of result.selected)
|
|
54
|
+
log(`Selected Vitest report ${selected.suite} from attempt ${selected.attempt} (${selected.sources.join('+')})`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Reads a GitHub Actions attempt as a canonical, positive decimal integer. */
|
|
2
|
+
export function parseGitHubRunAttempt(rawAttempt) {
|
|
3
|
+
if (!rawAttempt)
|
|
4
|
+
throw new Error('GITHUB_RUN_ATTEMPT is required');
|
|
5
|
+
if (!/^[1-9][0-9]*$/.test(rawAttempt))
|
|
6
|
+
throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
|
|
7
|
+
const attempt = Number(rawAttempt);
|
|
8
|
+
if (!Number.isSafeInteger(attempt))
|
|
9
|
+
throw new Error('GITHUB_RUN_ATTEMPT must be a positive integer');
|
|
10
|
+
return attempt;
|
|
11
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { findExpectations } from './manifest-version-parser.mjs';
|
|
4
|
+
import { buildDependencyMatchers, SEMVER_LITERAL } from './manifest-version-patterns.mjs';
|
|
5
|
+
const TEST_SOURCE_FILE = /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
6
|
+
const PACKAGE_JSON_FILE = /(?:^|\/)package\.json$/u;
|
|
7
|
+
const TEST_OR_FIXTURE_DIRECTORY = /(?:^|\/)(?:test|tests|__tests__|fixture|fixtures|__fixtures__)(?:\/|$)/u;
|
|
8
|
+
const DEPENDENCY_FIELD = /\b(?:dependencies|devDependencies|optionalDependencies|peerDependencies)\b/u;
|
|
9
|
+
const STRING_LITERAL = /'([^'\\]*(?:\\.[^'\\]*)*)'|"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\$]*(?:\\.[^`\\$]*)*)`/gu;
|
|
10
|
+
function readTrackedSource(ctx, file) {
|
|
11
|
+
try {
|
|
12
|
+
return ctx.readTrackedFile
|
|
13
|
+
? ctx.readTrackedFile(file)
|
|
14
|
+
: readFileSync(join(ctx.repoRoot, file), 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function dependencyNames(ctx) {
|
|
21
|
+
const names = new Set();
|
|
22
|
+
for (const file of ctx.trackedFiles) {
|
|
23
|
+
if (!PACKAGE_JSON_FILE.test(file) || TEST_OR_FIXTURE_DIRECTORY.test(file))
|
|
24
|
+
continue;
|
|
25
|
+
const source = readTrackedSource(ctx, file);
|
|
26
|
+
if (source === null)
|
|
27
|
+
continue;
|
|
28
|
+
let manifest;
|
|
29
|
+
try {
|
|
30
|
+
manifest = JSON.parse(source);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
|
|
36
|
+
continue;
|
|
37
|
+
for (const field of [
|
|
38
|
+
'dependencies',
|
|
39
|
+
'devDependencies',
|
|
40
|
+
'optionalDependencies',
|
|
41
|
+
'peerDependencies',
|
|
42
|
+
]) {
|
|
43
|
+
const dependencies = manifest[field];
|
|
44
|
+
if (typeof dependencies !== 'object' || dependencies === null || Array.isArray(dependencies))
|
|
45
|
+
continue;
|
|
46
|
+
for (const name of Object.keys(dependencies))
|
|
47
|
+
names.add(name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
52
|
+
function literalValues(source) {
|
|
53
|
+
return [...source.matchAll(STRING_LITERAL)].map((match) => (match[1] ?? match[2] ?? match[3]));
|
|
54
|
+
}
|
|
55
|
+
function expectedDependencyNames(values, names) {
|
|
56
|
+
return values.some((value) => SEMVER_LITERAL.test(value))
|
|
57
|
+
? values.filter((value) => names.has(value))
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
/** Keeps Dependabot updates independent from literal dependency-version test assertions. */
|
|
61
|
+
export function checkManifestDependencyVersionAssertions(ctx, errors) {
|
|
62
|
+
const names = dependencyNames(ctx);
|
|
63
|
+
if (names.size === 0)
|
|
64
|
+
return;
|
|
65
|
+
const matchers = buildDependencyMatchers(names);
|
|
66
|
+
for (const file of ctx.trackedFiles) {
|
|
67
|
+
if (!TEST_SOURCE_FILE.test(file))
|
|
68
|
+
continue;
|
|
69
|
+
const source = readTrackedSource(ctx, file);
|
|
70
|
+
if (source === null) {
|
|
71
|
+
errors.push(`::error file=${file}::${file}: failed to read test source for manifest dependency version assertions`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const expectation of findExpectations(source)) {
|
|
75
|
+
const asserted = new Set();
|
|
76
|
+
if (DEPENDENCY_FIELD.test(expectation.expression)) {
|
|
77
|
+
const values = literalValues(expectation.expected);
|
|
78
|
+
if (values.length === 1 && SEMVER_LITERAL.test(values[0])) {
|
|
79
|
+
for (const matcher of matchers)
|
|
80
|
+
if (matcher.member.test(expectation.expression))
|
|
81
|
+
asserted.add(matcher.name);
|
|
82
|
+
}
|
|
83
|
+
for (const name of expectedDependencyNames(values, names))
|
|
84
|
+
asserted.add(name);
|
|
85
|
+
for (const matcher of matchers)
|
|
86
|
+
if (matcher.objectValue.test(expectation.expected))
|
|
87
|
+
asserted.add(matcher.name);
|
|
88
|
+
}
|
|
89
|
+
for (const value of literalValues(expectation.expected)) {
|
|
90
|
+
for (const matcher of matchers)
|
|
91
|
+
if (matcher.packageSpec.test(value))
|
|
92
|
+
asserted.add(matcher.name);
|
|
93
|
+
}
|
|
94
|
+
for (const name of asserted) {
|
|
95
|
+
const line = source.slice(0, expectation.index).split('\n').length;
|
|
96
|
+
errors.push(`::error file=${file},line=${line}::${file}:${line}: tests must not assert the exact version of declared dependency "${name}"; assert dependency membership or derive the value from the manifest instead`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|