@aifabrix/builder 2.55.2 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent-kit/session-report.js +76 -0
- package/lib/agent-kit/start.js +124 -57
- package/lib/agent-kit/tmux-sessions.js +163 -0
- package/lib/api/environments.api.js +38 -0
- package/lib/api/marketplace-bootstrap.api.js +31 -0
- package/lib/api/pipeline.api.js +32 -3
- package/lib/app/deploy.js +6 -5
- package/lib/cli/setup-app.js +7 -6
- package/lib/cli/setup-environment.js +76 -19
- package/lib/cli/setup-external-system-help.js +15 -3
- package/lib/cli/setup-external-system.js +21 -3
- package/lib/commands/agent-kit-help.js +32 -15
- package/lib/commands/agent-kit.js +90 -8
- package/lib/commands/repair-datasource-auto-rbac.js +4 -9
- package/lib/commands/upload-openapi-sync-followup.js +59 -0
- package/lib/commands/upload-qualification-client.js +131 -0
- package/lib/commands/upload-qualification-followups.js +104 -0
- package/lib/commands/upload-qualification-poll.js +143 -0
- package/lib/commands/upload.js +37 -156
- package/lib/deployment/deploy-poll-ui.js +14 -10
- package/lib/deployment/deployer-status.js +4 -4
- package/lib/deployment/installation/azure-infra-stage.js +160 -0
- package/lib/deployment/installation/azure-preflight.js +102 -0
- package/lib/deployment/installation/azure-readiness.js +71 -0
- package/lib/deployment/installation/docker-register.js +112 -0
- package/lib/deployment/installation/environment-auth.js +76 -0
- package/lib/deployment/installation/environment-manifest.js +165 -0
- package/lib/deployment/installation/environment-stage.js +191 -0
- package/lib/deployment/installation/index.js +39 -0
- package/lib/deployment/installation/infra-catalog.js +165 -0
- package/lib/deployment/installation/infra-manifest.js +161 -0
- package/lib/deployment/installation/infra-projection.js +86 -0
- package/lib/deployment/installation/public-ip.js +65 -0
- package/lib/deployment/installation/stage-input.js +169 -0
- package/lib/deployment/installation/stage-output.js +53 -0
- package/lib/deployment/installation/workspace-env.js +76 -0
- package/lib/deployment/installation/workspace-files.js +178 -0
- package/lib/deployment/installation/workspace-schema.js +40 -0
- package/lib/deployment/installation/workspace-secrets.js +66 -0
- package/lib/deployment/installation/workspace.js +128 -0
- package/lib/deployment/marketplace-environment.js +305 -0
- package/lib/external-system/deploy.js +46 -51
- package/lib/generator/builders.js +4 -0
- package/lib/generator/helpers.js +55 -99
- package/lib/generator/index.js +0 -4
- package/lib/generator/split-variables.js +11 -5
- package/lib/internal/fs-real-sync.js +29 -0
- package/lib/programmatic/builder-api-request.js +8 -4
- package/lib/programmatic/capability-registry.js +2 -0
- package/lib/programmatic/get-deploy-status.js +45 -0
- package/lib/programmatic/get-upload-status.js +45 -0
- package/lib/programmatic/http-accepted-response.js +31 -0
- package/lib/programmatic/openapi-descriptions.js +14 -4
- package/lib/programmatic/request-timeout.js +21 -15
- package/lib/programmatic/route-handler-map.js +6 -0
- package/lib/programmatic/run-deploy.js +7 -3
- package/lib/programmatic/run-upload.js +11 -7
- package/lib/role-assistant/package-validate.js +4 -0
- package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
- package/lib/role-assistant/test-runner.js +14 -17
- package/lib/schema/application-schema.json +20 -2
- package/lib/schema/infra-parameter.schema.json +7 -0
- package/lib/schema/infra.parameter.yaml +32 -43
- package/lib/schema/infrastructure-schema.json +1480 -399
- package/lib/schema/installation-environment.schema.json +601 -0
- package/lib/utils/cli-utils.js +3 -2
- package/lib/utils/qualification-poll-ui.js +136 -0
- package/lib/utils/variable-transformer.js +3 -0
- package/lib/validation/required-variables.js +41 -0
- package/lib/validation/validator.js +8 -5
- package/package.json +7 -2
- package/templates/README.md +4 -0
- package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
- package/templates/agent-kit/skills/shared/hosts.md +10 -10
- package/templates/applications/builder-api/env.template +4 -0
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/keycloak/Dockerfile +12 -5
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
- package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
- package/templates/applications/keycloak/env.template +13 -1
- package/templates/applications/miso-controller/application.yaml +3 -2
- package/templates/applications/miso-controller/env.template +28 -22
- package/templates/marketplace/createUiDefinition.json +77 -0
- package/templates/marketplace/install.parameters.example.json +9 -0
- package/templates/marketplace/main.json +4087 -0
- package/lib/generator/deploy-manifest-azure-kv.js +0 -81
|
@@ -0,0 +1,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 };
|
|
@@ -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
|
};
|