@aifabrix/builder 2.55.2 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent-kit/session-report.js +76 -0
- package/lib/agent-kit/start.js +124 -57
- package/lib/agent-kit/tmux-sessions.js +163 -0
- package/lib/api/environments.api.js +38 -0
- package/lib/api/marketplace-bootstrap.api.js +31 -0
- package/lib/api/pipeline.api.js +32 -3
- package/lib/app/deploy.js +6 -5
- package/lib/cli/setup-app.js +7 -6
- package/lib/cli/setup-environment.js +76 -19
- package/lib/cli/setup-external-system-help.js +15 -3
- package/lib/cli/setup-external-system.js +21 -3
- package/lib/commands/agent-kit-help.js +32 -15
- package/lib/commands/agent-kit.js +90 -8
- package/lib/commands/repair-datasource-auto-rbac.js +4 -9
- package/lib/commands/upload-openapi-sync-followup.js +59 -0
- package/lib/commands/upload-qualification-client.js +131 -0
- package/lib/commands/upload-qualification-followups.js +104 -0
- package/lib/commands/upload-qualification-poll.js +143 -0
- package/lib/commands/upload.js +37 -156
- package/lib/deployment/deploy-poll-ui.js +14 -10
- package/lib/deployment/deployer-status.js +4 -4
- package/lib/deployment/installation/azure-infra-stage.js +160 -0
- package/lib/deployment/installation/azure-preflight.js +102 -0
- package/lib/deployment/installation/azure-readiness.js +71 -0
- package/lib/deployment/installation/docker-register.js +112 -0
- package/lib/deployment/installation/environment-auth.js +76 -0
- package/lib/deployment/installation/environment-manifest.js +165 -0
- package/lib/deployment/installation/environment-stage.js +191 -0
- package/lib/deployment/installation/index.js +39 -0
- package/lib/deployment/installation/infra-catalog.js +165 -0
- package/lib/deployment/installation/infra-manifest.js +161 -0
- package/lib/deployment/installation/infra-projection.js +86 -0
- package/lib/deployment/installation/public-ip.js +65 -0
- package/lib/deployment/installation/stage-input.js +169 -0
- package/lib/deployment/installation/stage-output.js +53 -0
- package/lib/deployment/installation/workspace-env.js +76 -0
- package/lib/deployment/installation/workspace-files.js +178 -0
- package/lib/deployment/installation/workspace-schema.js +40 -0
- package/lib/deployment/installation/workspace-secrets.js +66 -0
- package/lib/deployment/installation/workspace.js +128 -0
- package/lib/deployment/marketplace-environment.js +305 -0
- package/lib/external-system/deploy.js +46 -51
- package/lib/generator/builders.js +4 -0
- package/lib/generator/helpers.js +55 -99
- package/lib/generator/index.js +0 -4
- package/lib/generator/split-variables.js +11 -5
- package/lib/internal/fs-real-sync.js +29 -0
- package/lib/programmatic/builder-api-request.js +8 -4
- package/lib/programmatic/capability-registry.js +2 -0
- package/lib/programmatic/get-deploy-status.js +45 -0
- package/lib/programmatic/get-upload-status.js +45 -0
- package/lib/programmatic/http-accepted-response.js +31 -0
- package/lib/programmatic/openapi-descriptions.js +14 -4
- package/lib/programmatic/request-timeout.js +21 -15
- package/lib/programmatic/route-handler-map.js +6 -0
- package/lib/programmatic/run-deploy.js +7 -3
- package/lib/programmatic/run-upload.js +11 -7
- package/lib/role-assistant/package-validate.js +4 -0
- package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
- package/lib/role-assistant/test-runner.js +14 -17
- package/lib/schema/application-schema.json +20 -2
- package/lib/schema/infra-parameter.schema.json +7 -0
- package/lib/schema/infra.parameter.yaml +32 -43
- package/lib/schema/infrastructure-schema.json +1480 -399
- package/lib/schema/installation-environment.schema.json +601 -0
- package/lib/utils/cli-utils.js +3 -2
- package/lib/utils/qualification-poll-ui.js +136 -0
- package/lib/utils/variable-transformer.js +3 -0
- package/lib/validation/required-variables.js +41 -0
- package/lib/validation/validator.js +8 -5
- package/package.json +7 -2
- package/templates/README.md +4 -0
- package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
- package/templates/agent-kit/skills/shared/hosts.md +10 -10
- package/templates/applications/builder-api/env.template +4 -0
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/keycloak/Dockerfile +12 -5
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
- package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
- package/templates/applications/keycloak/env.template +13 -1
- package/templates/applications/miso-controller/application.yaml +3 -2
- package/templates/applications/miso-controller/env.template +28 -22
- package/templates/marketplace/createUiDefinition.json +77 -0
- package/templates/marketplace/install.parameters.example.json +9 -0
- package/templates/marketplace/main.json +4087 -0
- package/lib/generator/deploy-manifest-azure-kv.js +0 -81
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Marketplace journals are durable runtime state. Use the repository's real-sync
|
|
4
|
+
// adapter so a caller (or another Jest suite) mocking `fs` cannot make an
|
|
5
|
+
// existing completed journal disappear and trigger a duplicate ARM deployment.
|
|
6
|
+
const fs = require('../internal/fs-real-sync');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const config = require('../core/config');
|
|
11
|
+
const { getRuntimeConfigDir } = require('../core/config-runtime-paths');
|
|
12
|
+
|
|
13
|
+
const SHIPPED_TEMPLATE = path.resolve(__dirname, '../../templates/marketplace/main.json');
|
|
14
|
+
// Resolved per call (not a module-load constant) so it honors AIFABRIX_HOME/AIFABRIX_CONFIG
|
|
15
|
+
// the same way the rest of the CLI's config does -- including the Jest sandbox tests use.
|
|
16
|
+
function journalDir() {
|
|
17
|
+
return path.join(getRuntimeConfigDir(), 'installations');
|
|
18
|
+
}
|
|
19
|
+
const SAFE_NAME = /^[a-z][a-z0-9-]{2,39}$/;
|
|
20
|
+
const SUBSCRIPTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
|
+
|
|
22
|
+
function readJson(file, label) {
|
|
23
|
+
const resolved = path.resolve(file);
|
|
24
|
+
let document;
|
|
25
|
+
try {
|
|
26
|
+
document = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
27
|
+
} catch (_) {
|
|
28
|
+
throw new Error(`${label} is missing or invalid JSON: ${resolved}`);
|
|
29
|
+
}
|
|
30
|
+
return { document, resolved };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parameterValue(parameters, name) {
|
|
34
|
+
return parameters?.parameters?.[name]?.value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateStageTarget(envKey, options) {
|
|
38
|
+
if (!['dev', 'tst', 'pro'].includes(envKey)) {
|
|
39
|
+
throw new Error('Marketplace platform stage requires an entitled workload environment: dev, tst or pro');
|
|
40
|
+
}
|
|
41
|
+
const installId = String(options.installId || '');
|
|
42
|
+
const resourceGroup = String(options.resourceGroup || '');
|
|
43
|
+
if (!SAFE_NAME.test(installId) || (resourceGroup && !SAFE_NAME.test(resourceGroup))) {
|
|
44
|
+
throw new Error('Install ID and resource group must be lowercase Azure-safe names (3-40 characters)');
|
|
45
|
+
}
|
|
46
|
+
const stage = options.stage || 'infra';
|
|
47
|
+
if (!['infra', 'platform', 'all'].includes(stage)) {
|
|
48
|
+
throw new Error('Stage must be infra, platform or all');
|
|
49
|
+
}
|
|
50
|
+
if (stage === 'all' && !options.controllerUrl) {
|
|
51
|
+
throw new Error('--stage all requires --controller-url from a completed wizard; use --stage infra for a new installation');
|
|
52
|
+
}
|
|
53
|
+
if (stage === 'platform') {
|
|
54
|
+
return { envKey, installId, stage, controllerUrl: options.controllerUrl };
|
|
55
|
+
}
|
|
56
|
+
return { envKey, installId, resourceGroup, stage };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateInfraFiles(target, options) {
|
|
60
|
+
const { resourceGroup } = target;
|
|
61
|
+
if (!resourceGroup || !SUBSCRIPTION_ID.test(String(options.subscription || ''))) {
|
|
62
|
+
throw new Error('Infrastructure stage requires --subscription and --resource-group');
|
|
63
|
+
}
|
|
64
|
+
if (!options.parameters) {
|
|
65
|
+
throw new Error('Infrastructure stage requires a secure --parameters file');
|
|
66
|
+
}
|
|
67
|
+
const template = readJson(options.marketplaceJson || SHIPPED_TEMPLATE, 'Marketplace template');
|
|
68
|
+
const params = readJson(options.parameters, 'ARM parameter file');
|
|
69
|
+
if (fs.statSync(params.resolved).mode & 0o077) {
|
|
70
|
+
throw new Error('ARM parameter file may contain secrets and must be owner-only (chmod 600)');
|
|
71
|
+
}
|
|
72
|
+
if (!template.document.resources || !template.document.parameters || !params.document.parameters) {
|
|
73
|
+
throw new Error('Expected an ARM deployment template and deploymentParameters JSON');
|
|
74
|
+
}
|
|
75
|
+
return { template, params };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateInfraParameters(target, options, files) {
|
|
79
|
+
const { installId, resourceGroup } = target;
|
|
80
|
+
const { params } = files;
|
|
81
|
+
if (typeof parameterValue(params.document, 'adminPassword') !== 'string' ||
|
|
82
|
+
!parameterValue(params.document, 'adminPassword')) {
|
|
83
|
+
throw new Error('Secure ARM parameter file must supply adminPassword');
|
|
84
|
+
}
|
|
85
|
+
// Bicep module 14_marketplace-mori-bootstrap.bicep's own contract: empty/absent
|
|
86
|
+
// marketplaceSubscriptionId is a valid, intentional direct deploy -- Mori falls back
|
|
87
|
+
// to Community. Only reject a wrong *type*; do not require a non-empty value here,
|
|
88
|
+
// that would force a fabricated id past Mori's real Marketplace Fulfillment check.
|
|
89
|
+
const marketplaceSubscriptionId = parameterValue(params.document, 'marketplaceSubscriptionId');
|
|
90
|
+
if (marketplaceSubscriptionId !== undefined && typeof marketplaceSubscriptionId !== 'string') {
|
|
91
|
+
throw new Error('marketplaceSubscriptionId must be a string when present (empty string means a direct deploy, Community fallback)');
|
|
92
|
+
}
|
|
93
|
+
const nameOverride = parameterValue(params.document, 'infrastructureNameOverride');
|
|
94
|
+
if ((nameOverride && nameOverride !== installId) || (!nameOverride && resourceGroup !== installId)) {
|
|
95
|
+
throw new Error('Installation naming mismatch: infrastructureNameOverride must equal --install-id, or install ID must equal resource group');
|
|
96
|
+
}
|
|
97
|
+
const location = parameterValue(params.document, 'location') || options.location;
|
|
98
|
+
if (typeof location !== 'string' || !/^[a-z0-9]+$/i.test(location)) {
|
|
99
|
+
throw new Error('Infrastructure stage requires a valid Azure location in the parameter file or --location');
|
|
100
|
+
}
|
|
101
|
+
return location;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function preflight(envKey, options) {
|
|
105
|
+
const target = validateStageTarget(envKey, options);
|
|
106
|
+
if (target.stage === 'platform') return target;
|
|
107
|
+
const files = validateInfraFiles(target, options);
|
|
108
|
+
const location = validateInfraParameters(target, options, files);
|
|
109
|
+
return {
|
|
110
|
+
...target, location,
|
|
111
|
+
subscription: options.subscription,
|
|
112
|
+
template: files.template.resolved,
|
|
113
|
+
parameters: files.params.resolved,
|
|
114
|
+
templateHash: crypto.createHash('sha256').update(fs.readFileSync(files.template.resolved)).digest('hex')
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function journalPath(installId) {
|
|
119
|
+
return path.join(journalDir(), `${installId}.json`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readJournal(installId) {
|
|
123
|
+
const file = journalPath(installId);
|
|
124
|
+
if (!fs.existsSync(file)) throw new Error(`No infrastructure journal for ${installId}. Run --stage infra first.`);
|
|
125
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeJournal(record) {
|
|
129
|
+
fs.mkdirSync(journalDir(), { recursive: true, mode: 0o700 });
|
|
130
|
+
fs.writeFileSync(journalPath(record.installId), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runAz(args) {
|
|
134
|
+
const result = spawnSync('az', args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
135
|
+
if (result.error || result.status !== 0) {
|
|
136
|
+
throw new Error(`Azure CLI ${args.slice(0, 3).join(' ')} failed (exit ${result.status ?? 'unavailable'}). Check Azure deployment diagnostics; secret-bearing output was withheld.`);
|
|
137
|
+
}
|
|
138
|
+
return result.stdout.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function azureArgs(base, subscription) {
|
|
142
|
+
return [...base, '--subscription', subscription];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkExistingJournal(input) {
|
|
146
|
+
const old = fs.existsSync(journalPath(input.installId)) ? readJournal(input.installId) : null;
|
|
147
|
+
if (old) {
|
|
148
|
+
// Workspace mode (inputHash present) owns template/parameter updates through the input hash;
|
|
149
|
+
// the legacy explicit mode still rejects any template change for an existing install ID.
|
|
150
|
+
if (old.subscription !== input.subscription || old.resourceGroup !== input.resourceGroup ||
|
|
151
|
+
(!input.inputHash && old.templateHash !== input.templateHash)) {
|
|
152
|
+
throw new Error('Existing install journal has a different subscription, group or template; use a new install ID');
|
|
153
|
+
}
|
|
154
|
+
if (old.infraComplete && old.inputHash === input.inputHash) return old;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isOwnResumableDeployment(input, az) {
|
|
160
|
+
// ARM deployments are declarative/idempotent by name: re-running `deployment group
|
|
161
|
+
// create` with the same name only touches resources that still need work (e.g. a
|
|
162
|
+
// nested deployment script that failed) and leaves already-succeeded ones alone. A
|
|
163
|
+
// resource group that already has resources from *our own* prior attempt at this
|
|
164
|
+
// install ID is therefore safe to resume into, not "not empty" in the rejecting sense.
|
|
165
|
+
const deploymentName = `${input.installId}-marketplace-infra`;
|
|
166
|
+
const deployments = JSON.parse(az(azureArgs([
|
|
167
|
+
'deployment', 'group', 'list', '--resource-group', input.resourceGroup, '--output', 'json'
|
|
168
|
+
], input.subscription)));
|
|
169
|
+
return deployments.some(deployment => deployment.name === deploymentName);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ensureEmptyGroup(input, az) {
|
|
173
|
+
const account = JSON.parse(az(['account', 'show', '--output', 'json']));
|
|
174
|
+
if (account.id !== input.subscription) {
|
|
175
|
+
throw new Error('Selected az account subscription differs from --subscription; run az account set before deployment');
|
|
176
|
+
}
|
|
177
|
+
const exists = az(azureArgs(['group', 'exists', '--name', input.resourceGroup, '--output', 'tsv'], input.subscription)) === 'true';
|
|
178
|
+
if (exists) {
|
|
179
|
+
const resources = JSON.parse(az(azureArgs([
|
|
180
|
+
'resource', 'list', '--resource-group', input.resourceGroup, '--output', 'json'
|
|
181
|
+
], input.subscription)));
|
|
182
|
+
if (resources.length && !isOwnResumableDeployment(input, az)) {
|
|
183
|
+
throw new Error('Target resource group is not empty; choose a new isolated group');
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
az(azureArgs(['group', 'create', '--name', input.resourceGroup, '--location', input.location, '--output', 'none'], input.subscription));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function runArmDeployment(input, az) {
|
|
191
|
+
const deploymentName = `${input.installId}-marketplace-infra`;
|
|
192
|
+
const common = [
|
|
193
|
+
'--resource-group', input.resourceGroup, '--name', deploymentName,
|
|
194
|
+
'--template-file', input.template, '--parameters', `@${input.parameters}`,
|
|
195
|
+
'--no-prompt', 'true'
|
|
196
|
+
];
|
|
197
|
+
az(azureArgs(['deployment', 'group', 'validate', ...common, '--output', 'none'], input.subscription));
|
|
198
|
+
az(azureArgs(['deployment', 'group', 'what-if', ...common,
|
|
199
|
+
'--result-format', 'ResourceIdOnly', '--output', 'none'], input.subscription));
|
|
200
|
+
return { deploymentName, deployment: JSON.parse(az(azureArgs([
|
|
201
|
+
'deployment', 'group', 'create', ...common, '--output', 'json'
|
|
202
|
+
], input.subscription))) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function deployInfra(input, az = runAz) {
|
|
206
|
+
const old = checkExistingJournal(input);
|
|
207
|
+
if (old) return old;
|
|
208
|
+
ensureEmptyGroup(input, az);
|
|
209
|
+
const { deploymentName, deployment } = runArmDeployment(input, az);
|
|
210
|
+
const outputMap = deployment.properties?.outputs || {};
|
|
211
|
+
const outputs = Object.fromEntries(Object.entries(outputMap).map(([key, item]) => [key, item.value]));
|
|
212
|
+
if (!outputs.webAppUrl || !outputs.keyVaultName || !outputs.resourceGroupName) {
|
|
213
|
+
throw new Error('ARM deployment succeeded without required controller/vault/group outputs; inspect deployment before resume');
|
|
214
|
+
}
|
|
215
|
+
const record = {
|
|
216
|
+
installId: input.installId, envKey: input.envKey, subscription: input.subscription,
|
|
217
|
+
resourceGroup: input.resourceGroup, templateHash: input.templateHash, inputHash: input.inputHash,
|
|
218
|
+
deploymentName, deploymentId: deployment.id, correlationId: deployment.properties?.correlationId || null,
|
|
219
|
+
outputs, infraComplete: true,
|
|
220
|
+
platformComplete: false, recordedAt: new Date().toISOString()
|
|
221
|
+
};
|
|
222
|
+
writeJournal(record);
|
|
223
|
+
return record;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function assertAppDeploymentComplete(appName, deployment) {
|
|
227
|
+
if (deployment?.usedExternalDeploy || deployment?.result?.status?.status !== 'completed') {
|
|
228
|
+
throw new Error(`${appName} deployment did not reach completed status; resume platform stage after inspecting controller deployment`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function deployWorkloadApps(input, dependencies) {
|
|
233
|
+
const deployApp = dependencies.deployApp || require('../app').deployApp;
|
|
234
|
+
const deployEnvironment = dependencies.deployEnvironment || require('./environment').deployEnvironment;
|
|
235
|
+
const environment = await deployEnvironment(input.envKey, { poll: true });
|
|
236
|
+
if (environment?.status !== 'ready') {
|
|
237
|
+
throw new Error(`Environment ${input.envKey} did not reach ready status; resume platform stage after controller deployment completes`);
|
|
238
|
+
}
|
|
239
|
+
await (dependencies.setEnvironment || config.setCurrentEnvironment)(input.envKey);
|
|
240
|
+
assertAppDeploymentComplete('dataplane', await deployApp('dataplane', { yes: true, poll: true }));
|
|
241
|
+
assertAppDeploymentComplete('builder-api', await deployApp('builder-api', { yes: true, poll: true }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function deployPlatform(input, dependencies = {}) {
|
|
245
|
+
const record = (dependencies.readJournal || readJournal)(input.installId);
|
|
246
|
+
if (record.envKey !== input.envKey) throw new Error('Journal workload environment differs from requested environment');
|
|
247
|
+
if (record.platformComplete) return record;
|
|
248
|
+
const controller = input.controllerUrl || record.publicControllerUrl;
|
|
249
|
+
if (typeof controller !== 'string' || !/^https:\/\/[^/]+\/miso\/?$/.test(controller)) {
|
|
250
|
+
throw new Error('Platform stage requires --controller-url https://<ready-public-host>/miso from the completed wizard');
|
|
251
|
+
}
|
|
252
|
+
if (!record.runtimeReady) {
|
|
253
|
+
throw new Error('Controller runtime is not ready; rerun infrastructure readiness before the platform stage');
|
|
254
|
+
}
|
|
255
|
+
await (dependencies.setControllerUrl || config.setControllerUrl)(controller);
|
|
256
|
+
await deployWorkloadApps(input, dependencies);
|
|
257
|
+
record.publicControllerUrl = controller.replace(/\/$/, '');
|
|
258
|
+
record.platformComplete = true;
|
|
259
|
+
record.platformRecordedAt = new Date().toISOString();
|
|
260
|
+
(dependencies.writeJournal || writeJournal)(record);
|
|
261
|
+
return record;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function deployMarketplaceEnvironment(envKey, options, dependencies = {}) {
|
|
265
|
+
const input = preflight(envKey, options);
|
|
266
|
+
if (input.stage === 'platform') return deployPlatform(input, dependencies);
|
|
267
|
+
const record = deployInfra(input, dependencies.az);
|
|
268
|
+
const probe = dependencies.probe || probeController;
|
|
269
|
+
await probe(record.outputs.webAppUrl);
|
|
270
|
+
record.runtimeReady = true;
|
|
271
|
+
writeJournal(record);
|
|
272
|
+
if (input.stage === 'infra') return record;
|
|
273
|
+
return deployPlatform({ ...input, controllerUrl: options.controllerUrl }, dependencies);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function probeController(controllerUrl) {
|
|
277
|
+
return probeHttps(controllerUrl, '/api/v1/health/readiness', 'Controller');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Waits until an HTTPS endpoint answers successfully (App Service startup lags ARM success).
|
|
282
|
+
* @param {string} baseUrl - HTTPS base URL from ARM outputs
|
|
283
|
+
* @param {string} pathname - Probe path
|
|
284
|
+
* @param {string} label - Service label for errors
|
|
285
|
+
* @param {number} [attempts=30] - Attempts at 10-second intervals
|
|
286
|
+
* @returns {Promise<void>}
|
|
287
|
+
*/
|
|
288
|
+
async function probeHttps(baseUrl, pathname, label, attempts = 30) {
|
|
289
|
+
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('https://')) {
|
|
290
|
+
throw new Error(`ARM output does not contain a valid HTTPS ${label} URL`);
|
|
291
|
+
}
|
|
292
|
+
const url = new URL(pathname, baseUrl);
|
|
293
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
296
|
+
if (response.ok) return;
|
|
297
|
+
} catch (_) {
|
|
298
|
+
// App Service startup can lag successful ARM provisioning.
|
|
299
|
+
}
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`${label} readiness did not pass at ${url.origin}; inspect App Service before resuming`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = { preflight, deployInfra, deployPlatform, deployMarketplaceEnvironment, runAz, probeHttps, SAFE_NAME };
|
|
@@ -28,7 +28,6 @@ const { generateControllerManifest } = require('../generator/external-controller
|
|
|
28
28
|
const { validateExternalSystemComplete } = require('../validation/validate');
|
|
29
29
|
const { displayValidationResults } = require('../validation/validate-display');
|
|
30
30
|
const { syncDeployJsonFromSources } = require('./sync-deploy-manifest');
|
|
31
|
-
const { runDeployGateOrchestrator } = require('../commands/deploy-gate-orchestrator');
|
|
32
31
|
const { runPostPublishDataSync } = require('../commands/run-post-publish-data-sync');
|
|
33
32
|
const { uploadDigitalWorkerIfNeeded, deployRoleAssistantShellIfNeeded } = require('./deploy-helpers');
|
|
34
33
|
const { createDataplanePublishedCheck } = require('./deploy-dataplane-published');
|
|
@@ -199,7 +198,9 @@ function buildExternalDeployPollOpts(options, extra = {}) {
|
|
|
199
198
|
return {
|
|
200
199
|
...options,
|
|
201
200
|
...extra,
|
|
202
|
-
|
|
201
|
+
// Default true (CLI blocks and shows a spinner); explicit false (builder-api web path,
|
|
202
|
+
// plan 191.0 task 3) accepts and returns immediately — the caller polls its own status route.
|
|
203
|
+
poll: options.poll !== false,
|
|
203
204
|
pollInterval:
|
|
204
205
|
options.pollInterval !== undefined ? options.pollInterval : EXTERNAL_DEPLOY_POLL_INTERVAL_MS,
|
|
205
206
|
pollMaxAttempts:
|
|
@@ -210,28 +211,15 @@ function buildExternalDeployPollOpts(options, extra = {}) {
|
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
dataplaneUrl,
|
|
219
|
-
authConfig,
|
|
220
|
-
payload,
|
|
221
|
-
manifest.key
|
|
222
|
-
);
|
|
223
|
-
if (res?.success === false) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
res.formattedError || res.error || 'Failed to promote external system lifecycle after deploy'
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return res;
|
|
229
|
-
}
|
|
230
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Reads back dataplane readiness after Miso's deploy call. Miso (226.0/191.0 addendum) already
|
|
216
|
+
* decided and executed fast-qualified promotion or the legacy full ladder server-side — Builder
|
|
217
|
+
* never re-publishes here; that was the duplicate work this task removes.
|
|
218
|
+
*/
|
|
231
219
|
async function finalizeExternalDeployDataplaneWork(manifest, controllerUrl, environment, authConfig, options) {
|
|
232
220
|
const compact = options.verbose !== true;
|
|
233
221
|
const silent = compact;
|
|
234
|
-
const
|
|
222
|
+
const refreshedCtx = await fetchDataplaneDeployReadiness(
|
|
235
223
|
controllerUrl,
|
|
236
224
|
environment,
|
|
237
225
|
authConfig,
|
|
@@ -239,21 +227,6 @@ async function finalizeExternalDeployDataplaneWork(manifest, controllerUrl, envi
|
|
|
239
227
|
{ dataplaneUrl: options.knownDataplaneUrl, silent }
|
|
240
228
|
);
|
|
241
229
|
|
|
242
|
-
if (ctx.dataplaneUrl && !ctx.error) {
|
|
243
|
-
await promoteExternalSystemLifecycleAfterDeploy(manifest, ctx.dataplaneUrl, authConfig);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const refreshedCtx =
|
|
247
|
-
ctx.dataplaneUrl && !ctx.error
|
|
248
|
-
? await fetchDataplaneDeployReadiness(
|
|
249
|
-
controllerUrl,
|
|
250
|
-
environment,
|
|
251
|
-
authConfig,
|
|
252
|
-
manifest.key,
|
|
253
|
-
{ dataplaneUrl: ctx.dataplaneUrl, silent: true }
|
|
254
|
-
)
|
|
255
|
-
: ctx;
|
|
256
|
-
|
|
257
230
|
const probeData = await runOptionalExternalDeployProbe(refreshedCtx, manifest.key, authConfig, options);
|
|
258
231
|
return { refreshedCtx, probeData };
|
|
259
232
|
}
|
|
@@ -294,6 +267,21 @@ async function runPostDeployExternalSystemSteps(manifest, controllerUrl, environ
|
|
|
294
267
|
});
|
|
295
268
|
}
|
|
296
269
|
|
|
270
|
+
// Builder-api web path (plan 191.0 task 3): accept-only, raw controller accept response
|
|
271
|
+
// (deploymentId, not-yet-terminal status) — caller polls getDeployStatus for terminal truth.
|
|
272
|
+
function logCompactDeployFailure(manifest, environment, deploymentOutcome, options, result) {
|
|
273
|
+
logDeployReadinessSummary({
|
|
274
|
+
environment,
|
|
275
|
+
dataplaneUrl: null,
|
|
276
|
+
manifest,
|
|
277
|
+
deploymentOk: false,
|
|
278
|
+
deploymentDetail: deploymentOutcome,
|
|
279
|
+
verbose: false,
|
|
280
|
+
gateOutcomes: options.gateOutcomes || null,
|
|
281
|
+
draftDeploymentId: result?.deploymentId || null
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
297
285
|
async function executeDeployAndDisplay(manifest, controllerUrl, environment, authConfig, options) {
|
|
298
286
|
const compact = options.verbose !== true;
|
|
299
287
|
const deployer = require('../deployment/deployer');
|
|
@@ -313,6 +301,10 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
313
301
|
})
|
|
314
302
|
);
|
|
315
303
|
|
|
304
|
+
if (options.poll === false) {
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
316
308
|
const deploymentOutcome = parseControllerDeploymentOutcome(result);
|
|
317
309
|
if (!compact) {
|
|
318
310
|
logImmediateControllerDeploymentOutcome(deploymentOutcome);
|
|
@@ -320,16 +312,7 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
320
312
|
|
|
321
313
|
if (!deploymentOutcome.ok) {
|
|
322
314
|
if (compact) {
|
|
323
|
-
|
|
324
|
-
environment,
|
|
325
|
-
dataplaneUrl: null,
|
|
326
|
-
manifest,
|
|
327
|
-
deploymentOk: false,
|
|
328
|
-
deploymentDetail: deploymentOutcome,
|
|
329
|
-
verbose: false,
|
|
330
|
-
gateOutcomes: options.gateOutcomes || null,
|
|
331
|
-
draftDeploymentId: result?.deploymentId || null
|
|
332
|
-
});
|
|
315
|
+
logCompactDeployFailure(manifest, environment, deploymentOutcome, options, result);
|
|
333
316
|
}
|
|
334
317
|
return result;
|
|
335
318
|
}
|
|
@@ -422,15 +405,28 @@ async function validateDataplaneManifestForDeploy(manifest, authConfig, dataplan
|
|
|
422
405
|
}
|
|
423
406
|
}
|
|
424
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Deploy no longer runs local unit/integration tests or optional --gate certification steps
|
|
410
|
+
* (plan 191.0 task 2): that ladder now runs once, durably, during `aifabrix upload`. A caller
|
|
411
|
+
* still passing --gate gets an actionable notice instead of a silently-ignored flag.
|
|
412
|
+
*/
|
|
413
|
+
function warnIfDeployGateFlagIsNowNoop(systemKey, options) {
|
|
414
|
+
if (!options.gate) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
logger.log(
|
|
418
|
+
chalk.yellow(
|
|
419
|
+
`⚠ --gate is no longer run during deploy; it runs once during "aifabrix upload ${systemKey}".`
|
|
420
|
+
)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
425
424
|
async function runConnectedSystemDeployAfterDetect(appName, options, compact) {
|
|
426
425
|
if (!compact) {
|
|
427
426
|
logger.log(chalk.blue(`\n🚀 Deploying external system: ${appName}`));
|
|
428
427
|
}
|
|
429
428
|
await validateExternalSystemForDeploy(appName, options, compact);
|
|
430
|
-
|
|
431
|
-
...options,
|
|
432
|
-
quietDeployUi: options.json === true
|
|
433
|
-
});
|
|
429
|
+
warnIfDeployGateFlagIsNowNoop(appName, options);
|
|
434
430
|
await syncDeployJsonFromSources(appName);
|
|
435
431
|
const manifest = await generateControllerManifest(appName, options);
|
|
436
432
|
const digitalWorkerResult = await uploadDigitalWorkerIfNeeded(
|
|
@@ -454,7 +450,6 @@ async function runConnectedSystemDeployAfterDetect(appName, options, compact) {
|
|
|
454
450
|
await validateDataplaneManifestForDeploy(manifest, authConfig, dataplaneUrl, compact);
|
|
455
451
|
return executeDeployAndDisplay(manifest, controllerUrl, environment, authConfig, {
|
|
456
452
|
...options,
|
|
457
|
-
gateOutcomes,
|
|
458
453
|
knownDataplaneUrl: dataplaneUrl
|
|
459
454
|
});
|
|
460
455
|
}
|
|
@@ -359,6 +359,10 @@ function addSimpleOptionalFields(deployment, variables) {
|
|
|
359
359
|
if (variables.environmentScopedResources === true) {
|
|
360
360
|
deployment.environmentScopedResources = true;
|
|
361
361
|
}
|
|
362
|
+
// Non-arrays pass through so schema validation reports them; an empty list stays omitted.
|
|
363
|
+
if (variables.required !== undefined && !(Array.isArray(variables.required) && variables.required.length === 0)) {
|
|
364
|
+
deployment.required = variables.required;
|
|
365
|
+
}
|
|
362
366
|
}
|
|
363
367
|
|
|
364
368
|
function buildOptionalFields(deployment, variables, rbac) {
|