@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.
- 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-external-system-help.js +15 -3
- package/lib/cli/setup-external-system.js +21 -3
- 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/external-system/deploy.js +46 -51
- package/lib/generator/helpers.js +4 -47
- package/lib/generator/index.js +0 -4
- 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/schema/infra.parameter.yaml +6 -7
- package/lib/utils/cli-utils.js +3 -2
- package/lib/utils/qualification-poll-ui.js +136 -0
- package/package.json +3 -2
- 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 +1 -1
- package/templates/applications/miso-controller/env.template +13 -16
- 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
|
+
};
|
package/lib/commands/upload.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
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}
|
|
279
|
-
* @returns {Promise<
|
|
236
|
+
* @param {Object} options - { resume?: string, wait?: boolean, json?: boolean, timeoutMs? }
|
|
237
|
+
* @returns {Promise<{ accepted: Object, terminal: Object|null }>}
|
|
280
238
|
*/
|
|
281
|
-
async function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
282
|
+
accepted,
|
|
283
|
+
job: terminal
|
|
405
284
|
});
|
|
406
285
|
}
|
|
407
286
|
|
|
408
287
|
/**
|
|
409
|
-
* Uploads external system:
|
|
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
|
-
* @
|
|
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
|
|
53
|
-
ensureSpinner(buildDeployPollSpinnerText(deploymentData,
|
|
55
|
+
onPollProgress: (deploymentData) => {
|
|
56
|
+
ensureSpinner(buildDeployPollSpinnerText(deploymentData, elapsedMs()));
|
|
54
57
|
},
|
|
55
|
-
onPollWait: (
|
|
56
|
-
ensureSpinner(buildDeployPollWaitText(
|
|
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
|
|
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
|
-
|
|
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}
|
|
83
|
-
* @param {number} maxAttempts
|
|
86
|
+
* @param {number} elapsedMs
|
|
84
87
|
* @returns {string}
|
|
85
88
|
*/
|
|
86
|
-
function buildDeployPollWaitText(
|
|
87
|
-
|
|
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}
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|