nx 23.2.0-beta.7 → 23.2.0-beta.8
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/dist/bin/init-local.js +3 -5
- package/dist/release/changelog-renderer/index.js +4 -5
- package/dist/src/adapter/ngcli-adapter.js +60 -1
- package/dist/src/command-line/completion/command-object.js +7 -12
- package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +17 -23
- package/dist/src/command-line/generate/generate.js +35 -35
- package/dist/src/command-line/import/import.js +18 -40
- package/dist/src/command-line/init/ai-agent-prompts.js +5 -11
- package/dist/src/command-line/init/command-object.js +4 -2
- package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +12 -32
- package/dist/src/command-line/init/implementation/add-nx-to-nest.js +8 -21
- package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +8 -21
- package/dist/src/command-line/init/implementation/angular/index.js +6 -15
- package/dist/src/command-line/init/init-v1.js +8 -18
- package/dist/src/command-line/init/init-v2.js +26 -56
- package/dist/src/command-line/migrate/agentic/definitions.js +35 -8
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +22 -3
- package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +13 -10
- package/dist/src/command-line/migrate/agentic/handoff.d.ts +3 -1
- package/dist/src/command-line/migrate/agentic/handoff.js +4 -2
- package/dist/src/command-line/migrate/agentic/run-step.js +1 -0
- package/dist/src/command-line/migrate/agentic/runner.js +24 -24
- package/dist/src/command-line/migrate/agentic/select.js +20 -30
- package/dist/src/command-line/migrate/agentic/types.d.ts +19 -3
- package/dist/src/command-line/migrate/agentic/types.js +13 -4
- package/dist/src/command-line/migrate/command-object.d.ts +3 -0
- package/dist/src/command-line/migrate/command-object.js +13 -0
- package/dist/src/command-line/migrate/execute-migration.d.ts +14 -0
- package/dist/src/command-line/migrate/execute-migration.js +30 -9
- package/dist/src/command-line/migrate/migrate-analytics.d.ts +28 -0
- package/dist/src/command-line/migrate/migrate-analytics.js +51 -1
- package/dist/src/command-line/migrate/migrate-commits.d.ts +8 -2
- package/dist/src/command-line/migrate/migrate-commits.js +66 -19
- package/dist/src/command-line/migrate/migrate-config.d.ts +4 -2
- package/dist/src/command-line/migrate/migrate-config.js +12 -4
- package/dist/src/command-line/migrate/migrate.d.ts +9 -2
- package/dist/src/command-line/migrate/migrate.js +179 -56
- package/dist/src/command-line/migrate/multi-major.js +7 -10
- package/dist/src/command-line/migrate/resolve-package-version.js +4 -8
- package/dist/src/command-line/migrate/run/agent-output.d.ts +20 -0
- package/dist/src/command-line/migrate/run/agent-output.js +65 -0
- package/dist/src/command-line/migrate/run/index.d.ts +7 -0
- package/dist/src/command-line/migrate/run/index.js +19 -1
- package/dist/src/command-line/migrate/run/orchestrator.d.ts +20 -0
- package/dist/src/command-line/migrate/run/orchestrator.js +1200 -0
- package/dist/src/command-line/migrate/run/run-id.d.ts +16 -0
- package/dist/src/command-line/migrate/run/run-id.js +56 -0
- package/dist/src/command-line/migrate/run/run-state.d.ts +157 -0
- package/dist/src/command-line/migrate/run/run-state.js +438 -0
- package/dist/src/command-line/migrate/run/state-lock.d.ts +25 -0
- package/dist/src/command-line/migrate/run/state-lock.js +76 -0
- package/dist/src/command-line/migrate/run/state-machine.d.ts +84 -0
- package/dist/src/command-line/migrate/run/state-machine.js +315 -0
- package/dist/src/command-line/migrate/run/util.d.ts +58 -0
- package/dist/src/command-line/migrate/run/util.js +139 -7
- package/dist/src/command-line/migrate/run/worker.d.ts +1 -0
- package/dist/src/command-line/migrate/run/worker.js +308 -45
- package/dist/src/command-line/migrate/safe-prompt.d.ts +14 -26
- package/dist/src/command-line/migrate/safe-prompt.js +32 -43
- package/dist/src/command-line/migrate/step-actions.d.ts +5 -0
- package/dist/src/command-line/migrate/step-actions.js +12 -0
- package/dist/src/command-line/migrate/text.d.ts +18 -0
- package/dist/src/command-line/migrate/text.js +26 -0
- package/dist/src/command-line/migrate/version-skew-guard.d.ts +6 -4
- package/dist/src/command-line/migrate/version-skew-guard.js +34 -14
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +16 -20
- package/dist/src/command-line/release/changelog.js +6 -17
- package/dist/src/command-line/release/plan.js +15 -28
- package/dist/src/command-line/release/release.js +6 -17
- package/dist/src/command-line/release/utils/remote-release-clients/github.js +16 -22
- package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +13 -22
- package/dist/src/command-line/release/utils/resolve-semver-specifier.js +19 -41
- package/dist/src/command-line/release/version/resolve-current-version.js +7 -10
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/devkit-internals.d.ts +2 -0
- package/dist/src/devkit-internals.js +12 -4
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
- package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
- package/dist/src/project-graph/utils/project-configuration/target-normalization.js +128 -2
- package/dist/src/tasks-runner/run-command.js +17 -32
- package/dist/src/tasks-runner/task-env.js +5 -2
- package/dist/src/tasks-runner/utils.js +2 -6
- package/dist/src/utils/analytics-prompt.js +4 -7
- package/dist/src/utils/exit-codes.d.ts +9 -0
- package/dist/src/utils/exit-codes.js +19 -0
- package/dist/src/utils/fileutils.d.ts +5 -0
- package/dist/src/utils/fileutils.js +7 -2
- package/dist/src/utils/git-utils.d.ts +33 -2
- package/dist/src/utils/git-utils.js +156 -8
- package/dist/src/utils/long-running-target.d.ts +10 -0
- package/dist/src/utils/long-running-target.js +19 -0
- package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
- package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
- package/dist/src/utils/min-release-age/packument.js +1 -1
- package/dist/src/utils/nx-console-prompt.js +3 -6
- package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
- package/dist/src/utils/package-manager-config/bunfig.js +49 -0
- package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
- package/dist/src/utils/package-manager-config/npmrc.js +124 -0
- package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
- package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
- package/dist/src/utils/package-manager.d.ts +21 -2
- package/dist/src/utils/package-manager.js +244 -47
- package/dist/src/utils/params.d.ts +7 -1
- package/dist/src/utils/params.js +64 -6
- package/dist/src/utils/prompt-helpers.d.ts +53 -0
- package/dist/src/utils/prompt-helpers.js +111 -0
- package/dist/src/utils/provenance.js +6 -14
- package/dist/src/utils/registry-config/bun.d.ts +2 -0
- package/dist/src/utils/registry-config/bun.js +257 -0
- package/dist/src/utils/registry-config/index.d.ts +13 -0
- package/dist/src/utils/registry-config/index.js +102 -0
- package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
- package/dist/src/utils/registry-config/pnpm.js +1551 -0
- package/dist/src/utils/registry-config/utils.d.ts +195 -0
- package/dist/src/utils/registry-config/utils.js +496 -0
- package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
- package/dist/src/utils/registry-config/yarn-berry.js +612 -0
- package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
- package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
- package/dist/src/utils/safe-spawn.d.ts +24 -0
- package/dist/src/utils/safe-spawn.js +104 -0
- package/migrations.json +6 -0
- package/package.json +17 -13
- package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
- package/dist/src/utils/min-release-age/npmrc.js +0 -45
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type MigrateRunState } from './run-state';
|
|
2
|
+
/**
|
|
3
|
+
* Runs `fn` while holding the run's state lock, releasing it afterwards even
|
|
4
|
+
* if `fn` throws. writeRunState's tmp+rename gives per-write atomicity, but a
|
|
5
|
+
* writer that reads state, applies an event, then writes still races a second
|
|
6
|
+
* nx migrate process that read the same state first; this lock serializes
|
|
7
|
+
* those sequences so the event always applies to the freshest on-disk state.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withRunStateLock<T>(runDirPath: string, fn: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* Serializes active-run discovery and run creation across nx migrate
|
|
12
|
+
* processes. Two concurrent inits could otherwise both observe "no active
|
|
13
|
+
* run" and create competing runs against the same workspace; the per-run
|
|
14
|
+
* state lock cannot cover that window because the run directory does not
|
|
15
|
+
* exist yet. Callers must redo their active-run check inside `fn`: a check
|
|
16
|
+
* done before acquiring the lock may predate a concurrent creation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withRunCreationLock<T>(root: string, fn: () => T): T;
|
|
19
|
+
/**
|
|
20
|
+
* Reads the run state fresh under the lock, hands it to `apply`, and writes the
|
|
21
|
+
* result back. `apply` returning null means "no change" and skips the write.
|
|
22
|
+
* `apply` must be pure and synchronous; a corrupt or newer-format run.json
|
|
23
|
+
* propagates from the read.
|
|
24
|
+
*/
|
|
25
|
+
export declare function updateRunState(runDirPath: string, apply: (fresh: MigrateRunState) => MigrateRunState | null): MigrateRunState;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Cross-process mutual exclusion for the migrate run flow, built on the
|
|
3
|
+
// native flock-based FileLock (the same primitive the project graph uses to
|
|
4
|
+
// serialize nx processes): acquisition blocks until the holder releases, and
|
|
5
|
+
// the kernel releases a dead holder's lock automatically, so there is no
|
|
6
|
+
// stale-lock state to detect or clean up. Under WASM the native lock is
|
|
7
|
+
// unavailable and sections run unserialized, matching the project graph's
|
|
8
|
+
// IS_WASM guard for this lock. Not part of run/'s public surface (not
|
|
9
|
+
// re-exported via ./index): import directly within run/.
|
|
10
|
+
//
|
|
11
|
+
// Locked sections must stay synchronous. lock() blocks the whole thread, so
|
|
12
|
+
// if a holder parked on an await while a second in-process caller reached
|
|
13
|
+
// lock(), the holder's continuation could never run to release it. Git and
|
|
14
|
+
// child-process side effects belong outside the lock for the same reason.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.withRunStateLock = withRunStateLock;
|
|
17
|
+
exports.withRunCreationLock = withRunCreationLock;
|
|
18
|
+
exports.updateRunState = updateRunState;
|
|
19
|
+
const fs_1 = require("fs");
|
|
20
|
+
const path_1 = require("path");
|
|
21
|
+
const native_1 = require("../../../native");
|
|
22
|
+
const run_state_1 = require("./run-state");
|
|
23
|
+
const STATE_LOCK_FILE_NAME = 'run.json.lock';
|
|
24
|
+
const CREATION_LOCK_FILE_NAME = 'init.lock';
|
|
25
|
+
function withFileLock(lockPath, fn) {
|
|
26
|
+
if (native_1.IS_WASM) {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
const lock = new native_1.FileLock(lockPath);
|
|
30
|
+
lock.lock();
|
|
31
|
+
try {
|
|
32
|
+
return fn();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
lock.unlock();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs `fn` while holding the run's state lock, releasing it afterwards even
|
|
40
|
+
* if `fn` throws. writeRunState's tmp+rename gives per-write atomicity, but a
|
|
41
|
+
* writer that reads state, applies an event, then writes still races a second
|
|
42
|
+
* nx migrate process that read the same state first; this lock serializes
|
|
43
|
+
* those sequences so the event always applies to the freshest on-disk state.
|
|
44
|
+
*/
|
|
45
|
+
function withRunStateLock(runDirPath, fn) {
|
|
46
|
+
return withFileLock((0, path_1.join)(runDirPath, STATE_LOCK_FILE_NAME), fn);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Serializes active-run discovery and run creation across nx migrate
|
|
50
|
+
* processes. Two concurrent inits could otherwise both observe "no active
|
|
51
|
+
* run" and create competing runs against the same workspace; the per-run
|
|
52
|
+
* state lock cannot cover that window because the run directory does not
|
|
53
|
+
* exist yet. Callers must redo their active-run check inside `fn`: a check
|
|
54
|
+
* done before acquiring the lock may predate a concurrent creation.
|
|
55
|
+
*/
|
|
56
|
+
function withRunCreationLock(root, fn) {
|
|
57
|
+
const dir = (0, run_state_1.migrateRunsDir)(root);
|
|
58
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
59
|
+
return withFileLock((0, path_1.join)(dir, CREATION_LOCK_FILE_NAME), fn);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads the run state fresh under the lock, hands it to `apply`, and writes the
|
|
63
|
+
* result back. `apply` returning null means "no change" and skips the write.
|
|
64
|
+
* `apply` must be pure and synchronous; a corrupt or newer-format run.json
|
|
65
|
+
* propagates from the read.
|
|
66
|
+
*/
|
|
67
|
+
function updateRunState(runDirPath, apply) {
|
|
68
|
+
return withRunStateLock(runDirPath, () => {
|
|
69
|
+
const fresh = (0, run_state_1.readRunState)(runDirPath);
|
|
70
|
+
const next = apply(fresh);
|
|
71
|
+
if (next === null)
|
|
72
|
+
return fresh;
|
|
73
|
+
(0, run_state_1.writeRunState)(runDirPath, next);
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CommitResult } from '../migrate-commits';
|
|
2
|
+
import type { MigrateCommitLedgerEntry, MigrateRunState, MigrateStepOutcome, MigrateStepPromptOutcome } from './run-state';
|
|
3
|
+
import type { StepAction } from '../step-actions';
|
|
4
|
+
export type { StepAction };
|
|
5
|
+
export type StepEvent = {
|
|
6
|
+
type: 'dispense';
|
|
7
|
+
stepId: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'start';
|
|
10
|
+
stepId: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'succeed';
|
|
15
|
+
stepId: string;
|
|
16
|
+
finishedAt: string;
|
|
17
|
+
outcome?: MigrateStepOutcome;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'fail';
|
|
20
|
+
stepId: string;
|
|
21
|
+
finishedAt: string;
|
|
22
|
+
outcome?: MigrateStepOutcome;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'awaitPromptOutcome';
|
|
25
|
+
stepId: string;
|
|
26
|
+
finishedAt: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'foldPromptOutcome';
|
|
29
|
+
stepId: string;
|
|
30
|
+
attempt: number;
|
|
31
|
+
promptOutcome: MigrateStepPromptOutcome;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'markGeneratorCompleted';
|
|
34
|
+
stepId: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'markDied';
|
|
37
|
+
stepId: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'stepAction';
|
|
41
|
+
stepId: string;
|
|
42
|
+
action: StepAction;
|
|
43
|
+
attempt: number;
|
|
44
|
+
};
|
|
45
|
+
export type ApplyStepEventResult = {
|
|
46
|
+
kind: 'ok';
|
|
47
|
+
state: MigrateRunState;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'error';
|
|
50
|
+
reason: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function applyStepEvent(state: MigrateRunState, event: StepEvent): ApplyStepEventResult;
|
|
53
|
+
/**
|
|
54
|
+
* Records that the run could not install the dependency changes a step left
|
|
55
|
+
* behind. Not a {@link StepEvent}: it annotates a step instead of moving it,
|
|
56
|
+
* and every status can carry it, since the orchestrator marks a step it has
|
|
57
|
+
* just settled while the worker marks one it is about to fail.
|
|
58
|
+
*/
|
|
59
|
+
export declare function markInstallFailed(state: MigrateRunState, stepId: string): MigrateRunState;
|
|
60
|
+
export declare function uncoveredFailedStepIds(state: MigrateRunState): string[];
|
|
61
|
+
/**
|
|
62
|
+
* A step has commit debt when a 'failed' ledger entry names it and no later
|
|
63
|
+
* 'landed' entry also names it (checkpoint entries neither create nor cover
|
|
64
|
+
* debt). There is no per-step commit object; debt is always derived from the
|
|
65
|
+
* ledger.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasPendingCommitDebt(state: MigrateRunState): boolean;
|
|
68
|
+
export declare function coveringLandedEntries(state: MigrateRunState, stepId: string): MigrateCommitLedgerEntry[];
|
|
69
|
+
export declare function latestRound(state: MigrateRunState): MigrateRunState['rounds'][number] | undefined;
|
|
70
|
+
export declare function splitMigrationId(id: string): {
|
|
71
|
+
package: string;
|
|
72
|
+
name: string;
|
|
73
|
+
};
|
|
74
|
+
export declare function stepsToPendingMigrations(state: MigrateRunState, stepIds: string[]): {
|
|
75
|
+
package: string;
|
|
76
|
+
name: string;
|
|
77
|
+
}[];
|
|
78
|
+
/**
|
|
79
|
+
* Classifies a commit attempt into the ledger entry to record, or null when
|
|
80
|
+
* there is nothing to record ('no-changes' / 'disabled'). A landed entry
|
|
81
|
+
* covers the absorbed steps too: the commit's `git add -A` captured their
|
|
82
|
+
* diffs. A failed entry records only this step's debt.
|
|
83
|
+
*/
|
|
84
|
+
export declare function commitResultToLedgerEntry(result: CommitResult, stepId: string, absorbedStepIds: string[]): MigrateCommitLedgerEntry | null;
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyStepEvent = applyStepEvent;
|
|
4
|
+
exports.markInstallFailed = markInstallFailed;
|
|
5
|
+
exports.uncoveredFailedStepIds = uncoveredFailedStepIds;
|
|
6
|
+
exports.hasPendingCommitDebt = hasPendingCommitDebt;
|
|
7
|
+
exports.coveringLandedEntries = coveringLandedEntries;
|
|
8
|
+
exports.latestRound = latestRound;
|
|
9
|
+
exports.splitMigrationId = splitMigrationId;
|
|
10
|
+
exports.stepsToPendingMigrations = stepsToPendingMigrations;
|
|
11
|
+
exports.commitResultToLedgerEntry = commitResultToLedgerEntry;
|
|
12
|
+
const PROMPT_OUTCOME_TO_STEP_STATUS = {
|
|
13
|
+
completed: 'succeeded',
|
|
14
|
+
skipped: 'skipped',
|
|
15
|
+
failed: 'failed',
|
|
16
|
+
};
|
|
17
|
+
function applyStepEvent(state, event) {
|
|
18
|
+
const index = state.steps.findIndex((s) => s.id === event.stepId);
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
return {
|
|
21
|
+
kind: 'error',
|
|
22
|
+
reason: `No step with id '${event.stepId}' in run state.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const step = state.steps[index];
|
|
26
|
+
switch (event.type) {
|
|
27
|
+
case 'dispense':
|
|
28
|
+
// 'failed' and 'died' steps are re-armed to 'pending' by a stepAction
|
|
29
|
+
// first; this event never dispenses them directly.
|
|
30
|
+
if (step.status !== 'pending')
|
|
31
|
+
return illegal(step, event.type);
|
|
32
|
+
return commit(state, index, {
|
|
33
|
+
...step,
|
|
34
|
+
status: 'dispensed',
|
|
35
|
+
dispenseCount: step.dispenseCount + 1,
|
|
36
|
+
});
|
|
37
|
+
case 'start':
|
|
38
|
+
if (step.status !== 'dispensed')
|
|
39
|
+
return illegal(step, event.type);
|
|
40
|
+
return commit(state, index, {
|
|
41
|
+
...step,
|
|
42
|
+
status: 'running',
|
|
43
|
+
pid: event.pid,
|
|
44
|
+
startedAt: event.startedAt,
|
|
45
|
+
});
|
|
46
|
+
case 'succeed':
|
|
47
|
+
if (step.status !== 'running')
|
|
48
|
+
return illegal(step, event.type);
|
|
49
|
+
return commit(state, index, {
|
|
50
|
+
...step,
|
|
51
|
+
status: 'succeeded',
|
|
52
|
+
finishedAt: event.finishedAt,
|
|
53
|
+
...(event.outcome ? { outcome: event.outcome } : {}),
|
|
54
|
+
});
|
|
55
|
+
case 'fail':
|
|
56
|
+
if (step.status !== 'running')
|
|
57
|
+
return illegal(step, event.type);
|
|
58
|
+
return commit(state, index, {
|
|
59
|
+
...step,
|
|
60
|
+
status: 'failed',
|
|
61
|
+
finishedAt: event.finishedAt,
|
|
62
|
+
...(event.outcome ? { outcome: event.outcome } : {}),
|
|
63
|
+
});
|
|
64
|
+
case 'awaitPromptOutcome':
|
|
65
|
+
if (step.status !== 'running')
|
|
66
|
+
return illegal(step, event.type);
|
|
67
|
+
return commit(state, index, {
|
|
68
|
+
...step,
|
|
69
|
+
status: 'awaiting-prompt-outcome',
|
|
70
|
+
finishedAt: event.finishedAt,
|
|
71
|
+
});
|
|
72
|
+
case 'markGeneratorCompleted':
|
|
73
|
+
if (step.status !== 'running')
|
|
74
|
+
return illegal(step, event.type);
|
|
75
|
+
return commit(state, index, { ...step, generatorCompleted: true });
|
|
76
|
+
case 'foldPromptOutcome':
|
|
77
|
+
if (step.status !== 'awaiting-prompt-outcome')
|
|
78
|
+
return illegal(step, event.type);
|
|
79
|
+
if (step.attempt !== event.attempt)
|
|
80
|
+
return staleAttempt(step, event.type, event.attempt);
|
|
81
|
+
return commit(state, index, {
|
|
82
|
+
...step,
|
|
83
|
+
status: PROMPT_OUTCOME_TO_STEP_STATUS[event.promptOutcome.status],
|
|
84
|
+
promptOutcome: event.promptOutcome,
|
|
85
|
+
});
|
|
86
|
+
case 'markDied':
|
|
87
|
+
// A step awaiting a prompt outcome has no live process left to die;
|
|
88
|
+
// only a running step can.
|
|
89
|
+
if (step.status !== 'running')
|
|
90
|
+
return illegal(step, event.type);
|
|
91
|
+
if (step.attempt !== event.attempt)
|
|
92
|
+
return staleAttempt(step, event.type, event.attempt);
|
|
93
|
+
return commit(state, index, { ...step, status: 'died' });
|
|
94
|
+
case 'stepAction':
|
|
95
|
+
if (step.attempt !== event.attempt)
|
|
96
|
+
return staleAttempt(step, event.type, event.attempt);
|
|
97
|
+
return applyStepAction(state, index, step, event.action);
|
|
98
|
+
default: {
|
|
99
|
+
const exhaustive = event;
|
|
100
|
+
return exhaustive;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function applyStepAction(state, index, step, action) {
|
|
105
|
+
if (step.status === 'failed') {
|
|
106
|
+
switch (action) {
|
|
107
|
+
case 'retry':
|
|
108
|
+
// Nothing resets the tree, so the generator's changes are still in it.
|
|
109
|
+
// A pre-marker retry is additionally gated at the reconcile acceptance
|
|
110
|
+
// site, which can read the live tree; this pure layer cannot.
|
|
111
|
+
return commit(state, index, rearm(step, true));
|
|
112
|
+
case 'retry-clean':
|
|
113
|
+
// A failed generator can have written before throwing (a direct fs or
|
|
114
|
+
// exec side effect, or a crash mid-flush), so a reset-backed retry is
|
|
115
|
+
// offered under the same guard as for a death.
|
|
116
|
+
return commit(state, index, cleanRearm(state, step));
|
|
117
|
+
case 'skip':
|
|
118
|
+
return commit(state, index, { ...step, status: 'skipped' });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (step.status === 'died') {
|
|
122
|
+
switch (action) {
|
|
123
|
+
case 'retry':
|
|
124
|
+
// Same no-reset rearm as from 'failed', and legal only once the
|
|
125
|
+
// generator half is recorded (or the step never had one): that is
|
|
126
|
+
// what leaves the redispensed worker something to do (a prompt, or
|
|
127
|
+
// the install and commit) other than reapplying the generator over
|
|
128
|
+
// its own changes.
|
|
129
|
+
if (step.generatorCompleted === true || step.hasGenerator === false) {
|
|
130
|
+
return commit(state, index, rearm(step, true));
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
kind: 'error',
|
|
134
|
+
reason: `Cannot apply action 'retry' to step '${step.id}': the worker died before recording that its generator ran, so keeping the current tree could apply the migration twice. Use 'retry-clean', 'adopt' or 'skip' instead.`,
|
|
135
|
+
};
|
|
136
|
+
case 'retry-clean':
|
|
137
|
+
return commit(state, index, cleanRearm(state, step));
|
|
138
|
+
case 'adopt':
|
|
139
|
+
return commit(state, index, {
|
|
140
|
+
...step,
|
|
141
|
+
status: 'succeeded',
|
|
142
|
+
outcome: { ...step.outcome, summary: adoptedSummary(step) },
|
|
143
|
+
});
|
|
144
|
+
case 'skip':
|
|
145
|
+
// Same as skipping a failure: the tree stays as the worker left it.
|
|
146
|
+
return commit(state, index, { ...step, status: 'skipped' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
kind: 'error',
|
|
151
|
+
reason: `Cannot apply action '${action}' to step '${step.id}' in status '${step.status}'.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Re-arms for a retry that resets the tree first. The reset target predates
|
|
155
|
+
// the generator unless a commit of this step's own already carries it, so the
|
|
156
|
+
// marker only survives when such a commit exists; otherwise the reset discards
|
|
157
|
+
// the generator's changes and the retry has to run it again.
|
|
158
|
+
function cleanRearm(state, step) {
|
|
159
|
+
return rearm(step, coveringLandedEntries(state, step.id).length > 0);
|
|
160
|
+
}
|
|
161
|
+
// An adopted death records how far the worker got, since 'succeeded' alone
|
|
162
|
+
// says the migration was applied and cannot say by what.
|
|
163
|
+
function adoptedSummary(step) {
|
|
164
|
+
return step.generatorCompleted === true
|
|
165
|
+
? "Adopted after the worker died: its generator had run, and the working tree it left was taken as this migration's result."
|
|
166
|
+
: "Adopted after the worker died before recording that its generator had run; the working tree it left was taken as this migration's result.";
|
|
167
|
+
}
|
|
168
|
+
// Re-arms a step for a fresh attempt. Drops every field the previous attempt
|
|
169
|
+
// wrote (pid, timestamps, git ref, tree state, outcomes) so a later success
|
|
170
|
+
// can't carry a stale failure outcome; dispenseCount stays cumulative across
|
|
171
|
+
// attempts.
|
|
172
|
+
// `keepGeneratorCompleted` says whether the generator's changes reach the new
|
|
173
|
+
// attempt. They do when nothing resets the tree, and when the reset target
|
|
174
|
+
// already contains the commit that landed them; re-running the generator there
|
|
175
|
+
// would apply them twice. They do not when the reset discards them, and
|
|
176
|
+
// keeping the marker then would skip the generator and record a success for a
|
|
177
|
+
// migration that never ran. The step kind is a plan fact, not an attempt's,
|
|
178
|
+
// and always survives. So does the dependency baseline: it tracks
|
|
179
|
+
// the last dependencies that were installed, so dropping it here would leave
|
|
180
|
+
// the retry with nothing to detect the previous attempt's package.json edits.
|
|
181
|
+
function rearm(step, keepGeneratorCompleted) {
|
|
182
|
+
return {
|
|
183
|
+
id: step.id,
|
|
184
|
+
roundIndex: step.roundIndex,
|
|
185
|
+
migrationId: step.migrationId,
|
|
186
|
+
status: 'pending',
|
|
187
|
+
attempt: step.attempt + 1,
|
|
188
|
+
dispenseCount: step.dispenseCount,
|
|
189
|
+
...(step.hasGenerator !== undefined
|
|
190
|
+
? { hasGenerator: step.hasGenerator }
|
|
191
|
+
: {}),
|
|
192
|
+
...(step.depsHashAtDispense !== undefined
|
|
193
|
+
? { depsHashAtDispense: step.depsHashAtDispense }
|
|
194
|
+
: {}),
|
|
195
|
+
...(keepGeneratorCompleted && step.generatorCompleted
|
|
196
|
+
? { generatorCompleted: true }
|
|
197
|
+
: {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// A guarded transition whose observation was made against an earlier attempt
|
|
201
|
+
// of the same step: the status recurred, so the observation says nothing about
|
|
202
|
+
// the attempt on disk now.
|
|
203
|
+
function staleAttempt(step, eventType, observedAttempt) {
|
|
204
|
+
return {
|
|
205
|
+
kind: 'error',
|
|
206
|
+
reason: `Cannot apply '${eventType}' to step '${step.id}': it was observed on attempt ${observedAttempt} and the step is now on attempt ${step.attempt}.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function illegal(step, eventType) {
|
|
210
|
+
return {
|
|
211
|
+
kind: 'error',
|
|
212
|
+
reason: `Cannot apply '${eventType}' to step '${step.id}' in status '${step.status}'.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function commit(state, index, updatedStep) {
|
|
216
|
+
const nextSteps = state.steps.slice();
|
|
217
|
+
nextSteps[index] = updatedStep;
|
|
218
|
+
return { kind: 'ok', state: { ...state, steps: nextSteps } };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Records that the run could not install the dependency changes a step left
|
|
222
|
+
* behind. Not a {@link StepEvent}: it annotates a step instead of moving it,
|
|
223
|
+
* and every status can carry it, since the orchestrator marks a step it has
|
|
224
|
+
* just settled while the worker marks one it is about to fail.
|
|
225
|
+
*/
|
|
226
|
+
function markInstallFailed(state, stepId) {
|
|
227
|
+
return {
|
|
228
|
+
...state,
|
|
229
|
+
steps: state.steps.map((s) => s.id === stepId ? { ...s, installFailed: true } : s),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Step ids named by a 'failed' ledger entry with no later 'landed' entry
|
|
233
|
+
// covering them.
|
|
234
|
+
function uncoveredFailedStepIds(state) {
|
|
235
|
+
const uncovered = new Set();
|
|
236
|
+
const { commits } = state;
|
|
237
|
+
for (let i = 0; i < commits.length; i++) {
|
|
238
|
+
if (commits[i].kind !== 'failed')
|
|
239
|
+
continue;
|
|
240
|
+
for (const stepId of commits[i].stepIds) {
|
|
241
|
+
const covered = commits
|
|
242
|
+
.slice(i + 1)
|
|
243
|
+
.some((c) => c.kind === 'landed' && c.stepIds.includes(stepId));
|
|
244
|
+
if (!covered)
|
|
245
|
+
uncovered.add(stepId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...uncovered];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A step has commit debt when a 'failed' ledger entry names it and no later
|
|
252
|
+
* 'landed' entry also names it (checkpoint entries neither create nor cover
|
|
253
|
+
* debt). There is no per-step commit object; debt is always derived from the
|
|
254
|
+
* ledger.
|
|
255
|
+
*/
|
|
256
|
+
function hasPendingCommitDebt(state) {
|
|
257
|
+
return uncoveredFailedStepIds(state).length > 0;
|
|
258
|
+
}
|
|
259
|
+
// The 'landed' ledger entries naming the given step, in ledger order.
|
|
260
|
+
function coveringLandedEntries(state, stepId) {
|
|
261
|
+
return state.commits.filter((commit) => commit.kind === 'landed' && commit.stepIds.includes(stepId));
|
|
262
|
+
}
|
|
263
|
+
// The round with the highest index.
|
|
264
|
+
function latestRound(state) {
|
|
265
|
+
return state.rounds.reduce((newest, round) => (!newest || round.index > newest.index ? round : newest), undefined);
|
|
266
|
+
}
|
|
267
|
+
// '<package>:<name>' splits on the first ':', leaving names that contain a ':'
|
|
268
|
+
// intact; a bare id has no package.
|
|
269
|
+
function splitMigrationId(id) {
|
|
270
|
+
const colon = id.indexOf(':');
|
|
271
|
+
return colon === -1
|
|
272
|
+
? { package: '', name: id }
|
|
273
|
+
: { package: id.slice(0, colon), name: id.slice(colon + 1) };
|
|
274
|
+
}
|
|
275
|
+
// Maps absorbed step ids to `{package, name}` for the commit body; an id with
|
|
276
|
+
// no matching step, or one whose migration id carries no package, can't be
|
|
277
|
+
// attributed there.
|
|
278
|
+
function stepsToPendingMigrations(state, stepIds) {
|
|
279
|
+
const pending = [];
|
|
280
|
+
for (const id of stepIds) {
|
|
281
|
+
const migrationId = state.steps.find((s) => s.id === id)?.migrationId;
|
|
282
|
+
if (!migrationId)
|
|
283
|
+
continue;
|
|
284
|
+
const { package: pkg, name } = splitMigrationId(migrationId);
|
|
285
|
+
if (!pkg)
|
|
286
|
+
continue;
|
|
287
|
+
pending.push({ package: pkg, name });
|
|
288
|
+
}
|
|
289
|
+
return pending;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Classifies a commit attempt into the ledger entry to record, or null when
|
|
293
|
+
* there is nothing to record ('no-changes' / 'disabled'). A landed entry
|
|
294
|
+
* covers the absorbed steps too: the commit's `git add -A` captured their
|
|
295
|
+
* diffs. A failed entry records only this step's debt.
|
|
296
|
+
*/
|
|
297
|
+
function commitResultToLedgerEntry(result, stepId, absorbedStepIds) {
|
|
298
|
+
switch (result.status) {
|
|
299
|
+
case 'committed':
|
|
300
|
+
return {
|
|
301
|
+
kind: 'landed',
|
|
302
|
+
...(result.sha ? { sha: result.sha } : {}),
|
|
303
|
+
stepIds: [stepId, ...absorbedStepIds],
|
|
304
|
+
};
|
|
305
|
+
case 'failed':
|
|
306
|
+
return { kind: 'failed', stepIds: [stepId] };
|
|
307
|
+
case 'no-changes':
|
|
308
|
+
case 'disabled':
|
|
309
|
+
return null;
|
|
310
|
+
default: {
|
|
311
|
+
const exhaustive = result;
|
|
312
|
+
return exhaustive;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -1 +1,59 @@
|
|
|
1
|
+
import type { MigrateStep } from './run-state';
|
|
2
|
+
export declare function nowIso(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Fingerprints the workspace dependencies so a step can persist what they
|
|
5
|
+
* looked like and a later actor can still tell whether the migration changed
|
|
6
|
+
* them. Hashed rather than stored verbatim to keep run.json small; the value
|
|
7
|
+
* is only ever compared for equality.
|
|
8
|
+
*
|
|
9
|
+
* `null` means the probe itself failed. Callers must not read that as
|
|
10
|
+
* "unchanged": the value is persisted and compared across processes, so a
|
|
11
|
+
* failed read on either end would otherwise silently skip an install that was
|
|
12
|
+
* needed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function depsHash(root: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Installs when the workspace dependencies differ from the step's recorded
|
|
17
|
+
* baseline, so a prompt applied by another actor, or a retry that only has the
|
|
18
|
+
* commit left to do, still installs what the changes need. Comparing against
|
|
19
|
+
* the persisted baseline is what makes that possible: by the time either runs,
|
|
20
|
+
* the edits are already on disk, so a snapshot taken here would see them as
|
|
21
|
+
* the starting point and never detect a change.
|
|
22
|
+
*
|
|
23
|
+
* The baseline is re-pointed at the current dependencies once the install
|
|
24
|
+
* lands, so the next actor to compare does not install the same change again.
|
|
25
|
+
*
|
|
26
|
+
* Every probe failure installs, on either end of the comparison. A step whose
|
|
27
|
+
* dispense-time probe failed has no baseline, which says the dependencies it
|
|
28
|
+
* started from are unknown rather than unchanged, and skipping there would
|
|
29
|
+
* strand the change with nothing left to detect it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installDepsChangedSinceDispense(root: string, dir: string, step: MigrateStep, skipInstall: boolean, rerunCommand?: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Records what an install that just landed means for the run: the installing
|
|
34
|
+
* step's dependency baseline moves to what is now on disk, and every step's
|
|
35
|
+
* install-failure mark clears.
|
|
36
|
+
*
|
|
37
|
+
* The baseline has to move, or the next actor to compare (the prompt fold, a
|
|
38
|
+
* retry, the step's own commit) reads the step's own edits as an unapplied
|
|
39
|
+
* change and pays a second full install. The marks clear because the package
|
|
40
|
+
* manager installs the whole workspace package.json, so this install also
|
|
41
|
+
* covers the dependency edits of every earlier step that failed to install its
|
|
42
|
+
* own.
|
|
43
|
+
*
|
|
44
|
+
* A failed probe leaves the baseline alone: an install that runs twice costs
|
|
45
|
+
* time, one that never runs leaves the workspace inconsistent with its
|
|
46
|
+
* package.json.
|
|
47
|
+
*
|
|
48
|
+
* Never throws. It runs inside callers that treat a throw as "the install
|
|
49
|
+
* failed", and the install has already succeeded by then. A run state this
|
|
50
|
+
* cannot read or write is a real problem, but it is reported by the next
|
|
51
|
+
* mandatory write rather than misattributed here, and the only cost of losing
|
|
52
|
+
* this one is a redundant install later.
|
|
53
|
+
*/
|
|
54
|
+
export declare function recordInstallLanded(root: string, dir: string, stepId: string): void;
|
|
55
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
56
|
+
export declare function summarizeError(e: unknown): string;
|
|
57
|
+
export declare function warnCommitFailed(name: string, cause?: unknown): void;
|
|
1
58
|
export declare function pmExecPrefix(root: string): string;
|
|
59
|
+
export declare function pmInstallCommand(root: string): string;
|