swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.11
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.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +46 -25
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +72 -12
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +59 -28
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.js +2 -2
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +93 -0
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +116 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +126 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +23 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +49 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +50 -28
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +72 -12
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Plan / Apply Provision — Azure プロビジョニングの事前計画と承認付き実行。
|
|
4
4
|
*
|
|
5
|
-
* - plan provision:
|
|
6
|
-
* ネットワークアクセスや az login を必要とする操作は行わない。
|
|
7
|
-
* --what-if を明示した場合のみ az deployment group what-if を実行する。
|
|
5
|
+
* - plan provision: Bicep と Azure の現在の状態を確認する。リソースは変更しない。
|
|
8
6
|
* - apply provision: 常に承認(--approve)必須。承認なしでは requires-human で停止する。
|
|
7
|
+
* - 対話的な `swallowkit provision` は同じ計画をメモリ上で作成・表示し、確認後に
|
|
8
|
+
* executeProvisionPlan で同じ安全確認を経て実行する(計画ファイルは保存しない)。
|
|
9
9
|
*/
|
|
10
10
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
11
|
if (k2 === undefined) k2 = k;
|
|
@@ -41,17 +41,23 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
};
|
|
42
42
|
})();
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.INTERACTIVE_PROVISION_RETRY_HINT = void 0;
|
|
44
45
|
exports.planProvisionOperation = planProvisionOperation;
|
|
46
|
+
exports.provisionNextActions = provisionNextActions;
|
|
47
|
+
exports.inspectProvisionOperation = inspectProvisionOperation;
|
|
45
48
|
exports.applyProvisionOperation = applyProvisionOperation;
|
|
49
|
+
exports.executeProvisionPlan = executeProvisionPlan;
|
|
46
50
|
const crypto = __importStar(require("crypto"));
|
|
47
51
|
const fs = __importStar(require("fs"));
|
|
52
|
+
const azure_cli_1 = require("../../utils/azure-cli");
|
|
53
|
+
const azure_diagnostics_1 = require("../../utils/azure-diagnostics");
|
|
48
54
|
const path = __importStar(require("path"));
|
|
49
|
-
const child_process_1 = require("child_process");
|
|
50
55
|
const errors_1 = require("../../machine/errors");
|
|
51
56
|
const version_1 = require("../../version");
|
|
52
57
|
const infra_1 = require("../project/infra");
|
|
53
58
|
const state_1 = require("../project/state");
|
|
54
59
|
const file_session_1 = require("./file-session");
|
|
60
|
+
const provision_deployment_1 = require("./provision-deployment");
|
|
55
61
|
const migrations_1 = require("../project/migrations");
|
|
56
62
|
const RESOURCE_GROUP_PATTERN = /^[A-Za-z0-9._()-]{1,90}$/;
|
|
57
63
|
const LOCATION_PATTERN = /^[a-z0-9]{3,30}$/;
|
|
@@ -66,36 +72,38 @@ function validateProvisionInputs(options) {
|
|
|
66
72
|
if (options.subscription && !SUBSCRIPTION_PATTERN.test(options.subscription)) {
|
|
67
73
|
throw new errors_1.MachineCommandError("invalid-arguments", "Subscription must be a GUID.", undefined, "failed");
|
|
68
74
|
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const result = (0, child_process_1.spawnSync)("az", ["--version"], { shell: false, stdio: "ignore", timeout: 30000 });
|
|
73
|
-
return result.status === 0;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return false;
|
|
75
|
+
if (options.resumeDeployment && (!/^[A-Za-z0-9._()-]{1,64}$/.test(options.resumeDeployment) || options.reconcile || options.adoptExisting)) {
|
|
76
|
+
throw new errors_1.MachineCommandError("invalid-arguments", "--resume-deployment requires a valid deployment name and cannot be combined with adoption or reconcile.", undefined, "blocked");
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
function azCliUnavailableWarning(availability) {
|
|
80
|
+
return availability.reason === "not-found"
|
|
81
|
+
? "az-cli-not-found: Azure CLI was not found on PATH. Install it (https://aka.ms/azure-cli) or add it to PATH, then try again."
|
|
82
|
+
: `az-cli-not-runnable: Azure CLI was found but could not be started. ${availability.detail}`;
|
|
83
|
+
}
|
|
79
84
|
function displayCommand(command) {
|
|
80
85
|
return [command.file, ...command.args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg)].join(" ");
|
|
81
86
|
}
|
|
82
|
-
function runAz(args, timeout =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
function runAz(args, timeout = 60000) {
|
|
88
|
+
return (0, azure_cli_1.runAzCli)(args, { timeout });
|
|
89
|
+
}
|
|
90
|
+
function azQueryError(code, message, result, secretValues = []) {
|
|
91
|
+
const diagnostic = (0, azure_diagnostics_1.azureCliDiagnostic)(result, "azure-state", secretValues);
|
|
92
|
+
const output = (diagnostic.stderr || diagnostic.stdout).trim().slice(0, 2000);
|
|
93
|
+
if (/az login|not logged in|AADSTS|expired/i.test(output)) {
|
|
94
|
+
return new errors_1.MachineCommandError("az-not-logged-in", `Azure CLI is not signed in (or the session expired). Run "az login", then try again.\n${output.slice(0, 1000)}`, { diagnostic }, "requires-human");
|
|
95
|
+
}
|
|
96
|
+
return new errors_1.MachineCommandError(code, `${message}: ${output || result.errorCode || result.failureKind || `exit ${result.status}`}`, { diagnostic }, "blocked");
|
|
89
97
|
}
|
|
90
98
|
function subscriptionArgs(subscription) {
|
|
91
99
|
return subscription ? ["--subscription", subscription] : [];
|
|
92
100
|
}
|
|
93
101
|
function buildBootstrapCommands(options, mainBicepPath, parametersPath) {
|
|
94
102
|
const commands = [];
|
|
95
|
-
commands.push({ file: "az", args: ["group", "create", "--name", options.resourceGroup, "--location", options.location, ...subscriptionArgs(options.subscription)], kind: "resource-group" });
|
|
103
|
+
commands.push({ file: "az", args: ["group", "create", "--name", options.resourceGroup, "--location", options.location, ...subscriptionArgs(options.subscription), "--query", "id", "-o", "json"], kind: "resource-group" });
|
|
96
104
|
commands.push({
|
|
97
105
|
file: "az",
|
|
98
|
-
args: ["deployment", "group", "create", "--name", "swallowkit-baseline", "--resource-group", options.resourceGroup, "--template-file", mainBicepPath, "--parameters", parametersPath, "--parameters", `location=${options.location}`, `swaLocation=${options.swaLocation}`, ...subscriptionArgs(options.subscription)],
|
|
106
|
+
args: ["deployment", "group", "create", "--name", "swallowkit-baseline", "--resource-group", options.resourceGroup, "--template-file", mainBicepPath, "--parameters", parametersPath, "--parameters", `location=${options.location}`, `swaLocation=${options.swaLocation}`, ...subscriptionArgs(options.subscription), "--no-wait", "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json"],
|
|
99
107
|
kind: "deployment",
|
|
100
108
|
});
|
|
101
109
|
return commands;
|
|
@@ -114,23 +122,32 @@ function readProjectName(parametersPath) {
|
|
|
114
122
|
}
|
|
115
123
|
function inspectRemoteGroup(options) {
|
|
116
124
|
const existsResult = runAz(["group", "exists", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription)], 60000);
|
|
117
|
-
if (existsResult.status !== 0) {
|
|
118
|
-
throw
|
|
125
|
+
if (existsResult.status !== 0 || existsResult.failureKind || !/^(true|false)$/.test(existsResult.stdout.trim())) {
|
|
126
|
+
throw azQueryError("azure-state-query-failed", "Could not query resource group", existsResult);
|
|
119
127
|
}
|
|
120
128
|
if ((existsResult.stdout || "").trim() !== "true")
|
|
121
129
|
return { exists: false, tags: {} };
|
|
122
130
|
const showResult = runAz(["group", "show", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], 60000);
|
|
123
|
-
if (showResult.status !== 0) {
|
|
124
|
-
throw
|
|
131
|
+
if (showResult.status !== 0 || showResult.failureKind || showResult.stdoutTruncated) {
|
|
132
|
+
throw azQueryError("azure-state-query-failed", "Could not read resource group tags", showResult);
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(showResult.stdout);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw azQueryError("azure-state-query-failed", "Invalid resource group response", showResult);
|
|
140
|
+
}
|
|
141
|
+
if (!parsed || typeof parsed.id !== "string") {
|
|
142
|
+
throw azQueryError("azure-state-query-failed", "Invalid resource group identity", showResult);
|
|
125
143
|
}
|
|
126
|
-
const parsed = JSON.parse(showResult.stdout || "{}");
|
|
127
144
|
return { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
|
|
128
145
|
}
|
|
129
146
|
function tagCommand(resourceId, key, state, expectedState) {
|
|
130
|
-
return { file: "az", args: ["tag", "update", "--resource-id", resourceId, "--operation", "Merge", "--tags", `${key}=${(0, migrations_1.encodeMigrationState)(state)}
|
|
147
|
+
return { file: "az", args: ["tag", "update", "--resource-id", resourceId, "--operation", "Merge", "--tags", `${key}=${(0, migrations_1.encodeMigrationState)(state)}`, "-o", "none"], kind: "tag", expectedState };
|
|
131
148
|
}
|
|
132
149
|
function bootstrapTagCommand(options, key, state) {
|
|
133
|
-
return { file: "az", args: ["group", "update", "--name", options.resourceGroup, "--set", `tags.${key}=${(0, migrations_1.encodeMigrationState)(state)}`, ...subscriptionArgs(options.subscription)], kind: "tag", expectedState: null };
|
|
150
|
+
return { file: "az", args: ["group", "update", "--name", options.resourceGroup, "--set", `tags.${key}=${(0, migrations_1.encodeMigrationState)(state)}`, ...subscriptionArgs(options.subscription), "-o", "none"], kind: "tag", expectedState: null };
|
|
134
151
|
}
|
|
135
152
|
function runBaseWhatIf(options, mainBicepPath, parametersPath) {
|
|
136
153
|
const result = runAz([
|
|
@@ -145,7 +162,7 @@ function runBaseWhatIf(options, mainBicepPath, parametersPath) {
|
|
|
145
162
|
"-o", "json",
|
|
146
163
|
]);
|
|
147
164
|
if (result.status !== 0) {
|
|
148
|
-
throw
|
|
165
|
+
throw azQueryError("what-if-failed", "az deployment group what-if failed", result);
|
|
149
166
|
}
|
|
150
167
|
return result.stdout.trim();
|
|
151
168
|
}
|
|
@@ -163,7 +180,93 @@ function assertAdoptionWhatIfIsSafe(whatIfResult) {
|
|
|
163
180
|
throw new errors_1.MachineCommandError("what-if-invalid", "Azure what-if did not return valid JSON for legacy adoption.", undefined, "blocked");
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
|
|
183
|
+
function resolveSubscription(options) {
|
|
184
|
+
if (options.subscription)
|
|
185
|
+
return options.subscription;
|
|
186
|
+
const result = runAz(["account", "show", "--query", "id", "-o", "tsv"]);
|
|
187
|
+
const subscription = result.stdout.trim();
|
|
188
|
+
if (result.status !== 0 || !SUBSCRIPTION_PATTERN.test(subscription)) {
|
|
189
|
+
throw azQueryError("azure-subscription-query-failed", "Could not identify the Azure subscription for this provision run", result);
|
|
190
|
+
}
|
|
191
|
+
return subscription;
|
|
192
|
+
}
|
|
193
|
+
function canonicalTemplate(value) {
|
|
194
|
+
const normalize = (item) => {
|
|
195
|
+
if (Array.isArray(item))
|
|
196
|
+
return item.map(normalize);
|
|
197
|
+
if (item && typeof item === "object") {
|
|
198
|
+
return Object.fromEntries(Object.entries(item).filter(([key]) => key !== "_generator")
|
|
199
|
+
.sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalize(entry)]));
|
|
200
|
+
}
|
|
201
|
+
return item;
|
|
202
|
+
};
|
|
203
|
+
return JSON.stringify(normalize(value));
|
|
204
|
+
}
|
|
205
|
+
async function verifyExistingBaseline(options, mainBicepPath, parametersPath) {
|
|
206
|
+
const projectRoot = path.dirname(path.dirname(mainBicepPath));
|
|
207
|
+
const secretValues = provisionSecretValues({
|
|
208
|
+
fingerprints: Object.fromEntries((0, migrations_1.collectLegacyBaselineFiles)(projectRoot).map(file => [file.path, file.checksum])),
|
|
209
|
+
}, projectRoot);
|
|
210
|
+
const reference = {
|
|
211
|
+
subscription: options.subscription, resourceGroup: options.resourceGroup, name: options.resumeDeployment,
|
|
212
|
+
};
|
|
213
|
+
const observation = await (0, provision_deployment_1.queryAzureDeployment)(reference, { secretValues });
|
|
214
|
+
if (observation.state === "unknown") {
|
|
215
|
+
throw new errors_1.MachineCommandError("recovery-deployment-unknown", "The existing deployment could not be identified. No Bicep will be submitted.", { deployment: observation }, "blocked");
|
|
216
|
+
}
|
|
217
|
+
const compiled = await (0, azure_cli_1.runAzCliAsync)(["bicep", "build", "--file", mainBicepPath, "--stdout"], { timeout: 60000, captureBytes: 16777216 });
|
|
218
|
+
const exported = await (0, azure_cli_1.runAzCliAsync)([
|
|
219
|
+
"deployment", "group", "export", "--name", reference.name,
|
|
220
|
+
"--resource-group", reference.resourceGroup, "--subscription", reference.subscription,
|
|
221
|
+
], { timeout: 60000, captureBytes: 16777216 });
|
|
222
|
+
const parameters = await (0, azure_cli_1.runAzCliAsync)([
|
|
223
|
+
"deployment", "group", "show", "--name", reference.name, "--resource-group", reference.resourceGroup,
|
|
224
|
+
"--subscription", reference.subscription, "--query", "properties.parameters", "-o", "json",
|
|
225
|
+
], { timeout: 30000 });
|
|
226
|
+
if ([compiled, exported, parameters].some((result) => result.status !== 0 || result.failureKind || result.stdoutTruncated)) {
|
|
227
|
+
throw new errors_1.MachineCommandError("recovery-evidence-unavailable", "Recovery requires the existing deployment template and parameters to match the local baseline. Evidence could not be read; nothing will be redeployed.", {
|
|
228
|
+
diagnostics: [compiled, exported, parameters].map((result, index) => ({
|
|
229
|
+
...(0, azure_diagnostics_1.azureCliDiagnostic)(result, ["local-bicep-build", "recovery-template", "recovery-parameters"][index], secretValues),
|
|
230
|
+
stdout: "[template/parameter payload omitted]", stderr: "[template/parameter payload omitted]", outputTruncated: true,
|
|
231
|
+
})),
|
|
232
|
+
}, "blocked");
|
|
233
|
+
}
|
|
234
|
+
let template;
|
|
235
|
+
let remoteParameters;
|
|
236
|
+
let localParameters;
|
|
237
|
+
try {
|
|
238
|
+
template = JSON.parse(compiled.stdout);
|
|
239
|
+
if (canonicalTemplate(template) !== canonicalTemplate(JSON.parse(exported.stdout))) {
|
|
240
|
+
throw new errors_1.MachineCommandError("recovery-template-mismatch", "The deployed ARM template differs from the local Bicep. Restore the matching reviewed files before recovery; do not redeploy to repair migration bookkeeping.", undefined, "blocked");
|
|
241
|
+
}
|
|
242
|
+
remoteParameters = JSON.parse(parameters.stdout);
|
|
243
|
+
localParameters = JSON.parse(fs.readFileSync(parametersPath, "utf-8")).parameters;
|
|
244
|
+
if (!remoteParameters || !localParameters || typeof remoteParameters !== "object" || typeof localParameters !== "object")
|
|
245
|
+
throw new Error("Invalid parameters");
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error instanceof errors_1.MachineCommandError)
|
|
249
|
+
throw error;
|
|
250
|
+
throw new errors_1.MachineCommandError("recovery-invalid-evidence", "Recovery template/parameter evidence is not valid JSON.", undefined, "blocked");
|
|
251
|
+
}
|
|
252
|
+
const declarations = template.parameters;
|
|
253
|
+
if (Object.values(declarations ?? {}).some((parameter) => /^secure/i.test(parameter.type ?? ""))) {
|
|
254
|
+
throw new errors_1.MachineCommandError("recovery-secure-parameters", "This untracked deployment has secure parameters that cannot be verified. Recovery requires the original execution checkpoint; no secret values will be requested or printed.", undefined, "blocked");
|
|
255
|
+
}
|
|
256
|
+
const expected = { ...localParameters, location: { value: options.location }, swaLocation: { value: options.swaLocation } };
|
|
257
|
+
for (const [name, parameter] of Object.entries(expected)) {
|
|
258
|
+
if (!parameter || !Object.prototype.hasOwnProperty.call(parameter, "value")
|
|
259
|
+
|| canonicalTemplate(parameter.value) !== canonicalTemplate(remoteParameters[name]?.value)) {
|
|
260
|
+
throw new errors_1.MachineCommandError("recovery-parameters-mismatch", "The deployed parameters differ from the local reviewed parameters/regions. No tag or ledger will be changed.", undefined, "blocked");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return observation.reference;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Builds the provision plan. Machine/MCP callers persist it for a later `apply provision --plan`;
|
|
267
|
+
* the interactive `swallowkit provision` command keeps it in memory and executes it after confirmation.
|
|
268
|
+
*/
|
|
269
|
+
async function planProvisionOperation(options, planOptions = {}) {
|
|
167
270
|
const projectRoot = process.cwd();
|
|
168
271
|
validateProvisionInputs(options);
|
|
169
272
|
const infra = (0, infra_1.inspectInfra)(projectRoot);
|
|
@@ -174,9 +277,10 @@ async function planProvisionOperation(options) {
|
|
|
174
277
|
if (!infra.parametersFile.exists) {
|
|
175
278
|
warnings.push("infra/main.parameters.json not found; deployment will fail until it is created.");
|
|
176
279
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
280
|
+
const azCli = (0, azure_cli_1.checkAzCliAvailability)();
|
|
281
|
+
const azCliAvailable = azCli.available;
|
|
282
|
+
if (!azCli.available) {
|
|
283
|
+
warnings.push(azCliUnavailableWarning(azCli));
|
|
180
284
|
}
|
|
181
285
|
const mainBicepPath = path.join(projectRoot, "infra", "main.bicep");
|
|
182
286
|
const parametersPath = path.join(projectRoot, "infra", "main.parameters.json");
|
|
@@ -188,6 +292,7 @@ async function planProvisionOperation(options) {
|
|
|
188
292
|
let whatIfResult;
|
|
189
293
|
let manifestAfterApply;
|
|
190
294
|
let baselineDrift = [];
|
|
295
|
+
let recoveryDeployment;
|
|
191
296
|
const manifestPath = path.join(projectRoot, migrations_1.MIGRATION_MANIFEST_PATH);
|
|
192
297
|
const manifest = fs.existsSync(manifestPath) ? (0, migrations_1.loadMigrationManifest)(projectRoot) : null;
|
|
193
298
|
if (manifest) {
|
|
@@ -202,6 +307,12 @@ async function planProvisionOperation(options) {
|
|
|
202
307
|
baselineDrift = (0, migrations_1.findBaselineDrift)(manifest, projectRoot);
|
|
203
308
|
}
|
|
204
309
|
if (azCliAvailable) {
|
|
310
|
+
options = { ...options, subscription: resolveSubscription(options) };
|
|
311
|
+
const unfinished = readRuns(projectRoot).find((run) => run.outcome !== "succeeded" && run.outcome !== "failed" && run.plan.resourceGroup === options.resourceGroup
|
|
312
|
+
&& run.plan.subscription === options.subscription);
|
|
313
|
+
if (unfinished) {
|
|
314
|
+
throw new errors_1.MachineCommandError("provision-run-pending", `Provision run ${unfinished.runId} is unfinished. Inspect or resume that run instead of submitting Bicep again.`, { runId: unfinished.runId, outcome: unfinished.outcome }, "blocked", provisionNextActions(unfinished.runId));
|
|
315
|
+
}
|
|
205
316
|
const remoteGroup = inspectRemoteGroup(options);
|
|
206
317
|
if (!manifest) {
|
|
207
318
|
if (remoteGroup.exists) {
|
|
@@ -210,7 +321,33 @@ async function planProvisionOperation(options) {
|
|
|
210
321
|
throw new errors_1.MachineCommandError("migration-manifest-not-found", "Initialize a migration manifest before the first provision.", undefined, "blocked");
|
|
211
322
|
}
|
|
212
323
|
migrationTagKey = (0, migrations_1.getMigrationTagKey)(manifest.projectId);
|
|
213
|
-
if (
|
|
324
|
+
if (options.resumeDeployment) {
|
|
325
|
+
if (!remoteGroup.exists || manifest.baseline.legacy) {
|
|
326
|
+
throw new errors_1.MachineCommandError("recovery-baseline-required", "--resume-deployment requires an existing resource group and the original non-legacy baseline manifest.", undefined, "blocked");
|
|
327
|
+
}
|
|
328
|
+
const tagValue = remoteGroup.tags[migrationTagKey];
|
|
329
|
+
remoteState = tagValue ? (0, migrations_1.decodeMigrationState)(tagValue) : null;
|
|
330
|
+
if (remoteState && remoteState.version !== 0) {
|
|
331
|
+
throw new errors_1.MachineCommandError("recovery-baseline-only", "Untracked-deployment recovery supports baseline 0 only. Use the original checkpoint to resume migrations.", undefined, "blocked");
|
|
332
|
+
}
|
|
333
|
+
recoveryDeployment = await verifyExistingBaseline(options, mainBicepPath, parametersPath);
|
|
334
|
+
const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
|
|
335
|
+
manifestAfterApply = {
|
|
336
|
+
...manifest,
|
|
337
|
+
baseline: { ...manifest.baseline, files: baselineFiles, checksum: (0, migrations_1.computeBaselineChecksum)(baselineFiles) },
|
|
338
|
+
};
|
|
339
|
+
if (remoteState && remoteState.checksum !== manifestAfterApply.baseline.checksum) {
|
|
340
|
+
throw new errors_1.MachineCommandError("recovery-checksum-mismatch", "The existing baseline tag does not match this project. Nothing will be overwritten.", undefined, "blocked");
|
|
341
|
+
}
|
|
342
|
+
mode = "recover";
|
|
343
|
+
warnings.push("Recovery only observes the matching existing deployment, then records migration state after Azure Succeeded. It never submits Bicep.");
|
|
344
|
+
commandSpecs = [{
|
|
345
|
+
file: "az",
|
|
346
|
+
args: ["deployment", "group", "show", "--name", recoveryDeployment.name, "--resource-group", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json"],
|
|
347
|
+
kind: "deployment",
|
|
348
|
+
}, tagCommand(remoteGroup.id, migrationTagKey, { schemaVersion: 1, version: 0, checksum: manifestAfterApply.baseline.checksum }, remoteState)];
|
|
349
|
+
}
|
|
350
|
+
else if (!remoteGroup.exists) {
|
|
214
351
|
mode = "bootstrap";
|
|
215
352
|
const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
|
|
216
353
|
manifestAfterApply = {
|
|
@@ -233,7 +370,7 @@ async function planProvisionOperation(options) {
|
|
|
233
370
|
commandSpecs = [];
|
|
234
371
|
if (options.reconcile && !manifest.baseline.legacy) {
|
|
235
372
|
whatIfResult = runBaseWhatIf(options, mainBicepPath, parametersPath);
|
|
236
|
-
warnings.push("
|
|
373
|
+
warnings.push("Explicit reconcile redeploys the untagged baseline. For an interrupted wait/tag/ledger update use --resume-deployment instead.");
|
|
237
374
|
mode = "reconcile";
|
|
238
375
|
const baselineFiles = (0, migrations_1.collectLegacyBaselineFiles)(projectRoot);
|
|
239
376
|
manifestAfterApply = {
|
|
@@ -289,12 +426,13 @@ async function planProvisionOperation(options) {
|
|
|
289
426
|
file: "az",
|
|
290
427
|
args: [
|
|
291
428
|
"deployment", "group", "create",
|
|
292
|
-
"--name",
|
|
429
|
+
"--name", `swallowkit-migration-${migration.version}-${migration.checksum.slice(0, 12)}`,
|
|
293
430
|
"--resource-group", options.resourceGroup,
|
|
294
431
|
"--template-file", path.join(projectRoot, migration.template),
|
|
295
432
|
...(migration.parameters ? ["--parameters", path.join(projectRoot, migration.parameters)] : []),
|
|
296
433
|
"--parameters", `projectName=${projectName}`, `location=${options.location}`,
|
|
297
434
|
...subscriptionArgs(options.subscription),
|
|
435
|
+
"--no-wait", "--query", provision_deployment_1.DEPLOYMENT_QUERY, "-o", "json",
|
|
298
436
|
],
|
|
299
437
|
kind: "migration",
|
|
300
438
|
expectedState,
|
|
@@ -306,8 +444,9 @@ async function planProvisionOperation(options) {
|
|
|
306
444
|
}
|
|
307
445
|
}
|
|
308
446
|
}
|
|
309
|
-
else if (options.whatIf || options.adoptExisting || options.reconcile) {
|
|
310
|
-
|
|
447
|
+
else if (options.whatIf || options.adoptExisting || options.reconcile || options.resumeDeployment) {
|
|
448
|
+
const detail = azCli.available ? "" : ` ${azCliUnavailableWarning(azCli)}`;
|
|
449
|
+
throw new errors_1.MachineCommandError(!azCli.available && azCli.reason === "not-runnable" ? "az-cli-not-runnable" : "az-cli-not-found", `Azure CLI is required for --what-if, --adopt-existing, --reconcile, and --resume-deployment.${detail}`, undefined, "blocked");
|
|
311
450
|
}
|
|
312
451
|
const fingerprints = {
|
|
313
452
|
"infra/main.bicep": (0, file_session_1.hashFileIfExists)(mainBicepPath),
|
|
@@ -337,6 +476,8 @@ async function planProvisionOperation(options) {
|
|
|
337
476
|
mode,
|
|
338
477
|
remoteState,
|
|
339
478
|
pendingMigrations: pendingMigrations.map((migration) => ({ version: migration.version, checksum: migration.checksum })),
|
|
479
|
+
recoveryDeployment,
|
|
480
|
+
nonce: crypto.randomUUID(),
|
|
340
481
|
baselineSnapshotChecksum,
|
|
341
482
|
fingerprints,
|
|
342
483
|
}))
|
|
@@ -365,116 +506,513 @@ async function planProvisionOperation(options) {
|
|
|
365
506
|
requiresApproval: true,
|
|
366
507
|
fingerprints,
|
|
367
508
|
...(baselineSnapshotChecksum ? { baselineSnapshotChecksum } : {}),
|
|
509
|
+
...(recoveryDeployment ? { recoveryDeployment } : {}),
|
|
368
510
|
};
|
|
369
|
-
(
|
|
511
|
+
if (planOptions.persist !== false)
|
|
512
|
+
(0, state_1.savePlanState)(plan, projectRoot);
|
|
370
513
|
return plan;
|
|
371
514
|
}
|
|
372
515
|
function statesEqual(left, right) {
|
|
373
516
|
return left?.version === right?.version && left?.checksum === right?.checksum;
|
|
374
517
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
518
|
+
const MACHINE_RETRY_HINT = "Re-run plan provision.";
|
|
519
|
+
exports.INTERACTIVE_PROVISION_RETRY_HINT = 'Run "swallowkit provision" again to review the current state.';
|
|
520
|
+
function provisionNextActions(runId) {
|
|
521
|
+
return [
|
|
522
|
+
{ command: `swallowkit machine inspect provision --plan ${runId}`, description: "Read the original Azure deployment state without creating, cancelling, deleting, tagging, or updating the ledger." },
|
|
523
|
+
{ command: `swallowkit machine apply provision --plan ${runId} --approve`, description: "After explicit human approval, resume this checkpoint: observe the original deployment and finish tags/ledger; never resubmit an attempted deployment." },
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
function resultForRun(run, outcome = run.outcome, interrupted) {
|
|
527
|
+
run.warnings = run.warnings.slice(-20);
|
|
528
|
+
run.diagnostics = run.diagnostics.slice(-20);
|
|
529
|
+
return {
|
|
530
|
+
status: outcome === "succeeded" ? "complete" : outcome === "in-progress" ? "in-progress" : outcome === "failed" ? "failed" : "blocked",
|
|
531
|
+
outcome, runId: run.runId, planId: run.plan.planId,
|
|
532
|
+
resourceGroup: run.plan.resourceGroup, location: run.plan.location, swaLocation: run.plan.swaLocation,
|
|
533
|
+
executedCommands: run.executedCommands, warnings: run.warnings, diagnostics: run.diagnostics,
|
|
534
|
+
...(run.observation ? { deployment: run.observation } : {}),
|
|
535
|
+
nextActions: outcome === "succeeded"
|
|
536
|
+
? [{ command: "swallowkit machine inspect infra", description: "Inspect local infrastructure assets after deployment and migration bookkeeping completed." }]
|
|
537
|
+
: provisionNextActions(run.runId),
|
|
538
|
+
...(interrupted ? { interrupted } : {}),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function readRun(runId, projectRoot) {
|
|
542
|
+
let run;
|
|
543
|
+
try {
|
|
544
|
+
run = (0, state_1.loadProvisionRun)(runId, projectRoot);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "The execution checkpoint cannot be read. Do not re-submit Bicep; inspect the original deployment first.", { runId }, "blocked");
|
|
548
|
+
}
|
|
549
|
+
if (run && (run.schemaVersion !== 1 || run.runId !== runId || run.plan?.planId !== runId
|
|
550
|
+
|| run.projectRoot !== path.resolve(projectRoot) || !Number.isInteger(run.cursor)
|
|
551
|
+
|| !Array.isArray(run.plan.commandSpecs) || !run.plan.fingerprints
|
|
552
|
+
|| !Array.isArray(run.warnings) || !Array.isArray(run.diagnostics) || !Array.isArray(run.executedCommands)
|
|
553
|
+
|| !["succeeded", "in-progress", "unknown", "partial", "failed"].includes(run.outcome)
|
|
554
|
+
|| run.cursor < 0 || run.cursor > run.plan.commandSpecs.length)) {
|
|
555
|
+
throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "The execution checkpoint is invalid or belongs to another checkout. Nothing will be re-submitted.", { runId }, "blocked");
|
|
556
|
+
}
|
|
557
|
+
return run;
|
|
558
|
+
}
|
|
559
|
+
function readRuns(projectRoot) {
|
|
560
|
+
try {
|
|
561
|
+
return (0, state_1.listProvisionRuns)(projectRoot).map(record => {
|
|
562
|
+
if (!record || typeof record.runId !== "string")
|
|
563
|
+
throw new Error("Invalid checkpoint identity.");
|
|
564
|
+
const run = readRun(record.runId, projectRoot);
|
|
565
|
+
if (!run)
|
|
566
|
+
throw new Error("Missing execution checkpoint.");
|
|
567
|
+
return run;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (error instanceof errors_1.MachineCommandError)
|
|
572
|
+
throw error;
|
|
573
|
+
throw new errors_1.MachineCommandError("invalid-provision-checkpoint", "An execution checkpoint is corrupt. Preserve it and inspect the original deployment; nothing will be re-submitted.", undefined, "blocked");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function validateWaitSeconds(value = provision_deployment_1.DEFAULT_PROVISION_WAIT_SECONDS) {
|
|
577
|
+
if (!Number.isInteger(value) || value < 0 || value > provision_deployment_1.MAX_PROVISION_WAIT_SECONDS) {
|
|
578
|
+
throw new errors_1.MachineCommandError("invalid-arguments", `--wait-seconds must be an integer from 0 to ${provision_deployment_1.MAX_PROVISION_WAIT_SECONDS}.`, undefined, "blocked");
|
|
579
|
+
}
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
function assertLocalFiles(plan, projectRoot, retryHint, allowRecordedManifest = false) {
|
|
583
|
+
const changedFiles = Object.entries(plan.fingerprints).filter(([relativePath, expectedHash]) => {
|
|
584
|
+
const file = path.join(projectRoot, relativePath);
|
|
585
|
+
if (allowRecordedManifest && relativePath === migrations_1.MIGRATION_MANIFEST_PATH && plan.manifestAfterApply && fs.existsSync(file)
|
|
586
|
+
&& fs.readFileSync(file, "utf-8") === `${JSON.stringify(plan.manifestAfterApply, null, 2)}\n`)
|
|
587
|
+
return false;
|
|
588
|
+
return (0, file_session_1.hashFileIfExists)(file) !== expectedHash;
|
|
589
|
+
}).map(([file]) => file);
|
|
590
|
+
if (changedFiles.length || (plan.baselineSnapshotChecksum
|
|
591
|
+
&& (0, migrations_1.computeBaselineChecksum)((0, migrations_1.collectLegacyBaselineFiles)(projectRoot)) !== plan.baselineSnapshotChecksum)) {
|
|
592
|
+
throw new errors_1.MachineCommandError("stale-plan", `${changedFiles.join(", ") || "Baseline infrastructure files"} changed after the preview was computed. ${retryHint}`, { planId: plan.planId, changedFiles }, "blocked");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function readMigrationState(plan, signal, secretValues = []) {
|
|
596
|
+
const exists = await (0, azure_cli_1.runAzCliAsync)(["group", "exists", "--name", plan.resourceGroup, ...subscriptionArgs(plan.subscription)], { timeout: 30000, signal });
|
|
597
|
+
if (exists.status !== 0 || exists.failureKind || !/^(true|false)$/.test(exists.stdout.trim())) {
|
|
598
|
+
throw azQueryError("azure-state-query-failed", "Could not query resource group", exists, secretValues);
|
|
599
|
+
}
|
|
600
|
+
if (exists.stdout.trim() === "false")
|
|
601
|
+
return { group: { exists: false, tags: {} }, state: null };
|
|
602
|
+
const show = await (0, azure_cli_1.runAzCliAsync)(["group", "show", "--name", plan.resourceGroup, ...subscriptionArgs(plan.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], { timeout: 30000, signal });
|
|
603
|
+
if (show.status !== 0 || show.failureKind || show.stdoutTruncated)
|
|
604
|
+
throw azQueryError("azure-state-query-failed", "Could not read resource group tags", show, secretValues);
|
|
605
|
+
let parsed;
|
|
606
|
+
try {
|
|
607
|
+
parsed = JSON.parse(show.stdout);
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
throw azQueryError("azure-state-query-failed", "Invalid resource group response", show, secretValues);
|
|
611
|
+
}
|
|
612
|
+
const expectedId = `/subscriptions/${plan.subscription}/resourceGroups/${plan.resourceGroup}`;
|
|
613
|
+
if (typeof parsed.id !== "string" || parsed.id.toLowerCase() !== expectedId.toLowerCase()) {
|
|
614
|
+
throw new errors_1.MachineCommandError("azure-resource-group-mismatch", "Azure returned a different resource group/subscription. Nothing will be updated.", undefined, "blocked");
|
|
615
|
+
}
|
|
616
|
+
const group = { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
|
|
617
|
+
const tag = plan.migrationTagKey ? group.tags[plan.migrationTagKey] : undefined;
|
|
618
|
+
return { group, state: tag ? (0, migrations_1.decodeMigrationState)(tag) : null };
|
|
619
|
+
}
|
|
620
|
+
function desiredTagState(command, key) {
|
|
621
|
+
const value = command.args.find((arg) => arg.startsWith(`${key}=`) || arg.startsWith(`tags.${key}=`));
|
|
622
|
+
if (!value)
|
|
623
|
+
throw new errors_1.MachineCommandError("incompatible-plan", "The reviewed migration tag is missing.", undefined, "blocked");
|
|
624
|
+
return (0, migrations_1.decodeMigrationState)(value.slice(value.indexOf("=") + 1));
|
|
381
625
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
|
|
626
|
+
function provisionSecretValues(plan, projectRoot) {
|
|
627
|
+
const files = Object.keys(plan.fingerprints);
|
|
628
|
+
const secureBicep = files.filter(file => file.endsWith(".bicep")).some(file => /@secure\s*\(/.test(fs.readFileSync(path.join(projectRoot, file), "utf-8")));
|
|
629
|
+
const values = [];
|
|
630
|
+
const collect = (value) => {
|
|
631
|
+
if (typeof value === "string" && value)
|
|
632
|
+
values.push(value);
|
|
633
|
+
else if (Array.isArray(value))
|
|
634
|
+
value.forEach(collect);
|
|
635
|
+
else if (value && typeof value === "object")
|
|
636
|
+
Object.values(value).forEach(collect);
|
|
637
|
+
};
|
|
638
|
+
for (const file of files.filter(file => file.endsWith(".json") && file !== migrations_1.MIGRATION_MANIFEST_PATH)) {
|
|
639
|
+
let parsed;
|
|
640
|
+
try {
|
|
641
|
+
parsed = JSON.parse(fs.readFileSync(path.join(projectRoot, file), "utf-8"));
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
throw new errors_1.MachineCommandError("invalid-infra-parameters", "Infrastructure JSON is invalid. Correct it before provision; unstructured Azure output cannot safely redact unknown parameter secrets.", undefined, "blocked");
|
|
645
|
+
}
|
|
646
|
+
for (const [name, parameter] of Object.entries(parsed.parameters ?? {})) {
|
|
647
|
+
if (secureBicep || /secret|password|token|key|connection.?string/i.test(name))
|
|
648
|
+
collect(parameter?.value);
|
|
389
649
|
}
|
|
390
|
-
return;
|
|
391
650
|
}
|
|
392
|
-
|
|
393
|
-
|
|
651
|
+
return [...new Set(values)].sort((left, right) => right.length - left.length);
|
|
652
|
+
}
|
|
653
|
+
function rememberDiagnostic(run, result, phase, secretValues) {
|
|
654
|
+
run.diagnostics.push((0, azure_diagnostics_1.azureCliDiagnostic)(result, phase, secretValues));
|
|
655
|
+
run.diagnostics = run.diagnostics.slice(-20);
|
|
656
|
+
if (result.stderr.trim()) {
|
|
657
|
+
run.warnings.push(result.stderrTruncated ? "Azure CLI stderr exceeded the capture limit; output omitted." : (0, azure_diagnostics_1.redactAzureText)(result.stderr, secretValues).slice(0, 4000));
|
|
658
|
+
run.warnings = run.warnings.slice(-20);
|
|
394
659
|
}
|
|
395
660
|
}
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
if (!
|
|
399
|
-
throw new errors_1.MachineCommandError("
|
|
661
|
+
async function inspectProvisionOperation(options) {
|
|
662
|
+
const run = readRun(options.planId, process.cwd());
|
|
663
|
+
if (!run)
|
|
664
|
+
throw new errors_1.MachineCommandError("provision-run-not-found", "No execution checkpoint exists for this ID. For an older interrupted bootstrap use plan provision --resume-deployment <name> with the original resource group/subscription/regions.", { runId: options.planId }, "blocked");
|
|
665
|
+
const waitSeconds = validateWaitSeconds(options.waitSeconds ?? 0);
|
|
666
|
+
if (run.deployment) {
|
|
667
|
+
let secretValues = [];
|
|
668
|
+
let omitOutput = false;
|
|
669
|
+
try {
|
|
670
|
+
assertLocalFiles(run.plan, process.cwd(), "Reviewed files no longer match.", true);
|
|
671
|
+
secretValues = provisionSecretValues(run.plan, process.cwd());
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
omitOutput = true;
|
|
675
|
+
run.warnings.push("Reviewed files are unavailable or changed; deployment state is still read, but diagnostic output/causes are omitted to avoid exposing unknown secrets.");
|
|
676
|
+
}
|
|
677
|
+
run.observation = await (0, provision_deployment_1.waitForAzureDeployment)(run.deployment, { deadline: Date.now() + waitSeconds * 1000, secretValues });
|
|
678
|
+
if (omitOutput) {
|
|
679
|
+
run.observation.errors = [];
|
|
680
|
+
run.observation.diagnostic = { ...run.observation.diagnostic, stdout: "[output omitted: reviewed parameters unavailable]", stderr: "", outputTruncated: true };
|
|
681
|
+
}
|
|
682
|
+
run.diagnostics = [...run.diagnostics, run.observation.diagnostic].slice(-20);
|
|
683
|
+
let outcome = run.observation.state === "Failed" || run.observation.state === "Canceled" ? "failed"
|
|
684
|
+
: run.observation.state === "Succeeded" ? run.outcome === "succeeded" ? "succeeded" : "partial"
|
|
685
|
+
: run.observation.state === "unknown" ? "unknown" : "in-progress";
|
|
686
|
+
if (outcome === "succeeded") {
|
|
687
|
+
try {
|
|
688
|
+
assertLocalFiles(run.plan, process.cwd(), "The original reviewed files no longer match.", true);
|
|
689
|
+
const tag = [...run.plan.commandSpecs].reverse().find(command => command.kind === "tag");
|
|
690
|
+
if (tag && !statesEqual((await readMigrationState(run.plan, undefined, secretValues)).state, desiredTagState(tag, run.plan.migrationTagKey))) {
|
|
691
|
+
outcome = "partial";
|
|
692
|
+
run.warnings.push("Azure Succeeded, but the completed run's migration tag no longer matches. No bookkeeping or deployment write was performed.");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
outcome = "partial";
|
|
697
|
+
run.warnings.push("Azure Succeeded, but current local files or migration bookkeeping could not be verified. No writes were performed.");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return resultForRun(run, outcome);
|
|
400
701
|
}
|
|
702
|
+
return resultForRun(run);
|
|
401
703
|
}
|
|
402
704
|
async function applyProvisionOperation(options) {
|
|
403
705
|
const projectRoot = process.cwd();
|
|
404
|
-
const
|
|
706
|
+
const run = /^[a-f0-9]{12}$/.test(options.planId) ? readRun(options.planId, projectRoot) : null;
|
|
707
|
+
const plan = run?.plan ?? (0, state_1.loadPlanState)(options.planId, projectRoot);
|
|
405
708
|
if (!plan) {
|
|
406
709
|
throw new errors_1.MachineCommandError("plan-not-found", `Plan "${options.planId}" was not found. Run "plan provision" to create a new plan.`, { planId: options.planId }, "blocked");
|
|
407
710
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (plan.baselineSnapshotChecksum) {
|
|
419
|
-
const currentBaselineChecksum = (0, migrations_1.computeBaselineChecksum)((0, migrations_1.collectLegacyBaselineFiles)(projectRoot));
|
|
420
|
-
if (currentBaselineChecksum !== plan.baselineSnapshotChecksum) {
|
|
421
|
-
throw new errors_1.MachineCommandError("stale-plan", `Plan "${options.planId}" is stale: baseline infrastructure inventory changed after the plan was created.`, { planId: options.planId }, "blocked");
|
|
711
|
+
const result = await executeProvisionPlan(plan, {
|
|
712
|
+
approve: options.approve, waitSeconds: options.waitSeconds, waitUntilComplete: options.waitUntilComplete,
|
|
713
|
+
retryHint: MACHINE_RETRY_HINT, projectRoot,
|
|
714
|
+
});
|
|
715
|
+
if (result.status === "complete") {
|
|
716
|
+
try {
|
|
717
|
+
(0, state_1.deletePlanState)(plan.planId, projectRoot);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
result.warnings.push("Azure and migration bookkeeping completed, but the old preview could not be removed. The completed execution checkpoint prevents replay.");
|
|
422
721
|
}
|
|
423
722
|
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Runs an approved provision plan with every safety check: local files and Azure state must still
|
|
727
|
+
* match the reviewed preview, and each migration/tag step re-checks Azure state before it runs.
|
|
728
|
+
*/
|
|
729
|
+
async function executeProvisionPlan(plan, options = {}) {
|
|
730
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
731
|
+
const retryHint = options.retryHint ?? MACHINE_RETRY_HINT;
|
|
732
|
+
const waitUntilComplete = options.waitUntilComplete === true && options.waitSeconds === undefined;
|
|
733
|
+
const waitSeconds = waitUntilComplete ? 0 : validateWaitSeconds(options.waitSeconds);
|
|
424
734
|
if (!options.approve) {
|
|
425
|
-
throw new errors_1.MachineCommandError("approval-required", "Provisioning creates billable Azure resources and always requires human approval. Review plan.commands and re-run with --approve.", { planId: plan.planId, commands: plan.commands }, "requires-human");
|
|
735
|
+
throw new errors_1.MachineCommandError("approval-required", "Provisioning creates billable Azure resources and always requires human approval. Review plan.commands and re-run with --approve.", { planId: plan.planId, commands: plan.commands }, "requires-human", [{ command: `swallowkit machine apply provision --plan ${plan.planId} --approve`, description: "Review the commands and obtain explicit human consent before applying or resuming." }]);
|
|
426
736
|
}
|
|
427
737
|
if (!plan.mode || !plan.commandSpecs) {
|
|
428
|
-
throw new errors_1.MachineCommandError("incompatible-plan",
|
|
738
|
+
throw new errors_1.MachineCommandError("incompatible-plan", `This provision plan was created by an older SwallowKit version. ${retryHint}`, undefined, "blocked");
|
|
429
739
|
}
|
|
430
740
|
if (plan.mode === "offline") {
|
|
431
|
-
throw new errors_1.MachineCommandError("online-plan-required",
|
|
741
|
+
throw new errors_1.MachineCommandError("online-plan-required", `Azure state was not read because Azure CLI was unavailable, so nothing can be applied. Make az available and signed in (az login). ${retryHint}`, { planId: plan.planId, warnings: plan.warnings }, "blocked");
|
|
432
742
|
}
|
|
433
743
|
if (plan.mode === "adoption-required") {
|
|
434
|
-
throw new errors_1.MachineCommandError("adoption-required", 'This
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
if (command.kind === "tag") {
|
|
448
|
-
assertTagExpectedState(plan, command.expectedState);
|
|
744
|
+
throw new errors_1.MachineCommandError("adoption-required", 'This resource group has no migration tag. Recover a matching existing baseline with --resume-deployment <name>; adoption applies only to a legacy manifest. Do not reconcile just to repair bookkeeping.', { planId: plan.planId }, "requires-human");
|
|
745
|
+
}
|
|
746
|
+
let run = readRun(plan.planId, projectRoot);
|
|
747
|
+
const resuming = run !== null;
|
|
748
|
+
if (run)
|
|
749
|
+
plan = run.plan;
|
|
750
|
+
try {
|
|
751
|
+
assertLocalFiles(plan, projectRoot, run ? "Restore the original reviewed files before resuming this checkpoint; do not resubmit Bicep." : retryHint, run?.cursor === plan.commandSpecs.length);
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
if (run && error instanceof errors_1.MachineCommandError) {
|
|
755
|
+
throw new errors_1.MachineCommandError(error.code, error.message, { ...resultForRun(run), cause: error.details }, error.status, provisionNextActions(run.runId));
|
|
449
756
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
const secretValues = provisionSecretValues(plan, projectRoot);
|
|
760
|
+
if (!plan.subscription || plan.commandSpecs.some((command) => (command.kind === "deployment" || command.kind === "migration")
|
|
761
|
+
&& !plan.recoveryDeployment && !command.args.includes("--no-wait"))) {
|
|
762
|
+
throw new errors_1.MachineCommandError("incompatible-plan", `Create a fresh preview with bounded deployment tracking. For an older interrupted deployment use --resume-deployment instead of resubmitting Bicep. ${retryHint}`, undefined, "blocked");
|
|
763
|
+
}
|
|
764
|
+
const azCli = (0, azure_cli_1.checkAzCliAvailability)();
|
|
765
|
+
if (!azCli.available) {
|
|
766
|
+
throw new errors_1.MachineCommandError(azCli.reason === "not-found" ? "az-cli-not-found" : "az-cli-not-runnable", azCliUnavailableWarning(azCli), undefined, "blocked");
|
|
767
|
+
}
|
|
768
|
+
const targetKey = crypto.createHash("sha256").update(`${plan.subscription}/${plan.resourceGroup.toLowerCase()}`).digest("hex");
|
|
769
|
+
let unlock;
|
|
770
|
+
try {
|
|
771
|
+
unlock = (0, state_1.lockProvisionTarget)(targetKey, projectRoot);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
throw new errors_1.MachineCommandError("provision-target-locked", error instanceof Error ? error.message : "Provision target is locked.", { runId: plan.planId }, "blocked", provisionNextActions(plan.planId));
|
|
775
|
+
}
|
|
776
|
+
const controller = new AbortController();
|
|
777
|
+
let interrupted;
|
|
778
|
+
const interrupt = (signal) => {
|
|
779
|
+
interrupted = signal;
|
|
780
|
+
controller.abort();
|
|
781
|
+
};
|
|
782
|
+
const sigint = () => interrupt("SIGINT");
|
|
783
|
+
const sigterm = () => interrupt("SIGTERM");
|
|
784
|
+
const externalAbort = () => controller.abort();
|
|
785
|
+
process.on("SIGINT", sigint);
|
|
786
|
+
process.on("SIGTERM", sigterm);
|
|
787
|
+
options.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
788
|
+
if (options.signal?.aborted)
|
|
789
|
+
controller.abort();
|
|
790
|
+
const signal = controller.signal;
|
|
791
|
+
const deadline = waitUntilComplete ? Number.POSITIVE_INFINITY : Date.now() + waitSeconds * 1000;
|
|
792
|
+
const persist = () => {
|
|
793
|
+
if (!run)
|
|
794
|
+
return;
|
|
795
|
+
run.warnings = run.warnings.slice(-20);
|
|
796
|
+
run.diagnostics = run.diagnostics.slice(-20);
|
|
797
|
+
run.updatedAt = new Date().toISOString();
|
|
798
|
+
(0, state_1.saveProvisionRun)(run, projectRoot);
|
|
799
|
+
};
|
|
800
|
+
const pause = (outcome, warning) => {
|
|
801
|
+
run.outcome = outcome;
|
|
802
|
+
run.warnings.push(warning);
|
|
803
|
+
persist();
|
|
804
|
+
return resultForRun(run, outcome, interrupted);
|
|
805
|
+
};
|
|
806
|
+
const observe = async () => {
|
|
807
|
+
const observation = await (0, provision_deployment_1.waitForAzureDeployment)(run.deployment, {
|
|
808
|
+
deadline: signal.aborted ? Date.now() : deadline,
|
|
809
|
+
// A final short read after Ctrl+C must not inherit the aborted wait signal.
|
|
810
|
+
...(signal.aborted ? {} : { signal }),
|
|
811
|
+
...(signal.aborted ? { queryTimeoutMs: 5000 } : {}),
|
|
812
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
813
|
+
...(waitUntilComplete ? { transientRetries: provision_deployment_1.UNBOUNDED_WAIT_TRANSIENT_RETRIES } : {}),
|
|
814
|
+
secretValues,
|
|
815
|
+
onObservation: (value) => {
|
|
816
|
+
run.observation = value;
|
|
817
|
+
if (value.state !== "unknown")
|
|
818
|
+
run.deployment = value.reference;
|
|
819
|
+
persist();
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
run.observation = observation;
|
|
823
|
+
run.diagnostics.push(observation.diagnostic);
|
|
824
|
+
run.diagnostics = run.diagnostics.slice(-20);
|
|
825
|
+
return observation;
|
|
826
|
+
};
|
|
827
|
+
try {
|
|
828
|
+
if (!run) {
|
|
829
|
+
const unfinished = readRuns(projectRoot).find((other) => other.outcome !== "succeeded" && other.outcome !== "failed"
|
|
830
|
+
&& other.plan.resourceGroup === plan.resourceGroup && other.plan.subscription === plan.subscription);
|
|
831
|
+
if (unfinished)
|
|
832
|
+
throw new errors_1.MachineCommandError("provision-run-pending", "Resume the existing execution checkpoint instead of creating another deployment.", { runId: unfinished.runId }, "blocked", provisionNextActions(unfinished.runId));
|
|
833
|
+
const current = await readMigrationState(plan, signal, secretValues);
|
|
834
|
+
if (plan.mode === "bootstrap" ? current.group.exists : !current.group.exists || !statesEqual(current.state, plan.remoteState)) {
|
|
835
|
+
throw new errors_1.MachineCommandError("stale-azure-state", plan.mode === "bootstrap"
|
|
836
|
+
? `The resource group was created after the preview was computed. ${retryHint}`
|
|
837
|
+
: `Azure migration state changed after the preview was computed. ${retryHint}`, { expected: plan.remoteState, actual: current.state }, "blocked");
|
|
838
|
+
}
|
|
839
|
+
const checkpointPlan = { ...plan };
|
|
840
|
+
delete checkpointPlan.whatIfResult;
|
|
841
|
+
run = {
|
|
842
|
+
schemaVersion: 1, runId: plan.planId, projectRoot: path.resolve(projectRoot), plan: checkpointPlan,
|
|
843
|
+
cursor: 0, outcome: "in-progress", executedCommands: [], warnings: [...plan.warnings], diagnostics: [],
|
|
844
|
+
updatedAt: new Date().toISOString(),
|
|
845
|
+
};
|
|
846
|
+
persist();
|
|
455
847
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
848
|
+
for (; run.cursor < plan.commandSpecs.length;) {
|
|
849
|
+
if (signal.aborted)
|
|
850
|
+
return pause(run.observation?.state === "Succeeded" ? "partial" : "unknown", "Local provisioning was interrupted. Azure may continue; use status/resume. No Azure cancel/delete was requested and the ledger is not marked successful.");
|
|
851
|
+
assertLocalFiles(plan, projectRoot, "Restore the original reviewed files before resuming; do not resubmit Bicep.");
|
|
852
|
+
const command = plan.commandSpecs[run.cursor];
|
|
853
|
+
if (command.kind === "deployment" || command.kind === "migration") {
|
|
854
|
+
if (command.kind === "migration" && run.attemptedIndex !== run.cursor && run.observation?.state === "Succeeded" && Date.now() >= deadline) {
|
|
855
|
+
return pause("partial", "The bounded wait budget ended before the next reviewed migration. Resume this checkpoint after approval; completed deployments will not be re-submitted.");
|
|
856
|
+
}
|
|
857
|
+
if (run.attemptedIndex !== run.cursor) {
|
|
858
|
+
let observationOnly = Boolean(plan.recoveryDeployment);
|
|
859
|
+
if (plan.recoveryDeployment) {
|
|
860
|
+
run.deployment = plan.recoveryDeployment;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
const current = await readMigrationState(plan, signal, secretValues);
|
|
864
|
+
if (!current.group.exists || !statesEqual(current.state, command.expectedState ?? plan.remoteState)) {
|
|
865
|
+
throw new errors_1.MachineCommandError("stale-azure-state", `Azure migration state changed before deployment. ${retryHint}`, { expected: command.expectedState ?? plan.remoteState, actual: current.state }, "blocked");
|
|
866
|
+
}
|
|
867
|
+
const name = command.args[command.args.indexOf("--name") + 1];
|
|
868
|
+
const reference = { name, resourceGroup: plan.resourceGroup, subscription: plan.subscription };
|
|
869
|
+
const previous = await (0, provision_deployment_1.queryAzureDeployment)(reference, { signal, secretValues });
|
|
870
|
+
if (previous.state === "unknown" && previous.reason !== "not-found") {
|
|
871
|
+
run.deployment = reference;
|
|
872
|
+
run.observation = previous;
|
|
873
|
+
return pause("unknown", "The deployment name could not be checked before submission. No create request was sent; inspect the original deployment before proceeding.");
|
|
874
|
+
}
|
|
875
|
+
if (previous.state !== "unknown" && (plan.mode !== "reconcile" || previous.state === "Running" || previous.state === "Accepted")) {
|
|
876
|
+
run.deployment = previous.reference;
|
|
877
|
+
run.observation = previous;
|
|
878
|
+
if (!resuming || command.kind !== "deployment") {
|
|
879
|
+
return pause("unknown", "A deployment already exists under the reviewed name. No create request was sent. Resume only after matching its template/parameters; do not overwrite or re-submit it.");
|
|
880
|
+
}
|
|
881
|
+
run.deployment = await verifyExistingBaseline({ ...plan, resumeDeployment: name }, path.join(projectRoot, "infra", "main.bicep"), path.join(projectRoot, "infra", "main.parameters.json"));
|
|
882
|
+
observationOnly = true;
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
run.deployment = {
|
|
886
|
+
...reference,
|
|
887
|
+
...(previous.state !== "unknown" ? { previousCorrelationId: previous.reference.correlationId } : {}),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Persist intent before spawn: even a hard process termination must never replay this create.
|
|
892
|
+
run.attemptedIndex = run.cursor;
|
|
893
|
+
persist();
|
|
894
|
+
if (!observationOnly) {
|
|
895
|
+
const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
|
|
896
|
+
run.executedCommands.push(displayCommand(command));
|
|
897
|
+
rememberDiagnostic(run, result, command.kind === "migration" ? "migration-submit" : "baseline-submit", secretValues);
|
|
898
|
+
persist();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const observation = await observe();
|
|
902
|
+
if (signal.aborted)
|
|
903
|
+
return pause(observation.state === "Succeeded" ? "partial" : observation.state === "Running" || observation.state === "Accepted" ? "in-progress" : "unknown", "Local provisioning was interrupted. Azure may continue. The original deployment is tracked; no cancellation or successful ledger update was performed.");
|
|
904
|
+
if (observation.state === "Failed" || observation.state === "Canceled") {
|
|
905
|
+
const operations = await (0, provision_deployment_1.queryDeploymentOperationErrors)(observation.reference, secretValues, signal);
|
|
906
|
+
observation.errors.push(...operations.errors);
|
|
907
|
+
run.diagnostics.push(operations.diagnostic);
|
|
908
|
+
run.outcome = "failed";
|
|
909
|
+
persist();
|
|
910
|
+
throw new errors_1.MachineCommandError("azure-deployment-failed", `Azure deployment ${observation.reference.name} is ${observation.state}.${observation.errors.length ? `\n${observation.errors.map((error) => `${error.code}: ${error.message}`).join("\n")}` : " Azure did not return a structured cause; inspect deployment operations."}`, resultForRun(run), "failed", provisionNextActions(run.runId));
|
|
911
|
+
}
|
|
912
|
+
if (observation.state !== "Succeeded") {
|
|
913
|
+
return pause(observation.state === "unknown" ? "unknown" : "in-progress", observation.state === "unknown" ? `Azure deployment result is unknown (${observation.reason}); a local az error is not proof that Azure failed. No create will be automatically retried.`
|
|
914
|
+
: `Azure deployment is ${observation.state}. The bounded local wait ended; status/resume tracks the same deployment without submitting Bicep again.`);
|
|
915
|
+
}
|
|
460
916
|
}
|
|
461
|
-
|
|
462
|
-
|
|
917
|
+
else if (command.kind === "tag") {
|
|
918
|
+
if (run.deployment) {
|
|
919
|
+
const observation = await (0, provision_deployment_1.queryAzureDeployment)(run.deployment, { signal, secretValues });
|
|
920
|
+
run.observation = observation;
|
|
921
|
+
if (observation.state !== "Succeeded")
|
|
922
|
+
return pause("partial", "Azure Succeeded was previously observed, but its identity/terminal state could not be confirmed before migration tagging. No tag or ledger update was performed.");
|
|
923
|
+
}
|
|
924
|
+
const current = await readMigrationState(plan, signal, secretValues);
|
|
925
|
+
const desired = desiredTagState(command, plan.migrationTagKey);
|
|
926
|
+
if (!current.group.exists)
|
|
927
|
+
throw new errors_1.MachineCommandError("stale-azure-state", "The resource group disappeared before tagging.", undefined, "blocked");
|
|
928
|
+
if (!statesEqual(current.state, desired)) {
|
|
929
|
+
if (!statesEqual(current.state, command.expectedState)) {
|
|
930
|
+
throw new errors_1.MachineCommandError("stale-azure-state", `Azure migration state changed before its tag could be updated. ${retryHint}`, { expected: command.expectedState, actual: current.state }, "blocked");
|
|
931
|
+
}
|
|
932
|
+
run.attemptedIndex = run.cursor;
|
|
933
|
+
persist();
|
|
934
|
+
const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
|
|
935
|
+
run.executedCommands.push(displayCommand(command));
|
|
936
|
+
rememberDiagnostic(run, result, "migration-tag", secretValues);
|
|
937
|
+
if (result.status !== 0 || result.failureKind || signal.aborted) {
|
|
938
|
+
return pause("partial", "Azure deployment succeeded, but migration tagging did not complete locally. Resume this checkpoint to verify/finish tagging; do not redeploy the baseline.");
|
|
939
|
+
}
|
|
940
|
+
const after = await readMigrationState(plan, signal, secretValues);
|
|
941
|
+
if (!statesEqual(after.state, desired))
|
|
942
|
+
return pause("partial", "The tag update returned successfully but its version/checksum could not be confirmed. Resume bookkeeping without redeploying Bicep.");
|
|
943
|
+
}
|
|
463
944
|
}
|
|
945
|
+
else {
|
|
946
|
+
const current = await readMigrationState(plan, signal, secretValues);
|
|
947
|
+
if (run.attemptedIndex === run.cursor) {
|
|
948
|
+
if (!current.group.exists)
|
|
949
|
+
return pause("unknown", "Resource group creation was attempted, but its result is unknown. Inspect the target before resuming; nothing will be re-created automatically.");
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
if (current.group.exists)
|
|
953
|
+
throw new errors_1.MachineCommandError("stale-azure-state", `The resource group was created after the preview was computed. ${retryHint}`, undefined, "blocked");
|
|
954
|
+
run.attemptedIndex = run.cursor;
|
|
955
|
+
persist();
|
|
956
|
+
const result = await (0, azure_cli_1.runAzCliAsync)(command.args, { timeout: 60000, signal });
|
|
957
|
+
run.executedCommands.push(displayCommand(command));
|
|
958
|
+
rememberDiagnostic(run, result, "resource-group-create", secretValues);
|
|
959
|
+
if (result.status !== 0 || result.failureKind || signal.aborted)
|
|
960
|
+
return pause("unknown", "Local resource-group creation did not complete. Azure may have accepted it; inspect/resume this checkpoint, not a new bootstrap.");
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
run.cursor++;
|
|
964
|
+
delete run.attemptedIndex;
|
|
965
|
+
persist();
|
|
966
|
+
}
|
|
967
|
+
if (signal.aborted)
|
|
968
|
+
return pause("partial", "Local provisioning was interrupted before ledger completion. Resume bookkeeping; no deployment will be submitted again.");
|
|
969
|
+
assertLocalFiles(plan, projectRoot, "Restore the reviewed files before completing the local ledger.", true);
|
|
970
|
+
const finalTag = [...plan.commandSpecs].reverse().find((command) => command.kind === "tag");
|
|
971
|
+
if (finalTag) {
|
|
972
|
+
const current = await readMigrationState(plan, signal, secretValues);
|
|
973
|
+
if (!statesEqual(current.state, desiredTagState(finalTag, plan.migrationTagKey)))
|
|
974
|
+
return pause("partial", "The final remote migration version/checksum changed before local ledger completion. No ledger success was recorded.");
|
|
464
975
|
}
|
|
976
|
+
if (run.deployment) {
|
|
977
|
+
run.observation = await (0, provision_deployment_1.queryAzureDeployment)(run.deployment, { signal, secretValues });
|
|
978
|
+
if (run.observation.state !== "Succeeded")
|
|
979
|
+
return pause("partial", "The original deployment identity/terminal state could not be confirmed before local ledger completion. No successful ledger update was performed.");
|
|
980
|
+
}
|
|
981
|
+
if (plan.manifestAfterApply && run.outcome !== "succeeded")
|
|
982
|
+
(0, migrations_1.replaceMigrationManifest)(plan.manifestAfterApply, projectRoot);
|
|
983
|
+
run.outcome = "succeeded";
|
|
984
|
+
persist();
|
|
985
|
+
return resultForRun(run);
|
|
465
986
|
}
|
|
466
|
-
|
|
467
|
-
|
|
987
|
+
catch (error) {
|
|
988
|
+
if (!run)
|
|
989
|
+
throw error;
|
|
990
|
+
if (error instanceof errors_1.MachineCommandError && error.code === "azure-deployment-failed")
|
|
991
|
+
throw error;
|
|
992
|
+
run.outcome = run.observation?.state === "Succeeded" ? "partial" : "unknown";
|
|
993
|
+
run.warnings.push(`Provisioning stopped: ${(0, azure_diagnostics_1.redactAzureText)(error instanceof Error ? error.message : "Unknown local error", secretValues)}. Azure deployment and local bookkeeping are separate; inspect/resume this checkpoint.`);
|
|
994
|
+
try {
|
|
995
|
+
persist();
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
run.warnings.push("The checkpoint could not be updated. Preserve the existing checkpoint; inspect the original Azure deployment before any further write.");
|
|
999
|
+
}
|
|
1000
|
+
if (error instanceof errors_1.MachineCommandError) {
|
|
1001
|
+
throw new errors_1.MachineCommandError(error.code, error.message, { ...resultForRun(run), cause: error.details }, error.status ?? "blocked", provisionNextActions(run.runId));
|
|
1002
|
+
}
|
|
1003
|
+
return resultForRun(run, run.outcome, interrupted);
|
|
1004
|
+
}
|
|
1005
|
+
finally {
|
|
1006
|
+
process.removeListener("SIGINT", sigint);
|
|
1007
|
+
process.removeListener("SIGTERM", sigterm);
|
|
1008
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
1009
|
+
try {
|
|
1010
|
+
unlock();
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
run?.warnings.push("The local execution lock could not be removed. Inspect its owner before retrying; Azure deployment/ledger results are unchanged.");
|
|
1014
|
+
console.warn("Provision lock cleanup failed; inspect the checkpoint before another apply.");
|
|
1015
|
+
}
|
|
468
1016
|
}
|
|
469
|
-
(0, state_1.deletePlanState)(plan.planId, projectRoot);
|
|
470
|
-
return {
|
|
471
|
-
planId: plan.planId,
|
|
472
|
-
resourceGroup: plan.resourceGroup,
|
|
473
|
-
location: plan.location,
|
|
474
|
-
swaLocation: plan.swaLocation,
|
|
475
|
-
executedCommands,
|
|
476
|
-
...(deploymentOutputs ? { deploymentOutputs } : {}),
|
|
477
|
-
warnings,
|
|
478
|
-
};
|
|
479
1017
|
}
|
|
480
1018
|
//# sourceMappingURL=provision-operations.js.map
|