vouchington-tooling 0.11.1 → 0.12.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 +5 -0
- package/dist/gh-cli/body-length.d.mts +15 -0
- package/dist/gh-cli/body-length.mjs +16 -0
- package/dist/gh-cli/index.d.mts +2 -0
- package/dist/gh-cli/index.mjs +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/dist/pnpm-install/install-operations.d.mts +2 -1
- package/dist/pnpm-install/install-operations.mjs +16 -4
- package/dist/pnpm-install/metadata.d.mts +3 -3
- package/dist/pnpm-install/metadata.mjs +9 -24
- package/dist/pnpm-install/pending-builds.d.mts +6 -14
- package/dist/pnpm-install/pending-builds.mjs +23 -93
- package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +15 -1
- package/dist/pnpm-install/runner.mjs +27 -59
- package/dist/pnpm-install/transition.d.mts +2 -7
- package/dist/pnpm-install/transition.mjs +3 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,6 +215,7 @@ import {
|
|
|
215
215
|
import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
|
|
216
216
|
import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackboard'
|
|
217
217
|
import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
|
|
218
|
+
import { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from 'vouchington-tooling'
|
|
218
219
|
import { createPullRequest, getDiffAgainstBase, runGh, runGit } from 'vouchington-tooling/gh-cli'
|
|
219
220
|
import {
|
|
220
221
|
shellScriptViolations,
|
|
@@ -231,6 +232,10 @@ throws on a `run:` value that is a YAML alias or a multiline PLAIN scalar, shape
|
|
|
231
232
|
scan safely. Both functions scan already-in-scope source text — deciding which files count as a
|
|
232
233
|
shell script or a workflow/action YAML file is left to the caller.
|
|
233
234
|
|
|
235
|
+
`validateGitHubBodyLength` measures an issue or pull-request body against GitHub's 65,536 Unicode
|
|
236
|
+
code-point limit without altering it. Its result includes the UTF-8 byte count for diagnostics; that
|
|
237
|
+
byte count is not a validation limit.
|
|
238
|
+
|
|
234
239
|
`agent-harness-config` merges classifier-auto and sandbox keys into Claude, Codex, Grok, and Cursor
|
|
235
240
|
config files. See [docs/agent-harness-config.md](./docs/agent-harness-config.md). `--global` updates
|
|
236
241
|
home-directory configs; `--repo` updates a checkout. It does not copy allowlists, hooks, or plugins.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** GitHub's maximum issue or pull-request body length, counted in Unicode code points. */
|
|
2
|
+
export declare const GITHUB_BODY_MAX_CHARACTERS = 65536;
|
|
3
|
+
/** The measured GitHub body length and whether it fits within GitHub's character limit. */
|
|
4
|
+
export interface GitHubBodyLengthValidation {
|
|
5
|
+
readonly ok: boolean;
|
|
6
|
+
readonly characterCount: number;
|
|
7
|
+
readonly utf8ByteCount: number;
|
|
8
|
+
readonly maxCharacterCount: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Measures a GitHub issue or pull-request body without altering its content.
|
|
12
|
+
*
|
|
13
|
+
* GitHub's limit is evaluated in Unicode code points. UTF-8 bytes are returned only for diagnostics.
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateGitHubBodyLength(body: string): GitHubBodyLengthValidation;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** GitHub's maximum issue or pull-request body length, counted in Unicode code points. */
|
|
2
|
+
export const GITHUB_BODY_MAX_CHARACTERS = 65_536;
|
|
3
|
+
/**
|
|
4
|
+
* Measures a GitHub issue or pull-request body without altering its content.
|
|
5
|
+
*
|
|
6
|
+
* GitHub's limit is evaluated in Unicode code points. UTF-8 bytes are returned only for diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
export function validateGitHubBodyLength(body) {
|
|
9
|
+
const characterCount = Array.from(body).length;
|
|
10
|
+
return {
|
|
11
|
+
ok: characterCount <= GITHUB_BODY_MAX_CHARACTERS,
|
|
12
|
+
characterCount,
|
|
13
|
+
utf8ByteCount: Buffer.byteLength(body, 'utf8'),
|
|
14
|
+
maxCharacterCount: GITHUB_BODY_MAX_CHARACTERS,
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/gh-cli/index.d.mts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createCommandRunner, runGh, runGit } from './exec.mts';
|
|
2
2
|
export type { ExecFileText, RunTextCommand } from './exec.mts';
|
|
3
3
|
export { getDiffAgainstBase } from './diff.mts';
|
|
4
|
+
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './body-length.mts';
|
|
5
|
+
export type { GitHubBodyLengthValidation } from './body-length.mts';
|
|
4
6
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mts';
|
|
5
7
|
export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './pr-create.mts';
|
package/dist/gh-cli/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createCommandRunner, runGh, runGit } from './exec.mjs';
|
|
2
2
|
export { getDiffAgainstBase } from './diff.mjs';
|
|
3
|
+
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './body-length.mjs';
|
|
3
4
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './pr-create.mjs';
|
package/dist/index.d.mts
CHANGED
|
@@ -89,5 +89,7 @@ export type { DiagnosticReportLimitOptions, DiagnosticReportSummary, } from './v
|
|
|
89
89
|
export { createCommandRunner, runGh, runGit } from './gh-cli/index.mts';
|
|
90
90
|
export type { ExecFileText, RunTextCommand } from './gh-cli/index.mts';
|
|
91
91
|
export { getDiffAgainstBase } from './gh-cli/index.mts';
|
|
92
|
+
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './gh-cli/index.mts';
|
|
93
|
+
export type { GitHubBodyLengthValidation } from './gh-cli/index.mts';
|
|
92
94
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mts';
|
|
93
95
|
export type { AssertHeadPushedOptions, BuildGhPrCreateArgsOptions, CreatePullRequestExecutors, CreatePullRequestOptions, } from './gh-cli/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -50,4 +50,5 @@ export { validateResolvedPinDelta } from './swift-resolved-pin-delta/index.mjs';
|
|
|
50
50
|
export { DEFAULT_MAX_DIAGNOSTIC_REPORTS, DEFAULT_MAX_FORMATTED_DIAGNOSTIC_REPORTS, formatDiagnosticReportSummaries, HARD_MAX_DIAGNOSTIC_REPORTS, readDiagnosticReportSummaries, summarizeDiagnosticReport, } from './vitest-diagnostics/index.mjs';
|
|
51
51
|
export { createCommandRunner, runGh, runGit } from './gh-cli/index.mjs';
|
|
52
52
|
export { getDiffAgainstBase } from './gh-cli/index.mjs';
|
|
53
|
+
export { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from './gh-cli/index.mjs';
|
|
53
54
|
export { assertHeadPushed, buildGhPrCreateArgs, createPullRequest, DetachedHeadError, HeadNotPushedError, HeadOutOfDateError, resolveHeadBranch, } from './gh-cli/index.mjs';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
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
|
-
export declare function reconcileOrFail(options: InstallOptions
|
|
4
|
+
export declare function reconcileOrFail(options: InstallOptions): Promise<void>;
|
|
5
|
+
export declare function finalizePendingBuilds(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, phase: string): Promise<import("./pending-builds.mts").PendingBuildState>;
|
|
5
6
|
export declare function repairIsolatedNativeMismatch(options: InstallOptions, runCapture: (args: string[]) => Promise<CommandResult>, mismatchedNativePaths: string[]): Promise<boolean>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { scheduler } from 'node:timers/promises';
|
|
2
2
|
import { runPnpm } from './exec.mjs';
|
|
3
3
|
import { nativeBinariesMatchRuntime, repairedNativeBinariesMatchRuntime } from './native-health.mjs';
|
|
4
|
-
import { buildLedgersAllowNativeRepair } from './pending-builds.mjs';
|
|
4
|
+
import { buildLedgersAllowNativeRepair, pendingBuilds } from './pending-builds.mjs';
|
|
5
5
|
import { INSTALL_TERMINATION_FAILED } from './process.mjs';
|
|
6
6
|
import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
|
|
7
7
|
import { findWorkspaceLinkMismatches, forcedInstallArgs, logWorkspaceLinkMismatches, } from './support.mjs';
|
|
@@ -28,10 +28,9 @@ export async function install(args, options, label) {
|
|
|
28
28
|
}
|
|
29
29
|
fail(`${label} failed after ${options.maxAttempts} attempt${options.maxAttempts === 1 ? '' : 's'}`);
|
|
30
30
|
}
|
|
31
|
-
export async function reconcileOrFail(options
|
|
31
|
+
export async function reconcileOrFail(options) {
|
|
32
32
|
await install([...forcedInstallArgs, '--ignore-scripts', '--ignore-pnpmfile'], options, 'script-free reconciliation');
|
|
33
|
-
await install(
|
|
34
|
-
await verifyInstallHealth(runCapture, 'persistent reconciliation');
|
|
33
|
+
await install([...forcedInstallArgs, '--ignore-scripts'], options, 'second script-free persistent reconciliation');
|
|
35
34
|
}
|
|
36
35
|
async function verifyInstallHealth(runCapture, phase, repairedNativePaths = []) {
|
|
37
36
|
const [nativesMatch, remaining] = await Promise.all([
|
|
@@ -47,6 +46,19 @@ async function verifyInstallHealth(runCapture, phase, repairedNativePaths = [])
|
|
|
47
46
|
fail(`${phase} completed with invalid workspace links`);
|
|
48
47
|
}
|
|
49
48
|
}
|
|
49
|
+
export async function finalizePendingBuilds(options, runCapture, phase) {
|
|
50
|
+
const before = await pendingBuilds();
|
|
51
|
+
if (options.installScripts && before.kind === 'unknown')
|
|
52
|
+
fail(`${phase} completed without a clear pending build ledger`);
|
|
53
|
+
if (options.installScripts && before.kind === 'pending') {
|
|
54
|
+
await install(['rebuild', '--pending', '--recursive'], options, 'pending scripts rebuild');
|
|
55
|
+
const after = await pendingBuilds();
|
|
56
|
+
if (after.kind !== 'clear')
|
|
57
|
+
fail(`${phase} completed without a clear pending build ledger`);
|
|
58
|
+
}
|
|
59
|
+
await verifyInstallHealth(runCapture, phase);
|
|
60
|
+
return options.installScripts ? { kind: 'clear' } : before;
|
|
61
|
+
}
|
|
50
62
|
export async function repairIsolatedNativeMismatch(options, runCapture, mismatchedNativePaths) {
|
|
51
63
|
if (!(await buildLedgersAllowNativeRepair()))
|
|
52
64
|
return false;
|
|
@@ -3,7 +3,7 @@ import { type ProvenanceStatus } from './transition.mts';
|
|
|
3
3
|
declare const componentNames: readonly ['lockfile', 'npm-config', 'pnpm', 'pnpmfiles', 'runtime', 'workspace-config', 'workspace-manifests'];
|
|
4
4
|
type ComponentName = (typeof componentNames)[number];
|
|
5
5
|
type StructuralProvenance = Record<ComponentName, string>;
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function persistentMetadataFingerprintV5(runCapture: CaptureCommand): Promise<{
|
|
7
7
|
lockfile: string;
|
|
8
8
|
'npm-config': string;
|
|
9
9
|
pnpm: string;
|
|
@@ -12,7 +12,7 @@ export declare function persistentMetadataFingerprintV4(runCapture: CaptureComma
|
|
|
12
12
|
'workspace-config': string;
|
|
13
13
|
'workspace-manifests': string;
|
|
14
14
|
}>;
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
15
|
+
export declare function persistentMetadataStatusV5(provenance: StructuralProvenance): Promise<ProvenanceStatus>;
|
|
16
|
+
export declare function writePersistentMetadataStampV5(provenance: StructuralProvenance, installScripts: boolean, scriptsEnabledInstallVerified: boolean): Promise<void>;
|
|
17
17
|
export declare function persistentDependencyTreeIsCold(): Promise<boolean>;
|
|
18
18
|
export {};
|
|
@@ -47,7 +47,7 @@ function runtimePlatformIdentity() {
|
|
|
47
47
|
platform: process.platform,
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
export async function
|
|
50
|
+
export async function persistentMetadataFingerprintV5(runCapture) {
|
|
51
51
|
const workspaces = (await listWorkspaces(runCapture)).toSorted((left, right) => left.path.localeCompare(right.path));
|
|
52
52
|
const pnpmVersion = await runCapture(['--version']);
|
|
53
53
|
if (pnpmVersion.code !== 0)
|
|
@@ -75,15 +75,12 @@ function validStamp(value) {
|
|
|
75
75
|
return false;
|
|
76
76
|
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish the candidate stamp type before complete field validation
|
|
77
77
|
const stamp = value;
|
|
78
|
-
return (stamp.version ===
|
|
78
|
+
return (stamp.version === 5 &&
|
|
79
79
|
typeof stamp.lastInvocationInstallScripts === 'boolean' &&
|
|
80
|
-
typeof stamp.
|
|
80
|
+
typeof stamp.scriptsEnabledInstallVerified === 'boolean' &&
|
|
81
81
|
typeof stamp.provenance === 'object' &&
|
|
82
82
|
stamp.provenance !== null &&
|
|
83
|
-
componentNames.every((name) => typeof stamp.provenance?.[name] === 'string')
|
|
84
|
-
(stamp.pendingDependencyBuilds === undefined ||
|
|
85
|
-
(Array.isArray(stamp.pendingDependencyBuilds) &&
|
|
86
|
-
stamp.pendingDependencyBuilds.every((id) => typeof id === 'string'))));
|
|
83
|
+
componentNames.every((name) => typeof stamp.provenance?.[name] === 'string'));
|
|
87
84
|
}
|
|
88
85
|
async function readPersistentMetadataState() {
|
|
89
86
|
try {
|
|
@@ -98,11 +95,7 @@ async function readPersistentMetadataState() {
|
|
|
98
95
|
: { kind: 'unsafe' };
|
|
99
96
|
}
|
|
100
97
|
}
|
|
101
|
-
async function
|
|
102
|
-
const state = await readPersistentMetadataState();
|
|
103
|
-
return state.kind === 'stamp' ? state.stamp : undefined;
|
|
104
|
-
}
|
|
105
|
-
export async function persistentMetadataStatusV4(provenance) {
|
|
98
|
+
export async function persistentMetadataStatusV5(provenance) {
|
|
106
99
|
const state = await readPersistentMetadataState();
|
|
107
100
|
if (state.kind === 'missing')
|
|
108
101
|
return { kind: 'absent' };
|
|
@@ -115,23 +108,15 @@ export async function persistentMetadataStatusV4(provenance) {
|
|
|
115
108
|
: {
|
|
116
109
|
kind: 'matching',
|
|
117
110
|
lastInvocationInstallScripts: stamp.lastInvocationInstallScripts,
|
|
118
|
-
|
|
119
|
-
scriptsEnabledInstallSucceeded: stamp.scriptsEnabledInstallSucceeded,
|
|
111
|
+
scriptsEnabledInstallVerified: stamp.scriptsEnabledInstallVerified,
|
|
120
112
|
};
|
|
121
113
|
}
|
|
122
|
-
export async function
|
|
123
|
-
const existing = await readPersistentMetadataStamp();
|
|
124
|
-
const existingMatches = existing && componentNames.every((name) => existing.provenance[name] === provenance[name]);
|
|
114
|
+
export async function writePersistentMetadataStampV5(provenance, installScripts, scriptsEnabledInstallVerified) {
|
|
125
115
|
const stamp = {
|
|
126
116
|
lastInvocationInstallScripts: installScripts,
|
|
127
|
-
pendingDependencyBuilds: (pendingDependencyBuilds ??
|
|
128
|
-
(existingMatches ? existing.pendingDependencyBuilds : undefined) ??
|
|
129
|
-
[]).toSorted(),
|
|
130
117
|
provenance,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
Boolean(existingMatches && existing.scriptsEnabledInstallSucceeded)),
|
|
134
|
-
version: 4,
|
|
118
|
+
scriptsEnabledInstallVerified,
|
|
119
|
+
version: 5,
|
|
135
120
|
};
|
|
136
121
|
const stampPath = persistentMetadataStampPath();
|
|
137
122
|
const directory = path.dirname(stampPath);
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
export type
|
|
2
|
-
kind: '
|
|
3
|
-
ids: Set<string>;
|
|
1
|
+
export type PendingBuildState = {
|
|
2
|
+
kind: 'clear';
|
|
4
3
|
} | {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type PendingBuildDelta = {
|
|
8
|
-
kind: 'unknown';
|
|
4
|
+
ids: string[];
|
|
5
|
+
kind: 'pending';
|
|
9
6
|
} | {
|
|
10
|
-
kind: '
|
|
11
|
-
dependencyIds: string[];
|
|
12
|
-
workspaceIds: string[];
|
|
7
|
+
kind: 'unknown';
|
|
13
8
|
};
|
|
14
|
-
export declare function pendingBuilds(): Promise<
|
|
9
|
+
export declare function pendingBuilds(): Promise<PendingBuildState>;
|
|
15
10
|
export declare function buildLedgersAllowNativeRepair(): Promise<boolean>;
|
|
16
|
-
export declare function validDependencyBuildIds(ids: [string, ...string[]]): Promise<[string, ...string[]] | undefined>;
|
|
17
|
-
export declare function clearPendingDependencyBuilds(ids: string[]): Promise<boolean>;
|
|
18
|
-
export declare function pendingBuildDelta(before: PendingBuilds, after: PendingBuilds): Promise<PendingBuildDelta>;
|
|
@@ -1,106 +1,36 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { parse
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
|
|
7
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
8
|
-
return { kind: 'unknown' };
|
|
9
|
-
const pending = value.pendingBuilds ?? [];
|
|
10
|
-
if (!Array.isArray(pending) || !pending.every((id) => typeof id === 'string'))
|
|
11
|
-
return { kind: 'unknown' };
|
|
12
|
-
return { kind: 'known', ids: new Set(pending) };
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return { kind: 'unknown' };
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export async function buildLedgersAllowNativeRepair() {
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
async function buildLedgers() {
|
|
19
5
|
try {
|
|
20
6
|
const value = parse(await readFile(path.join(process.cwd(), 'node_modules', '.modules.yaml'), 'utf8'));
|
|
21
7
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
22
|
-
return false;
|
|
23
|
-
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a record view after validating the pnpm manifest object
|
|
24
|
-
const record = value;
|
|
25
|
-
const ignored = record.ignoredBuilds;
|
|
26
|
-
const pending = record.pendingBuilds ?? [];
|
|
27
|
-
return (Array.isArray(ignored) &&
|
|
28
|
-
ignored.length === 0 &&
|
|
29
|
-
Array.isArray(pending) &&
|
|
30
|
-
pending.length === 0);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function lockfileDependencyIds() {
|
|
37
|
-
try {
|
|
38
|
-
const lockfile = parse(await readFile(path.join(process.cwd(), 'pnpm-lock.yaml'), 'utf8'));
|
|
39
|
-
if (typeof lockfile !== 'object' || lockfile === null || !('packages' in lockfile))
|
|
40
8
|
return undefined;
|
|
41
|
-
const
|
|
42
|
-
|
|
9
|
+
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- validate the parsed YAML object before reading its fields
|
|
10
|
+
const record = value;
|
|
11
|
+
const ignored = Object.hasOwn(record, 'ignoredBuilds') ? record.ignoredBuilds : undefined;
|
|
12
|
+
const pending = Object.hasOwn(record, 'pendingBuilds') ? record.pendingBuilds : [];
|
|
13
|
+
if ((ignored !== undefined &&
|
|
14
|
+
(!Array.isArray(ignored) || !ignored.every((id) => typeof id === 'string'))) ||
|
|
15
|
+
!Array.isArray(pending) ||
|
|
16
|
+
!pending.every((id) => typeof id === 'string'))
|
|
43
17
|
return undefined;
|
|
44
|
-
return
|
|
18
|
+
return {
|
|
19
|
+
ignoredBuilds: ignored,
|
|
20
|
+
pendingBuilds: pending.length === 0 ? { kind: 'clear' } : { ids: pending.toSorted(), kind: 'pending' },
|
|
21
|
+
};
|
|
45
22
|
}
|
|
46
23
|
catch {
|
|
47
24
|
return undefined;
|
|
48
25
|
}
|
|
49
26
|
}
|
|
50
|
-
export async function
|
|
51
|
-
|
|
52
|
-
if (!packages || !ids.every((id) => packages.has(id)))
|
|
53
|
-
return undefined;
|
|
54
|
-
const [first, ...remaining] = ids.toSorted();
|
|
55
|
-
return [first, ...remaining];
|
|
56
|
-
}
|
|
57
|
-
export async function clearPendingDependencyBuilds(ids) {
|
|
58
|
-
const filename = path.join(process.cwd(), 'node_modules', '.modules.yaml');
|
|
59
|
-
try {
|
|
60
|
-
const value = parse(await readFile(filename, 'utf8'));
|
|
61
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
62
|
-
return false;
|
|
63
|
-
// oxlint-disable-next-line no-mistakes/ts-no-const-aliases -- establish a record view after validating the pnpm manifest object
|
|
64
|
-
const record = value;
|
|
65
|
-
if (!Array.isArray(record.pendingBuilds) ||
|
|
66
|
-
!record.pendingBuilds.every((id) => typeof id === 'string'))
|
|
67
|
-
return false;
|
|
68
|
-
const ignored = record.ignoredBuilds ?? [];
|
|
69
|
-
if (!Array.isArray(ignored) ||
|
|
70
|
-
!ignored.every((id) => typeof id === 'string') ||
|
|
71
|
-
ids.some((id) => ignored.includes(id)))
|
|
72
|
-
return false;
|
|
73
|
-
record.pendingBuilds = record.pendingBuilds.filter((id) => !ids.includes(id));
|
|
74
|
-
const temporary = `${filename}.${process.pid}.tmp`;
|
|
75
|
-
try {
|
|
76
|
-
await writeFile(temporary, stringify(record));
|
|
77
|
-
await rename(temporary, filename);
|
|
78
|
-
}
|
|
79
|
-
finally {
|
|
80
|
-
await rm(temporary, { force: true });
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
27
|
+
export async function pendingBuilds() {
|
|
28
|
+
return (await buildLedgers())?.pendingBuilds ?? { kind: 'unknown' };
|
|
87
29
|
}
|
|
88
|
-
export async function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const dependencyIds = [];
|
|
95
|
-
const workspaceIds = [];
|
|
96
|
-
for (const id of after.ids) {
|
|
97
|
-
if (before.ids.has(id))
|
|
98
|
-
continue;
|
|
99
|
-
(packages.has(id) ? dependencyIds : workspaceIds).push(id);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
kind: 'known',
|
|
103
|
-
dependencyIds: dependencyIds.toSorted(),
|
|
104
|
-
workspaceIds: workspaceIds.toSorted(),
|
|
105
|
-
};
|
|
30
|
+
export async function buildLedgersAllowNativeRepair() {
|
|
31
|
+
const ledgers = await buildLedgers();
|
|
32
|
+
return (ledgers !== undefined &&
|
|
33
|
+
ledgers.ignoredBuilds !== undefined &&
|
|
34
|
+
ledgers.ignoredBuilds.length === 0 &&
|
|
35
|
+
ledgers.pendingBuilds.kind === 'clear');
|
|
106
36
|
}
|
|
@@ -29,18 +29,32 @@ if [ "\${PNPM_FAIL_RELEASE_AGE_CALL:-0}" = "$calls" ]; then
|
|
|
29
29
|
exit 1
|
|
30
30
|
fi
|
|
31
31
|
if [ -n "\${PNPM_SLEEP_SECONDS:-}" ]; then sleep "$PNPM_SLEEP_SECONDS"; fi
|
|
32
|
-
if [
|
|
32
|
+
if [ "\${PNPM_INVALID_PENDING_BUILDS:-0}" = 1 ]; then
|
|
33
|
+
mkdir -p "$PNPM_NODE_MODULES"
|
|
34
|
+
printf 'pendingBuilds: invalid\n' > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
35
|
+
elif [ -n "\${PNPM_PENDING_BUILDS:-}" ]; then
|
|
33
36
|
mkdir -p "$PNPM_NODE_MODULES"
|
|
34
37
|
printf 'pendingBuilds: [%s]\\n' "$PNPM_PENDING_BUILDS" > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
38
|
+
elif [ ! -f "$PNPM_NODE_MODULES/.modules.yaml" ]; then
|
|
39
|
+
mkdir -p "$PNPM_NODE_MODULES"
|
|
40
|
+
printf 'pendingBuilds: []\\n' > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
35
41
|
fi
|
|
36
42
|
case " $* " in
|
|
37
43
|
*' rebuild '*)
|
|
44
|
+
if [ "\${PNPM_REPAIR_NATIVE_ON_REBUILD:-0}" = 1 ]; then
|
|
45
|
+
cp "$PNPM_NATIVE_REPLACEMENT" "$PNPM_NATIVE_ADDON"
|
|
46
|
+
fi
|
|
38
47
|
if [ "\${PNPM_REBUILD_INVALID_LEDGER:-0}" = 1 ]; then
|
|
39
48
|
printf 'pendingBuilds: invalid\n' > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
49
|
+
else
|
|
50
|
+
printf 'pendingBuilds: []\n' > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
40
51
|
fi
|
|
41
52
|
if [ "\${PNPM_REBUILD_BREAK_LINK:-0}" = 1 ]; then rm -f "$PNPM_DEPENDENCY_LINK"; fi
|
|
42
53
|
;;
|
|
43
54
|
*' --force '*)
|
|
55
|
+
if [ -z "\${PNPM_PENDING_BUILDS:-}" ]; then
|
|
56
|
+
printf 'pendingBuilds: []\n' > "$PNPM_NODE_MODULES/.modules.yaml"
|
|
57
|
+
fi
|
|
44
58
|
if [ "\${PNPM_DELETE_NATIVE:-0}" = 1 ]; then
|
|
45
59
|
rm -f "$PNPM_NATIVE_ADDON"
|
|
46
60
|
elif [ "\${PNPM_REPAIR_NATIVE:-0}" = 1 ]; then
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { persistentDependencyTreeIsCold,
|
|
1
|
+
import { persistentDependencyTreeIsCold, persistentMetadataFingerprintV5, persistentMetadataStatusV5, writePersistentMetadataStampV5, } from './metadata.mjs';
|
|
2
2
|
import { runPnpm } from './exec.mjs';
|
|
3
3
|
import { mismatchedNativeBinaries } from './native-health.mjs';
|
|
4
4
|
// oxfmt-ignore
|
|
5
|
-
import {
|
|
5
|
+
import { pendingBuilds } from './pending-builds.mjs';
|
|
6
6
|
// oxfmt-ignore
|
|
7
|
-
import { install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
|
|
7
|
+
import { finalizePendingBuilds, install, reconcileOrFail, repairIsolatedNativeMismatch, withScriptPolicy } from './install-operations.mjs';
|
|
8
8
|
// oxfmt-ignore
|
|
9
9
|
import { baseInstallArgs, findWorkspaceLinkMismatches, logWorkspaceLinkMismatches } from './support.mjs';
|
|
10
10
|
import { persistentInstallTransition, persistentProvenanceDiagnostic } from './transition.mjs';
|
|
@@ -14,33 +14,30 @@ async function persistent(options) {
|
|
|
14
14
|
if (options.ephemeralWorkspaces.trim())
|
|
15
15
|
fail('ephemeral-workspaces is only valid for ephemeral runners');
|
|
16
16
|
const runCapture = (args) => runPnpm(args, options, true);
|
|
17
|
-
const fingerprint = await
|
|
18
|
-
const provenance = await
|
|
17
|
+
const fingerprint = await persistentMetadataFingerprintV5(runCapture);
|
|
18
|
+
const provenance = await persistentMetadataStatusV5(fingerprint);
|
|
19
|
+
const pendingBefore = await pendingBuilds();
|
|
19
20
|
const mismatchedNatives = await mismatchedNativeBinaries();
|
|
20
21
|
const nativesMatch = mismatchedNatives.length === 0;
|
|
21
22
|
const repairedNativeMismatch = !nativesMatch &&
|
|
22
23
|
provenance.kind === 'matching' &&
|
|
23
24
|
(await repairIsolatedNativeMismatch(options, runCapture, mismatchedNatives));
|
|
24
25
|
if (repairedNativeMismatch) {
|
|
26
|
+
const pendingAfterRepair = await finalizePendingBuilds(options, runCapture, 'native health reconciliation');
|
|
25
27
|
console.warn('persistent optional native binaries do not match this runtime; reconciled');
|
|
26
28
|
console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
|
|
27
29
|
action: 'reconcile',
|
|
28
30
|
reason: 'native-health-mismatch',
|
|
29
31
|
}));
|
|
30
|
-
await
|
|
32
|
+
await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterRepair));
|
|
31
33
|
return 'persistent native health reconciled';
|
|
32
34
|
}
|
|
33
35
|
const provisionalTransition = persistentInstallTransition(provenance, options.installScripts);
|
|
34
|
-
|
|
35
|
-
?
|
|
36
|
-
:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!ids)
|
|
40
|
-
transition = { action: 'upgrade-scripts', reason: 'invalid-pending-dependency-builds' };
|
|
41
|
-
else
|
|
42
|
-
transition = { ...transition, pendingDependencyBuilds: ids };
|
|
43
|
-
}
|
|
36
|
+
const transition = !nativesMatch
|
|
37
|
+
? { action: 'reconcile', reason: 'native-health-mismatch' }
|
|
38
|
+
: provenance.kind !== 'matching' || pendingBefore.kind === 'clear'
|
|
39
|
+
? provisionalTransition
|
|
40
|
+
: { action: 'reconcile', reason: 'pending-build-ledger-unverified' };
|
|
44
41
|
const provenanceOk = provenance.kind === 'matching' && transition.action !== 'reconcile' && nativesMatch;
|
|
45
42
|
const cold = !provenanceOk && (await persistentDependencyTreeIsCold());
|
|
46
43
|
if (!provenanceOk && !cold) {
|
|
@@ -51,54 +48,19 @@ async function persistent(options) {
|
|
|
51
48
|
console.warn(provenance.kind === 'matching' && !nativesMatch
|
|
52
49
|
? 'persistent optional native binaries do not match this runtime; reconciling'
|
|
53
50
|
: 'persistent dependency metadata provenance is missing or changed; reconciling');
|
|
54
|
-
await reconcileOrFail(options
|
|
55
|
-
await
|
|
51
|
+
await reconcileOrFail(options);
|
|
52
|
+
const pendingAfterReconcile = await finalizePendingBuilds(options, runCapture, 'persistent reconciliation');
|
|
53
|
+
await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterReconcile));
|
|
56
54
|
return 'persistent metadata reconciled';
|
|
57
55
|
}
|
|
58
56
|
if (provenance.kind === 'absent')
|
|
59
57
|
console.warn('persistent dependency tree is absent; installing cold');
|
|
60
|
-
|
|
61
|
-
await install(withScriptPolicy([...baseInstallArgs], transition.action.startsWith('upgrade-') ? false : options.installScripts), options, 'ordinary persistent install');
|
|
58
|
+
await install(withScriptPolicy([...baseInstallArgs], options.installScripts), options, 'ordinary persistent install');
|
|
62
59
|
const stale = await findWorkspaceLinkMismatches(runCapture);
|
|
63
60
|
if (stale.length === 0) {
|
|
64
|
-
|
|
65
|
-
const pendingDependencyBuilds = transition.action === 'upgrade-dependencies'
|
|
66
|
-
? transition.pendingDependencyBuilds
|
|
67
|
-
: undefined;
|
|
68
|
-
const rebuild = pendingDependencyBuilds === undefined
|
|
69
|
-
? ['rebuild', '--pending', '--recursive']
|
|
70
|
-
: ['rebuild', '--recursive', '--', ...pendingDependencyBuilds];
|
|
71
|
-
await install(rebuild, options, 'pending scripts rebuild');
|
|
72
|
-
const rebuiltStale = await findWorkspaceLinkMismatches(runCapture);
|
|
73
|
-
if (rebuiltStale.length > 0) {
|
|
74
|
-
logWorkspaceLinkMismatches(rebuiltStale);
|
|
75
|
-
await reconcileOrFail(options, runCapture);
|
|
76
|
-
console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, {
|
|
77
|
-
action: 'reconcile',
|
|
78
|
-
reason: 'workspace-links-stale-after-rebuild',
|
|
79
|
-
}));
|
|
80
|
-
await writePersistentMetadataStampV4(fingerprint, options.installScripts, true, []);
|
|
81
|
-
return 'persistent reconciled';
|
|
82
|
-
}
|
|
83
|
-
if (pendingDependencyBuilds !== undefined &&
|
|
84
|
-
!(await clearPendingDependencyBuilds(pendingDependencyBuilds)))
|
|
85
|
-
fail('dependency rebuild completed but pending build ledger could not be updated safely');
|
|
86
|
-
}
|
|
61
|
+
const pendingAfterInstall = await finalizePendingBuilds(options, runCapture, 'persistent install');
|
|
87
62
|
console.warn(persistentProvenanceDiagnostic(provenance, options.installScripts, nativesMatch, transition));
|
|
88
|
-
|
|
89
|
-
const delta = pendingBefore && pendingAfter
|
|
90
|
-
? await pendingBuildDelta(pendingBefore, pendingAfter)
|
|
91
|
-
: undefined;
|
|
92
|
-
const unsafePending = delta?.kind === 'unknown' || Boolean(delta?.workspaceIds.length);
|
|
93
|
-
const pendingDependencyBuilds = transition.action.startsWith('upgrade-') || unsafePending
|
|
94
|
-
? []
|
|
95
|
-
: [
|
|
96
|
-
...new Set([
|
|
97
|
-
...(provenance.kind === 'matching' ? provenance.pendingDependencyBuilds : []),
|
|
98
|
-
...(delta?.kind === 'known' ? delta.dependencyIds : []),
|
|
99
|
-
]),
|
|
100
|
-
].toSorted();
|
|
101
|
-
await writePersistentMetadataStampV4(fingerprint, options.installScripts, unsafePending, pendingDependencyBuilds);
|
|
63
|
+
await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterInstall));
|
|
102
64
|
return provenance.kind === 'absent' ? 'persistent cold' : 'persistent ordinary';
|
|
103
65
|
}
|
|
104
66
|
logWorkspaceLinkMismatches(stale);
|
|
@@ -106,10 +68,16 @@ async function persistent(options) {
|
|
|
106
68
|
action: 'reconcile',
|
|
107
69
|
reason: 'workspace-links-stale',
|
|
108
70
|
}));
|
|
109
|
-
await reconcileOrFail(options
|
|
110
|
-
await
|
|
71
|
+
await reconcileOrFail(options);
|
|
72
|
+
const pendingAfterReconcile = await finalizePendingBuilds(options, runCapture, 'persistent reconciliation');
|
|
73
|
+
await writePersistentMetadataStampV5(fingerprint, options.installScripts, options.installScripts || persistentStampRemainsVerified(provenance, pendingAfterReconcile));
|
|
111
74
|
return 'persistent reconciled';
|
|
112
75
|
}
|
|
76
|
+
function persistentStampRemainsVerified(provenance, pending) {
|
|
77
|
+
return (provenance.kind === 'matching' &&
|
|
78
|
+
provenance.scriptsEnabledInstallVerified &&
|
|
79
|
+
pending.kind === 'clear');
|
|
80
|
+
}
|
|
113
81
|
function isUnbracedPathClosureSelector(selector) {
|
|
114
82
|
return /^\.{0,2}\//.test(selector) && selector.endsWith('...') && !selector.startsWith('{');
|
|
115
83
|
}
|
|
@@ -6,17 +6,12 @@ export type ProvenanceStatus = {
|
|
|
6
6
|
} | {
|
|
7
7
|
kind: 'matching';
|
|
8
8
|
lastInvocationInstallScripts: boolean;
|
|
9
|
-
|
|
10
|
-
scriptsEnabledInstallSucceeded: boolean;
|
|
9
|
+
scriptsEnabledInstallVerified: boolean;
|
|
11
10
|
} | {
|
|
12
11
|
kind: 'unsafe';
|
|
13
12
|
};
|
|
14
13
|
export type PersistentInstallTransition = {
|
|
15
|
-
action: 'ordinary' | 'reconcile'
|
|
16
|
-
reason: string;
|
|
17
|
-
} | {
|
|
18
|
-
action: 'upgrade-dependencies';
|
|
19
|
-
pendingDependencyBuilds: [string, ...string[]];
|
|
14
|
+
action: 'ordinary' | 'reconcile';
|
|
20
15
|
reason: string;
|
|
21
16
|
};
|
|
22
17
|
export declare function persistentInstallTransition(provenance: ProvenanceStatus, installScripts: boolean): PersistentInstallTransition;
|
|
@@ -2,15 +2,8 @@
|
|
|
2
2
|
// evidence that this structural tree has already completed a scripts-enabled install.
|
|
3
3
|
export function persistentInstallTransition(provenance, installScripts) {
|
|
4
4
|
if (provenance.kind === 'matching') {
|
|
5
|
-
if (installScripts && !provenance.
|
|
6
|
-
return { action: '
|
|
7
|
-
const [firstPendingDependencyBuild, ...remainingPendingDependencyBuilds] = provenance.pendingDependencyBuilds;
|
|
8
|
-
if (installScripts && firstPendingDependencyBuild !== undefined)
|
|
9
|
-
return {
|
|
10
|
-
action: 'upgrade-dependencies',
|
|
11
|
-
pendingDependencyBuilds: [firstPendingDependencyBuild, ...remainingPendingDependencyBuilds],
|
|
12
|
-
reason: 'pending-dependency-rebuild',
|
|
13
|
-
};
|
|
5
|
+
if (installScripts && !provenance.scriptsEnabledInstallVerified)
|
|
6
|
+
return { action: 'reconcile', reason: 'scripts-enabled-install-unverified' };
|
|
14
7
|
return { action: 'ordinary', reason: 'matching-structural-provenance' };
|
|
15
8
|
}
|
|
16
9
|
if (provenance.kind === 'absent')
|
|
@@ -26,9 +19,8 @@ export function persistentProvenanceDiagnostic(provenance, installScripts, nativ
|
|
|
26
19
|
changedComponents: provenance.kind === 'changed' ? provenance.components : [],
|
|
27
20
|
event: 'pnpm-install-persistent-provenance',
|
|
28
21
|
installScripts,
|
|
29
|
-
|
|
22
|
+
scriptsEnabledInstallVerified: provenance.kind === 'matching' ? provenance.scriptsEnabledInstallVerified : false,
|
|
30
23
|
lastInvocationInstallScripts: provenance.kind === 'matching' ? provenance.lastInvocationInstallScripts : null,
|
|
31
|
-
pendingDependencyBuildCount: provenance.kind === 'matching' ? provenance.pendingDependencyBuilds.length : 0,
|
|
32
24
|
nativeBinariesMatchRuntime,
|
|
33
25
|
reason: transition.reason,
|
|
34
26
|
state: provenance.kind,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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": {
|