@aifabrix/builder 2.59.0 → 2.61.2
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/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/setup.js +3 -0
- package/lib/agent-kit/start.js +45 -1
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -3
- package/lib/cli/setup-app.js +19 -1
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +97 -0
- package/lib/core/secrets-env-content.js +42 -4
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/azure-infra-stage.js +3 -1
- package/lib/deployment/installation/infra-catalog.js +2 -5
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +13 -6
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +205 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +416 -66
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +16 -9
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +5 -4
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
- package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +13 -3
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +76 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +36 -5
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +96 -1
- package/templates/applications/miso-controller/env.template +63 -37
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +73 -225
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"key": "infrastructure-schema",
|
|
8
8
|
"name": "Installation Infrastructure Manifest Schema",
|
|
9
9
|
"description": "JSON schema for infrastructure/<resource-group>/environment-infra.json",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.1.0",
|
|
11
11
|
"type": "schema",
|
|
12
12
|
"category": "infrastructure",
|
|
13
13
|
"author": "AI Fabrix Team",
|
|
14
14
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
15
|
-
"updatedAt": "2026-09-
|
|
15
|
+
"updatedAt": "2026-09-24T00:00:00Z",
|
|
16
16
|
"compatibility": {
|
|
17
17
|
"minVersion": "2.0.0",
|
|
18
18
|
"maxVersion": "2.0.0",
|
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": [],
|
|
28
28
|
"changelog": [
|
|
29
|
+
{
|
|
30
|
+
"version": "2.1.0",
|
|
31
|
+
"date": "2026-09-24T00:00:00Z",
|
|
32
|
+
"breaking": false,
|
|
33
|
+
"changes": [
|
|
34
|
+
"Removed moriCallbackApiKey and entraCallbackStateSecret: the shipped Marketplace template no longer declares them. The installation receives its Mori key at licence registration, and Entra consent state is issued server-side (Mori 227.5)."
|
|
35
|
+
]
|
|
36
|
+
},
|
|
29
37
|
{
|
|
30
38
|
"version": "2.0.0",
|
|
31
39
|
"date": "2026-09-16T00:00:00Z",
|
|
@@ -501,8 +509,6 @@
|
|
|
501
509
|
"acrResourceId",
|
|
502
510
|
"deployRoleAssignments",
|
|
503
511
|
"moriBaseUrl",
|
|
504
|
-
"moriCallbackApiKey",
|
|
505
|
-
"entraCallbackStateSecret",
|
|
506
512
|
"entraIdClientId",
|
|
507
513
|
"entraIdClientSecret",
|
|
508
514
|
"entraIdTenantId",
|
|
@@ -1058,37 +1064,6 @@
|
|
|
1058
1064
|
}
|
|
1059
1065
|
}
|
|
1060
1066
|
},
|
|
1061
|
-
"moriCallbackApiKey": {
|
|
1062
|
-
"$ref": "#/definitions/envReference"
|
|
1063
|
-
},
|
|
1064
|
-
"entraCallbackStateSecret": {
|
|
1065
|
-
"type": "object",
|
|
1066
|
-
"required": [
|
|
1067
|
-
"value",
|
|
1068
|
-
"source",
|
|
1069
|
-
"description"
|
|
1070
|
-
],
|
|
1071
|
-
"additionalProperties": false,
|
|
1072
|
-
"properties": {
|
|
1073
|
-
"value": {
|
|
1074
|
-
"type": "null"
|
|
1075
|
-
},
|
|
1076
|
-
"source": {
|
|
1077
|
-
"type": "string",
|
|
1078
|
-
"minLength": 1
|
|
1079
|
-
},
|
|
1080
|
-
"description": {
|
|
1081
|
-
"type": "string",
|
|
1082
|
-
"minLength": 1
|
|
1083
|
-
},
|
|
1084
|
-
"required": {
|
|
1085
|
-
"type": "boolean"
|
|
1086
|
-
},
|
|
1087
|
-
"requiredAt": {
|
|
1088
|
-
"const": "tenantActivation"
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
},
|
|
1092
1067
|
"entraIdClientId": {
|
|
1093
1068
|
"type": "object",
|
|
1094
1069
|
"required": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const fsSync = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const buildCopy = require('./build-copy');
|
|
14
|
+
const { prepareLocalDataMount } = require('./prepare-local-data-mount');
|
|
14
15
|
const paths = require('./paths');
|
|
15
16
|
const { isSystemBuilderAppName } = require('./paths-system-builder-keys');
|
|
16
17
|
const {
|
|
@@ -187,14 +188,14 @@ function resolveStorageMountSourceRoot(appName, buildContext) {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
|
-
* Host `mount
|
|
191
|
+
* Host `mount/data/<appName>/` directory bind-mounted to `/mnt/data` when storage is required.
|
|
191
192
|
*
|
|
192
193
|
* @param {object} ctx - Compose template context (`devMountPath`, `appName`, `appConfig`, `serviceConfig`)
|
|
193
194
|
* @returns {string|null}
|
|
194
195
|
*/
|
|
195
196
|
function resolveLocalDataMountPathForCompose(ctx) {
|
|
196
197
|
if (ctx.devMountPath && typeof ctx.devMountPath === 'string') {
|
|
197
|
-
return path.join(ctx.devMountPath, 'mount').replace(/\\/g, '/');
|
|
198
|
+
return path.join(ctx.devMountPath, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
|
|
198
199
|
}
|
|
199
200
|
const requiresStorage =
|
|
200
201
|
ctx.serviceConfig?.requiresStorage ||
|
|
@@ -212,7 +213,7 @@ function resolveLocalDataMountPathForCompose(ctx) {
|
|
|
212
213
|
if (!sourceRoot) {
|
|
213
214
|
return null;
|
|
214
215
|
}
|
|
215
|
-
return path.join(sourceRoot, 'mount').replace(/\\/g, '/');
|
|
216
|
+
return path.join(sourceRoot, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
|
|
216
217
|
} catch {
|
|
217
218
|
return null;
|
|
218
219
|
}
|
|
@@ -229,12 +230,8 @@ function buildComposeTemplateContext(ctx) {
|
|
|
229
230
|
ctx.appConfig?.requires?.storage ||
|
|
230
231
|
ctx.appConfig?.services?.storage ||
|
|
231
232
|
false;
|
|
232
|
-
if (localDataMountPath && requiresStorage
|
|
233
|
-
|
|
234
|
-
fsSync.mkdirSync(localDataMountPath, { recursive: true });
|
|
235
|
-
} catch {
|
|
236
|
-
// Non-fatal: tests may use non-writable devMountPath; host must create mount/ before run.
|
|
237
|
-
}
|
|
233
|
+
if (localDataMountPath && requiresStorage) {
|
|
234
|
+
prepareLocalDataMount(localDataMountPath, ctx.envFileAbsolutePath);
|
|
238
235
|
}
|
|
239
236
|
return {
|
|
240
237
|
...ctx.serviceConfig,
|
|
@@ -249,6 +246,16 @@ function buildComposeTemplateContext(ctx) {
|
|
|
249
246
|
networkName: ctx.networkName,
|
|
250
247
|
containerName: ctx.containerName,
|
|
251
248
|
misoEnvironment: resolveMisoEnvironment(ctx.options),
|
|
249
|
+
// Containers are run as the invoking host user so bind-mounted files stay writable.
|
|
250
|
+
// An app that administers its own operating system — creating accounts, managing
|
|
251
|
+
// sshd — cannot work that way and declares `runAsRoot: true` to opt out.
|
|
252
|
+
runAsRoot: ctx.appConfig?.runAsRoot === true,
|
|
253
|
+
// Extra protocol ports use the main port offset formula to avoid developer collisions.
|
|
254
|
+
additionalPorts: (ctx.appConfig?.additionalPorts ?? []).map((entry) => ({
|
|
255
|
+
name: entry.name,
|
|
256
|
+
containerPort: entry.containerPort,
|
|
257
|
+
hostPort: ctx.idNum === 0 ? entry.hostPort : entry.hostPort + ctx.idNum * 100
|
|
258
|
+
})),
|
|
252
259
|
devMountPath: ctx.devMountPath,
|
|
253
260
|
localDataMountPath,
|
|
254
261
|
reloadStartCommand: ctx.reloadStartCommand,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** @fileoverview Safe binary metadata and keyed document persistence evidence. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { keyedGetEvidence, hasExecutedKeyedGet } = require('./datasource-test-run-capability-scope');
|
|
5
|
+
const BINARY_FIELDS = new Set(['contentType', 'size', 'filename', 'sha256']);
|
|
6
|
+
|
|
7
|
+
/** @param {*} value @returns {Object} */
|
|
8
|
+
function binarySummary(value) {
|
|
9
|
+
if (!value || typeof value !== 'object') return {};
|
|
10
|
+
return Object.fromEntries(Object.entries(value).filter(([key, item]) =>
|
|
11
|
+
BINARY_FIELDS.has(key) && ['string', 'number'].includes(typeof item)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Limit binary containers to metadata before printing or saving a report.
|
|
16
|
+
* Ordinary JSON record fields remain intact.
|
|
17
|
+
* @param {*} value
|
|
18
|
+
* @param {WeakSet} seen
|
|
19
|
+
* @returns {*}
|
|
20
|
+
*/
|
|
21
|
+
function sanitizeBinaryEvidence(value, seen = new WeakSet()) {
|
|
22
|
+
if (!value || typeof value !== 'object') return value;
|
|
23
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return '[binary omitted]';
|
|
24
|
+
if (seen.has(value)) return '[Circular]';
|
|
25
|
+
seen.add(value);
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const items = value.map(item => sanitizeBinaryEvidence(item, seen));
|
|
28
|
+
seen.delete(value);
|
|
29
|
+
return items;
|
|
30
|
+
}
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, item] of Object.entries(value)) {
|
|
33
|
+
if (['getBinary', 'listBinary', '_binary'].includes(key)) out[key] = binarySummary(item);
|
|
34
|
+
else if (value.keyedGet === true && key === 'document') {
|
|
35
|
+
out[key] = { ...binarySummary(item) };
|
|
36
|
+
if (typeof item?.key === 'string') out[key].key = item.key;
|
|
37
|
+
if (typeof item?.persisted === 'boolean') out[key].persisted = item.persisted;
|
|
38
|
+
} else if (value.keyedGet === true && ['data', 'bytes', 'body', 'content', 'base64'].includes(key)) continue;
|
|
39
|
+
else out[key] = sanitizeBinaryEvidence(item, seen);
|
|
40
|
+
}
|
|
41
|
+
seen.delete(value);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {Object} value @param {string[]} lines */
|
|
46
|
+
function appendBinaryAndPersistence(value, lines) {
|
|
47
|
+
if (!value || typeof value !== 'object') return;
|
|
48
|
+
if (value.getBinary) lines.push(` getBinary: ${JSON.stringify(binarySummary(value.getBinary))}`);
|
|
49
|
+
if (value.scope?.expectedSource === 'get') {
|
|
50
|
+
const observed = value.observed || {};
|
|
51
|
+
lines.push(` Document persistence (expectedSource: get): documents=${Number(observed.documents) || 0}, chunks=${Number(observed.chunks) || 0}`);
|
|
52
|
+
}
|
|
53
|
+
for (const [key, child] of Object.entries(value)) {
|
|
54
|
+
if (key !== 'getBinary') appendBinaryAndPersistence(child, lines);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {Object} envelope @returns {string[]} */
|
|
59
|
+
function binaryEvidenceLines(envelope) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const job of keyedGetEvidence(envelope)) {
|
|
62
|
+
const status = job.skipped === true ? 'skipped' : job.ok === true ? 'executed' : 'failed';
|
|
63
|
+
lines.push(` Keyed get: ${status} (${job.datasourceKey || envelope.datasourceKey || 'unknown'})`);
|
|
64
|
+
if (job.document) lines.push(` Document: ${JSON.stringify(binarySummary(job.document))}`);
|
|
65
|
+
}
|
|
66
|
+
appendBinaryAndPersistence(envelope?.integration?.stepResults, lines);
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {Object} envelope @param {string} key @returns {boolean} */
|
|
71
|
+
function hasKeyedDocumentPersistence(envelope, key) {
|
|
72
|
+
if (!hasExecutedKeyedGet(envelope, key)) return false;
|
|
73
|
+
const steps = envelope?.integration?.stepResults || [];
|
|
74
|
+
return steps.some(step => {
|
|
75
|
+
if (step.name !== 'document_persistence' || step.success === false || step.status === 'failed') return false;
|
|
76
|
+
const row = step.evidence?.datasources?.[key];
|
|
77
|
+
const model = row?.model;
|
|
78
|
+
return model?.scope?.expectedSource === 'get' && model.observed?.documents > 0 &&
|
|
79
|
+
model.observed?.chunks > 0 && (!Array.isArray(row.failures) || row.failures.length === 0);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {Object} result @param {string[]} keys @returns {boolean} */
|
|
84
|
+
function hasAllKeyedDocumentPersistence(result, keys) {
|
|
85
|
+
return keys.length > 0 && keys.every(key => {
|
|
86
|
+
const row = result?.results?.find(item => item.key === key);
|
|
87
|
+
return row?.success === true && hasKeyedDocumentPersistence(row.datasourceTestRun, key);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { binarySummary, sanitizeBinaryEvidence, binaryEvidenceLines, hasKeyedDocumentPersistence,
|
|
92
|
+
hasAllKeyedDocumentPersistence };
|
|
@@ -4,6 +4,43 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Keyed document retrieval has no list operation, so the `get` runs once against a business
|
|
9
|
+
* key instead of over a sync page. The dataplane records that run as step evidence
|
|
10
|
+
* (`sync_step_keyed_get.py`), which is why execution is read from evidence rather than from
|
|
11
|
+
* a row count. `IntegrationStepResult.evidence` is an open object by contract.
|
|
12
|
+
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
13
|
+
* @returns {Array<Object>} Keyed-get evidence entries, newest first as reported
|
|
14
|
+
*/
|
|
15
|
+
function keyedGetEvidence(envelope) {
|
|
16
|
+
const integration = envelope && typeof envelope === 'object' ? envelope.integration : null;
|
|
17
|
+
const steps = integration && Array.isArray(integration.stepResults) ? integration.stepResults : [];
|
|
18
|
+
return steps
|
|
19
|
+
.map(step => (step && typeof step.evidence === 'object' ? step.evidence : null))
|
|
20
|
+
.flatMap(evidence => evidence && Array.isArray(evidence.jobs) ? evidence.jobs : [evidence])
|
|
21
|
+
.filter(evidence => evidence && evidence.keyedGet === true);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A keyed `get` counts as executed only when it actually returned a document. A skipped run
|
|
26
|
+
* (no primary key value) or a failed one is not execution, and reporting it as such would
|
|
27
|
+
* turn `--strict-capability-scope` into a rubber stamp.
|
|
28
|
+
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
29
|
+
* @param {string|undefined|null} datasourceKey - Restrict to one datasource when given
|
|
30
|
+
* @returns {boolean} True when a keyed get ran successfully
|
|
31
|
+
*/
|
|
32
|
+
function hasExecutedKeyedGet(envelope, datasourceKey) {
|
|
33
|
+
const wanted = datasourceKey === undefined || datasourceKey === null || String(datasourceKey).trim() === ''
|
|
34
|
+
? ''
|
|
35
|
+
: String(datasourceKey).trim();
|
|
36
|
+
return keyedGetEvidence(envelope).some(evidence => {
|
|
37
|
+
if (evidence.ok !== true || evidence.skipped === true) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return wanted === '' || String(evidence.datasourceKey || '') === wanted;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
/**
|
|
8
45
|
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
9
46
|
* @param {string|undefined|null} requestedCapabilityKey - From positional [capabilityKey]
|
|
@@ -23,6 +60,10 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
|
|
|
23
60
|
envelope && typeof envelope === 'object' && Array.isArray(envelope.capabilities)
|
|
24
61
|
? envelope.capabilities
|
|
25
62
|
: [];
|
|
63
|
+
if (key === 'get' && keyedGetEvidence(envelope).length > 0 &&
|
|
64
|
+
!hasExecutedKeyedGet(envelope, envelope.datasourceKey)) {
|
|
65
|
+
return { violated: true, message: 'Capabilities scope: keyed get was not executed successfully.' };
|
|
66
|
+
}
|
|
26
67
|
if (caps.length <= 1) {
|
|
27
68
|
return { violated: false };
|
|
28
69
|
}
|
|
@@ -39,5 +80,7 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
|
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
module.exports = {
|
|
42
|
-
analyzeCapabilityScope
|
|
83
|
+
analyzeCapabilityScope,
|
|
84
|
+
keyedGetEvidence,
|
|
85
|
+
hasExecutedKeyedGet
|
|
43
86
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
|
|
7
8
|
const { SEP, appendReferenceLayoutLines } = require('./datasource-test-run-display');
|
|
8
9
|
const { buildDebugEnvelopeSlice } = require('./datasource-test-run-debug-slice');
|
|
9
10
|
const {
|
|
@@ -250,6 +251,7 @@ function applyRawLineCap(text) {
|
|
|
250
251
|
* @returns {string}
|
|
251
252
|
*/
|
|
252
253
|
function formatDatasourceTestRunDebugBlock(envelope, mode, isTTY) {
|
|
254
|
+
envelope = sanitizeBinaryEvidence(envelope);
|
|
253
255
|
if (!envelope || typeof envelope !== 'object') return '';
|
|
254
256
|
|
|
255
257
|
const lines = ['', SEP, `Debug (${mode})`];
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const chalk = require('chalk');
|
|
8
|
+
const { binaryEvidenceLines } = require('./datasource-binary-evidence');
|
|
8
9
|
const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
|
|
9
10
|
const { appendCertificateTTY, appendCertificateIssuanceTTY } = require('./datasource-test-run-certificate-tty');
|
|
10
11
|
const { buildTtyMetaLines: buildTtyMetaLinesCore } = require('./datasource-test-run-tty-meta-lines');
|
|
12
|
+
const { appendStructuredIssueGuidance } = require('./datasource-test-run-issue-guidance');
|
|
11
13
|
|
|
12
14
|
const SEP = '────────────────────────────────';
|
|
13
15
|
|
|
@@ -297,9 +299,12 @@ function appendValidationIssueLines(lines, envelope, maxIssues = 5) {
|
|
|
297
299
|
const cap = Math.min(maxIssues, issues.length);
|
|
298
300
|
for (let i = 0; i < cap; i += 1) {
|
|
299
301
|
const iss = issues[i];
|
|
300
|
-
const
|
|
302
|
+
const issueCode = iss && (iss.stableCode || iss.code);
|
|
303
|
+
const code = issueCode ? chalk.red(`[${issueCode}] `) : '';
|
|
304
|
+
const severity = iss?.severity ? chalk.gray(`[${String(iss.severity)}] `) : '';
|
|
301
305
|
const msg = iss && iss.message ? String(iss.message) : JSON.stringify(iss);
|
|
302
|
-
lines.push(` ${code}${chalk.yellow(msg)}`);
|
|
306
|
+
lines.push(` ${code}${severity}${chalk.yellow(msg)}`);
|
|
307
|
+
appendStructuredIssueGuidance(lines, iss);
|
|
303
308
|
appendDpSec013Details(lines, iss);
|
|
304
309
|
}
|
|
305
310
|
if (issues.length > cap) {
|
|
@@ -448,6 +453,7 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
|
|
|
448
453
|
appendRefsSectionIfEnabled(lines, envelope, options);
|
|
449
454
|
appendValidationIssueLines(lines, envelope);
|
|
450
455
|
appendIntegrationStepLines(lines, envelope);
|
|
456
|
+
lines.push(...binaryEvidenceLines(envelope));
|
|
451
457
|
const focus = normalizedFocusCapabilityKey(options.focusCapabilityKey);
|
|
452
458
|
if (focus) {
|
|
453
459
|
lines.push(formatCapabilityFocusSection(envelope, focus));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured human guidance for DatasourceTestRun validation issues.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Preserve actionable issue fields in TTY output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
const STEP_VERBS = {
|
|
12
|
+
locate_file: 'open',
|
|
13
|
+
update_path: 'update',
|
|
14
|
+
add_mapping: 'add mapping',
|
|
15
|
+
set_value: 'set',
|
|
16
|
+
remove_value: 'remove',
|
|
17
|
+
add_item: 'add to',
|
|
18
|
+
remove_item: 'remove from'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const UPSERT_GUIDANCE = {
|
|
22
|
+
'DP-CIP-030': {
|
|
23
|
+
observed: 'The declared upsert does not contain the required create, update, and records-output composition.',
|
|
24
|
+
consequence: 'The runtime cannot execute one deterministic create-or-update operation.',
|
|
25
|
+
aiFixable: true,
|
|
26
|
+
remediation: [
|
|
27
|
+
'Run aifabrix datasource capability upsert <file-or-key> --overwrite --dry-run, review the patch, then apply and validate it.'
|
|
28
|
+
],
|
|
29
|
+
prevention: 'Author upsert through the capability command and keep both source halves valid.',
|
|
30
|
+
residualRisk: 'Publication remains blocked while the composition is malformed.',
|
|
31
|
+
rerun: 'Rerun the same focused datasource validation or E2E case.'
|
|
32
|
+
},
|
|
33
|
+
'DP-CIP-031': {
|
|
34
|
+
observed: 'The upsert declares no usable business identity.',
|
|
35
|
+
consequence: 'Existence cannot be resolved, so a retry may create a duplicate or update the wrong record.',
|
|
36
|
+
aiFixable: true,
|
|
37
|
+
remediation: [
|
|
38
|
+
'Run aifabrix datasource capability upsert <file-or-key> --identity <fields> --dry-run with author-approved mapped non-primary fields.'
|
|
39
|
+
],
|
|
40
|
+
prevention: 'Model business identity before exposing a composed write.',
|
|
41
|
+
residualRisk: 'Upsert must remain unavailable until identity is explicit and validated.',
|
|
42
|
+
rerun: 'Rerun the same focused datasource validation or E2E case.'
|
|
43
|
+
},
|
|
44
|
+
'DP-CIP-040': {
|
|
45
|
+
observed: 'No declared read operation can constrain the complete business identity.',
|
|
46
|
+
consequence: 'The source record may be missed, creating a duplicate or selecting the wrong update target.',
|
|
47
|
+
aiFixable: false,
|
|
48
|
+
remediation: [
|
|
49
|
+
'Declare a real exact lookup only if the source API supports it.',
|
|
50
|
+
'Otherwise keep separate create and update capabilities and withhold upsert readiness.'
|
|
51
|
+
],
|
|
52
|
+
prevention: 'Require complete equality lookup coverage before authoring upsert.',
|
|
53
|
+
residualRisk: 'AI cannot repair a source API that lacks exact lookup or uniqueness.',
|
|
54
|
+
rerun: 'After a supported lookup is declared, rerun the same focused validation and E2E proof.'
|
|
55
|
+
},
|
|
56
|
+
'DP-CIP-041': {
|
|
57
|
+
observed: 'A covering identity read is declared, but live filtering has not been proven.',
|
|
58
|
+
consequence: 'Static coverage alone cannot show that the source applies the constraint or exposes writes in time.',
|
|
59
|
+
aiFixable: false,
|
|
60
|
+
remediation: ['Run the focused live validation and two-write no-duplicate E2E proof; do not add a duplicate read declaration.'],
|
|
61
|
+
prevention: 'Keep exact lookup and no-duplicate proof scenarios with every upsert datasource.',
|
|
62
|
+
residualRisk: 'Read-after-write delay can leave a bounded duplicate window until runtime protection is proven.',
|
|
63
|
+
rerun: 'Rerun the same focused datasource validation and upsert E2E case.'
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function normalizeOptionalString(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasGuidanceValue(value) {
|
|
72
|
+
if (Array.isArray(value)) return value.some(hasGuidanceValue);
|
|
73
|
+
if (typeof value === 'boolean') return true;
|
|
74
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
75
|
+
return normalizeOptionalString(value) !== '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function firstGuidanceValue(...values) {
|
|
79
|
+
return values.find(hasGuidanceValue);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function guidancePayload(item) {
|
|
83
|
+
return normalizeOptionalString(item.value) || normalizeOptionalString(item.template);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function guidanceVerb(step) {
|
|
87
|
+
return STEP_VERBS[step] || step.replace(/_/g, ' ') || 'update';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render one guidance entry, which may be a string or a structured fix instruction.
|
|
92
|
+
*
|
|
93
|
+
* `fix.instructions[]` entries are objects ({step, path, value, template, note}),
|
|
94
|
+
* so stringifying them printed `[object Object]` and the remediation — the part a
|
|
95
|
+
* developer acts on — was lost.
|
|
96
|
+
*
|
|
97
|
+
* @param {unknown} item guidance entry
|
|
98
|
+
* @returns {string|null} display text, or null when there is nothing to show
|
|
99
|
+
*/
|
|
100
|
+
function formatGuidanceItem(item) {
|
|
101
|
+
if (typeof item === 'string') return item.trim() || null;
|
|
102
|
+
if (!item || typeof item !== 'object') return null;
|
|
103
|
+
|
|
104
|
+
const step = normalizeOptionalString(item.step);
|
|
105
|
+
const note = normalizeOptionalString(item.note);
|
|
106
|
+
const target = normalizeOptionalString(item.path);
|
|
107
|
+
const payload = guidancePayload(item);
|
|
108
|
+
|
|
109
|
+
// `explain` carries no path or value; its note *is* the instruction.
|
|
110
|
+
if (step === 'explain' || (!target && !payload)) {
|
|
111
|
+
return note || (step ? step.replace(/_/g, ' ') : null);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const verb = guidanceVerb(step);
|
|
115
|
+
// With no path the payload *is* the target (e.g. locate_file carries a path in
|
|
116
|
+
// `value`), so it reads as "open <file>" rather than "open: <file>".
|
|
117
|
+
let text = target ? `${verb} ${target}` : `${verb} ${payload}`;
|
|
118
|
+
if (target && payload) text += step === 'set_value' ? ` = ${payload}` : `: ${payload}`;
|
|
119
|
+
return note ? `${text} — ${note}` : text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appendGuidanceValue(lines, label, value) {
|
|
123
|
+
const values = Array.isArray(value) ? value : [value];
|
|
124
|
+
values
|
|
125
|
+
.map(formatGuidanceItem)
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.forEach((text) => {
|
|
128
|
+
lines.push(` ${chalk.gray(`${label}:`)} ${chalk.white(text)}`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function enrichUpsertIssueGuidance(issue) {
|
|
133
|
+
if (!issue || typeof issue !== 'object') return issue;
|
|
134
|
+
const code = issue.stableCode || issue.code;
|
|
135
|
+
const fallback = UPSERT_GUIDANCE[code];
|
|
136
|
+
if (!fallback) return issue;
|
|
137
|
+
const enriched = { ...issue };
|
|
138
|
+
Object.entries(fallback).forEach(([key, value]) => {
|
|
139
|
+
if (!hasGuidanceValue(enriched[key])) enriched[key] = value;
|
|
140
|
+
});
|
|
141
|
+
return enriched;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendStructuredIssueGuidance(lines, issue) {
|
|
145
|
+
if (!issue || typeof issue !== 'object') return;
|
|
146
|
+
const display = enrichUpsertIssueGuidance(issue);
|
|
147
|
+
appendGuidanceValue(lines, 'Observed', firstGuidanceValue(
|
|
148
|
+
issue.observed, issue.details?.diagnosis, display.observed
|
|
149
|
+
));
|
|
150
|
+
appendGuidanceValue(lines, 'Consequence', firstGuidanceValue(
|
|
151
|
+
issue.impact, issue.consequence, issue.reason, display.consequence
|
|
152
|
+
));
|
|
153
|
+
if (typeof display.aiFixable === 'boolean') {
|
|
154
|
+
appendGuidanceValue(lines, 'Can AI fix?', display.aiFixable
|
|
155
|
+
? 'Yes — only as an authorized deterministic local manifest change.'
|
|
156
|
+
: 'No automatic fix is safe for this source or live-proof limitation.');
|
|
157
|
+
}
|
|
158
|
+
appendGuidanceValue(lines, 'Safe action', firstGuidanceValue(
|
|
159
|
+
issue.fix?.instructions, issue.remediation, issue.hint, display.remediation
|
|
160
|
+
));
|
|
161
|
+
appendGuidanceValue(lines, 'Prevention', firstGuidanceValue(
|
|
162
|
+
issue.prevention, issue.fix?.prevention, display.prevention
|
|
163
|
+
));
|
|
164
|
+
appendGuidanceValue(lines, 'Residual risk', firstGuidanceValue(
|
|
165
|
+
issue.residualRisk, issue.risk, display.residualRisk
|
|
166
|
+
));
|
|
167
|
+
appendGuidanceValue(lines, 'Rerun', firstGuidanceValue(
|
|
168
|
+
issue.rerun, issue.fix?.rerun, display.rerun
|
|
169
|
+
));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
appendStructuredIssueGuidance,
|
|
174
|
+
enrichUpsertIssueGuidance,
|
|
175
|
+
formatGuidanceItem
|
|
176
|
+
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
7
|
const chalk = require('chalk');
|
|
8
|
+
const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
|
|
8
9
|
const logger = require('./logger');
|
|
9
10
|
const { getReportVersionStderrMessage } = require('./datasource-test-run-report-version');
|
|
10
11
|
const {
|
|
@@ -42,6 +43,7 @@ function emitCapabilityScopeDiagnostics(envelope, opts = {}) {
|
|
|
42
43
|
* @param {string} [options.requestedCapabilityKey]
|
|
43
44
|
*/
|
|
44
45
|
function printDatasourceTestRunForTTY(envelope, options = {}) {
|
|
46
|
+
envelope = sanitizeBinaryEvidence(envelope);
|
|
45
47
|
const mode = resolveDebugDisplayMode(options.debug);
|
|
46
48
|
const displayOpts = {
|
|
47
49
|
focusCapabilityKey: options.requestedCapabilityKey,
|
|
@@ -96,8 +96,15 @@ function handleDockerClose(code, ctx) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs,
|
|
99
|
+
function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs, options = {}) {
|
|
100
|
+
const { noCache = false, target } = options;
|
|
100
101
|
const args = ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath];
|
|
102
|
+
// A multi-stage Dockerfile builds its last stage unless told otherwise. `build.target`
|
|
103
|
+
// lets one Dockerfile serve several images from the same source, which is the only way
|
|
104
|
+
// an app can ship two variants without duplicating its build.
|
|
105
|
+
if (target) {
|
|
106
|
+
args.push('--target', String(target));
|
|
107
|
+
}
|
|
101
108
|
for (const [key, value] of Object.entries(buildArgs)) {
|
|
102
109
|
if (value !== null && value !== undefined && String(value).length > 0) {
|
|
103
110
|
args.push('--build-arg', `${key}=${String(value)}`);
|
|
@@ -111,9 +118,12 @@ function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildAr
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
function createDockerSpawnParams(options) {
|
|
114
|
-
const { imageName, tag, dockerfilePath, contextPath, buildArgs, noCache, env } = options;
|
|
121
|
+
const { imageName, tag, dockerfilePath, contextPath, buildArgs, noCache, env, target } = options;
|
|
115
122
|
const spawnEnv = { ...process.env, ...(env || {}) };
|
|
116
|
-
const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs || {},
|
|
123
|
+
const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs || {}, {
|
|
124
|
+
noCache,
|
|
125
|
+
target
|
|
126
|
+
});
|
|
117
127
|
return { spawnEnv, args };
|
|
118
128
|
}
|
|
119
129
|
|
|
@@ -163,7 +173,8 @@ function runDockerBuildProcess(buildOpts) {
|
|
|
163
173
|
reject,
|
|
164
174
|
env = {},
|
|
165
175
|
buildArgs = {},
|
|
166
|
-
noCache = false
|
|
176
|
+
noCache = false,
|
|
177
|
+
target
|
|
167
178
|
} = buildOpts;
|
|
168
179
|
const { spawnEnv, args } = createDockerSpawnParams({
|
|
169
180
|
imageName,
|
|
@@ -172,7 +183,8 @@ function runDockerBuildProcess(buildOpts) {
|
|
|
172
183
|
contextPath,
|
|
173
184
|
buildArgs,
|
|
174
185
|
noCache,
|
|
175
|
-
env
|
|
186
|
+
env,
|
|
187
|
+
target
|
|
176
188
|
});
|
|
177
189
|
const dockerProcess = spawn('docker', args, {
|
|
178
190
|
shell: process.platform === 'win32',
|
|
@@ -192,7 +204,12 @@ function runDockerBuildProcess(buildOpts) {
|
|
|
192
204
|
* @returns {Promise<void>} Resolves when build completes
|
|
193
205
|
* @throws {Error} If build fails
|
|
194
206
|
*/
|
|
195
|
-
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {},
|
|
207
|
+
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {}, options = false) {
|
|
208
|
+
// Historically the sixth argument was the `noCache` boolean. It is now an options
|
|
209
|
+
// object so a build stage can be named too; the boolean form still works, because
|
|
210
|
+
// this function is exported and callers outside this module pass it that way.
|
|
211
|
+
const { noCache = false, target } =
|
|
212
|
+
typeof options === 'boolean' ? { noCache: options } : options || {};
|
|
196
213
|
const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
|
|
197
214
|
const fsSync = require('fs');
|
|
198
215
|
const path = require('path');
|
|
@@ -228,7 +245,8 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
|
|
|
228
245
|
reject,
|
|
229
246
|
env: dockerCliEnv,
|
|
230
247
|
buildArgs: resolvedBuildArgs,
|
|
231
|
-
noCache
|
|
248
|
+
noCache,
|
|
249
|
+
target
|
|
232
250
|
});
|
|
233
251
|
});
|
|
234
252
|
}
|
|
@@ -251,7 +269,10 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
|
|
|
251
269
|
options &&
|
|
252
270
|
(options.cache === false || options.noCache === true || options['no-cache'] === true)
|
|
253
271
|
);
|
|
254
|
-
await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs,
|
|
272
|
+
await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs, {
|
|
273
|
+
noCache,
|
|
274
|
+
target: options && options.target
|
|
275
|
+
});
|
|
255
276
|
await applyTagAliases(imageName, tag, resolveTagAliases(tag, options));
|
|
256
277
|
}
|
|
257
278
|
|
|
@@ -81,6 +81,41 @@ async function mergeDockerManifestPublishedPort(envVars, appDoc) {
|
|
|
81
81
|
envVars[publicPortKey] = String(devIdNum > 0 ? pubBase + devIdNum * 100 : pubBase);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Publish `<NAME>_PUBLIC_PORT` for every `additionalPorts` entry.
|
|
86
|
+
*
|
|
87
|
+
* An application serving a second protocol needs to tell clients which port to use, and
|
|
88
|
+
* that is the *published* host port, not the one it listens on inside the container. The
|
|
89
|
+
* two differ by the developer-id offset, so a template that types the number by hand
|
|
90
|
+
* silently disagrees with the manifest the moment either changes — and the application
|
|
91
|
+
* then advertises a port nothing is listening on.
|
|
92
|
+
*
|
|
93
|
+
* Named after the port, so `additionalPorts: [{ name: ssh, hostPort: 2200 }]` gives
|
|
94
|
+
* `SSH_PUBLIC_PORT`, which an env.template consumes as `${SSH_PUBLIC_PORT}`.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} envVars - Mutated map from buildEnvVarMap
|
|
97
|
+
* @param {Object|null|undefined} appDoc - application.yaml root
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
async function mergeAdditionalPublishedPorts(envVars, appDoc) {
|
|
101
|
+
const entries = appDoc && Array.isArray(appDoc.additionalPorts) ? appDoc.additionalPorts : [];
|
|
102
|
+
if (entries.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const devIdNum = await require('./env-map').getDeveloperIdNumber(null);
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (!entry || typeof entry.name !== 'string') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const base = Number(entry.hostPort);
|
|
111
|
+
if (!Number.isFinite(base)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = `${entry.name.trim().toUpperCase().replace(/-/g, '_')}_PUBLIC_PORT`;
|
|
115
|
+
envVars[key] = String(devIdNum > 0 ? base + devIdNum * 100 : base);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/**
|
|
85
120
|
* Rewrite first line for the matched *_PUBLIC_PORT after interpolateEnvVars (early kv pass may have wrong value).
|
|
86
121
|
* @param {string} resolved
|
|
@@ -111,6 +146,7 @@ function rewriteDockerManifestPublicPortEnvLine(resolved, envVars, appDoc) {
|
|
|
111
146
|
module.exports = {
|
|
112
147
|
findDockerHostKeyForAppKey,
|
|
113
148
|
publicPortKeyForAppKey,
|
|
149
|
+
mergeAdditionalPublishedPorts,
|
|
114
150
|
mergeDockerManifestPublishedPort,
|
|
115
151
|
rewriteDockerManifestPublicPortEnvLine
|
|
116
152
|
};
|