hereya-cli 0.85.4 → 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 -4
- package/dist/commands/executor/start/index.js +89 -332
- 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 +52 -15
- package/package.json +1 -1
|
@@ -3,70 +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 { resolveAppParameters, substituteAppParameters } from '../../../lib/app-parameters.js';
|
|
13
11
|
import * as appSourceLib from '../../../lib/app-source.js';
|
|
14
12
|
import { SimpleConfigManager } from '../../../lib/config/simple.js';
|
|
15
|
-
import { getEnvManager } from '../../../lib/env/index.js';
|
|
16
13
|
import { cloneWithCredentialHelper, gitUtils } from '../../../lib/git-utils.js';
|
|
17
|
-
import { stripOrgPrefix } from '../../../lib/org-utils.js';
|
|
18
14
|
import { runShell, shellUtils } from '../../../lib/shell.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
catch {
|
|
27
|
-
// try next
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
function normalizePackageList(raw) {
|
|
33
|
-
if (!raw)
|
|
34
|
-
return [];
|
|
35
|
-
if (Array.isArray(raw)) {
|
|
36
|
-
return raw
|
|
37
|
-
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
38
|
-
.map((entry) => {
|
|
39
|
-
const [name, version] = entry.split('@');
|
|
40
|
-
return {
|
|
41
|
-
name,
|
|
42
|
-
spec: entry,
|
|
43
|
-
version,
|
|
44
|
-
};
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
if (typeof raw === 'object') {
|
|
48
|
-
return Object.entries(raw).map(([name, info]) => {
|
|
49
|
-
const version = info?.version;
|
|
50
|
-
return {
|
|
51
|
-
name,
|
|
52
|
-
spec: version ? `${name}@${version}` : name,
|
|
53
|
-
version,
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
async function runPreDeployCommand(input) {
|
|
60
|
-
if (!input.command)
|
|
61
|
-
return { success: true };
|
|
62
|
-
try {
|
|
63
|
-
input.logger.info('Running pre-deploy command...');
|
|
64
|
-
await runShell(input.command, [], { directory: input.directory, logger: input.logger });
|
|
65
|
-
return { success: true };
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
return { reason: `pre-deploy command failed: ${error.message}`, success: false };
|
|
69
|
-
}
|
|
15
|
+
/**
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeWorkspaceForFilename(workspace) {
|
|
21
|
+
return workspace.replaceAll('/', '-');
|
|
70
22
|
}
|
|
71
23
|
/**
|
|
72
24
|
* Strip credentials from log strings before flushing them to the cloud.
|
|
@@ -213,12 +165,14 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
213
165
|
}
|
|
214
166
|
this.log('Executor stopped.');
|
|
215
167
|
}
|
|
216
|
-
async
|
|
168
|
+
async executeAppJob(job, cloudBackend, logger) {
|
|
169
|
+
const isDeploy = job.type === 'app-deploy';
|
|
170
|
+
const subcommand = isDeploy ? 'deploy' : 'destroy';
|
|
217
171
|
const payload = job.payload;
|
|
218
172
|
const { appName, commit, parameters: providedParameters, repository, sha256, version, workspace } = payload;
|
|
219
173
|
if (!appName || !workspace || !repository) {
|
|
220
174
|
return {
|
|
221
|
-
reason:
|
|
175
|
+
reason: `Missing appName, workspace, or repository in ${job.type} job payload`,
|
|
222
176
|
success: false,
|
|
223
177
|
};
|
|
224
178
|
}
|
|
@@ -228,128 +182,53 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
228
182
|
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
229
183
|
cleanup = downloadResult.cleanup;
|
|
230
184
|
const { rootDir } = downloadResult;
|
|
231
|
-
//
|
|
232
|
-
const paramsResult = await this.resolveAndSubstituteParams({
|
|
233
|
-
logger,
|
|
234
|
-
provided: providedParameters,
|
|
235
|
-
rootDir,
|
|
236
|
-
});
|
|
237
|
-
if (!paramsResult.success) {
|
|
238
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: paramsResult.reason, workspace }, cloudBackend);
|
|
239
|
-
return { reason: paramsResult.reason, success: false };
|
|
240
|
-
}
|
|
241
|
-
const { parameters: resolvedParameters } = paramsResult;
|
|
242
|
-
// Read hereya.yaml from app source
|
|
243
|
-
const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
|
|
244
|
-
if (!hereyaYamlPath) {
|
|
245
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: 'Missing hereya.yaml', workspace }, cloudBackend);
|
|
246
|
-
return { reason: 'App source does not contain a hereya.yaml file', success: false };
|
|
247
|
-
}
|
|
248
|
-
const hereyaYamlContent = await fs.readFile(hereyaYamlPath, 'utf8');
|
|
249
|
-
const parsedHereyaYaml = (yaml.parse(hereyaYamlContent) ?? {});
|
|
250
|
-
// Build package lists
|
|
251
|
-
const packages = normalizePackageList(parsedHereyaYaml.packages);
|
|
252
|
-
const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
|
|
253
|
-
// Mark deployment as deploying
|
|
185
|
+
// Mark deployment as deploying / destroying.
|
|
254
186
|
await cloudBackend.updateAppDeployment({
|
|
255
187
|
lastJobId: job.id,
|
|
256
188
|
name: appName,
|
|
257
|
-
status: 'deploying',
|
|
258
|
-
workspace,
|
|
259
|
-
});
|
|
260
|
-
const preDeployResult = await runPreDeployCommand({
|
|
261
|
-
command: parsedHereyaYaml.preDeployCommand,
|
|
262
|
-
directory: rootDir,
|
|
263
|
-
logger,
|
|
264
|
-
});
|
|
265
|
-
if (!preDeployResult.success) {
|
|
266
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: preDeployResult.reason, workspace }, cloudBackend);
|
|
267
|
-
return { reason: preDeployResult.reason, success: false };
|
|
268
|
-
}
|
|
269
|
-
// Mirror project-deploy env machinery exactly:
|
|
270
|
-
// 1. Each regular pkg is provisioned in isolation. LocalExecutor.provision
|
|
271
|
-
// auto-loads workspace env; pkg outputs are persisted via
|
|
272
|
-
// envManager.addProjectEnv to .hereya/env.<workspace>.yaml.
|
|
273
|
-
// 2. projectEnv for deploy pkgs is built from envManager.getProjectEnv
|
|
274
|
-
// (.hereya/env.<workspace>.yaml + .env + hereyaconfig/hereyastaticenv)
|
|
275
|
-
// — explicitly NOT including workspace env. That keeps the deploy
|
|
276
|
-
// Lambda env block under AWS's 4 KB limit.
|
|
277
|
-
// 3. deployEnv accumulates only deploy-package outputs and is the
|
|
278
|
-
// AppDeployment's public env surface (cloud UI + `hereya app env`).
|
|
279
|
-
const executor = new LocalExecutor();
|
|
280
|
-
const envMgr = getEnvManager();
|
|
281
|
-
const deployEnv = {};
|
|
282
|
-
/* eslint-disable no-await-in-loop */
|
|
283
|
-
for (const pkg of packages) {
|
|
284
|
-
const result = await executor.provision({
|
|
285
|
-
isDeploying: true,
|
|
286
|
-
logger,
|
|
287
|
-
package: pkg.spec,
|
|
288
|
-
parameters: resolvedParameters,
|
|
289
|
-
project: appName,
|
|
290
|
-
projectRootDir: rootDir,
|
|
291
|
-
workspace,
|
|
292
|
-
});
|
|
293
|
-
if (!result.success) {
|
|
294
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
|
|
295
|
-
return { reason: `Failed to provision package ${pkg.name}: ${result.reason}`, success: false };
|
|
296
|
-
}
|
|
297
|
-
await envMgr.addProjectEnv({
|
|
298
|
-
env: result.env,
|
|
299
|
-
infra: result.metadata.originalInfra ?? result.metadata.infra,
|
|
300
|
-
pkg: result.pkgName,
|
|
301
|
-
projectRootDir: rootDir,
|
|
302
|
-
snakeCase: result.metadata.snakeCase,
|
|
303
|
-
workspace,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
// Build projectEnv for deploy packages from saved per-package outputs.
|
|
307
|
-
const profile = stripOrgPrefix(workspace);
|
|
308
|
-
const projectEnvResult = await envMgr.getProjectEnv({
|
|
309
|
-
markSecret: true,
|
|
310
|
-
profile,
|
|
311
|
-
project: appName,
|
|
312
|
-
projectRootDir: rootDir,
|
|
189
|
+
status: isDeploy ? 'deploying' : 'destroying',
|
|
313
190
|
workspace,
|
|
314
191
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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 },
|
|
323
209
|
logger,
|
|
324
|
-
package: pkg.spec,
|
|
325
|
-
parameters: resolvedParameters,
|
|
326
|
-
project: appName,
|
|
327
|
-
projectEnv,
|
|
328
|
-
projectRootDir: rootDir,
|
|
329
|
-
workspace,
|
|
330
210
|
});
|
|
331
|
-
if (!result.success) {
|
|
332
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
|
|
333
|
-
return { reason: `Failed to provision deploy package ${pkg.name}: ${result.reason}`, success: false };
|
|
334
|
-
}
|
|
335
|
-
Object.assign(deployEnv, result.env);
|
|
336
211
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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,
|
|
349
228
|
workspace,
|
|
350
229
|
});
|
|
351
|
-
if (!
|
|
352
|
-
return
|
|
230
|
+
if (!patchResult.success) {
|
|
231
|
+
return patchResult;
|
|
353
232
|
}
|
|
354
233
|
return { success: true };
|
|
355
234
|
}
|
|
@@ -360,127 +239,7 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
360
239
|
catch {
|
|
361
240
|
// best-effort
|
|
362
241
|
}
|
|
363
|
-
return { reason: `app
|
|
364
|
-
}
|
|
365
|
-
finally {
|
|
366
|
-
if (cleanup) {
|
|
367
|
-
await cleanup();
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
async executeAppDestroyJob(job, cloudBackend, logger) {
|
|
372
|
-
const payload = job.payload;
|
|
373
|
-
const { appName, commit, parameters: providedParameters, repository, sha256, workspace } = payload;
|
|
374
|
-
if (!appName || !workspace || !repository) {
|
|
375
|
-
return {
|
|
376
|
-
reason: 'Missing appName, workspace, or repository in app-destroy job payload',
|
|
377
|
-
success: false,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
let cleanup;
|
|
381
|
-
try {
|
|
382
|
-
logger.info(`Downloading app source for destroy: ${repository}${commit ? `@${commit}` : ''}...`);
|
|
383
|
-
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
384
|
-
cleanup = downloadResult.cleanup;
|
|
385
|
-
const { rootDir } = downloadResult;
|
|
386
|
-
// Re-read declarations + substitute hereyavars before any package work.
|
|
387
|
-
const paramsResult = await this.resolveAndSubstituteParams({
|
|
388
|
-
logger,
|
|
389
|
-
provided: providedParameters,
|
|
390
|
-
rootDir,
|
|
391
|
-
});
|
|
392
|
-
if (!paramsResult.success) {
|
|
393
|
-
await cloudBackend.updateAppDeployment({
|
|
394
|
-
lastJobId: job.id,
|
|
395
|
-
name: appName,
|
|
396
|
-
status: 'failed',
|
|
397
|
-
workspace,
|
|
398
|
-
});
|
|
399
|
-
return { reason: paramsResult.reason, success: false };
|
|
400
|
-
}
|
|
401
|
-
const { parameters: resolvedParameters } = paramsResult;
|
|
402
|
-
const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
|
|
403
|
-
if (!hereyaYamlPath) {
|
|
404
|
-
return { reason: 'App source does not contain a hereya.yaml file', success: false };
|
|
405
|
-
}
|
|
406
|
-
const parsedHereyaYaml = (yaml.parse(await fs.readFile(hereyaYamlPath, 'utf8')) ?? {});
|
|
407
|
-
const packages = normalizePackageList(parsedHereyaYaml.packages);
|
|
408
|
-
const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
|
|
409
|
-
// Mark deployment as destroying
|
|
410
|
-
await cloudBackend.updateAppDeployment({
|
|
411
|
-
lastJobId: job.id,
|
|
412
|
-
name: appName,
|
|
413
|
-
status: 'destroying',
|
|
414
|
-
workspace,
|
|
415
|
-
});
|
|
416
|
-
const preDeployResult = await runPreDeployCommand({
|
|
417
|
-
command: parsedHereyaYaml.preDeployCommand,
|
|
418
|
-
directory: rootDir,
|
|
419
|
-
logger,
|
|
420
|
-
});
|
|
421
|
-
if (!preDeployResult.success) {
|
|
422
|
-
await cloudBackend.updateAppDeployment({
|
|
423
|
-
lastJobId: job.id,
|
|
424
|
-
name: appName,
|
|
425
|
-
status: 'failed',
|
|
426
|
-
workspace,
|
|
427
|
-
});
|
|
428
|
-
return { reason: preDeployResult.reason, success: false };
|
|
429
|
-
}
|
|
430
|
-
// Mirror project-destroy env machinery:
|
|
431
|
-
// - LocalExecutor.destroy auto-loads workspace env.
|
|
432
|
-
// - projectEnv for deploy-pkg destroy comes from envManager.getProjectEnv
|
|
433
|
-
// (saved pkg outputs + .env + hereyastaticenv), NOT workspace env.
|
|
434
|
-
// Iterate in REVERSE: deploy packages first (depend on regular pkgs), then regular pkgs.
|
|
435
|
-
const executor = new LocalExecutor();
|
|
436
|
-
const envMgr = getEnvManager();
|
|
437
|
-
const profile = stripOrgPrefix(workspace);
|
|
438
|
-
const projectEnvResult = await envMgr.getProjectEnv({
|
|
439
|
-
markSecret: true,
|
|
440
|
-
profile,
|
|
441
|
-
project: appName,
|
|
442
|
-
projectRootDir: rootDir,
|
|
443
|
-
workspace,
|
|
444
|
-
});
|
|
445
|
-
const projectEnv = projectEnvResult.success ? projectEnvResult.env : {};
|
|
446
|
-
const reversed = [...deployPackages.reverse(), ...packages.reverse()];
|
|
447
|
-
/* eslint-disable no-await-in-loop */
|
|
448
|
-
for (const pkg of reversed) {
|
|
449
|
-
const result = await executor.destroy({
|
|
450
|
-
isDeploying: true,
|
|
451
|
-
logger,
|
|
452
|
-
package: pkg.spec,
|
|
453
|
-
parameters: resolvedParameters,
|
|
454
|
-
project: appName,
|
|
455
|
-
projectEnv,
|
|
456
|
-
projectRootDir: rootDir,
|
|
457
|
-
workspace,
|
|
458
|
-
});
|
|
459
|
-
if (!result.success) {
|
|
460
|
-
await cloudBackend.updateAppDeployment({
|
|
461
|
-
lastJobId: job.id,
|
|
462
|
-
name: appName,
|
|
463
|
-
status: 'failed',
|
|
464
|
-
workspace,
|
|
465
|
-
});
|
|
466
|
-
return { reason: `Failed to destroy package ${pkg.name}: ${result.reason}`, success: false };
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
/* eslint-enable no-await-in-loop */
|
|
470
|
-
const updateResult = await cloudBackend.updateAppDeployment({
|
|
471
|
-
env: {},
|
|
472
|
-
lastJobId: job.id,
|
|
473
|
-
name: appName,
|
|
474
|
-
status: 'destroyed',
|
|
475
|
-
workspace,
|
|
476
|
-
});
|
|
477
|
-
if (!updateResult.success) {
|
|
478
|
-
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
479
|
-
}
|
|
480
|
-
return { success: true };
|
|
481
|
-
}
|
|
482
|
-
catch (error) {
|
|
483
|
-
return { reason: `app-destroy failed: ${error.message}`, success: false };
|
|
242
|
+
return { reason: `app-${subcommand} failed: ${error.message}`, success: false };
|
|
484
243
|
}
|
|
485
244
|
finally {
|
|
486
245
|
if (cleanup) {
|
|
@@ -750,30 +509,17 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
750
509
|
this.log(`Job ${job.id} ${initResult.success ? 'completed' : 'failed'} (init)`);
|
|
751
510
|
return;
|
|
752
511
|
}
|
|
753
|
-
if (job.type === 'app-deploy') {
|
|
754
|
-
const
|
|
755
|
-
clearInterval(logInterval);
|
|
756
|
-
clearInterval(heartbeatInterval);
|
|
757
|
-
await cloudBackend.updateExecutorJob({
|
|
758
|
-
jobId: job.id,
|
|
759
|
-
logs: logBuffer,
|
|
760
|
-
result: appDeployResult,
|
|
761
|
-
status: appDeployResult.success ? 'completed' : 'failed',
|
|
762
|
-
});
|
|
763
|
-
this.log(`Job ${job.id} ${appDeployResult.success ? 'completed' : 'failed'} (app-deploy)`);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (job.type === 'app-destroy') {
|
|
767
|
-
const appDestroyResult = await this.executeAppDestroyJob(job, cloudBackend, logger);
|
|
512
|
+
if (job.type === 'app-deploy' || job.type === 'app-destroy') {
|
|
513
|
+
const appJobResult = await this.executeAppJob(job, cloudBackend, logger);
|
|
768
514
|
clearInterval(logInterval);
|
|
769
515
|
clearInterval(heartbeatInterval);
|
|
770
516
|
await cloudBackend.updateExecutorJob({
|
|
771
517
|
jobId: job.id,
|
|
772
518
|
logs: logBuffer,
|
|
773
|
-
result:
|
|
774
|
-
status:
|
|
519
|
+
result: appJobResult,
|
|
520
|
+
status: appJobResult.success ? 'completed' : 'failed',
|
|
775
521
|
});
|
|
776
|
-
this.log(`Job ${job.id} ${
|
|
522
|
+
this.log(`Job ${job.id} ${appJobResult.success ? 'completed' : 'failed'} (${job.type})`);
|
|
777
523
|
return;
|
|
778
524
|
}
|
|
779
525
|
const result = job.type === 'provision'
|
|
@@ -821,34 +567,45 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
821
567
|
// best-effort; failure of the PATCH must not bubble out
|
|
822
568
|
}
|
|
823
569
|
}
|
|
824
|
-
async
|
|
825
|
-
const
|
|
826
|
-
if (!
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
async resolveAndSubstituteParams(input) {
|
|
834
|
-
const declared = await this.readDeclaredParameters(input.rootDir);
|
|
835
|
-
const resolved = resolveAppParameters({ declared, provided: input.provided });
|
|
836
|
-
if (resolved.errors.length > 0) {
|
|
837
|
-
return { reason: `Parameter validation failed: ${resolved.errors.join('; ')}`, success: false };
|
|
838
|
-
}
|
|
839
|
-
try {
|
|
840
|
-
const { filesWritten } = await substituteAppParameters({
|
|
841
|
-
declaredNames: Object.keys(declared),
|
|
842
|
-
parameters: resolved.parameters,
|
|
843
|
-
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,
|
|
844
579
|
});
|
|
845
|
-
if (
|
|
846
|
-
|
|
580
|
+
if (!updateResult.success) {
|
|
581
|
+
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
847
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);
|
|
848
592
|
}
|
|
849
593
|
catch (error) {
|
|
850
|
-
|
|
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 };
|
|
851
608
|
}
|
|
852
|
-
return {
|
|
609
|
+
return { success: true };
|
|
853
610
|
}
|
|
854
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>;
|