@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.
Files changed (88) hide show
  1. package/lib/agent-kit/session-report.js +76 -0
  2. package/lib/agent-kit/start.js +124 -57
  3. package/lib/agent-kit/tmux-sessions.js +163 -0
  4. package/lib/api/environments.api.js +38 -0
  5. package/lib/api/marketplace-bootstrap.api.js +31 -0
  6. package/lib/api/pipeline.api.js +32 -3
  7. package/lib/app/deploy.js +6 -5
  8. package/lib/cli/setup-app.js +7 -6
  9. package/lib/cli/setup-environment.js +76 -19
  10. package/lib/cli/setup-external-system-help.js +15 -3
  11. package/lib/cli/setup-external-system.js +21 -3
  12. package/lib/commands/agent-kit-help.js +32 -15
  13. package/lib/commands/agent-kit.js +90 -8
  14. package/lib/commands/repair-datasource-auto-rbac.js +4 -9
  15. package/lib/commands/upload-openapi-sync-followup.js +59 -0
  16. package/lib/commands/upload-qualification-client.js +131 -0
  17. package/lib/commands/upload-qualification-followups.js +104 -0
  18. package/lib/commands/upload-qualification-poll.js +143 -0
  19. package/lib/commands/upload.js +37 -156
  20. package/lib/deployment/deploy-poll-ui.js +14 -10
  21. package/lib/deployment/deployer-status.js +4 -4
  22. package/lib/deployment/installation/azure-infra-stage.js +160 -0
  23. package/lib/deployment/installation/azure-preflight.js +102 -0
  24. package/lib/deployment/installation/azure-readiness.js +71 -0
  25. package/lib/deployment/installation/docker-register.js +112 -0
  26. package/lib/deployment/installation/environment-auth.js +76 -0
  27. package/lib/deployment/installation/environment-manifest.js +165 -0
  28. package/lib/deployment/installation/environment-stage.js +191 -0
  29. package/lib/deployment/installation/index.js +39 -0
  30. package/lib/deployment/installation/infra-catalog.js +165 -0
  31. package/lib/deployment/installation/infra-manifest.js +161 -0
  32. package/lib/deployment/installation/infra-projection.js +86 -0
  33. package/lib/deployment/installation/public-ip.js +65 -0
  34. package/lib/deployment/installation/stage-input.js +169 -0
  35. package/lib/deployment/installation/stage-output.js +53 -0
  36. package/lib/deployment/installation/workspace-env.js +76 -0
  37. package/lib/deployment/installation/workspace-files.js +178 -0
  38. package/lib/deployment/installation/workspace-schema.js +40 -0
  39. package/lib/deployment/installation/workspace-secrets.js +66 -0
  40. package/lib/deployment/installation/workspace.js +128 -0
  41. package/lib/deployment/marketplace-environment.js +305 -0
  42. package/lib/external-system/deploy.js +46 -51
  43. package/lib/generator/builders.js +4 -0
  44. package/lib/generator/helpers.js +55 -99
  45. package/lib/generator/index.js +0 -4
  46. package/lib/generator/split-variables.js +11 -5
  47. package/lib/internal/fs-real-sync.js +29 -0
  48. package/lib/programmatic/builder-api-request.js +8 -4
  49. package/lib/programmatic/capability-registry.js +2 -0
  50. package/lib/programmatic/get-deploy-status.js +45 -0
  51. package/lib/programmatic/get-upload-status.js +45 -0
  52. package/lib/programmatic/http-accepted-response.js +31 -0
  53. package/lib/programmatic/openapi-descriptions.js +14 -4
  54. package/lib/programmatic/request-timeout.js +21 -15
  55. package/lib/programmatic/route-handler-map.js +6 -0
  56. package/lib/programmatic/run-deploy.js +7 -3
  57. package/lib/programmatic/run-upload.js +11 -7
  58. package/lib/role-assistant/package-validate.js +4 -0
  59. package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
  60. package/lib/role-assistant/test-runner.js +14 -17
  61. package/lib/schema/application-schema.json +20 -2
  62. package/lib/schema/infra-parameter.schema.json +7 -0
  63. package/lib/schema/infra.parameter.yaml +32 -43
  64. package/lib/schema/infrastructure-schema.json +1480 -399
  65. package/lib/schema/installation-environment.schema.json +601 -0
  66. package/lib/utils/cli-utils.js +3 -2
  67. package/lib/utils/qualification-poll-ui.js +136 -0
  68. package/lib/utils/variable-transformer.js +3 -0
  69. package/lib/validation/required-variables.js +41 -0
  70. package/lib/validation/validator.js +8 -5
  71. package/package.json +7 -2
  72. package/templates/README.md +4 -0
  73. package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
  74. package/templates/agent-kit/skills/shared/hosts.md +10 -10
  75. package/templates/applications/builder-api/env.template +4 -0
  76. package/templates/applications/dataplane/application.yaml +1 -1
  77. package/templates/applications/dataplane/env.template +1 -1
  78. package/templates/applications/keycloak/Dockerfile +12 -5
  79. package/templates/applications/keycloak/application.yaml +2 -2
  80. package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
  81. package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
  82. package/templates/applications/keycloak/env.template +13 -1
  83. package/templates/applications/miso-controller/application.yaml +3 -2
  84. package/templates/applications/miso-controller/env.template +28 -22
  85. package/templates/marketplace/createUiDefinition.json +77 -0
  86. package/templates/marketplace/install.parameters.example.json +9 -0
  87. package/templates/marketplace/main.json +4087 -0
  88. 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
- poll: true,
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
- async function promoteExternalSystemLifecycleAfterDeploy(manifest, dataplaneUrl, authConfig) {
214
- const { buildUploadPayload } = require('../commands/upload');
215
- const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
216
- const payload = buildUploadPayload(manifest, { status: 'published' });
217
- const res = await uploadApplicationViaPipelineWithAuthRetry(
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 ctx = await fetchDataplaneDeployReadiness(
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
- logDeployReadinessSummary({
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
- const gateOutcomes = await runDeployGateOrchestrator(appName, {
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) {