@aifabrix/builder 2.55.2 → 2.56.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 (41) hide show
  1. package/lib/api/pipeline.api.js +32 -3
  2. package/lib/app/deploy.js +6 -5
  3. package/lib/cli/setup-app.js +7 -6
  4. package/lib/cli/setup-external-system-help.js +15 -3
  5. package/lib/cli/setup-external-system.js +21 -3
  6. package/lib/commands/repair-datasource-auto-rbac.js +4 -9
  7. package/lib/commands/upload-openapi-sync-followup.js +59 -0
  8. package/lib/commands/upload-qualification-client.js +131 -0
  9. package/lib/commands/upload-qualification-followups.js +104 -0
  10. package/lib/commands/upload-qualification-poll.js +143 -0
  11. package/lib/commands/upload.js +37 -156
  12. package/lib/deployment/deploy-poll-ui.js +14 -10
  13. package/lib/deployment/deployer-status.js +4 -4
  14. package/lib/external-system/deploy.js +46 -51
  15. package/lib/generator/helpers.js +4 -47
  16. package/lib/generator/index.js +0 -4
  17. package/lib/programmatic/builder-api-request.js +8 -4
  18. package/lib/programmatic/capability-registry.js +2 -0
  19. package/lib/programmatic/get-deploy-status.js +45 -0
  20. package/lib/programmatic/get-upload-status.js +45 -0
  21. package/lib/programmatic/http-accepted-response.js +31 -0
  22. package/lib/programmatic/openapi-descriptions.js +14 -4
  23. package/lib/programmatic/request-timeout.js +21 -15
  24. package/lib/programmatic/route-handler-map.js +6 -0
  25. package/lib/programmatic/run-deploy.js +7 -3
  26. package/lib/programmatic/run-upload.js +11 -7
  27. package/lib/schema/infra.parameter.yaml +6 -7
  28. package/lib/utils/cli-utils.js +3 -2
  29. package/lib/utils/qualification-poll-ui.js +136 -0
  30. package/package.json +3 -2
  31. package/templates/applications/builder-api/env.template +4 -0
  32. package/templates/applications/dataplane/application.yaml +1 -1
  33. package/templates/applications/dataplane/env.template +1 -1
  34. package/templates/applications/keycloak/Dockerfile +12 -5
  35. package/templates/applications/keycloak/application.yaml +2 -2
  36. package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
  37. package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
  38. package/templates/applications/keycloak/env.template +13 -1
  39. package/templates/applications/miso-controller/application.yaml +1 -1
  40. package/templates/applications/miso-controller/env.template +13 -16
  41. package/lib/generator/deploy-manifest-azure-kv.js +0 -81
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview Poll a Dataplane qualification (publish) job until terminal (plan 191.0 task 1).
3
+ * Never invents success and never marks the candidate failed just because the CLI stopped polling —
4
+ * timing out returns a resumable jobId instead.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { getPublishJobStatus } = require('../api/pipeline.api');
12
+ const { formatApiError } = require('../utils/api-error-handler');
13
+
14
+ const POLL_INTERVAL_MS = 3000;
15
+ const POLL_TIMEOUT_MS = 30 * 60 * 1000;
16
+ const TRANSIENT_HTTP_STATUSES = new Set([502, 503, 504]);
17
+ const MAX_TRANSIENT_POLL_ERRORS = 20;
18
+
19
+ const GOVERNANCE_SCENARIOS_MISSING = 'GOVERNANCE_SCENARIOS_MISSING';
20
+
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+
25
+ /**
26
+ * Convert repeated server error codes into one user-facing qualification failure.
27
+ * The raw job remains attached to the thrown error for JSON/programmatic callers.
28
+ * @param {Object} job
29
+ * @returns {{ message: string, userFacing: boolean }}
30
+ */
31
+ function formatQualificationJobFailure(job) {
32
+ const raw = String(job && job.error ? job.error : '').trim();
33
+ const parts = raw.split(';').map((part) => part.trim()).filter(Boolean);
34
+ const missingCount = parts.filter((part) => part === GOVERNANCE_SCENARIOS_MISSING).length;
35
+ if (missingCount > 0 && missingCount === parts.length) {
36
+ const scope = `${missingCount} datasource${missingCount === 1 ? '' : 's'}`;
37
+ return {
38
+ message: `Governance checks could not run because scenario packs are missing for ${scope}.`,
39
+ userFacing: true
40
+ };
41
+ }
42
+ const unique = [...new Set(parts)];
43
+ return {
44
+ message: unique.join('; ') || `Upload checks failed for job ${job && job.jobId ? job.jobId : ''}`.trim(),
45
+ userFacing: false
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {number|string} status
51
+ * @returns {boolean}
52
+ */
53
+ function isTransientQualificationPollError(status) {
54
+ return TRANSIENT_HTTP_STATUSES.has(Number(status));
55
+ }
56
+
57
+ /**
58
+ * @param {string} dataplaneUrl
59
+ * @param {Object} authConfig
60
+ * @param {string} jobId
61
+ * @param {Object} [pollUi]
62
+ * @returns {Promise<Object>} Raw job status data
63
+ */
64
+ async function fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, pollUi = {}) {
65
+ const started = Date.now();
66
+ for (let attempt = 0; attempt <= MAX_TRANSIENT_POLL_ERRORS; attempt += 1) {
67
+ const res = await getPublishJobStatus(dataplaneUrl, authConfig, jobId);
68
+ if (res?.success !== false) {
69
+ return res.data;
70
+ }
71
+ if (!isTransientQualificationPollError(res.status)) {
72
+ const err = new Error(formatApiError(res, dataplaneUrl));
73
+ const status = Number(res.status);
74
+ err.statusCode = status >= 400 && status < 600 ? status : 400;
75
+ throw err;
76
+ }
77
+ if (attempt >= MAX_TRANSIENT_POLL_ERRORS) {
78
+ throw new Error(
79
+ `The upload service returned HTTP ${res.status} repeatedly while waiting for job ${jobId}.`
80
+ );
81
+ }
82
+ if (typeof pollUi.onTransientError === 'function') {
83
+ pollUi.onTransientError(res.status, Date.now() - started);
84
+ }
85
+ await sleep(POLL_INTERVAL_MS);
86
+ }
87
+ throw new Error(`Failed to check upload job ${jobId} after transient retries`);
88
+ }
89
+
90
+ /**
91
+ * Poll a Dataplane qualification job to terminal state (completed|failed). The timeout only
92
+ * bounds how long *this CLI process* waits — Dataplane keeps running the job regardless; timing
93
+ * out returns a resumable jobId, never a fabricated failure (plan 191.0 task 4).
94
+ * @param {string} dataplaneUrl
95
+ * @param {Object} authConfig
96
+ * @param {string} jobId
97
+ * @param {Object} [pollUi] - { onPollProgress(job), onTransientError(status, elapsedMs) }
98
+ * @param {number} [timeoutMs] - Defaults to POLL_TIMEOUT_MS (30 min); CLI `--timeout` overrides it.
99
+ * @returns {Promise<Object>} Terminal job status data (state: 'completed')
100
+ * @throws {Error} With `.jobId` set, when the job fails or the CLI-side wait times out (resumable).
101
+ */
102
+ async function pollQualificationJobUntilTerminal(
103
+ dataplaneUrl,
104
+ authConfig,
105
+ jobId,
106
+ pollUi = {},
107
+ timeoutMs = POLL_TIMEOUT_MS
108
+ ) {
109
+ const started = Date.now();
110
+ while (Date.now() - started < timeoutMs) {
111
+ const job = await fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, pollUi);
112
+ if (typeof pollUi.onPollProgress === 'function') {
113
+ pollUi.onPollProgress(job);
114
+ }
115
+ if (job.state === 'failed') {
116
+ const failure = formatQualificationJobFailure(job);
117
+ const err = new Error(failure.message);
118
+ err.jobId = jobId;
119
+ err.job = job;
120
+ err.skipDoctor = failure.userFacing;
121
+ throw err;
122
+ }
123
+ if (job.state === 'completed') {
124
+ return job;
125
+ }
126
+ await sleep(POLL_INTERVAL_MS);
127
+ }
128
+ const timeoutErr = new Error(
129
+ `Timed out waiting for upload job ${jobId}. It is still running — resume with: aifabrix upload <systemKey> --resume ${jobId}`
130
+ );
131
+ timeoutErr.jobId = jobId;
132
+ timeoutErr.resumable = true;
133
+ throw timeoutErr;
134
+ }
135
+
136
+ module.exports = {
137
+ POLL_INTERVAL_MS,
138
+ POLL_TIMEOUT_MS,
139
+ isTransientQualificationPollError,
140
+ fetchQualificationJobWithRetry,
141
+ formatQualificationJobFailure,
142
+ pollQualificationJobUntilTerminal
143
+ };
@@ -1,6 +1,5 @@
1
1
  const {
2
2
  formatSuccessLine,
3
- formatWarningLine,
4
3
  sectionTitle,
5
4
  headerKeyValue,
6
5
  metadata
@@ -21,8 +20,6 @@ const SEP = chalk.gray('──────────────────
21
20
  const { resolveControllerUrl } = require('../utils/controller-url');
22
21
  const { getDeploymentAuth } = require('../utils/token-manager');
23
22
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
24
- const { resolveExternalSystemPath } = require('../utils/app-path-resolve');
25
- const { runWithUploadPipelineSpinner } = require('../utils/upload-pipeline-ui');
26
23
  const {
27
24
  buildResolvedEnvMapForIntegration,
28
25
  resolveConfigurationValues
@@ -33,21 +30,16 @@ const { generateControllerManifest } = require('../generator/external-controller
33
30
  const {
34
31
  validatePipelineConfig
35
32
  } = require('../api/pipeline.api');
36
- const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
37
33
  const { formatApiError } = require('../utils/api-error-handler');
38
- const { unwrapPublicationResult, unwrapApiData, isPublicationUnchanged } = require('../utils/external-system-readiness-core');
34
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
39
35
  const {
40
36
  logServerValidationWarnings
41
37
  } = require('../utils/external-system-readiness-display');
42
- const { maybeRunUploadProbe } = require('./upload-probe');
43
- const { maybeSyncOpenApiFilesForMcp } = require('./repair-openapi-sync');
44
- const { exportProgrammaticOpenApiForRepair } = require('./repair-openapi-export');
45
38
  const { resolveUploadPublishContext } = require('./upload-publish-context');
39
+ const { runQualificationUpload, resumeQualificationUpload } = require('./upload-qualification-client');
40
+ const { completeQualifiedUpload } = require('./upload-qualification-followups');
46
41
  const { syncDeployJsonFromSources } = require('../external-system/sync-deploy-manifest');
47
42
  const { assertUploadPackageIdentity } = require('../external-system/upload-identity');
48
- const { handlePublicationAndFollowups } = require('./upload-sidecar-followups');
49
- const { finalizeUploadPublishResult, finishUnchangedUploadPublication } = require('./upload-data-sync-finalize');
50
- const { pushAndLogCredentialSecrets } = require('./upload-credential-push-log');
51
43
  const {
52
44
  SYSTEM_KIND_DIGITAL_WORKER,
53
45
  readRoleAssistantSettings,
@@ -129,39 +121,6 @@ async function resolveDataplaneAndAuth(systemKey, opts = {}) {
129
121
  return { dataplaneUrl, authConfig, environment };
130
122
  }
131
123
 
132
- /**
133
- * Runs single pipeline upload (upload → validate → publish) on the dataplane.
134
- * @param {string} dataplaneUrl - Dataplane base URL
135
- * @param {Object} authConfig - Auth config
136
- * @param {Object} payload - { version, application, dataSources, status: "draft" }
137
- * @param {string} systemKey
138
- * @param {Object} [options]
139
- * @param {number|string} [options.timeoutMs]
140
- * @returns {Promise<Object>} Raw API response envelope
141
- */
142
- async function runUploadValidatePublish(
143
- dataplaneUrl,
144
- authConfig,
145
- payload,
146
- systemKey,
147
- options = {}
148
- ) {
149
- const res = await uploadApplicationViaPipelineWithAuthRetry(
150
- dataplaneUrl,
151
- authConfig,
152
- payload,
153
- systemKey,
154
- { timeoutMs: options.timeoutMs }
155
- );
156
- if (res?.success === false) {
157
- const err = new Error(formatApiError(res, dataplaneUrl));
158
- const status = Number(res.status || res.statusCode);
159
- err.statusCode = status >= 400 && status < 600 ? status : 400;
160
- throw err;
161
- }
162
- return res;
163
- }
164
-
165
124
  /**
166
125
  * @param {Object} validationResult - Result from validateExternalSystemComplete
167
126
  * @returns {string}
@@ -269,97 +228,30 @@ async function buildValidatedUploadManifestPayload(systemKey, opts = {}) {
269
228
  }
270
229
 
271
230
  /**
272
- * Upload local `integration/<systemKey>/openapi/*.json` specs when present so dataplane can store
273
- * vendor OpenAPI keyed by each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
274
- * Call this **after** a successful pipeline publish: list/upload endpoints require the external system row.
275
- * First publish still succeeds because pipeline materializes internal specs from `openapi.operations` when needed.
276
- *
231
+ * Accept (unless --resume) then poll a Dataplane qualification job to terminal state.
232
+ * @param {string} dataplaneUrl
233
+ * @param {Object} authConfig
234
+ * @param {Object} payload
277
235
  * @param {string} systemKey
278
- * @param {Object} manifest
279
- * @returns {Promise<void>}
236
+ * @param {Object} options - { resume?: string, wait?: boolean, json?: boolean, timeoutMs? }
237
+ * @returns {Promise<{ accepted: Object, terminal: Object|null }>}
280
238
  */
281
- async function logAndSyncLocalOpenApiForUpload(systemKey, manifest, uploadContext = {}) {
282
- const pathOptions = uploadContext.pathOptions || {};
283
- const appPath = resolveExternalSystemPath(systemKey, pathOptions);
284
- const ei = manifest && manifest.externalIntegration;
285
- const datasourceFiles = ei && Array.isArray(ei.dataSources) ? ei.dataSources : [];
286
- await exportProgrammaticOpenApiForRepair({
287
- appPath,
288
- systemKey,
289
- datasourceFiles,
290
- dryRun: false,
291
- backupCtx: { dryRun: false, changedFiles: [] }
292
- });
293
- const openapiSyncLines = await maybeSyncOpenApiFilesForMcp({
294
- enabled: true,
295
- dryRun: false,
296
- appPath,
297
- systemKey,
298
- datasourceFiles,
299
- dataplaneUrl: uploadContext.dataplaneUrl,
300
- authConfig: uploadContext.authConfig,
301
- // Always replace SpecFile rows keyed by openapi.documentKey so vendor/full
302
- // specs are not left missing while only thin internal:* slices remain.
303
- force: true
304
- });
305
- if (!uploadContext.quiet) {
306
- for (const line of openapiSyncLines) {
307
- logger.log(line.startsWith('Skipped') ? formatWarningLine(line) : formatSuccessLine(line));
308
- }
239
+ async function acceptOrResumeQualification(dataplaneUrl, authConfig, payload, systemKey, options) {
240
+ if (options.resume) {
241
+ const terminal = await resumeQualificationUpload(dataplaneUrl, authConfig, options.resume, systemKey, options);
242
+ return {
243
+ accepted: { jobId: options.resume, candidateFingerprint: terminal.result?.candidateFingerprint },
244
+ terminal
245
+ };
309
246
  }
247
+ const result = await runQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options);
248
+ return { accepted: result, terminal: result.terminal };
310
249
  }
311
250
 
312
251
  /**
313
- * @param {string} systemKey
314
- * @param {Object} options
315
- * @param {Object} manifest
316
- * @param {Object} payload
317
- * @param {{ dataplaneUrl: string, authConfig: Object, environment: string, rawRes: Object }} ctx
318
- */
319
- async function completeUploadAfterPublish(systemKey, options, manifest, payload, ctx) {
320
- const quietCompact = options.minimal === true && options.verbose !== true;
321
- const pushMeta = await pushAndLogCredentialSecrets(ctx.dataplaneUrl, ctx.authConfig, systemKey, payload, {
322
- json: options.json === true,
323
- pathOptions: options.pathOptions,
324
- suppressCredentialLog: quietCompact,
325
- excludeSecretKeys: options.excludeSecretKeys
326
- });
327
- await logAndSyncLocalOpenApiForUpload(systemKey, manifest, {
328
- dataplaneUrl: ctx.dataplaneUrl,
329
- authConfig: ctx.authConfig,
330
- pathOptions: options.pathOptions,
331
- quiet: quietCompact
332
- });
333
- const followupResult = await handlePublicationAndFollowups(
334
- {
335
- systemKey,
336
- options,
337
- manifest,
338
- payload,
339
- environment: ctx.environment,
340
- dataplaneUrl: ctx.dataplaneUrl,
341
- authConfig: ctx.authConfig,
342
- rawRes: ctx.rawRes,
343
- credentialPush: pushMeta.credentialPush,
344
- configPush: pushMeta.configPush
345
- },
346
- maybeRunUploadProbe
347
- );
348
- return finalizeUploadPublishResult(
349
- {
350
- dataplaneUrl: ctx.dataplaneUrl,
351
- authConfig: ctx.authConfig,
352
- environment: ctx.environment,
353
- uploadSkipped: false,
354
- sidecar: followupResult?.sidecar || null
355
- },
356
- payload,
357
- options
358
- );
359
- }
360
-
361
- /**
362
- * Upload path after dry-run check.
252
+ * Upload path after dry-run check: submit for durable Dataplane qualification (async, no
253
+ * request-lifetime failure), then — unless `--no-wait` — poll to terminal and run the same
254
+ * downstream follow-ups (credential push, OpenAPI sync, sidecar, probe) as before.
363
255
  * @param {string} systemKey
364
256
  * @param {Object} options
365
257
  * @param {Object} manifest
@@ -373,40 +265,28 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
373
265
  options,
374
266
  payload
375
267
  );
376
- const rawRes = await runWithUploadPipelineSpinner(
377
- async() =>
378
- runUploadValidatePublish(dataplaneUrl, authConfig, payload, systemKey, {
379
- timeoutMs: options.timeoutMs
380
- }),
381
- {
382
- systemKey,
383
- json: options.json === true,
384
- syncMode: options.syncMode === true,
385
- suppressPipelineSpinner: options.suppressPipelineSpinner === true
386
- }
268
+ const { accepted, terminal } = await acceptOrResumeQualification(
269
+ dataplaneUrl,
270
+ authConfig,
271
+ payload,
272
+ systemKey,
273
+ options
387
274
  );
388
- const publication = unwrapPublicationResult(rawRes);
389
- if (isPublicationUnchanged(publication)) {
390
- return finishUnchangedUploadPublication({
391
- systemKey,
392
- options,
393
- payload,
394
- dataplaneUrl,
395
- authConfig,
396
- environment
397
- });
275
+ if (!terminal) {
276
+ return accepted;
398
277
  }
399
-
400
- return completeUploadAfterPublish(systemKey, options, manifest, payload, {
278
+ return completeQualifiedUpload(systemKey, options, manifest, payload, {
401
279
  dataplaneUrl,
402
280
  authConfig,
403
281
  environment,
404
- rawRes
282
+ accepted,
283
+ job: terminal
405
284
  });
406
285
  }
407
286
 
408
287
  /**
409
- * Uploads external system: publishes to dataplane and registers with controller (draft).
288
+ * Uploads external system: durably qualifies on dataplane (async ladder, no request-lifetime
289
+ * failure) then registers with controller (draft).
410
290
  * @param {string} systemKey - External system key (integration/<systemKey>/)
411
291
  * @param {Object} [options] - Options
412
292
  * @param {boolean} [options.dryRun] - Validate and build payload only; no API calls
@@ -415,7 +295,9 @@ async function runUploadPublishAndSummary(systemKey, options, manifest, payload)
415
295
  * @param {number} [options.probeTimeout] - Probe timeout ms (default 120000)
416
296
  * @param {number} [options.timeoutMs] - Pipeline publish timeout ms (HTTP + dataplane; default 300000)
417
297
  * @param {boolean} [options.minimal] - Short summary (default: compact unless --verbose)
418
- * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }|void>}
298
+ * @param {boolean} [options.wait=true] - Poll the qualification job to terminal state; --no-wait returns the job id immediately
299
+ * @param {string} [options.resume] - Poll an existing qualification job id instead of submitting a new candidate
300
+ * @returns {Promise<Object|void>} Terminal result when waited, `{jobId, candidateFingerprint}` when `--no-wait`, or `undefined` on dry-run
419
301
  * @throws {Error} If validation or API calls fail
420
302
  */
421
303
  async function uploadExternalSystem(systemKey, options = {}) {
@@ -461,6 +343,5 @@ module.exports = {
461
343
  buildValidatedUploadManifestPayload,
462
344
  buildUploadPayload,
463
345
  resolveDataplaneAndAuth,
464
- runUploadValidatePublish,
465
346
  validateSystemKeyFormat
466
347
  };
@@ -23,7 +23,10 @@ function shouldUseDeployPollSpinner() {
23
23
  * @returns {{ onPollProgress: Function, finish: () => void }}
24
24
  */
25
25
  function createDeployPollHandlers(maxAttempts, options = {}) {
26
+ void maxAttempts;
26
27
  const silent = options && options.silent === true;
28
+ const startedAt = Date.now();
29
+ const elapsedMs = () => Date.now() - startedAt;
27
30
  const noop = () => {};
28
31
  if (silent) {
29
32
  return {
@@ -49,11 +52,11 @@ function createDeployPollHandlers(maxAttempts, options = {}) {
49
52
  setText(text) {
50
53
  ensureSpinner(text);
51
54
  },
52
- onPollProgress: (deploymentData, attempt, maxA) => {
53
- ensureSpinner(buildDeployPollSpinnerText(deploymentData, attempt, maxA));
55
+ onPollProgress: (deploymentData) => {
56
+ ensureSpinner(buildDeployPollSpinnerText(deploymentData, elapsedMs()));
54
57
  },
55
- onPollWait: (attempt, maxA) => {
56
- ensureSpinner(buildDeployPollWaitText(attempt, maxA));
58
+ onPollWait: () => {
59
+ ensureSpinner(buildDeployPollWaitText(elapsedMs()));
57
60
  },
58
61
  finish: () => {
59
62
  if (spinner) {
@@ -64,13 +67,14 @@ function createDeployPollHandlers(maxAttempts, options = {}) {
64
67
  }
65
68
  return {
66
69
  setText: noop,
67
- onPollProgress: (deploymentData, attempt, maxA) => {
70
+ onPollProgress: (deploymentData) => {
68
71
  const status = deploymentData?.status ?? 'pending';
69
72
  const progress =
70
73
  deploymentData && Number.isFinite(Number(deploymentData.progress))
71
74
  ? Number(deploymentData.progress)
72
75
  : 0;
73
- logger.log(chalk.gray(`Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxA})`));
76
+ const elapsedSeconds = Math.max(0, Math.floor(elapsedMs() / 1000));
77
+ logger.log(chalk.gray(`Status: ${status} (${progress}%) — ${elapsedSeconds}s elapsed`));
74
78
  },
75
79
  onPollWait: noop,
76
80
  finish: noop
@@ -79,12 +83,12 @@ function createDeployPollHandlers(maxAttempts, options = {}) {
79
83
 
80
84
  /**
81
85
  * Plain progress while awaiting the next status poll (HTTP in flight).
82
- * @param {number} attempt
83
- * @param {number} maxAttempts
86
+ * @param {number} elapsedMs
84
87
  * @returns {string}
85
88
  */
86
- function buildDeployPollWaitText(attempt, maxAttempts) {
87
- return `Deploying application... Checking status (attempt ${attempt + 1}/${maxAttempts})`;
89
+ function buildDeployPollWaitText(elapsedMs) {
90
+ const elapsedSeconds = Math.max(0, Math.floor(Number(elapsedMs || 0) / 1000));
91
+ return `Deploying application — ${elapsedSeconds}s elapsed`;
88
92
  }
89
93
 
90
94
  /**
@@ -89,14 +89,14 @@ function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
89
89
  /**
90
90
  * Single-line ora text for deploy polling (updates in place; same style as guided infra spinners).
91
91
  * @param {Object|null|undefined} deploymentData - Latest deployment payload (optional before first response)
92
- * @param {number} attempt - Zero-based attempt index
93
- * @param {number} maxAttempts - Max polling attempts
92
+ * @param {number} elapsedMs - Time elapsed since deployment polling began
94
93
  * @returns {string}
95
94
  */
96
- function buildDeployPollSpinnerText(deploymentData, attempt, maxAttempts) {
95
+ function buildDeployPollSpinnerText(deploymentData, elapsedMs) {
97
96
  const status = deploymentData?.status ?? 'pending';
98
97
  const progress = deploymentData && Number.isFinite(Number(deploymentData.progress)) ? Number(deploymentData.progress) : 0;
99
- return `Deploying application... Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`;
98
+ const elapsedSeconds = Math.max(0, Math.floor(Number(elapsedMs || 0) / 1000));
99
+ return `Deploying application — ${elapsedSeconds}s elapsed · ${status} (${progress}%)`;
100
100
  }
101
101
 
102
102
  /**
@@ -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
  }