@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
package/lib/api/pipeline.api.js
CHANGED
|
@@ -278,7 +278,10 @@ function resolvePipelineUploadTimeoutMs(raw) {
|
|
|
278
278
|
* @param {Object} authConfig
|
|
279
279
|
* @param {Object} payload
|
|
280
280
|
* @param {Object} [options]
|
|
281
|
-
* @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000)
|
|
281
|
+
* @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000); ignored when options.async is true.
|
|
282
|
+
* @param {boolean} [options.async=false] - When true, durably accept the upload for qualification (202 jobId/candidateFingerprint) instead of blocking for the ladder (plan 191.0 task 1).
|
|
283
|
+
* @param {string} [options.gate] - Optional comma-separated checks: operations, trust, governance, full.
|
|
284
|
+
* @param {string} [options.subjectEmail] - Optional comma-separated governance subject emails.
|
|
282
285
|
*/
|
|
283
286
|
async function uploadApplicationViaPipeline(
|
|
284
287
|
dataplaneUrl,
|
|
@@ -287,8 +290,17 @@ async function uploadApplicationViaPipeline(
|
|
|
287
290
|
options = {}
|
|
288
291
|
) {
|
|
289
292
|
const body = { ...payload, status: payload.status ?? 'draft' };
|
|
290
|
-
const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
|
|
291
293
|
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
294
|
+
if (options.async === true) {
|
|
295
|
+
const params = { async: 'true' };
|
|
296
|
+
if (options.gate) params.gate = options.gate;
|
|
297
|
+
if (options.subjectEmail) params.subjectEmail = options.subjectEmail;
|
|
298
|
+
return await client.post('/api/v1/pipeline/upload', {
|
|
299
|
+
body,
|
|
300
|
+
params
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
|
|
292
304
|
return await client.post('/api/v1/pipeline/upload', {
|
|
293
305
|
body,
|
|
294
306
|
timeoutMs,
|
|
@@ -297,6 +309,23 @@ async function uploadApplicationViaPipeline(
|
|
|
297
309
|
});
|
|
298
310
|
}
|
|
299
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Get durable publish/qualification job status.
|
|
314
|
+
* GET /api/v1/publish-jobs/{jobId}
|
|
315
|
+
* @requiresPermission {Dataplane} external-system:publish or external-system:read
|
|
316
|
+
* @async
|
|
317
|
+
* @function getPublishJobStatus
|
|
318
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
319
|
+
* @param {Object} authConfig - Authentication configuration
|
|
320
|
+
* @param {string} jobId - Publish/qualification job id returned by uploadApplicationViaPipeline({ async: true })
|
|
321
|
+
* @returns {Promise<Object>} API envelope `{ success, data }` where `data` is `{ jobId, state, targetType, targetKey, steps, error, result }`
|
|
322
|
+
* @throws {Error} If request fails
|
|
323
|
+
*/
|
|
324
|
+
async function getPublishJobStatus(dataplaneUrl, authConfig, jobId) {
|
|
325
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
326
|
+
return await client.get(`/api/v1/publish-jobs/${encodeURIComponent(jobId)}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
300
329
|
module.exports = {
|
|
301
330
|
validatePipeline,
|
|
302
331
|
deployPipeline,
|
|
@@ -307,7 +336,7 @@ module.exports = {
|
|
|
307
336
|
testSystemViaPipeline,
|
|
308
337
|
testDatasourceViaPipeline,
|
|
309
338
|
uploadApplicationViaPipeline,
|
|
339
|
+
getPublishJobStatus,
|
|
310
340
|
resolvePipelineUploadTimeoutMs,
|
|
311
341
|
PIPELINE_UPLOAD_TIMEOUT_MS
|
|
312
342
|
};
|
|
313
|
-
|
package/lib/app/deploy.js
CHANGED
|
@@ -308,10 +308,12 @@ function validateImageIsPullable(imageRef, appName) {
|
|
|
308
308
|
*/
|
|
309
309
|
function throwIfDeploymentFailed(status, statusObj) {
|
|
310
310
|
if (status !== 'failed' && status !== 'cancelled') return;
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
311
|
+
const reportedErrors = Array.isArray(statusObj.errors) ? statusObj.errors.filter(Boolean).join('; ') : '';
|
|
312
|
+
// A failed deployment can retain its last progress message (for example
|
|
313
|
+
// "Database-only mode"). Prefer the terminal error so the CLI reports the cause.
|
|
314
|
+
const msg = status === 'failed'
|
|
315
|
+
? statusObj.error || reportedErrors || statusObj.message || 'Deployment failed'
|
|
316
|
+
: statusObj.message || statusObj.error || reportedErrors || 'Deployment cancelled';
|
|
315
317
|
const err = new Error(`Deployment ${status}: ${msg}`);
|
|
316
318
|
err.formatted = `Deployment ${status}.\n\n${msg}`;
|
|
317
319
|
err.deploymentStatus = statusObj;
|
|
@@ -495,4 +497,3 @@ module.exports = {
|
|
|
495
497
|
validateAppName,
|
|
496
498
|
validateImageIsPullable
|
|
497
499
|
};
|
|
498
|
-
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -56,20 +56,21 @@ Examples:
|
|
|
56
56
|
--client-id "$DEV_MISO_CLIENTID" \\
|
|
57
57
|
--client-secret "$DEV_MISO_CLIENTSECRET"
|
|
58
58
|
|
|
59
|
-
# CI/CD — external integration (
|
|
59
|
+
# CI/CD — external integration (qualify once via upload, then fast promote via deploy)
|
|
60
60
|
$ aifabrix validate hubspot-demo
|
|
61
61
|
$ aifabrix upload hubspot-demo --probe
|
|
62
62
|
$ aifabrix deploy hubspot-demo --probe -v
|
|
63
|
-
# Prove pillars in tst/CI, then promote (plain deploy seals cert on published Deploy)
|
|
64
|
-
$ aifabrix deploy ek-sales --gate full --subject-email ops@example.com
|
|
65
63
|
$ aifabrix deploy ek-sales
|
|
66
64
|
|
|
67
65
|
Notes:
|
|
68
66
|
- Login or set MISO_CONTROLLER_URL + environment (aifabrix login / aifabrix auth).
|
|
69
67
|
- CI: register once (aifabrix app register <appKey>), store DEV_MISO_CLIENTID and DEV_MISO_CLIENTSECRET in pipeline secrets.
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
- For Connected Systems, upload validates, publishes, and finalizes the system; optional
|
|
69
|
+
checks run only with upload --gate. Deploy promotes the completed upload — the
|
|
70
|
+
server decides this automatically, no flag needed. A system that has never been uploaded
|
|
71
|
+
falls back to the full legacy ladder at deploy time instead.
|
|
72
|
+
- --gate is a no-op on deploy (kept for compatibility; prints a notice) — gates now run once
|
|
73
|
+
during "aifabrix upload", not at deploy time.
|
|
73
74
|
- Default output is a compact status card; use -v for expanded preflight and readiness detail.
|
|
74
75
|
- Controller deploy always polls to a terminal status (no --poll / --no-poll on this command).
|
|
75
76
|
- --repository-url overrides repo URL for pipeline validate (default: application.yaml repository.repositoryUrl).
|
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
const UPLOAD_HELP_AFTER = `
|
|
8
8
|
Examples:
|
|
9
|
-
#
|
|
9
|
+
# Upload and wait for completion
|
|
10
10
|
$ aifabrix upload hubspot-demo
|
|
11
11
|
$ aifabrix upload hubspot-demo -v
|
|
12
12
|
$ aifabrix upload hubspot-demo --probe
|
|
13
|
-
$ aifabrix upload hubspot-demo --
|
|
13
|
+
$ aifabrix upload hubspot-demo --gate operations,trust
|
|
14
|
+
$ aifabrix upload hubspot-demo --gate full --subject-email user@example.com
|
|
15
|
+
|
|
16
|
+
# Queue the upload and return immediately (script/CI-friendly)
|
|
17
|
+
$ aifabrix upload hubspot-demo --no-wait
|
|
18
|
+
|
|
19
|
+
# Resume watching an existing upload job (e.g. after Ctrl-C or a closed terminal)
|
|
20
|
+
$ aifabrix upload hubspot-demo --resume <jobId>
|
|
14
21
|
|
|
15
22
|
# Build and validate payload only (no API calls)
|
|
16
23
|
$ aifabrix upload hubspot-demo --dry-run
|
|
@@ -25,9 +32,14 @@ Examples:
|
|
|
25
32
|
Notes:
|
|
26
33
|
- Requires login (aifabrix auth status).
|
|
27
34
|
- Reads files under integration/<systemKey>/ or role-assistant/<roleKey>/ for digital workers.
|
|
35
|
+
- Upload validates, publishes, and finalizes the system. Operations, trust, and governance
|
|
36
|
+
checks are optional and run only when selected with --gate. The upload keeps running on
|
|
37
|
+
Dataplane even if this CLI process is interrupted, closed, or times out — --resume re-attaches
|
|
38
|
+
to it later, it is never re-submitted or lost.
|
|
28
39
|
- Default output is a compact readiness summary; use -v for expanded detail and server-side validate warnings.
|
|
29
40
|
- --probe runs dataplane runtime checks after publish (slower).
|
|
30
|
-
- --timeout
|
|
41
|
+
- --timeout bounds only how long this CLI waits before printing a resume hint
|
|
42
|
+
(default 1800000ms/30min); it does not stop the upload job.
|
|
31
43
|
- --env selects role-assistant.yaml section (dev|tst|pro) for digital workers; default dev.
|
|
32
44
|
- Role Assistant packages under role-assistant/ do not use application.yaml; upload applies settings and Evidence only.
|
|
33
45
|
- test-e2e and test-integration publish local files first by default; use --no-publish on those commands to skip.
|
|
@@ -50,7 +50,11 @@ async function handleUploadCommandAction(systemKey, options) {
|
|
|
50
50
|
options.probeTimeout === undefined || options.probeTimeout === null
|
|
51
51
|
? 120000
|
|
52
52
|
: Number(options.probeTimeout);
|
|
53
|
-
|
|
53
|
+
// Only resolve --timeout when the caller explicitly set it; leaving it undefined otherwise
|
|
54
|
+
// lets the qualification poll fall back to its own default (30min) instead of the legacy
|
|
55
|
+
// 300000ms HTTP/dataplane-publish budget default, which no longer applies to the async path.
|
|
56
|
+
const timeoutMs =
|
|
57
|
+
options.timeout !== undefined ? resolvePipelineUploadTimeoutMs(options.timeout) : undefined;
|
|
54
58
|
await upload.uploadExternalSystem(systemKey, {
|
|
55
59
|
...options,
|
|
56
60
|
probeTimeout: Number.isFinite(probeTimeout) ? probeTimeout : 120000,
|
|
@@ -101,9 +105,17 @@ function setupUploadCommand(program) {
|
|
|
101
105
|
.option('-v, --verbose', 'Expanded upload summary and server-side pipeline validate warnings before publish')
|
|
102
106
|
.option('--probe', 'After publish, run dataplane runtime checks (validation/run); slower')
|
|
103
107
|
.option('--probe-timeout <ms>', 'Timeout for --probe (default: 120000)', '120000')
|
|
108
|
+
.option(
|
|
109
|
+
'--gate <gates>',
|
|
110
|
+
'Optional upload checks (comma-separated): operations, trust, governance, full'
|
|
111
|
+
)
|
|
112
|
+
.option(
|
|
113
|
+
'--subject-email <emails>',
|
|
114
|
+
'Subject email(s) for governance checks (comma-separated; used with --gate governance|full)'
|
|
115
|
+
)
|
|
104
116
|
.option(
|
|
105
117
|
'--timeout <ms>',
|
|
106
|
-
'
|
|
118
|
+
'How long this CLI waits for the upload job before printing a resume hint (default: 1800000/30min; min 5000, max 2700000). The job keeps running regardless.'
|
|
107
119
|
)
|
|
108
120
|
.option(
|
|
109
121
|
'--force',
|
|
@@ -114,6 +126,12 @@ function setupUploadCommand(program) {
|
|
|
114
126
|
'Role Assistant settings environment section: dev, tst, or pro (default: dev)',
|
|
115
127
|
'dev'
|
|
116
128
|
)
|
|
129
|
+
.option('--wait', 'Wait for the upload job to complete (default)', true)
|
|
130
|
+
.option('--no-wait', 'Return the upload job id immediately without waiting')
|
|
131
|
+
.option(
|
|
132
|
+
'--resume <jobId>',
|
|
133
|
+
'Wait for an existing upload job instead of submitting again'
|
|
134
|
+
)
|
|
117
135
|
.action(async(systemKey, options) => {
|
|
118
136
|
try {
|
|
119
137
|
await handleUploadCommandAction(systemKey, options);
|
|
@@ -341,4 +359,4 @@ function setupExternalSystemCommands(program) {
|
|
|
341
359
|
setupExternalSystemTestCommands(program);
|
|
342
360
|
}
|
|
343
361
|
|
|
344
|
-
module.exports = { setupExternalSystemCommands };
|
|
362
|
+
module.exports = { setupExternalSystemCommands, handleUploadCommandAction };
|
|
@@ -34,18 +34,14 @@ function buildRenameMap(operationMap) {
|
|
|
34
34
|
if (!operationMap || typeof operationMap !== 'object' || Array.isArray(operationMap)) return {};
|
|
35
35
|
const renameMap = {};
|
|
36
36
|
for (const k of Object.keys(operationMap)) {
|
|
37
|
-
//
|
|
38
|
-
//
|
|
37
|
+
// Operation keys use the capability naming contract, which allows camelCase.
|
|
38
|
+
// RBAC permission names are normalized separately when permissions are generated.
|
|
39
39
|
if (/^[a-z][a-zA-Z0-9]*$/.test(k)) {
|
|
40
|
-
if (/[A-Z]/.test(k)) {
|
|
41
|
-
renameMap[k] = k.toLowerCase();
|
|
42
|
-
}
|
|
43
40
|
continue;
|
|
44
41
|
}
|
|
45
42
|
const camel = toCamelCaseKey(k);
|
|
46
43
|
if (!camel || camel === k) continue;
|
|
47
|
-
|
|
48
|
-
renameMap[k] = camel.toLowerCase();
|
|
44
|
+
renameMap[k] = camel;
|
|
49
45
|
}
|
|
50
46
|
return renameMap;
|
|
51
47
|
}
|
|
@@ -212,7 +208,7 @@ function normalizeAutoRbacOperationKeys(parsed, changes) {
|
|
|
212
208
|
if (updated && Array.isArray(changes)) {
|
|
213
209
|
const pairs = keysToRename.map((k) => `${k}→${renameMap[k]}`).join(', ');
|
|
214
210
|
changes.push(
|
|
215
|
-
`Normalized autoRbac operation keys (permission names
|
|
211
|
+
`Normalized schema-invalid autoRbac operation aliases to camelCase keys (RBAC permission names remain lowercase): ${pairs}`
|
|
216
212
|
);
|
|
217
213
|
}
|
|
218
214
|
|
|
@@ -224,4 +220,3 @@ module.exports = {
|
|
|
224
220
|
toCamelCaseKey,
|
|
225
221
|
toKebabAliasKey
|
|
226
222
|
};
|
|
227
|
-
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Post-publish local OpenAPI sync follow-up, shared by the synchronous and
|
|
3
|
+
* qualification-async upload paths (extracted from upload.js per plan 191.0 file-size gate).
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
12
|
+
const { resolveExternalSystemPath } = require('../utils/app-path-resolve');
|
|
13
|
+
const { maybeSyncOpenApiFilesForMcp } = require('./repair-openapi-sync');
|
|
14
|
+
const { exportProgrammaticOpenApiForRepair } = require('./repair-openapi-export');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upload local `integration/<systemKey>/openapi/*.json` specs when present so dataplane can store
|
|
18
|
+
* vendor OpenAPI keyed by each datasource `openapi.documentKey` (same behavior as repair --api OpenAPI sync).
|
|
19
|
+
* Call this **after** the external system row exists (successful publish or completed qualification):
|
|
20
|
+
* list/upload endpoints require it. First publish still succeeds because pipeline materializes
|
|
21
|
+
* internal specs from `openapi.operations` when needed.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} systemKey
|
|
24
|
+
* @param {Object} manifest
|
|
25
|
+
* @param {Object} [uploadContext]
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async function logAndSyncLocalOpenApiForUpload(systemKey, manifest, uploadContext = {}) {
|
|
29
|
+
const pathOptions = uploadContext.pathOptions || {};
|
|
30
|
+
const appPath = resolveExternalSystemPath(systemKey, pathOptions);
|
|
31
|
+
const ei = manifest && manifest.externalIntegration;
|
|
32
|
+
const datasourceFiles = ei && Array.isArray(ei.dataSources) ? ei.dataSources : [];
|
|
33
|
+
await exportProgrammaticOpenApiForRepair({
|
|
34
|
+
appPath,
|
|
35
|
+
systemKey,
|
|
36
|
+
datasourceFiles,
|
|
37
|
+
dryRun: false,
|
|
38
|
+
backupCtx: { dryRun: false, changedFiles: [] }
|
|
39
|
+
});
|
|
40
|
+
const openapiSyncLines = await maybeSyncOpenApiFilesForMcp({
|
|
41
|
+
enabled: true,
|
|
42
|
+
dryRun: false,
|
|
43
|
+
appPath,
|
|
44
|
+
systemKey,
|
|
45
|
+
datasourceFiles,
|
|
46
|
+
dataplaneUrl: uploadContext.dataplaneUrl,
|
|
47
|
+
authConfig: uploadContext.authConfig,
|
|
48
|
+
// Always replace SpecFile rows keyed by openapi.documentKey so vendor/full
|
|
49
|
+
// specs are not left missing while only thin internal:* slices remain.
|
|
50
|
+
force: true
|
|
51
|
+
});
|
|
52
|
+
if (!uploadContext.quiet) {
|
|
53
|
+
for (const line of openapiSyncLines) {
|
|
54
|
+
logger.log(line.startsWith('Skipped') ? formatWarningLine(line) : formatSuccessLine(line));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { logAndSyncLocalOpenApiForUpload };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Resumable Dataplane qualification upload: accept, --wait/--no-wait, poll, resume.
|
|
3
|
+
* Mirrors dev-host-ops.js's job id + wait/no-wait contract (plan 191.0 task 1). Never invents
|
|
4
|
+
* qualification truth — accept/poll surface exactly what Dataplane returns.
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
13
|
+
const { formatApiError } = require('../utils/api-error-handler');
|
|
14
|
+
const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
|
|
15
|
+
const { pollQualificationJobUntilTerminal } = require('./upload-qualification-poll');
|
|
16
|
+
const { createQualificationPollHandlers } = require('../utils/qualification-poll-ui');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ctrl-C / disconnect during polling must detach without corrupting truth (plan 191.0 task 4):
|
|
20
|
+
* the qualification job keeps running on Dataplane regardless of whether this CLI process exits.
|
|
21
|
+
* This only makes that safe fact visible, printing the resume command before exiting 130.
|
|
22
|
+
* @param {string} jobId
|
|
23
|
+
* @param {string} systemKey
|
|
24
|
+
* @returns {() => void} Call to remove the listeners once polling finishes normally.
|
|
25
|
+
*/
|
|
26
|
+
function registerUploadInterruptHint(jobId, systemKey) {
|
|
27
|
+
const onSignal = () => {
|
|
28
|
+
logger.log(
|
|
29
|
+
formatWarningLine(
|
|
30
|
+
`Interrupted — upload job ${jobId} is still running, unaffected. Resume with: aifabrix upload ${systemKey} --resume ${jobId}`
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
process.exit(130);
|
|
34
|
+
};
|
|
35
|
+
process.once('SIGINT', onSignal);
|
|
36
|
+
process.once('SIGTERM', onSignal);
|
|
37
|
+
return () => {
|
|
38
|
+
process.removeListener('SIGINT', onSignal);
|
|
39
|
+
process.removeListener('SIGTERM', onSignal);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Submit the candidate for durable Dataplane qualification (async). Always returns quickly —
|
|
45
|
+
* a 202 accept, or the current state when an equivalent job is already in flight — never blocks
|
|
46
|
+
* for the ladder to finish.
|
|
47
|
+
* @param {string} dataplaneUrl
|
|
48
|
+
* @param {Object} authConfig
|
|
49
|
+
* @param {Object} payload
|
|
50
|
+
* @param {string} systemKey
|
|
51
|
+
* @param {Object} [options] - Forwarded to uploadApplicationViaPipeline (e.g. timeoutMs, ignored when async)
|
|
52
|
+
* @returns {Promise<{jobId: string, state: string, resumed: boolean, candidateFingerprint: string}>}
|
|
53
|
+
*/
|
|
54
|
+
async function acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
55
|
+
const res = await uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfig, payload, systemKey, {
|
|
56
|
+
...options,
|
|
57
|
+
async: true
|
|
58
|
+
});
|
|
59
|
+
if (res?.success === false) {
|
|
60
|
+
const err = new Error(formatApiError(res, dataplaneUrl));
|
|
61
|
+
const status = Number(res.status || res.statusCode);
|
|
62
|
+
err.statusCode = status >= 400 && status < 600 ? status : 400;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
return res.data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Poll an existing qualification job id to terminal state (resume without re-uploading).
|
|
70
|
+
* @param {string} dataplaneUrl
|
|
71
|
+
* @param {Object} authConfig
|
|
72
|
+
* @param {string} jobId
|
|
73
|
+
* @param {string} systemKey - For the printed resume hint if interrupted mid-poll.
|
|
74
|
+
* @param {{ json?: boolean, silent?: boolean, timeoutMs?: number|string }} [options] - `timeoutMs`
|
|
75
|
+
* bounds only how long this CLI process waits (CLI `--timeout`); Dataplane keeps running the
|
|
76
|
+
* job regardless, so timing out returns a resumable jobId, never a fabricated failure.
|
|
77
|
+
* @returns {Promise<Object>} Terminal job status
|
|
78
|
+
*/
|
|
79
|
+
async function resumeQualificationUpload(dataplaneUrl, authConfig, jobId, systemKey, options = {}) {
|
|
80
|
+
const pollUi = options.json || options.silent
|
|
81
|
+
? createQualificationPollHandlers(jobId, { silent: true })
|
|
82
|
+
: createQualificationPollHandlers(jobId, { verbose: options.verbose === true });
|
|
83
|
+
const unregisterInterruptHint = registerUploadInterruptHint(jobId, systemKey);
|
|
84
|
+
try {
|
|
85
|
+
return await pollQualificationJobUntilTerminal(
|
|
86
|
+
dataplaneUrl,
|
|
87
|
+
authConfig,
|
|
88
|
+
jobId,
|
|
89
|
+
pollUi,
|
|
90
|
+
options.timeoutMs !== undefined ? Number(options.timeoutMs) : undefined
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
pollUi.finish();
|
|
94
|
+
unregisterInterruptHint();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Accept then, unless `--no-wait`, poll to terminal. On `--no-wait`, prints/returns the
|
|
100
|
+
* resumable job id immediately and never blocks.
|
|
101
|
+
* @param {string} dataplaneUrl
|
|
102
|
+
* @param {Object} authConfig
|
|
103
|
+
* @param {Object} payload
|
|
104
|
+
* @param {string} systemKey
|
|
105
|
+
* @param {{ wait?: boolean, json?: boolean, timeoutMs?: number|string }} [options]
|
|
106
|
+
* @returns {Promise<{jobId: string, candidateFingerprint: string, terminal: Object|null}>}
|
|
107
|
+
*/
|
|
108
|
+
async function runQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
109
|
+
const accepted = await acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options);
|
|
110
|
+
const wait = options.wait !== false;
|
|
111
|
+
if (!wait) {
|
|
112
|
+
if (options.json) {
|
|
113
|
+
logger.log(JSON.stringify({ ...accepted, terminal: null }, null, 2));
|
|
114
|
+
} else {
|
|
115
|
+
logger.log(
|
|
116
|
+
formatSuccessLine(
|
|
117
|
+
`Upload queued (job ${accepted.jobId}). Resume with: aifabrix upload ${systemKey} --resume ${accepted.jobId}`
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return { ...accepted, terminal: null };
|
|
122
|
+
}
|
|
123
|
+
const terminal = await resumeQualificationUpload(dataplaneUrl, authConfig, accepted.jobId, systemKey, options);
|
|
124
|
+
return { ...accepted, terminal };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
acceptQualificationUpload,
|
|
129
|
+
resumeQualificationUpload,
|
|
130
|
+
runQualificationUpload
|
|
131
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Post-qualification follow-ups for the async upload path: credential push,
|
|
3
|
+
* local OpenAPI sync, sidecar governance/evidence sync, probe and a qualification-specific
|
|
4
|
+
* summary (plan 191.0 task 1).
|
|
5
|
+
*
|
|
6
|
+
* Note: this intentionally does not reconstruct the legacy tiered datasource-readiness summary
|
|
7
|
+
* (`logUploadReadinessSummary`) that the synchronous path shows — that summary classifies real
|
|
8
|
+
* server-assigned datasource state (status/isActive/mcpContract) which requires a fresh
|
|
9
|
+
* Dataplane fetch+remap not yet ported to this path. The qualification summary below reports
|
|
10
|
+
* what Dataplane actually returned instead of approximating server state from local data.
|
|
11
|
+
* @author AI Fabrix Team
|
|
12
|
+
* @version 1.0.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const logger = require('../utils/logger');
|
|
18
|
+
const { formatSuccessLine, metadata, sectionTitle } = require('../utils/cli-test-layout-chalk');
|
|
19
|
+
const { pushAndLogCredentialSecrets } = require('./upload-credential-push-log');
|
|
20
|
+
const { finalizeUploadPublishResult } = require('./upload-data-sync-finalize');
|
|
21
|
+
const { runUploadSidecarFollowups } = require('./upload-sidecar-followups');
|
|
22
|
+
const { logAndSyncLocalOpenApiForUpload } = require('./upload-openapi-sync-followup');
|
|
23
|
+
const { maybeRunUploadProbe } = require('./upload-probe');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} systemKey
|
|
27
|
+
* @param {Object} _accepted - Machine-readable qualification identity (not shown in CLI output)
|
|
28
|
+
* @param {Object} job - Terminal PublishJobStatusResponse data
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
*/
|
|
31
|
+
function logQualifiedUploadSummary(systemKey, _accepted, job, options) {
|
|
32
|
+
if (options.suppressSummary === true || options.json === true) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
logger.log('');
|
|
36
|
+
logger.log(sectionTitle('Upload complete'));
|
|
37
|
+
logger.log(metadata(`System: ${systemKey}`));
|
|
38
|
+
if (options.verbose === true) {
|
|
39
|
+
logger.log(metadata(`Job: ${job.jobId}`));
|
|
40
|
+
const certificateIds = job.result && Array.isArray(job.result.certificateIds) ? job.result.certificateIds : [];
|
|
41
|
+
if (certificateIds.length > 0) {
|
|
42
|
+
logger.log(metadata(`Certificates: ${certificateIds.join(', ')}`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
logger.log(formatSuccessLine(`${systemKey} uploaded successfully.`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs the same downstream follow-ups as the synchronous publish path (credential push, local
|
|
50
|
+
* OpenAPI sync, sidecar governance/evidence sync, optional probe) once a qualification job has
|
|
51
|
+
* reached `completed`, then finalizes with the same result-envelope shape as the sync path.
|
|
52
|
+
* @param {string} systemKey
|
|
53
|
+
* @param {Object} options
|
|
54
|
+
* @param {Object} manifest
|
|
55
|
+
* @param {Object} payload
|
|
56
|
+
* @param {{ dataplaneUrl: string, authConfig: Object, environment: string, accepted: Object, job: Object }} ctx
|
|
57
|
+
* @returns {Promise<Object>}
|
|
58
|
+
*/
|
|
59
|
+
async function completeQualifiedUpload(systemKey, options, manifest, payload, ctx) {
|
|
60
|
+
const quietCompact = options.minimal === true && options.verbose !== true;
|
|
61
|
+
const pushMeta = await pushAndLogCredentialSecrets(ctx.dataplaneUrl, ctx.authConfig, systemKey, payload, {
|
|
62
|
+
json: options.json === true,
|
|
63
|
+
pathOptions: options.pathOptions,
|
|
64
|
+
suppressCredentialLog: quietCompact,
|
|
65
|
+
excludeSecretKeys: options.excludeSecretKeys
|
|
66
|
+
});
|
|
67
|
+
await logAndSyncLocalOpenApiForUpload(systemKey, manifest, {
|
|
68
|
+
dataplaneUrl: ctx.dataplaneUrl,
|
|
69
|
+
authConfig: ctx.authConfig,
|
|
70
|
+
pathOptions: options.pathOptions,
|
|
71
|
+
quiet: quietCompact
|
|
72
|
+
});
|
|
73
|
+
const sidecar = await runUploadSidecarFollowups({
|
|
74
|
+
systemKey,
|
|
75
|
+
options,
|
|
76
|
+
payload,
|
|
77
|
+
dataplaneUrl: ctx.dataplaneUrl,
|
|
78
|
+
authConfig: ctx.authConfig,
|
|
79
|
+
environment: ctx.environment
|
|
80
|
+
});
|
|
81
|
+
logQualifiedUploadSummary(systemKey, ctx.accepted, ctx.job, options);
|
|
82
|
+
if (options.probe === true) {
|
|
83
|
+
await maybeRunUploadProbe(ctx.dataplaneUrl, systemKey, ctx.authConfig, options.probeTimeout, {
|
|
84
|
+
compact: quietCompact
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return finalizeUploadPublishResult(
|
|
88
|
+
{
|
|
89
|
+
dataplaneUrl: ctx.dataplaneUrl,
|
|
90
|
+
authConfig: ctx.authConfig,
|
|
91
|
+
environment: ctx.environment,
|
|
92
|
+
uploadSkipped: false,
|
|
93
|
+
sidecar,
|
|
94
|
+
credentialPush: pushMeta.credentialPush,
|
|
95
|
+
configPush: pushMeta.configPush,
|
|
96
|
+
jobId: ctx.job.jobId,
|
|
97
|
+
candidateFingerprint: ctx.accepted.candidateFingerprint
|
|
98
|
+
},
|
|
99
|
+
payload,
|
|
100
|
+
options
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { completeQualifiedUpload, logQualifiedUploadSummary };
|