swallowkit 2.0.0-alpha.7 → 2.0.0-alpha.9
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/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +21 -9
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +6 -3
- package/dist/cli/commands/provision.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/provision-deployment.d.ts +7 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -1
- package/dist/core/operations/provision-deployment.js +16 -3
- package/dist/core/operations/provision-deployment.js.map +1 -1
- package/dist/core/operations/provision-operations.d.ts +4 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +8 -3
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/capabilities.js +1 -1
- package/dist/core/project/capabilities.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/azure-cli.cjs +2 -1
- package/src/__tests__/init.test.ts +59 -0
- package/src/__tests__/provision-runtime.test.ts +61 -0
- package/src/cli/commands/init.ts +21 -9
- package/src/cli/commands/provision.ts +6 -3
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/provision-deployment.ts +17 -1
- package/src/core/operations/provision-operations.ts +17 -5
- package/src/core/project/capabilities.ts +1 -1
package/src/cli/commands/init.ts
CHANGED
|
@@ -1825,10 +1825,12 @@ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
|
1825
1825
|
|
|
1826
1826
|
This creates:
|
|
1827
1827
|
- Static Web App (\`swa-${projectName}\`)
|
|
1828
|
-
- Azure Functions (\`func-${projectName}
|
|
1829
|
-
- Cosmos DB (\`cosmos-${projectName}
|
|
1828
|
+
- Azure Functions (\`func-${projectName}-<suffix>\`)
|
|
1829
|
+
- Cosmos DB (\`cosmos-${projectName}-<suffix>\`)
|
|
1830
1830
|
- Storage Account
|
|
1831
1831
|
|
|
1832
|
+
Cosmos DB and Function App names must be globally unique, so they include a short suffix derived from the resource group. When provisioning completes, the command prints the actual resource names and the CI/CD settings to use.
|
|
1833
|
+
|
|
1832
1834
|
You will be prompted to select Azure regions:
|
|
1833
1835
|
1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
|
|
1834
1836
|
2. **Static Web App location**: Limited availability (default: East Asia)
|
|
@@ -1844,12 +1846,12 @@ ${cicdChoice === 'github' ? `#### GitHub Actions
|
|
|
1844
1846
|
|
|
1845
1847
|
2. Get Function App publish profile:
|
|
1846
1848
|
\`\`\`bash
|
|
1847
|
-
az webapp deployment list-publishing-profiles --name
|
|
1849
|
+
az webapp deployment list-publishing-profiles --name <functions-app-name> --resource-group <rg-name> --xml
|
|
1848
1850
|
\`\`\`
|
|
1849
1851
|
|
|
1850
1852
|
3. Add secrets to GitHub repository:
|
|
1851
1853
|
- \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
|
|
1852
|
-
- \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}
|
|
1854
|
+
- \`AZURE_FUNCTIONAPP_NAME\`: Functions app name printed by \`swallowkit provision\` (\`func-${projectName}-<suffix>\`)
|
|
1853
1855
|
- \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
|
|
1854
1856
|
|
|
1855
1857
|
4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
|
|
@@ -1871,8 +1873,10 @@ az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --ap
|
|
|
1871
1873
|
\`\`\`bash
|
|
1872
1874
|
cd functions
|
|
1873
1875
|
${pmCmd.run} build
|
|
1874
|
-
func azure functionapp publish
|
|
1875
|
-
|
|
1876
|
+
func azure functionapp publish <functions-app-name>
|
|
1877
|
+
\`\`\`
|
|
1878
|
+
|
|
1879
|
+
Use the Functions app name printed by \`swallowkit provision\`.`}
|
|
1876
1880
|
|
|
1877
1881
|
## 🔧 Available Commands
|
|
1878
1882
|
|
|
@@ -2049,6 +2053,14 @@ param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
|
|
|
2049
2053
|
@description('Enable VNet integration')
|
|
2050
2054
|
param enableVNet bool = ${enableVNet}
|
|
2051
2055
|
|
|
2056
|
+
// Cosmos DB account and Function App names are globally unique across Azure.
|
|
2057
|
+
// A suffix derived from the resource group keeps names stable for redeployments
|
|
2058
|
+
// to the same resource group while avoiding collisions with other environments
|
|
2059
|
+
// and with names Azure still reserves after a previous resource group is deleted.
|
|
2060
|
+
var resourceSuffix = take(uniqueString(resourceGroup().id, projectName), 6)
|
|
2061
|
+
var cosmosAccountName = 'cosmos-\${take(toLower(projectName), 30)}-\${resourceSuffix}'
|
|
2062
|
+
var functionsAppName = 'func-\${take(projectName, 40)}-\${resourceSuffix}'
|
|
2063
|
+
|
|
2052
2064
|
// Shared Log Analytics Workspace (in Functions region for data residency)
|
|
2053
2065
|
module logAnalytics 'modules/loganalytics.bicep' = {
|
|
2054
2066
|
name: 'logAnalytics'
|
|
@@ -2102,7 +2114,7 @@ module vnet 'modules/vnet.bicep' = if (enableVNet) {
|
|
|
2102
2114
|
module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
|
|
2103
2115
|
name: 'cosmosDb'
|
|
2104
2116
|
params: {
|
|
2105
|
-
accountName:
|
|
2117
|
+
accountName: cosmosAccountName
|
|
2106
2118
|
databaseName: '\${projectName}Database'
|
|
2107
2119
|
location: location
|
|
2108
2120
|
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
@@ -2112,7 +2124,7 @@ module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode ==
|
|
|
2112
2124
|
module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
|
|
2113
2125
|
name: 'cosmosDb'
|
|
2114
2126
|
params: {
|
|
2115
|
-
accountName:
|
|
2127
|
+
accountName: cosmosAccountName
|
|
2116
2128
|
databaseName: '\${projectName}Database'
|
|
2117
2129
|
location: location
|
|
2118
2130
|
publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
|
|
@@ -2136,7 +2148,7 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
|
|
|
2136
2148
|
module functionsFlex 'modules/functions-flex.bicep' = {
|
|
2137
2149
|
name: 'functionsApp'
|
|
2138
2150
|
params: {
|
|
2139
|
-
name:
|
|
2151
|
+
name: functionsAppName
|
|
2140
2152
|
location: location
|
|
2141
2153
|
storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
|
|
2142
2154
|
appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
|
|
@@ -170,8 +170,8 @@ export function formatProvisionResult(result: ApplyProvisionResult): string {
|
|
|
170
170
|
lines.push(formatAzureDiagnostic(diagnostic));
|
|
171
171
|
}
|
|
172
172
|
if (result.status !== "complete") {
|
|
173
|
-
lines.push(`Check
|
|
174
|
-
if (result.outcome !== "failed") lines.push(`Resume: swallowkit provision --resume ${result.runId}`);
|
|
173
|
+
lines.push(`Check status: swallowkit provision --status ${result.runId}`);
|
|
174
|
+
if (result.outcome !== "failed") lines.push(`Resume and wait until completion: swallowkit provision --resume ${result.runId}`);
|
|
175
175
|
if (result.outcome !== "failed") lines.push("Azure may continue after Ctrl+C or process termination. No Azure cancellation or deletion is performed.");
|
|
176
176
|
else lines.push("Azure reported a terminal failure. Correct its cause before explicitly deploying again; resuming does not repair a failed deployment.");
|
|
177
177
|
}
|
|
@@ -222,7 +222,7 @@ export function createProvisionCommand(): Command {
|
|
|
222
222
|
.option("--status <runId>", "Read an existing execution checkpoint and Azure deployment state without writes")
|
|
223
223
|
.option("--resume <runId>", "Continue the original execution without resubmitting attempted deployments")
|
|
224
224
|
.option("--resume-deployment <name>", "Recover an older untracked baseline deployment after verifying its template and parameters")
|
|
225
|
-
.
|
|
225
|
+
.addOption(new Option("--wait-seconds <seconds>", "Optional limit on waiting, 0-3600 seconds (default: wait until Azure finishes; status: 0)").hideHelp())
|
|
226
226
|
.action(async (options: ProvisionCliOptions) => {
|
|
227
227
|
try {
|
|
228
228
|
ensureSwallowKitProject("provision");
|
|
@@ -261,6 +261,7 @@ export function createProvisionCommand(): Command {
|
|
|
261
261
|
planId: options.resume,
|
|
262
262
|
approve: true,
|
|
263
263
|
waitSeconds,
|
|
264
|
+
waitUntilComplete: waitSeconds === undefined,
|
|
264
265
|
});
|
|
265
266
|
console.log(formatProvisionResult(result));
|
|
266
267
|
await printProvisionSetup(result);
|
|
@@ -315,11 +316,13 @@ export function createProvisionCommand(): Command {
|
|
|
315
316
|
|
|
316
317
|
console.log(`\n${describeProvisionAction(preview)}`);
|
|
317
318
|
console.log("Deploying Azure infrastructure. Azure resources may incur charges.");
|
|
319
|
+
console.log("Waiting for the Azure deployment to finish. This often takes 10-20 minutes; resource information and CI/CD settings are shown when it completes.");
|
|
318
320
|
|
|
319
321
|
const result = await executeProvisionPlan(preview, {
|
|
320
322
|
approve: true,
|
|
321
323
|
retryHint: INTERACTIVE_PROVISION_RETRY_HINT,
|
|
322
324
|
waitSeconds,
|
|
325
|
+
waitUntilComplete: waitSeconds === undefined,
|
|
323
326
|
});
|
|
324
327
|
console.log(formatProvisionResult(result));
|
|
325
328
|
await printProvisionSetup(result);
|
|
@@ -152,6 +152,19 @@ param location string
|
|
|
152
152
|
return migration;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
const SUFFIXED_COSMOS_ACCOUNT_NAME = "'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'";
|
|
156
|
+
const LEGACY_COSMOS_ACCOUNT_NAME = "'cosmos-${projectName}'";
|
|
157
|
+
|
|
158
|
+
// Projects initialized before resource-group suffixes keep their original
|
|
159
|
+
// account name so model migrations target the account their baseline created.
|
|
160
|
+
function cosmosAccountNameExpression(session: FileOperationSession): string {
|
|
161
|
+
const mainBicep = path.join(session.rootDirectory, "infra", "main.bicep");
|
|
162
|
+
if (session.fileExists(mainBicep) && /var\s+resourceSuffix\s*=/.test(session.readFile(mainBicep))) {
|
|
163
|
+
return SUFFIXED_COSMOS_ACCOUNT_NAME;
|
|
164
|
+
}
|
|
165
|
+
return LEGACY_COSMOS_ACCOUNT_NAME;
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
export function generateModelContainerMigration(
|
|
156
169
|
model: { name: string; partitionKey: string },
|
|
157
170
|
session: FileOperationSession = new FileOperationSession("commit")
|
|
@@ -175,7 +188,7 @@ param projectName string
|
|
|
175
188
|
param location string
|
|
176
189
|
|
|
177
190
|
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
|
|
178
|
-
name:
|
|
191
|
+
name: ${cosmosAccountNameExpression(session)}
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
|
|
@@ -4,6 +4,8 @@ import { AzureCliDiagnostic, azureCliDiagnostic, redactAzureText } from "../../u
|
|
|
4
4
|
export const DEPLOYMENT_QUERY = "{id:id,name:name,state:properties.provisioningState,correlationId:properties.correlationId,templateHash:properties.templateHash,timestamp:properties.timestamp,error:properties.error}";
|
|
5
5
|
export const DEFAULT_PROVISION_WAIT_SECONDS = 300;
|
|
6
6
|
export const MAX_PROVISION_WAIT_SECONDS = 3_600;
|
|
7
|
+
/** Consecutive transient state-read failures tolerated while the human CLI waits for a terminal state. */
|
|
8
|
+
export const UNBOUNDED_WAIT_TRANSIENT_RETRIES = 24;
|
|
7
9
|
|
|
8
10
|
export interface DeploymentReference {
|
|
9
11
|
subscription: string;
|
|
@@ -116,9 +118,15 @@ export async function waitForAzureDeployment(
|
|
|
116
118
|
onObservation?: (observation: DeploymentObservation) => void;
|
|
117
119
|
secretValues?: readonly string[];
|
|
118
120
|
queryTimeoutMs?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Consecutive non-terminal "unknown" reads (query failure, not yet visible) to keep polling through.
|
|
123
|
+
* Required for an unbounded deadline so a missing deployment cannot be polled forever.
|
|
124
|
+
*/
|
|
125
|
+
transientRetries?: number;
|
|
119
126
|
}
|
|
120
127
|
): Promise<DeploymentObservation> {
|
|
121
128
|
let observation: DeploymentObservation;
|
|
129
|
+
let unknownReads = 0;
|
|
122
130
|
do {
|
|
123
131
|
const remaining = options.deadline - Date.now();
|
|
124
132
|
// Even when the wait budget is exhausted, one bounded read can identify an accepted submission.
|
|
@@ -130,7 +138,15 @@ export async function waitForAzureDeployment(
|
|
|
130
138
|
if (observation.state !== "unknown") reference = observation.reference;
|
|
131
139
|
options.onObservation?.(observation);
|
|
132
140
|
if (observation.state === "Succeeded" || observation.state === "Failed" || observation.state === "Canceled") return observation;
|
|
133
|
-
if (observation.state === "unknown"
|
|
141
|
+
if (observation.state === "unknown") {
|
|
142
|
+
unknownReads += 1;
|
|
143
|
+
const retryable = observation.reason === "not-yet-observed" || observation.reason === "not-found"
|
|
144
|
+
|| (options.transientRetries !== undefined && (observation.reason === "query-failed" || observation.reason === "invalid-response"));
|
|
145
|
+
if (!retryable) return observation;
|
|
146
|
+
if (!Number.isFinite(options.deadline) && unknownReads > (options.transientRetries ?? 0)) return observation;
|
|
147
|
+
} else {
|
|
148
|
+
unknownReads = 0;
|
|
149
|
+
}
|
|
134
150
|
if (Date.now() >= options.deadline || options.signal?.aborted) return observation;
|
|
135
151
|
await new Promise<void>((resolve) => {
|
|
136
152
|
const done = () => {
|
|
@@ -20,7 +20,7 @@ import { deletePlanState, listProvisionRuns, loadPlanState, loadProvisionRun, lo
|
|
|
20
20
|
import { hashFileIfExists } from "./file-session";
|
|
21
21
|
import {
|
|
22
22
|
DEFAULT_PROVISION_WAIT_SECONDS, DEPLOYMENT_QUERY, DeploymentObservation, DeploymentReference,
|
|
23
|
-
MAX_PROVISION_WAIT_SECONDS, queryAzureDeployment, queryDeploymentOperationErrors, waitForAzureDeployment,
|
|
23
|
+
MAX_PROVISION_WAIT_SECONDS, queryAzureDeployment, queryDeploymentOperationErrors, UNBOUNDED_WAIT_TRANSIENT_RETRIES, waitForAzureDeployment,
|
|
24
24
|
} from "./provision-deployment";
|
|
25
25
|
import {
|
|
26
26
|
MIGRATION_MANIFEST_PATH,
|
|
@@ -625,6 +625,8 @@ export interface ApplyProvisionOptions {
|
|
|
625
625
|
planId: string;
|
|
626
626
|
approve?: boolean;
|
|
627
627
|
waitSeconds?: number;
|
|
628
|
+
/** Human CLI: keep polling until Azure reaches a terminal state instead of using the bounded wait budget. */
|
|
629
|
+
waitUntilComplete?: boolean;
|
|
628
630
|
}
|
|
629
631
|
|
|
630
632
|
export interface ApplyProvisionResult {
|
|
@@ -848,7 +850,10 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
848
850
|
);
|
|
849
851
|
}
|
|
850
852
|
|
|
851
|
-
const result = await executeProvisionPlan(plan, {
|
|
853
|
+
const result = await executeProvisionPlan(plan, {
|
|
854
|
+
approve: options.approve, waitSeconds: options.waitSeconds, waitUntilComplete: options.waitUntilComplete,
|
|
855
|
+
retryHint: MACHINE_RETRY_HINT, projectRoot,
|
|
856
|
+
});
|
|
852
857
|
if (result.status === "complete") {
|
|
853
858
|
try { deletePlanState(plan.planId, projectRoot); } catch {
|
|
854
859
|
result.warnings.push("Azure and migration bookkeeping completed, but the old preview could not be removed. The completed execution checkpoint prevents replay.");
|
|
@@ -863,11 +868,17 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
|
|
|
863
868
|
*/
|
|
864
869
|
export async function executeProvisionPlan(
|
|
865
870
|
plan: ProvisionPlanData,
|
|
866
|
-
options: {
|
|
871
|
+
options: {
|
|
872
|
+
approve?: boolean; retryHint?: string; projectRoot?: string; waitSeconds?: number;
|
|
873
|
+
/** Human CLI only: no overall deadline; each az call stays bounded and the checkpoint stays resumable. */
|
|
874
|
+
waitUntilComplete?: boolean;
|
|
875
|
+
signal?: AbortSignal; pollIntervalMs?: number;
|
|
876
|
+
} = {}
|
|
867
877
|
): Promise<ApplyProvisionResult> {
|
|
868
878
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
869
879
|
const retryHint = options.retryHint ?? MACHINE_RETRY_HINT;
|
|
870
|
-
const
|
|
880
|
+
const waitUntilComplete = options.waitUntilComplete === true && options.waitSeconds === undefined;
|
|
881
|
+
const waitSeconds = waitUntilComplete ? 0 : validateWaitSeconds(options.waitSeconds);
|
|
871
882
|
if (!options.approve) {
|
|
872
883
|
throw new MachineCommandError(
|
|
873
884
|
"approval-required",
|
|
@@ -937,7 +948,7 @@ export async function executeProvisionPlan(
|
|
|
937
948
|
options.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
938
949
|
if (options.signal?.aborted) controller.abort();
|
|
939
950
|
const signal = controller.signal;
|
|
940
|
-
const deadline = Date.now() + waitSeconds * 1_000;
|
|
951
|
+
const deadline = waitUntilComplete ? Number.POSITIVE_INFINITY : Date.now() + waitSeconds * 1_000;
|
|
941
952
|
const persist = () => {
|
|
942
953
|
if (!run) return;
|
|
943
954
|
run.warnings = run.warnings.slice(-20);
|
|
@@ -958,6 +969,7 @@ export async function executeProvisionPlan(
|
|
|
958
969
|
...(signal.aborted ? {} : { signal }),
|
|
959
970
|
...(signal.aborted ? { queryTimeoutMs: 5_000 } : {}),
|
|
960
971
|
pollIntervalMs: options.pollIntervalMs,
|
|
972
|
+
...(waitUntilComplete ? { transientRetries: UNBOUNDED_WAIT_TRANSIENT_RETRIES } : {}),
|
|
961
973
|
secretValues,
|
|
962
974
|
onObservation: (value) => {
|
|
963
975
|
run!.observation = value;
|
|
@@ -233,7 +233,7 @@ export function inspectCapabilities(): CapabilitiesContract {
|
|
|
233
233
|
"(regular CLI) configure-mcp [--version auto|latest|<semver>] — re-select the MCP launcher version in .mcp.json",
|
|
234
234
|
],
|
|
235
235
|
provisioning: {
|
|
236
|
-
execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600). Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
|
|
236
|
+
execution: "Submit Bicep once with az deployment group create --no-wait, then bounded state polling (default 300 seconds, configurable 0-3600) for machine/MCP; the human `swallowkit provision` command waits until Azure reaches a terminal state. Azure CLI invocations are bounded separately. Warning-only stderr with exit 0 is not a failure; output capture limits do not terminate az.",
|
|
237
237
|
completion: "status complete/outcome succeeded requires Azure Succeeded plus verified migration tags and local ledger completion. Running/Accepted -> in-progress; unreadable/not-found state -> blocked/outcome unknown; bookkeeping interruption after Azure Succeeded -> blocked/outcome partial; Failed/Canceled -> failed with Azure causes.",
|
|
238
238
|
tracking: "Approved runs persist before Azure writes under .swallowkit/state/provision/. runId equals the original planId and preserves the subscription, RG, deployment ID/correlation, fingerprints and migration state. inspect provision is read-only; apply provision --plan resumes without re-submitting attempted deployments. Ctrl+C/termination may leave Azure running; never cancel/delete without explicit consent.",
|
|
239
239
|
recovery: "For an older untracked baseline, plan provision --resume-deployment <name> compares the existing ARM template and non-secure parameters to the local Bicep/parameters. It observes that deployment and only tags/records it after Succeeded. Mismatches, unreadable evidence, secure parameters or non-baseline migrations block; do not use reconcile just to repair bookkeeping.",
|