@aifabrix/builder 2.56.0 → 2.58.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/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +49 -2
- package/lib/cli/setup-environment.js +89 -19
- package/lib/commands/agent-kit-help.js +32 -15
- package/lib/commands/agent-kit.js +90 -8
- package/lib/commands/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -2
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/core/config-attach-extensions.js +4 -1
- package/lib/deployment/installation/azure-infra-stage.js +191 -0
- package/lib/deployment/installation/azure-preflight.js +147 -0
- package/lib/deployment/installation/azure-readiness.js +101 -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 +211 -0
- package/lib/deployment/installation/infra-manifest.js +176 -0
- package/lib/deployment/installation/infra-projection.js +97 -0
- package/lib/deployment/installation/public-ip.js +65 -0
- package/lib/deployment/installation/registry-auth-mode.js +98 -0
- package/lib/deployment/installation/stage-input.js +184 -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/generator/builders.js +12 -9
- package/lib/generator/helpers.js +55 -56
- package/lib/generator/split-variables.js +11 -5
- package/lib/internal/fs-real-sync.js +29 -0
- 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 +26 -36
- package/lib/schema/infrastructure-schema.json +1531 -397
- package/lib/schema/installation-environment.schema.json +601 -0
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-registry-preference.js +106 -0
- package/lib/utils/docker-build.js +53 -19
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/variable-transformer.js +5 -4
- package/lib/validation/required-variables.js +41 -0
- package/lib/validation/validator.js +8 -5
- package/package.json +6 -1
- 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/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +3 -2
- package/templates/applications/miso-controller/env.template +15 -6
- package/templates/marketplace/createUiDefinition.json +77 -0
- package/templates/marketplace/install.parameters.example.json +9 -0
- package/templates/marketplace/main.json +4087 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Installation workspace filesystem helpers: `infrastructure/<resource-group>/`
|
|
5
|
+
* resolution, deterministic atomic JSON writes, hash-guarded generated files and ignore checks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
|
|
9
|
+
const fs = require('../../internal/fs-real-sync');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const WORKSPACE_PARENT = 'infrastructure';
|
|
15
|
+
const FILES = Object.freeze({
|
|
16
|
+
template: 'azure-main.json',
|
|
17
|
+
infra: 'environment-infra.json',
|
|
18
|
+
envTemplate: 'env.template',
|
|
19
|
+
env: '.env'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} envKey - dev, tst or pro
|
|
24
|
+
* @returns {string} Environment file name
|
|
25
|
+
*/
|
|
26
|
+
function environmentFileName(envKey) {
|
|
27
|
+
return `environment-${envKey}.json`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha256(content) {
|
|
31
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runGit(args, cwd) {
|
|
35
|
+
return spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builder workspace root: the Git top-level of the current directory, else the current directory.
|
|
40
|
+
* @param {string} [cwd] - Starting directory
|
|
41
|
+
* @returns {string} Absolute root path
|
|
42
|
+
*/
|
|
43
|
+
function resolveWorkspaceRoot(cwd = process.cwd()) {
|
|
44
|
+
const result = runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
45
|
+
if (!result.error && result.status === 0 && result.stdout.trim()) {
|
|
46
|
+
return path.resolve(result.stdout.trim());
|
|
47
|
+
}
|
|
48
|
+
return path.resolve(cwd);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves `infrastructure/<resource-group>/` and proves it stays inside the workspace root.
|
|
53
|
+
* @param {string} root - Workspace root
|
|
54
|
+
* @param {string} resourceGroup - Validated resource-group key
|
|
55
|
+
* @returns {string} Absolute workspace directory
|
|
56
|
+
*/
|
|
57
|
+
function resolveWorkspaceDir(root, resourceGroup) {
|
|
58
|
+
const parent = path.join(root, WORKSPACE_PARENT);
|
|
59
|
+
const dir = path.join(parent, resourceGroup);
|
|
60
|
+
const relative = path.relative(parent, dir);
|
|
61
|
+
if (relative !== resourceGroup || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
62
|
+
throw new Error('Resolved installation workspace escapes the infrastructure directory');
|
|
63
|
+
}
|
|
64
|
+
for (const candidate of [parent, dir]) {
|
|
65
|
+
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
|
|
66
|
+
throw new Error(`Installation workspace path must not be a symbolic link: ${candidate}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return dir;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deterministic JSON text (two-space indentation, trailing newline).
|
|
74
|
+
* @param {Object} document - JSON document
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function toJsonText(document) {
|
|
78
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} file - JSON file path
|
|
83
|
+
* @returns {Object|null} Parsed document or null when absent
|
|
84
|
+
*/
|
|
85
|
+
function readJsonIfExists(file) {
|
|
86
|
+
if (!fs.existsSync(file)) return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
89
|
+
} catch (_) {
|
|
90
|
+
throw new Error(`Workspace file is not valid JSON: ${file}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Writes content atomically through an owner-only temporary file in the same directory.
|
|
96
|
+
* The temporary file is removed on every failure path.
|
|
97
|
+
* @param {string} file - Target path
|
|
98
|
+
* @param {string} content - Content
|
|
99
|
+
* @param {number} mode - Final file mode
|
|
100
|
+
*/
|
|
101
|
+
function writeFileAtomic(file, content, mode) {
|
|
102
|
+
const dir = path.dirname(file);
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
104
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
|
|
105
|
+
try {
|
|
106
|
+
fs.writeFileSync(tmp, content, { mode: 0o600, flag: 'wx' });
|
|
107
|
+
fs.chmodSync(tmp, mode);
|
|
108
|
+
fs.renameSync(tmp, file);
|
|
109
|
+
} finally {
|
|
110
|
+
if (fs.existsSync(tmp)) fs.rmSync(tmp, { force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} file - Target path
|
|
116
|
+
* @param {Object} document - JSON document
|
|
117
|
+
*/
|
|
118
|
+
function writeJsonAtomic(file, document) {
|
|
119
|
+
writeFileAtomic(file, toJsonText(document), 0o644);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Writes a committed generated file only when it is absent, unchanged since generation, or
|
|
124
|
+
* already identical. Returns the new SHA-256 hash.
|
|
125
|
+
* @param {string} file - Target path
|
|
126
|
+
* @param {string} content - Generated content
|
|
127
|
+
* @param {string|undefined} recordedHash - Hash recorded at the previous generation
|
|
128
|
+
* @returns {string} Hash of the written content
|
|
129
|
+
* @throws {Error} When a user-modified generated file would be replaced
|
|
130
|
+
*/
|
|
131
|
+
function writeGeneratedFile(file, content, recordedHash) {
|
|
132
|
+
const nextHash = sha256(content);
|
|
133
|
+
if (fs.existsSync(file)) {
|
|
134
|
+
const currentHash = sha256(fs.readFileSync(file));
|
|
135
|
+
if (currentHash === nextHash) return nextHash;
|
|
136
|
+
if (currentHash !== recordedHash) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Generated workspace file ${path.basename(file)} was modified after generation. ` +
|
|
139
|
+
`Review it with "git diff -- ${file}", restore it (git checkout -- <file>) or delete it, then rerun.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
writeFileAtomic(file, content, 0o644);
|
|
144
|
+
return nextHash;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Refuses to write a secret file that Git could track.
|
|
149
|
+
* @param {string} file - Secret file path
|
|
150
|
+
* @param {string} root - Workspace root
|
|
151
|
+
* @throws {Error} When the file is inside a Git work tree and not ignored, or already tracked
|
|
152
|
+
*/
|
|
153
|
+
function assertIgnoredSecretFile(file, root) {
|
|
154
|
+
const inside = runGit(['rev-parse', '--is-inside-work-tree'], root);
|
|
155
|
+
if (inside.error || inside.status !== 0) return;
|
|
156
|
+
const relative = path.relative(root, file);
|
|
157
|
+
if (runGit(['ls-files', '--error-unmatch', '--', relative], root).status === 0) {
|
|
158
|
+
throw new Error(`${relative} is tracked by Git; remove it from the index (git rm --cached) before continuing`);
|
|
159
|
+
}
|
|
160
|
+
if (runGit(['check-ignore', '-q', '--', relative], root).status !== 0) {
|
|
161
|
+
throw new Error(`${relative} is not ignored by Git; add ".env" to .gitignore before Builder writes secrets there`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
FILES,
|
|
167
|
+
WORKSPACE_PARENT,
|
|
168
|
+
environmentFileName,
|
|
169
|
+
sha256,
|
|
170
|
+
resolveWorkspaceRoot,
|
|
171
|
+
resolveWorkspaceDir,
|
|
172
|
+
toJsonText,
|
|
173
|
+
readJsonIfExists,
|
|
174
|
+
writeFileAtomic,
|
|
175
|
+
writeJsonAtomic,
|
|
176
|
+
writeGeneratedFile,
|
|
177
|
+
assertIgnoredSecretFile
|
|
178
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Validates installation workspace manifests against their Builder-owned schemas
|
|
5
|
+
* before they are written, so unknown or secret-bearing shapes never reach version control.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Ajv = require('ajv');
|
|
9
|
+
const addFormats = require('ajv-formats');
|
|
10
|
+
const { formatValidationErrors } = require('../../utils/error-formatter');
|
|
11
|
+
|
|
12
|
+
let validators = null;
|
|
13
|
+
|
|
14
|
+
function getValidators() {
|
|
15
|
+
if (!validators) {
|
|
16
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
17
|
+
addFormats(ajv);
|
|
18
|
+
validators = {
|
|
19
|
+
infra: ajv.compile(require('../../schema/infrastructure-schema.json')),
|
|
20
|
+
environment: ajv.compile(require('../../schema/installation-environment.schema.json'))
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return validators;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {'infra'|'environment'} kind - Manifest kind
|
|
28
|
+
* @param {Object} document - Manifest document
|
|
29
|
+
* @param {string} fileName - File name for messages
|
|
30
|
+
* @throws {Error} When the document does not match its schema
|
|
31
|
+
*/
|
|
32
|
+
function assertWorkspaceDocument(kind, document, fileName) {
|
|
33
|
+
const validate = getValidators()[kind];
|
|
34
|
+
if (!validate(document)) {
|
|
35
|
+
const messages = formatValidationErrors(validate.errors);
|
|
36
|
+
throw new Error(`${fileName} does not match its schema:\n - ${messages.join('\n - ')}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { assertWorkspaceDocument };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Resolves the workspace `env.template` contract through the configured Builder
|
|
5
|
+
* secret system and materializes values into the owner-only workspace `.env`.
|
|
6
|
+
* Automated writes target only the primary user secrets file, never the shared store.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { SECRET_BINDINGS, ENV_TEMPLATE_KEYS } = require('./infra-catalog');
|
|
10
|
+
const { mergeWorkspaceEnv } = require('./workspace-env');
|
|
11
|
+
|
|
12
|
+
function secretSetHint(binding) {
|
|
13
|
+
return `aifabrix secret set ${binding.kv} <value>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defaultSecretsApi() {
|
|
17
|
+
const { loadSecrets } = require('../../core/secrets');
|
|
18
|
+
const { ensureSecretsForKeys } = require('../../core/secrets-ensure');
|
|
19
|
+
return { loadSecrets, ensureSecretsForKeys };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function asValue(raw) {
|
|
23
|
+
if (raw === undefined || raw === null) return '';
|
|
24
|
+
return String(raw);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves and materializes the vendor's secret contract into `.env`.
|
|
29
|
+
* @param {Object} params - Inputs
|
|
30
|
+
* @param {string} params.vendor - AZURE or DOCKER
|
|
31
|
+
* @param {string} params.envPath - Workspace .env path
|
|
32
|
+
* @param {string} params.root - Workspace root
|
|
33
|
+
* @param {string[]} [params.names] - Subset of contract names to resolve (defaults to vendor contract)
|
|
34
|
+
* @param {Object} [params.secretsApi] - Injected secret-system functions (tests)
|
|
35
|
+
* @returns {Promise<Object.<string,string>>} Resolved values (never logged)
|
|
36
|
+
* @throws {Error} When a required secret is missing; message names only variable and kv key
|
|
37
|
+
*/
|
|
38
|
+
async function materializeWorkspaceSecrets({ vendor, envPath, root, names, secretsApi }) {
|
|
39
|
+
const api = secretsApi || defaultSecretsApi();
|
|
40
|
+
const selected = names || ENV_TEMPLATE_KEYS[vendor];
|
|
41
|
+
const generated = selected.filter(name => SECRET_BINDINGS[name].generate).map(name => SECRET_BINDINGS[name].kv);
|
|
42
|
+
if (generated.length > 0) {
|
|
43
|
+
await api.ensureSecretsForKeys(generated);
|
|
44
|
+
}
|
|
45
|
+
const secrets = (await api.loadSecrets()) || {};
|
|
46
|
+
const resolved = {};
|
|
47
|
+
const missing = [];
|
|
48
|
+
for (const name of selected) {
|
|
49
|
+
const binding = SECRET_BINDINGS[name];
|
|
50
|
+
const value = asValue(secrets[binding.kv]);
|
|
51
|
+
if (value.startsWith('kv://')) {
|
|
52
|
+
missing.push(`${name} (kv://${binding.kv}): unresolved reference; run ${secretSetHint(binding)}`);
|
|
53
|
+
} else if (value) {
|
|
54
|
+
resolved[name] = value;
|
|
55
|
+
} else if (binding.required) {
|
|
56
|
+
missing.push(`${name} (kv://${binding.kv}): run ${secretSetHint(binding)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
throw new Error(`Required installation secrets are not set:\n - ${missing.join('\n - ')}`);
|
|
61
|
+
}
|
|
62
|
+
mergeWorkspaceEnv(envPath, resolved, { root });
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { materializeWorkspaceSecrets };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Loads and generates the `infrastructure/<resource-group>/` installation workspace:
|
|
5
|
+
* vendor template snapshot, `environment-infra.json`, per-environment files and `env.template`,
|
|
6
|
+
* with recorded provenance hashes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
|
|
10
|
+
const fs = require('../../internal/fs-real-sync');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const files = require('./workspace-files');
|
|
13
|
+
const { buildEnvTemplate, assertTemplateCoverage } = require('./infra-catalog');
|
|
14
|
+
const { buildInfraDocument } = require('./infra-manifest');
|
|
15
|
+
const { buildEnvironmentDocument } = require('./environment-manifest');
|
|
16
|
+
const { ENVIRONMENT_STAGES } = require('./stage-input');
|
|
17
|
+
const { assertWorkspaceDocument } = require('./workspace-schema');
|
|
18
|
+
|
|
19
|
+
const SHIPPED_TEMPLATE = path.resolve(__dirname, '../../../templates/marketplace/main.json');
|
|
20
|
+
|
|
21
|
+
function builderVersion() {
|
|
22
|
+
return require('../../../package.json').version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves workspace paths and reads the persisted infra manifest (if any).
|
|
27
|
+
* @param {string} resourceGroup - Validated resource-group key
|
|
28
|
+
* @param {Object} [context] - Optional overrides
|
|
29
|
+
* @param {string} [context.root] - Workspace root
|
|
30
|
+
* @returns {{ root: string, dir: string, paths: Object, infra: Object|null }}
|
|
31
|
+
*/
|
|
32
|
+
function openWorkspace(resourceGroup, context = {}) {
|
|
33
|
+
const root = context.root || files.resolveWorkspaceRoot();
|
|
34
|
+
const dir = files.resolveWorkspaceDir(root, resourceGroup);
|
|
35
|
+
const paths = {
|
|
36
|
+
template: path.join(dir, files.FILES.template),
|
|
37
|
+
infra: path.join(dir, files.FILES.infra),
|
|
38
|
+
envTemplate: path.join(dir, files.FILES.envTemplate),
|
|
39
|
+
env: path.join(dir, files.FILES.env),
|
|
40
|
+
environment: envKey => path.join(dir, files.environmentFileName(envKey))
|
|
41
|
+
};
|
|
42
|
+
const infra = files.readJsonIfExists(paths.infra);
|
|
43
|
+
if (infra && infra.resourceGroup?.value !== resourceGroup) {
|
|
44
|
+
throw new Error(`Workspace identity mismatch: ${paths.infra} records resource group "${infra.resourceGroup?.value}"`);
|
|
45
|
+
}
|
|
46
|
+
return { root, dir, paths, infra };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadShippedTemplate(templatePath = SHIPPED_TEMPLATE) {
|
|
50
|
+
const text = fs.readFileSync(templatePath, 'utf8');
|
|
51
|
+
const template = JSON.parse(text);
|
|
52
|
+
assertTemplateCoverage(template);
|
|
53
|
+
return { text, template, hash: files.sha256(text) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeGenerated(workspace, doc, name, content) {
|
|
57
|
+
const recorded = doc.generated.files?.[name];
|
|
58
|
+
const hash = files.writeGeneratedFile(path.join(workspace.dir, name), content, recorded);
|
|
59
|
+
if (hash !== recorded) {
|
|
60
|
+
doc.generated.files = { ...(doc.generated.files || {}), [name]: hash };
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureEnvironmentFiles(workspace) {
|
|
67
|
+
for (const envKey of ENVIRONMENT_STAGES) {
|
|
68
|
+
const file = workspace.paths.environment(envKey);
|
|
69
|
+
if (!fs.existsSync(file)) saveEnvironment(workspace, envKey, buildEnvironmentDocument({ envKey, existing: null }));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates or refreshes the workspace for a vendor and persists `environment-infra.json`.
|
|
75
|
+
* @param {Object} workspace - Result of openWorkspace
|
|
76
|
+
* @param {{ vendor: string, resourceGroup: string }} input - Validated stage input
|
|
77
|
+
* @param {Object} [context] - Optional overrides
|
|
78
|
+
* @param {string} [context.templatePath] - Vendor template source (tests)
|
|
79
|
+
* @param {Function} [context.now] - Clock
|
|
80
|
+
* @returns {{ doc: Object, template: Object, templateHash: string }}
|
|
81
|
+
*/
|
|
82
|
+
function materializeWorkspace(workspace, input, context = {}) {
|
|
83
|
+
const shipped = loadShippedTemplate(context.templatePath);
|
|
84
|
+
const doc = buildInfraDocument({
|
|
85
|
+
vendor: input.vendor, resourceGroup: input.resourceGroup, template: shipped.template, existing: workspace.infra
|
|
86
|
+
});
|
|
87
|
+
let changed = !workspace.infra;
|
|
88
|
+
if (input.vendor === 'AZURE') {
|
|
89
|
+
changed = writeGenerated(workspace, doc, files.FILES.template, shipped.text) || changed;
|
|
90
|
+
ensureEnvironmentFiles(workspace);
|
|
91
|
+
}
|
|
92
|
+
changed = writeGenerated(workspace, doc, files.FILES.envTemplate, buildEnvTemplate(input.vendor)) || changed;
|
|
93
|
+
if (changed) {
|
|
94
|
+
doc.generated = {
|
|
95
|
+
...doc.generated,
|
|
96
|
+
builderVersion: builderVersion(),
|
|
97
|
+
templateContentVersion: shipped.template.contentVersion || null,
|
|
98
|
+
templateSha256: shipped.hash,
|
|
99
|
+
generatedAt: (context.now ? context.now() : new Date()).toISOString()
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
saveInfra(workspace, doc);
|
|
103
|
+
return { doc, template: shipped.template, templateHash: shipped.hash };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Persists the infra manifest.
|
|
108
|
+
* @param {Object} workspace - Workspace
|
|
109
|
+
* @param {Object} doc - Manifest
|
|
110
|
+
*/
|
|
111
|
+
function saveInfra(workspace, doc) {
|
|
112
|
+
assertWorkspaceDocument('infra', doc, files.FILES.infra);
|
|
113
|
+
files.writeJsonAtomic(workspace.paths.infra, doc);
|
|
114
|
+
workspace.infra = doc;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Persists an environment file after schema validation.
|
|
119
|
+
* @param {Object} workspace - Workspace
|
|
120
|
+
* @param {string} envKey - dev, tst or pro
|
|
121
|
+
* @param {Object} doc - Environment document
|
|
122
|
+
*/
|
|
123
|
+
function saveEnvironment(workspace, envKey, doc) {
|
|
124
|
+
assertWorkspaceDocument('environment', doc, files.environmentFileName(envKey));
|
|
125
|
+
files.writeJsonAtomic(workspace.paths.environment(envKey), doc);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { SHIPPED_TEMPLATE, openWorkspace, materializeWorkspace, saveInfra, saveEnvironment, loadShippedTemplate };
|