hereya-cli 0.85.3 → 0.85.5
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 +78 -67
- package/dist/backend/cloud/cloud-backend.js +3 -0
- package/dist/backend/common.d.ts +1 -0
- package/dist/commands/app/deploy/index.d.ts +4 -0
- package/dist/commands/app/deploy/index.js +211 -6
- package/dist/commands/app/destroy/index.d.ts +5 -0
- package/dist/commands/app/destroy/index.js +152 -3
- package/dist/commands/executor/start/index.d.ts +2 -14
- package/dist/commands/executor/start/index.js +87 -307
- package/dist/executor/interface.d.ts +1 -0
- package/dist/executor/local.js +6 -2
- package/dist/infrastructure/index.d.ts +3 -1
- package/dist/infrastructure/index.js +22 -15
- package/dist/lib/app-parameters.d.ts +23 -0
- package/dist/lib/app-parameters.js +53 -0
- package/oclif.manifest.json +101 -64
- package/package.json +1 -1
|
@@ -3,78 +3,22 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import * as yaml from 'yaml';
|
|
7
6
|
import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
|
|
8
7
|
import { loginWithToken } from '../../../backend/cloud/login.js';
|
|
9
8
|
import { saveCloudCredentials } from '../../../backend/config.js';
|
|
10
9
|
import { clearBackend, getBackend } from '../../../backend/index.js';
|
|
11
10
|
import { LocalExecutor } from '../../../executor/local.js';
|
|
12
|
-
import { destroyPackage, provisionPackage } from '../../../infrastructure/index.js';
|
|
13
|
-
import { resolveAppParameters, substituteAppParameters } from '../../../lib/app-parameters.js';
|
|
14
11
|
import * as appSourceLib from '../../../lib/app-source.js';
|
|
15
12
|
import { SimpleConfigManager } from '../../../lib/config/simple.js';
|
|
16
13
|
import { cloneWithCredentialHelper, gitUtils } from '../../../lib/git-utils.js';
|
|
17
14
|
import { runShell, shellUtils } from '../../../lib/shell.js';
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
16
|
+
* Workspaces look like `org/name`, which is not filesystem-safe. The
|
|
17
|
+
* write-side in `app deploy --local` (commands/app/deploy/index.ts) MUST use
|
|
18
|
+
* the same sanitization.
|
|
22
19
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
provisionPackage,
|
|
26
|
-
};
|
|
27
|
-
async function getAnyExisting(...candidates) {
|
|
28
|
-
for (const candidate of candidates) {
|
|
29
|
-
try {
|
|
30
|
-
// eslint-disable-next-line no-await-in-loop
|
|
31
|
-
await fs.access(candidate);
|
|
32
|
-
return candidate;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// try next
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
function normalizePackageList(raw) {
|
|
41
|
-
if (!raw)
|
|
42
|
-
return [];
|
|
43
|
-
if (Array.isArray(raw)) {
|
|
44
|
-
return raw
|
|
45
|
-
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
46
|
-
.map((entry) => {
|
|
47
|
-
const [name, version] = entry.split('@');
|
|
48
|
-
return {
|
|
49
|
-
name,
|
|
50
|
-
spec: entry,
|
|
51
|
-
version,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (typeof raw === 'object') {
|
|
56
|
-
return Object.entries(raw).map(([name, info]) => {
|
|
57
|
-
const version = info?.version;
|
|
58
|
-
return {
|
|
59
|
-
name,
|
|
60
|
-
spec: version ? `${name}@${version}` : name,
|
|
61
|
-
version,
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
async function runPreDeployCommand(input) {
|
|
68
|
-
if (!input.command)
|
|
69
|
-
return { success: true };
|
|
70
|
-
try {
|
|
71
|
-
input.logger.info('Running pre-deploy command...');
|
|
72
|
-
await runShell(input.command, [], { directory: input.directory, logger: input.logger });
|
|
73
|
-
return { success: true };
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
return { reason: `pre-deploy command failed: ${error.message}`, success: false };
|
|
77
|
-
}
|
|
20
|
+
function sanitizeWorkspaceForFilename(workspace) {
|
|
21
|
+
return workspace.replaceAll('/', '-');
|
|
78
22
|
}
|
|
79
23
|
/**
|
|
80
24
|
* Strip credentials from log strings before flushing them to the cloud.
|
|
@@ -221,12 +165,14 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
221
165
|
}
|
|
222
166
|
this.log('Executor stopped.');
|
|
223
167
|
}
|
|
224
|
-
async
|
|
168
|
+
async executeAppJob(job, cloudBackend, logger) {
|
|
169
|
+
const isDeploy = job.type === 'app-deploy';
|
|
170
|
+
const subcommand = isDeploy ? 'deploy' : 'destroy';
|
|
225
171
|
const payload = job.payload;
|
|
226
172
|
const { appName, commit, parameters: providedParameters, repository, sha256, version, workspace } = payload;
|
|
227
173
|
if (!appName || !workspace || !repository) {
|
|
228
174
|
return {
|
|
229
|
-
reason:
|
|
175
|
+
reason: `Missing appName, workspace, or repository in ${job.type} job payload`,
|
|
230
176
|
success: false,
|
|
231
177
|
};
|
|
232
178
|
}
|
|
@@ -236,110 +182,53 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
236
182
|
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
237
183
|
cleanup = downloadResult.cleanup;
|
|
238
184
|
const { rootDir } = downloadResult;
|
|
239
|
-
//
|
|
240
|
-
const paramsResult = await this.resolveAndSubstituteParams({
|
|
241
|
-
logger,
|
|
242
|
-
provided: providedParameters,
|
|
243
|
-
rootDir,
|
|
244
|
-
});
|
|
245
|
-
if (!paramsResult.success) {
|
|
246
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: paramsResult.reason, workspace }, cloudBackend);
|
|
247
|
-
return { reason: paramsResult.reason, success: false };
|
|
248
|
-
}
|
|
249
|
-
const { parameters: resolvedParameters } = paramsResult;
|
|
250
|
-
// Read hereya.yaml from app source
|
|
251
|
-
const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
|
|
252
|
-
if (!hereyaYamlPath) {
|
|
253
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: 'Missing hereya.yaml', workspace }, cloudBackend);
|
|
254
|
-
return { reason: 'App source does not contain a hereya.yaml file', success: false };
|
|
255
|
-
}
|
|
256
|
-
const hereyaYamlContent = await fs.readFile(hereyaYamlPath, 'utf8');
|
|
257
|
-
const parsedHereyaYaml = (yaml.parse(hereyaYamlContent) ?? {});
|
|
258
|
-
// Build package lists
|
|
259
|
-
const packages = normalizePackageList(parsedHereyaYaml.packages);
|
|
260
|
-
const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
|
|
261
|
-
// Resolve workspace env (used as projectEnv for deploy-marked packages)
|
|
262
|
-
const workspaceEnvResult = await cloudBackend.getWorkspaceEnv({ workspace });
|
|
263
|
-
const workspaceEnv = workspaceEnvResult.success ? workspaceEnvResult.env : {};
|
|
264
|
-
// Mark deployment as deploying
|
|
185
|
+
// Mark deployment as deploying / destroying.
|
|
265
186
|
await cloudBackend.updateAppDeployment({
|
|
266
187
|
lastJobId: job.id,
|
|
267
188
|
name: appName,
|
|
268
|
-
status: 'deploying',
|
|
189
|
+
status: isDeploy ? 'deploying' : 'destroying',
|
|
269
190
|
workspace,
|
|
270
191
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// depend on workspace-level outputs (clusterArn, masterSecretArn, ...) can
|
|
289
|
-
// resolve them. Mirrors the env-flow that `hereya add` produces in the
|
|
290
|
-
// project lifecycle.
|
|
291
|
-
/* eslint-disable no-await-in-loop */
|
|
292
|
-
for (const pkg of packages) {
|
|
293
|
-
const result = await appPackageOps.provisionPackage({
|
|
294
|
-
env: { ...workspaceEnv, ...mergedEnv },
|
|
295
|
-
isDeploying: true,
|
|
296
|
-
logger,
|
|
297
|
-
package: pkg.spec,
|
|
298
|
-
parameters: resolvedParameters,
|
|
299
|
-
projectRootDir: rootDir,
|
|
300
|
-
workspace,
|
|
301
|
-
});
|
|
302
|
-
if (!result.success) {
|
|
303
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
|
|
304
|
-
return { reason: `Failed to provision package ${pkg.name}: ${result.reason}`, success: false };
|
|
305
|
-
}
|
|
306
|
-
Object.assign(mergedEnv, result.env);
|
|
307
|
-
}
|
|
308
|
-
// 2) Provision deploy packages — these get workspace env merged in
|
|
309
|
-
for (const pkg of deployPackages) {
|
|
310
|
-
const result = await appPackageOps.provisionPackage({
|
|
311
|
-
env: { ...workspaceEnv, ...mergedEnv },
|
|
312
|
-
isDeploying: true,
|
|
192
|
+
// Build -p flag args from job payload parameters.
|
|
193
|
+
const parameters = providedParameters ?? {};
|
|
194
|
+
const paramFlags = [];
|
|
195
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
196
|
+
paramFlags.push('-p', `${key}=${value}`);
|
|
197
|
+
}
|
|
198
|
+
// For app-deploy, forward --version so the local command can serialize it into
|
|
199
|
+
// the deploy-env file's `state` block (which the cloud reads back via PATCH).
|
|
200
|
+
const versionFlags = isDeploy && version ? ['--version', version] : [];
|
|
201
|
+
// Shell out to `hereya app <subcommand> <appName> -w <workspace> [-p ...] [--version ...] --local`.
|
|
202
|
+
// The local command holds the provisioning logic (mirrors deploy/undeploy shell-out pattern).
|
|
203
|
+
logger.info(`Running hereya app ${subcommand} ${appName} -w ${workspace} --local...`);
|
|
204
|
+
let runResult;
|
|
205
|
+
try {
|
|
206
|
+
runResult = await shellUtils.runShell('hereya', ['app', subcommand, appName, '-w', workspace, ...paramFlags, ...versionFlags, '--local'], {
|
|
207
|
+
directory: rootDir,
|
|
208
|
+
env: { ...process.env, HEREYA_LOCAL_EXECUTION: 'true', HEREYA_PROJECT_ROOT_DIR: rootDir },
|
|
313
209
|
logger,
|
|
314
|
-
package: pkg.spec,
|
|
315
|
-
parameters: resolvedParameters,
|
|
316
|
-
projectEnv: { ...workspaceEnv, ...mergedEnv },
|
|
317
|
-
projectRootDir: rootDir,
|
|
318
|
-
workspace,
|
|
319
210
|
});
|
|
320
|
-
if (!result.success) {
|
|
321
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
|
|
322
|
-
return { reason: `Failed to provision deploy package ${pkg.name}: ${result.reason}`, success: false };
|
|
323
|
-
}
|
|
324
|
-
Object.assign(mergedEnv, result.env);
|
|
325
|
-
Object.assign(deployEnv, result.env);
|
|
326
211
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
212
|
+
catch (error) {
|
|
213
|
+
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: error.message, workspace }, cloudBackend);
|
|
214
|
+
return { reason: `hereya app ${subcommand} failed: ${error.message}`, success: false };
|
|
215
|
+
}
|
|
216
|
+
if (runResult && runResult.status !== 0) {
|
|
217
|
+
const reason = `hereya app ${subcommand} exited with code ${runResult.status}`;
|
|
218
|
+
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason, workspace }, cloudBackend);
|
|
219
|
+
return { reason, success: false };
|
|
220
|
+
}
|
|
221
|
+
// PATCH final state to the cloud.
|
|
222
|
+
const patchResult = await this.patchAppFinalState({
|
|
223
|
+
appName,
|
|
224
|
+
cloudBackend,
|
|
225
|
+
isDeploy,
|
|
226
|
+
jobId: job.id,
|
|
227
|
+
rootDir,
|
|
339
228
|
workspace,
|
|
340
229
|
});
|
|
341
|
-
if (!
|
|
342
|
-
return
|
|
230
|
+
if (!patchResult.success) {
|
|
231
|
+
return patchResult;
|
|
343
232
|
}
|
|
344
233
|
return { success: true };
|
|
345
234
|
}
|
|
@@ -350,114 +239,7 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
350
239
|
catch {
|
|
351
240
|
// best-effort
|
|
352
241
|
}
|
|
353
|
-
return { reason: `app
|
|
354
|
-
}
|
|
355
|
-
finally {
|
|
356
|
-
if (cleanup) {
|
|
357
|
-
await cleanup();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
async executeAppDestroyJob(job, cloudBackend, logger) {
|
|
362
|
-
const payload = job.payload;
|
|
363
|
-
const { appName, commit, parameters: providedParameters, repository, sha256, workspace } = payload;
|
|
364
|
-
if (!appName || !workspace || !repository) {
|
|
365
|
-
return {
|
|
366
|
-
reason: 'Missing appName, workspace, or repository in app-destroy job payload',
|
|
367
|
-
success: false,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
let cleanup;
|
|
371
|
-
try {
|
|
372
|
-
logger.info(`Downloading app source for destroy: ${repository}${commit ? `@${commit}` : ''}...`);
|
|
373
|
-
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
374
|
-
cleanup = downloadResult.cleanup;
|
|
375
|
-
const { rootDir } = downloadResult;
|
|
376
|
-
// Re-read declarations + substitute hereyavars before any package work.
|
|
377
|
-
const paramsResult = await this.resolveAndSubstituteParams({
|
|
378
|
-
logger,
|
|
379
|
-
provided: providedParameters,
|
|
380
|
-
rootDir,
|
|
381
|
-
});
|
|
382
|
-
if (!paramsResult.success) {
|
|
383
|
-
await cloudBackend.updateAppDeployment({
|
|
384
|
-
lastJobId: job.id,
|
|
385
|
-
name: appName,
|
|
386
|
-
status: 'failed',
|
|
387
|
-
workspace,
|
|
388
|
-
});
|
|
389
|
-
return { reason: paramsResult.reason, success: false };
|
|
390
|
-
}
|
|
391
|
-
const { parameters: resolvedParameters } = paramsResult;
|
|
392
|
-
const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
|
|
393
|
-
if (!hereyaYamlPath) {
|
|
394
|
-
return { reason: 'App source does not contain a hereya.yaml file', success: false };
|
|
395
|
-
}
|
|
396
|
-
const parsedHereyaYaml = (yaml.parse(await fs.readFile(hereyaYamlPath, 'utf8')) ?? {});
|
|
397
|
-
const packages = normalizePackageList(parsedHereyaYaml.packages);
|
|
398
|
-
const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
|
|
399
|
-
// Mark deployment as destroying
|
|
400
|
-
await cloudBackend.updateAppDeployment({
|
|
401
|
-
lastJobId: job.id,
|
|
402
|
-
name: appName,
|
|
403
|
-
status: 'destroying',
|
|
404
|
-
workspace,
|
|
405
|
-
});
|
|
406
|
-
const preDeployResult = await runPreDeployCommand({
|
|
407
|
-
command: parsedHereyaYaml.preDeployCommand,
|
|
408
|
-
directory: rootDir,
|
|
409
|
-
logger,
|
|
410
|
-
});
|
|
411
|
-
if (!preDeployResult.success) {
|
|
412
|
-
await cloudBackend.updateAppDeployment({
|
|
413
|
-
lastJobId: job.id,
|
|
414
|
-
name: appName,
|
|
415
|
-
status: 'failed',
|
|
416
|
-
workspace,
|
|
417
|
-
});
|
|
418
|
-
return { reason: preDeployResult.reason, success: false };
|
|
419
|
-
}
|
|
420
|
-
// Iterate in REVERSE: deploy packages first (depend on regular pkgs), then regular pkgs.
|
|
421
|
-
const reversed = [...deployPackages.reverse(), ...packages.reverse()];
|
|
422
|
-
const workspaceEnvResult = await cloudBackend.getWorkspaceEnv({ workspace });
|
|
423
|
-
const workspaceEnv = workspaceEnvResult.success ? workspaceEnvResult.env : {};
|
|
424
|
-
/* eslint-disable no-await-in-loop */
|
|
425
|
-
for (const pkg of reversed) {
|
|
426
|
-
const result = await appPackageOps.destroyPackage({
|
|
427
|
-
env: workspaceEnv,
|
|
428
|
-
isDeploying: true,
|
|
429
|
-
logger,
|
|
430
|
-
package: pkg.spec,
|
|
431
|
-
parameters: resolvedParameters,
|
|
432
|
-
projectEnv: workspaceEnv,
|
|
433
|
-
projectRootDir: rootDir,
|
|
434
|
-
workspace,
|
|
435
|
-
});
|
|
436
|
-
if (!result.success) {
|
|
437
|
-
await cloudBackend.updateAppDeployment({
|
|
438
|
-
lastJobId: job.id,
|
|
439
|
-
name: appName,
|
|
440
|
-
status: 'failed',
|
|
441
|
-
workspace,
|
|
442
|
-
});
|
|
443
|
-
return { reason: `Failed to destroy package ${pkg.name}: ${result.reason}`, success: false };
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
/* eslint-enable no-await-in-loop */
|
|
447
|
-
const updateResult = await cloudBackend.updateAppDeployment({
|
|
448
|
-
env: {},
|
|
449
|
-
lastJobId: job.id,
|
|
450
|
-
name: appName,
|
|
451
|
-
status: 'destroyed',
|
|
452
|
-
workspace,
|
|
453
|
-
});
|
|
454
|
-
if (!updateResult.success) {
|
|
455
|
-
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
456
|
-
}
|
|
457
|
-
return { success: true };
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
return { reason: `app-destroy failed: ${error.message}`, success: false };
|
|
242
|
+
return { reason: `app-${subcommand} failed: ${error.message}`, success: false };
|
|
461
243
|
}
|
|
462
244
|
finally {
|
|
463
245
|
if (cleanup) {
|
|
@@ -727,30 +509,17 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
727
509
|
this.log(`Job ${job.id} ${initResult.success ? 'completed' : 'failed'} (init)`);
|
|
728
510
|
return;
|
|
729
511
|
}
|
|
730
|
-
if (job.type === 'app-deploy') {
|
|
731
|
-
const
|
|
512
|
+
if (job.type === 'app-deploy' || job.type === 'app-destroy') {
|
|
513
|
+
const appJobResult = await this.executeAppJob(job, cloudBackend, logger);
|
|
732
514
|
clearInterval(logInterval);
|
|
733
515
|
clearInterval(heartbeatInterval);
|
|
734
516
|
await cloudBackend.updateExecutorJob({
|
|
735
517
|
jobId: job.id,
|
|
736
518
|
logs: logBuffer,
|
|
737
|
-
result:
|
|
738
|
-
status:
|
|
519
|
+
result: appJobResult,
|
|
520
|
+
status: appJobResult.success ? 'completed' : 'failed',
|
|
739
521
|
});
|
|
740
|
-
this.log(`Job ${job.id} ${
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
if (job.type === 'app-destroy') {
|
|
744
|
-
const appDestroyResult = await this.executeAppDestroyJob(job, cloudBackend, logger);
|
|
745
|
-
clearInterval(logInterval);
|
|
746
|
-
clearInterval(heartbeatInterval);
|
|
747
|
-
await cloudBackend.updateExecutorJob({
|
|
748
|
-
jobId: job.id,
|
|
749
|
-
logs: logBuffer,
|
|
750
|
-
result: appDestroyResult,
|
|
751
|
-
status: appDestroyResult.success ? 'completed' : 'failed',
|
|
752
|
-
});
|
|
753
|
-
this.log(`Job ${job.id} ${appDestroyResult.success ? 'completed' : 'failed'} (app-destroy)`);
|
|
522
|
+
this.log(`Job ${job.id} ${appJobResult.success ? 'completed' : 'failed'} (${job.type})`);
|
|
754
523
|
return;
|
|
755
524
|
}
|
|
756
525
|
const result = job.type === 'provision'
|
|
@@ -798,34 +567,45 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
798
567
|
// best-effort; failure of the PATCH must not bubble out
|
|
799
568
|
}
|
|
800
569
|
}
|
|
801
|
-
async
|
|
802
|
-
const
|
|
803
|
-
if (!
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
async resolveAndSubstituteParams(input) {
|
|
811
|
-
const declared = await this.readDeclaredParameters(input.rootDir);
|
|
812
|
-
const resolved = resolveAppParameters({ declared, provided: input.provided });
|
|
813
|
-
if (resolved.errors.length > 0) {
|
|
814
|
-
return { reason: `Parameter validation failed: ${resolved.errors.join('; ')}`, success: false };
|
|
815
|
-
}
|
|
816
|
-
try {
|
|
817
|
-
const { filesWritten } = await substituteAppParameters({
|
|
818
|
-
declaredNames: Object.keys(declared),
|
|
819
|
-
parameters: resolved.parameters,
|
|
820
|
-
rootDir: input.rootDir,
|
|
570
|
+
async patchAppFinalState(input) {
|
|
571
|
+
const { appName, cloudBackend, isDeploy, jobId, rootDir, workspace } = input;
|
|
572
|
+
if (!isDeploy) {
|
|
573
|
+
const updateResult = await cloudBackend.updateAppDeployment({
|
|
574
|
+
env: {},
|
|
575
|
+
lastJobId: jobId,
|
|
576
|
+
name: appName,
|
|
577
|
+
status: 'destroyed',
|
|
578
|
+
workspace,
|
|
821
579
|
});
|
|
822
|
-
if (
|
|
823
|
-
|
|
580
|
+
if (!updateResult.success) {
|
|
581
|
+
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
824
582
|
}
|
|
583
|
+
return { success: true };
|
|
584
|
+
}
|
|
585
|
+
// Read .hereya/deploy-env.<workspace>.json that was written by `app deploy --local`.
|
|
586
|
+
// Filename sanitization MUST match the write side (see `sanitizeWorkspaceForFilename`).
|
|
587
|
+
const deployEnvFile = path.join(rootDir, '.hereya', `deploy-env.${sanitizeWorkspaceForFilename(workspace)}.json`);
|
|
588
|
+
let parsed;
|
|
589
|
+
try {
|
|
590
|
+
const content = await fs.readFile(deployEnvFile, 'utf8');
|
|
591
|
+
parsed = JSON.parse(content);
|
|
825
592
|
}
|
|
826
593
|
catch (error) {
|
|
827
|
-
|
|
594
|
+
const reason = `Failed to read deploy env file at ${deployEnvFile}: ${error.message}`;
|
|
595
|
+
await this.markAppDeploymentFailed({ appName, jobId, reason, workspace }, cloudBackend);
|
|
596
|
+
return { reason, success: false };
|
|
597
|
+
}
|
|
598
|
+
const updateResult = await cloudBackend.updateAppDeployment({
|
|
599
|
+
env: parsed.env ?? {},
|
|
600
|
+
lastJobId: jobId,
|
|
601
|
+
name: appName,
|
|
602
|
+
state: parsed.state,
|
|
603
|
+
status: 'deployed',
|
|
604
|
+
workspace,
|
|
605
|
+
});
|
|
606
|
+
if (!updateResult.success) {
|
|
607
|
+
return { reason: `Provisioning succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
828
608
|
}
|
|
829
|
-
return {
|
|
609
|
+
return { success: true };
|
|
830
610
|
}
|
|
831
611
|
}
|
|
@@ -2,6 +2,7 @@ import { InfrastructureType } from '../infrastructure/common.js';
|
|
|
2
2
|
import { Logger } from '../lib/log.js';
|
|
3
3
|
import { IPackageMetadata } from '../lib/package/index.js';
|
|
4
4
|
export type ExecutorProvisionInput = {
|
|
5
|
+
app?: string;
|
|
5
6
|
isDeploying?: boolean;
|
|
6
7
|
logger?: Logger;
|
|
7
8
|
package: string;
|
package/dist/executor/local.js
CHANGED
|
@@ -4,8 +4,10 @@ import { mintInstallationToken } from '../lib/github-app.js';
|
|
|
4
4
|
import { resolvePackage } from '../lib/package/index.js';
|
|
5
5
|
export class LocalExecutor {
|
|
6
6
|
async destroy(input) {
|
|
7
|
+
// For apps, we use `app` as the project-name source for workspace env resolution
|
|
8
|
+
// (workspace name normalization just needs the org-prefixed scope name).
|
|
7
9
|
const getWorkspaceEnvOutput = await this.getWorkspaceEnv({
|
|
8
|
-
project: input.project,
|
|
10
|
+
project: input.app ?? input.project,
|
|
9
11
|
skipResolve: true,
|
|
10
12
|
workspace: input.workspace,
|
|
11
13
|
});
|
|
@@ -51,8 +53,10 @@ export class LocalExecutor {
|
|
|
51
53
|
return { metadata, pkgName, success: true, version };
|
|
52
54
|
}
|
|
53
55
|
async provision(input) {
|
|
56
|
+
// For apps, we use `app` as the project-name source for workspace env resolution
|
|
57
|
+
// (workspace name normalization just needs the org-prefixed scope name).
|
|
54
58
|
const getWorkspaceEnvOutput = await this.getWorkspaceEnv({
|
|
55
|
-
project: input.project,
|
|
59
|
+
project: input.app ?? input.project,
|
|
56
60
|
skipResolve: true,
|
|
57
61
|
workspace: input.workspace,
|
|
58
62
|
});
|
|
@@ -10,7 +10,8 @@ export declare function resolveEnvValues(env: Record<string, string>): Promise<R
|
|
|
10
10
|
export declare function getInfrastructure(input: GetInfrastructureInput): GetInfrastructureOutput;
|
|
11
11
|
export declare function destroyPackage(input: DestroyPackageInput): Promise<DestroyPackageOutput>;
|
|
12
12
|
export declare function provisionPackage(input: ProvisionPackageInput): Promise<ProvisionPackageOutput>;
|
|
13
|
-
export declare function getProvisioningLogicalId({ pkg, project, workspace }: {
|
|
13
|
+
export declare function getProvisioningLogicalId({ app, pkg, project, workspace }: {
|
|
14
|
+
app?: string;
|
|
14
15
|
pkg: string;
|
|
15
16
|
project?: string;
|
|
16
17
|
workspace?: string;
|
|
@@ -18,6 +19,7 @@ export declare function getProvisioningLogicalId({ pkg, project, workspace }: {
|
|
|
18
19
|
export type DestroyPackageInput = ProvisionPackageInput;
|
|
19
20
|
export type DestroyPackageOutput = ProvisionPackageOutput;
|
|
20
21
|
export type ProvisionPackageInput = {
|
|
22
|
+
app?: string;
|
|
21
23
|
env?: {
|
|
22
24
|
[key: string]: string;
|
|
23
25
|
};
|
|
@@ -43,6 +43,20 @@ export function getInfrastructure(input) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* When `input.app` is set, scope the provisioning row by `app` (workspace-scoped).
|
|
48
|
+
* Otherwise, scope by `project`. Apps and projects are mutually exclusive scopes
|
|
49
|
+
* for the cloud's provisioning-id row.
|
|
50
|
+
*/
|
|
51
|
+
function buildProvisioningIdInput({ canonicalName, input, pkgName }) {
|
|
52
|
+
return {
|
|
53
|
+
app: input.app,
|
|
54
|
+
logicalId: getProvisioningLogicalId({ app: input.app, pkg: pkgName, project: input.project, workspace: input.workspace }),
|
|
55
|
+
packageCanonicalName: canonicalName,
|
|
56
|
+
project: input.app ? undefined : input.project,
|
|
57
|
+
workspace: input.workspace,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
46
60
|
export async function destroyPackage(input) {
|
|
47
61
|
const resolvePackageOutput = await resolvePackage({ isDeploying: input.isDeploying, logger: input.logger, package: input.package, projectRootDir: input.projectRootDir });
|
|
48
62
|
if (!resolvePackageOutput.found) {
|
|
@@ -62,12 +76,7 @@ export async function destroyPackage(input) {
|
|
|
62
76
|
}
|
|
63
77
|
const { infrastructure } = infrastructure$;
|
|
64
78
|
const backend = await getBackend();
|
|
65
|
-
const id$ = await backend.getProvisioningId({
|
|
66
|
-
logicalId: getProvisioningLogicalId({ pkg: pkgName, project: input.project, workspace: input.workspace }),
|
|
67
|
-
packageCanonicalName: canonicalName,
|
|
68
|
-
project: input.project,
|
|
69
|
-
workspace: input.workspace,
|
|
70
|
-
});
|
|
79
|
+
const id$ = await backend.getProvisioningId(buildProvisioningIdInput({ canonicalName, input, pkgName }));
|
|
71
80
|
if (!id$.success) {
|
|
72
81
|
return { reason: id$.reason, success: false };
|
|
73
82
|
}
|
|
@@ -214,12 +223,7 @@ export async function provisionPackage(input) {
|
|
|
214
223
|
}
|
|
215
224
|
const { infrastructure } = infrastructure$;
|
|
216
225
|
const backend = await getBackend();
|
|
217
|
-
const id$ = await backend.getProvisioningId({
|
|
218
|
-
logicalId: getProvisioningLogicalId({ pkg: pkgName, project: input.project, workspace: input.workspace }),
|
|
219
|
-
packageCanonicalName: canonicalName,
|
|
220
|
-
project: input.project,
|
|
221
|
-
workspace: input.workspace,
|
|
222
|
-
});
|
|
226
|
+
const id$ = await backend.getProvisioningId(buildProvisioningIdInput({ canonicalName, input, pkgName }));
|
|
223
227
|
if (!id$.success) {
|
|
224
228
|
return { reason: id$.reason, success: false };
|
|
225
229
|
}
|
|
@@ -233,8 +237,11 @@ export async function provisionPackage(input) {
|
|
|
233
237
|
}
|
|
234
238
|
return { env: provisionOutput.env, metadata, pkgName, success: true, version: version || '' };
|
|
235
239
|
}
|
|
236
|
-
export function getProvisioningLogicalId({ pkg, project, workspace }) {
|
|
237
|
-
|
|
240
|
+
export function getProvisioningLogicalId({ app, pkg, project, workspace }) {
|
|
241
|
+
// Apps and projects are mutually exclusive scopes for the logical id. When `app` is set,
|
|
242
|
+
// the logical id is keyed by the app name (workspace-scoped, not project-scoped).
|
|
243
|
+
const scope = app ?? project;
|
|
244
|
+
const scopeName = scope ? stripOrgPrefix(scope) : scope;
|
|
238
245
|
const workspaceName = workspace ? stripOrgPrefix(workspace) : workspace;
|
|
239
|
-
return [pkg.replaceAll('/', '.'), workspaceName,
|
|
246
|
+
return [pkg.replaceAll('/', '.'), workspaceName, scopeName].filter(Boolean).join('.');
|
|
240
247
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Logger } from './log.js';
|
|
1
2
|
import { IParameterSpec } from './package/index.js';
|
|
2
3
|
export interface ResolveAppParametersInput {
|
|
3
4
|
declared?: Record<string, IParameterSpec>;
|
|
@@ -39,3 +40,25 @@ export interface SubstituteAppParametersOutput {
|
|
|
39
40
|
* If the hereyavars directory doesn't exist, this is a no-op.
|
|
40
41
|
*/
|
|
41
42
|
export declare function substituteAppParameters(input: SubstituteAppParametersInput): Promise<SubstituteAppParametersOutput>;
|
|
43
|
+
export interface ResolveAndSubstituteAppParamsInput {
|
|
44
|
+
logger?: Logger;
|
|
45
|
+
provided?: Record<string, string>;
|
|
46
|
+
rootDir: string;
|
|
47
|
+
}
|
|
48
|
+
export type ResolveAndSubstituteAppParamsOutput = {
|
|
49
|
+
parameters: Record<string, string>;
|
|
50
|
+
success: true;
|
|
51
|
+
} | {
|
|
52
|
+
reason: string;
|
|
53
|
+
success: false;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Read declared parameters from `<rootDir>/hereyarc.yaml`, validate provided
|
|
57
|
+
* values against the declarations, then substitute `{{name}}` placeholders in
|
|
58
|
+
* any `hereyaconfig/hereyavars/*.yaml` files. Returns the resolved parameter
|
|
59
|
+
* map for the caller to forward to `executor.provision({parameters})`.
|
|
60
|
+
*
|
|
61
|
+
* Used by `hereya app deploy --local`, `hereya app destroy --local`, and the
|
|
62
|
+
* remote executor wrapper (which now shells out to those commands).
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveAndSubstituteAppParams(input: ResolveAndSubstituteAppParamsInput): Promise<ResolveAndSubstituteAppParamsOutput>;
|