hereya-cli 0.83.2 → 0.84.1

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.
@@ -0,0 +1,50 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
3
+ import { getBackend } from '../../../backend/index.js';
4
+ export default class AppEnv extends Command {
5
+ static args = {
6
+ name: Args.string({ description: 'app name in org/name format', required: true }),
7
+ // eslint-disable-next-line perfectionist/sort-objects
8
+ key: Args.string({ description: 'optional env var key to print (omit to print all)', required: false }),
9
+ };
10
+ static description = 'Print environment variables exported by a hereya-app deployment.';
11
+ static examples = [
12
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace',
13
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace DATABASE_URL',
14
+ ];
15
+ static flags = {
16
+ workspace: Flags.string({
17
+ char: 'w',
18
+ description: 'workspace to read env outputs from',
19
+ required: true,
20
+ }),
21
+ };
22
+ async run() {
23
+ const { args, flags } = await this.parse(AppEnv);
24
+ const backend = await getBackend();
25
+ if (!(backend instanceof CloudBackend)) {
26
+ this.error('App env requires the cloud backend. Run `hereya login` first.');
27
+ }
28
+ const cloudBackend = backend;
29
+ const result = await cloudBackend.getAppDeployment({ name: args.name, workspace: flags.workspace });
30
+ if (!result.success) {
31
+ this.error(result.reason);
32
+ }
33
+ const env = result.deployment.env ?? {};
34
+ if (args.key) {
35
+ const value = env[args.key];
36
+ if (value === undefined) {
37
+ this.error(`Env var ${args.key} not found in deployment ${args.name}/${flags.workspace}`);
38
+ }
39
+ this.log(value);
40
+ return;
41
+ }
42
+ if (Object.keys(env).length === 0) {
43
+ this.log('(no env outputs)');
44
+ return;
45
+ }
46
+ for (const [key, value] of Object.entries(env)) {
47
+ this.log(`${key}=${value}`);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,30 @@
1
+ import { Command } from '@oclif/core';
2
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
3
+ import { getBackend } from '../../../backend/index.js';
4
+ export default class AppList extends Command {
5
+ static description = 'List hereya-apps available to your account.';
6
+ static examples = ['<%= config.bin %> <%= command.id %>'];
7
+ async run() {
8
+ const backend = await getBackend();
9
+ if (!(backend instanceof CloudBackend)) {
10
+ this.error('Listing apps requires the cloud backend. Run `hereya login` first.');
11
+ }
12
+ const cloudBackend = backend;
13
+ const result = await cloudBackend.listApps();
14
+ if (!result.success) {
15
+ this.error(`Failed to list apps: ${result.reason}`);
16
+ }
17
+ if (result.apps.length === 0) {
18
+ this.log('No apps found.');
19
+ return;
20
+ }
21
+ const nameWidth = Math.max(4, ...result.apps.map((a) => a.name.length));
22
+ const visibilityWidth = Math.max(10, ...result.apps.map((a) => (a.visibility ?? '').length));
23
+ this.log(`${'Name'.padEnd(nameWidth)} ${'Visibility'.padEnd(visibilityWidth)} Description`);
24
+ this.log('-'.repeat(nameWidth + visibilityWidth + 14));
25
+ for (const app of result.apps) {
26
+ const visibility = (app.visibility ?? '').padEnd(visibilityWidth);
27
+ this.log(`${app.name.padEnd(nameWidth)} ${visibility} ${app.description ?? ''}`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppNew extends Command {
3
+ static args: {
4
+ dirname: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,69 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export default class AppNew extends Command {
5
+ static args = {
6
+ dirname: Args.string({ description: 'directory to create the app skeleton in', required: true }),
7
+ };
8
+ static description = 'Scaffold a new hereya-app directory with starter hereyarc.yaml + hereya.yaml.';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> ./my-app -n my-org/my-app',
11
+ '<%= config.bin %> <%= command.id %> ./my-app -n my-org/my-app --description "An ai-app-builder app"',
12
+ ];
13
+ static flags = {
14
+ description: Flags.string({
15
+ description: 'description of the app',
16
+ required: false,
17
+ }),
18
+ name: Flags.string({
19
+ char: 'n',
20
+ description: 'app name in org/name format (e.g. my-org/my-app)',
21
+ required: true,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(AppNew);
26
+ const targetDir = path.resolve(args.dirname);
27
+ // Validate name format: org/name
28
+ if (!/^[\w.-]+\/[\w.-]+$/.test(flags.name)) {
29
+ this.error(`Invalid app name '${flags.name}'. Use the format org/name (e.g. my-org/my-app).`);
30
+ }
31
+ // Refuse if target dir exists and is non-empty.
32
+ try {
33
+ const entries = await fs.readdir(targetDir);
34
+ if (entries.length > 0) {
35
+ this.error(`Directory ${targetDir} already exists and is not empty.`);
36
+ }
37
+ }
38
+ catch (error) {
39
+ if (error.code !== 'ENOENT') {
40
+ this.error(`Failed to inspect directory ${targetDir}: ${error.message}`);
41
+ }
42
+ // ENOENT is fine — we'll create it below.
43
+ }
44
+ await fs.mkdir(targetDir, { recursive: true });
45
+ const hereyarcLines = [
46
+ `name: ${flags.name}`,
47
+ 'version: 0.1.0',
48
+ 'kind: app',
49
+ 'visibility: private',
50
+ ];
51
+ if (flags.description) {
52
+ hereyarcLines.splice(2, 0, `description: ${JSON.stringify(flags.description)}`);
53
+ }
54
+ const hereyarcContent = hereyarcLines.join('\n') + '\n';
55
+ const hereyaYamlContent = [
56
+ '# packages provisioned with this app deployment.',
57
+ 'packages: []',
58
+ '',
59
+ '# packages run after `packages:` and have access to the merged workspace env.',
60
+ 'deployPackages: []',
61
+ '',
62
+ ].join('\n');
63
+ await fs.writeFile(path.join(targetDir, 'hereyarc.yaml'), hereyarcContent, { encoding: 'utf8' });
64
+ await fs.writeFile(path.join(targetDir, 'hereya.yaml'), hereyaYamlContent, { encoding: 'utf8' });
65
+ this.log(`Created app skeleton at ${targetDir}`);
66
+ this.log(` - hereyarc.yaml (${flags.name}@0.1.0, kind: app)`);
67
+ this.log(' - hereya.yaml (empty packages/deployPackages)');
68
+ }
69
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppStatus extends Command {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,51 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
3
+ import { getBackend } from '../../../backend/index.js';
4
+ export default class AppStatus extends Command {
5
+ static args = {
6
+ name: Args.string({ description: 'app name in org/name format', required: true }),
7
+ };
8
+ static description = 'Show the deployment status of a hereya-app on a workspace.';
9
+ static examples = ['<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace'];
10
+ static flags = {
11
+ workspace: Flags.string({
12
+ char: 'w',
13
+ description: 'workspace to read deployment status from',
14
+ required: true,
15
+ }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(AppStatus);
19
+ const backend = await getBackend();
20
+ if (!(backend instanceof CloudBackend)) {
21
+ this.error('App status requires the cloud backend. Run `hereya login` first.');
22
+ }
23
+ const cloudBackend = backend;
24
+ const result = await cloudBackend.getAppDeployment({ name: args.name, workspace: flags.workspace });
25
+ if (!result.success) {
26
+ this.error(result.reason);
27
+ }
28
+ const { deployment } = result;
29
+ this.log(`App: ${args.name}`);
30
+ this.log(`Workspace: ${deployment.workspace}`);
31
+ this.log(`Status: ${deployment.status}`);
32
+ if (deployment.version) {
33
+ this.log(`Version: ${deployment.version}`);
34
+ }
35
+ if (deployment.lastJobId) {
36
+ this.log(`Last Job: ${deployment.lastJobId}`);
37
+ }
38
+ if (deployment.parameters && Object.keys(deployment.parameters).length > 0) {
39
+ this.log('\nParameters:');
40
+ for (const [key, value] of Object.entries(deployment.parameters)) {
41
+ this.log(` ${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
42
+ }
43
+ }
44
+ if (deployment.env && Object.keys(deployment.env).length > 0) {
45
+ this.log('\nEnv outputs:');
46
+ for (const [key, value] of Object.entries(deployment.env)) {
47
+ this.log(` ${key}=${value}`);
48
+ }
49
+ }
50
+ }
51
+ }
@@ -1,4 +1,14 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { destroyPackage, provisionPackage } from '../../../infrastructure/index.js';
3
+ /**
4
+ * Indirection for testability: tests can stub `appPackageOps.provisionPackage`
5
+ * and `appPackageOps.destroyPackage` to assert call shape without going through
6
+ * the real infrastructure layer.
7
+ */
8
+ export declare const appPackageOps: {
9
+ destroyPackage: typeof destroyPackage;
10
+ provisionPackage: typeof provisionPackage;
11
+ };
2
12
  /**
3
13
  * Strip credentials from log strings before flushing them to the cloud.
4
14
  * Removes:
@@ -14,7 +24,10 @@ export default class ExecutorStart extends Command {
14
24
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
25
  };
16
26
  run(): Promise<void>;
27
+ private executeAppDeployJob;
28
+ private executeAppDestroyJob;
17
29
  private executeDeployJob;
18
30
  private executeInitJob;
19
31
  private executeJob;
32
+ private markAppDeploymentFailed;
20
33
  }
@@ -3,14 +3,79 @@ 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';
6
7
  import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
7
8
  import { loginWithToken } from '../../../backend/cloud/login.js';
8
9
  import { saveCloudCredentials } from '../../../backend/config.js';
9
10
  import { clearBackend, getBackend } from '../../../backend/index.js';
10
11
  import { LocalExecutor } from '../../../executor/local.js';
12
+ import { destroyPackage, provisionPackage } from '../../../infrastructure/index.js';
13
+ import * as appSourceLib from '../../../lib/app-source.js';
14
+ import { applyHereyaVars } from '../../../lib/app-vars.js';
11
15
  import { SimpleConfigManager } from '../../../lib/config/simple.js';
12
16
  import { cloneWithCredentialHelper, gitUtils } from '../../../lib/git-utils.js';
13
17
  import { runShell, shellUtils } from '../../../lib/shell.js';
18
+ /**
19
+ * Indirection for testability: tests can stub `appPackageOps.provisionPackage`
20
+ * and `appPackageOps.destroyPackage` to assert call shape without going through
21
+ * the real infrastructure layer.
22
+ */
23
+ export const appPackageOps = {
24
+ destroyPackage,
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
+ }
78
+ }
14
79
  /**
15
80
  * Strip credentials from log strings before flushing them to the cloud.
16
81
  * Removes:
@@ -156,6 +221,228 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
156
221
  }
157
222
  this.log('Executor stopped.');
158
223
  }
224
+ async executeAppDeployJob(job, cloudBackend, logger) {
225
+ const payload = job.payload;
226
+ const { appName, commit, hereyaVarsYaml, parameters, repository, sha256, version, workspace } = payload;
227
+ if (!appName || !workspace || !repository) {
228
+ return {
229
+ reason: 'Missing appName, workspace, or repository in app-deploy job payload',
230
+ success: false,
231
+ };
232
+ }
233
+ let cleanup;
234
+ try {
235
+ logger.info(`Downloading app source ${repository}${commit ? `@${commit}` : ''}...`);
236
+ const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
237
+ cleanup = downloadResult.cleanup;
238
+ const { rootDir } = downloadResult;
239
+ // Apply hereyavars overrides
240
+ try {
241
+ const { filesWritten } = await applyHereyaVars(rootDir, hereyaVarsYaml);
242
+ if (filesWritten.length > 0) {
243
+ logger.info(`Wrote ${filesWritten.length} hereyavars override file(s).`);
244
+ }
245
+ }
246
+ catch (error) {
247
+ await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: error.message, workspace }, cloudBackend);
248
+ return { reason: `Failed to apply hereyavars overrides: ${error.message}`, success: false };
249
+ }
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
265
+ await cloudBackend.updateAppDeployment({
266
+ lastJobId: job.id,
267
+ name: appName,
268
+ status: 'deploying',
269
+ workspace,
270
+ });
271
+ const preDeployResult = await runPreDeployCommand({
272
+ command: parsedHereyaYaml.preDeployCommand,
273
+ directory: rootDir,
274
+ logger,
275
+ });
276
+ if (!preDeployResult.success) {
277
+ await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: preDeployResult.reason, workspace }, cloudBackend);
278
+ return { reason: preDeployResult.reason, success: false };
279
+ }
280
+ const mergedEnv = {};
281
+ // 1) Provision regular packages
282
+ /* eslint-disable no-await-in-loop */
283
+ for (const pkg of packages) {
284
+ const result = await appPackageOps.provisionPackage({
285
+ isDeploying: true,
286
+ logger,
287
+ package: pkg.spec,
288
+ parameters,
289
+ projectRootDir: rootDir,
290
+ workspace,
291
+ });
292
+ if (!result.success) {
293
+ await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
294
+ return { reason: `Failed to provision package ${pkg.name}: ${result.reason}`, success: false };
295
+ }
296
+ Object.assign(mergedEnv, result.env);
297
+ }
298
+ // 2) Provision deploy packages — these get workspace env merged in
299
+ for (const pkg of deployPackages) {
300
+ const result = await appPackageOps.provisionPackage({
301
+ isDeploying: true,
302
+ logger,
303
+ package: pkg.spec,
304
+ parameters,
305
+ projectEnv: { ...workspaceEnv, ...mergedEnv },
306
+ projectRootDir: rootDir,
307
+ workspace,
308
+ });
309
+ if (!result.success) {
310
+ await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: result.reason, workspace }, cloudBackend);
311
+ return { reason: `Failed to provision deploy package ${pkg.name}: ${result.reason}`, success: false };
312
+ }
313
+ Object.assign(mergedEnv, result.env);
314
+ }
315
+ /* eslint-enable no-await-in-loop */
316
+ // 3) PATCH AppDeployment with final state
317
+ const updateResult = await cloudBackend.updateAppDeployment({
318
+ env: mergedEnv,
319
+ lastJobId: job.id,
320
+ name: appName,
321
+ state: {
322
+ deployPackages: deployPackages.map((p) => ({ name: p.name, version: p.version })),
323
+ packages: packages.map((p) => ({ name: p.name, version: p.version })),
324
+ version,
325
+ },
326
+ status: 'deployed',
327
+ workspace,
328
+ });
329
+ if (!updateResult.success) {
330
+ return { reason: `Provisioning succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
331
+ }
332
+ return { success: true };
333
+ }
334
+ catch (error) {
335
+ try {
336
+ await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: error.message, workspace }, cloudBackend);
337
+ }
338
+ catch {
339
+ // best-effort
340
+ }
341
+ return { reason: `app-deploy failed: ${error.message}`, success: false };
342
+ }
343
+ finally {
344
+ if (cleanup) {
345
+ await cleanup();
346
+ }
347
+ }
348
+ }
349
+ async executeAppDestroyJob(job, cloudBackend, logger) {
350
+ const payload = job.payload;
351
+ const { appName, commit, hereyaVarsYaml, parameters, repository, sha256, workspace } = payload;
352
+ if (!appName || !workspace || !repository) {
353
+ return {
354
+ reason: 'Missing appName, workspace, or repository in app-destroy job payload',
355
+ success: false,
356
+ };
357
+ }
358
+ let cleanup;
359
+ try {
360
+ logger.info(`Downloading app source for destroy: ${repository}${commit ? `@${commit}` : ''}...`);
361
+ const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
362
+ cleanup = downloadResult.cleanup;
363
+ const { rootDir } = downloadResult;
364
+ // Apply hereyavars overrides — destroy needs the same vars to drive Terraform.
365
+ try {
366
+ await applyHereyaVars(rootDir, hereyaVarsYaml);
367
+ }
368
+ catch (error) {
369
+ return { reason: `Failed to apply hereyavars overrides: ${error.message}`, success: false };
370
+ }
371
+ const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
372
+ if (!hereyaYamlPath) {
373
+ return { reason: 'App source does not contain a hereya.yaml file', success: false };
374
+ }
375
+ const parsedHereyaYaml = (yaml.parse(await fs.readFile(hereyaYamlPath, 'utf8')) ?? {});
376
+ const packages = normalizePackageList(parsedHereyaYaml.packages);
377
+ const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
378
+ // Mark deployment as destroying
379
+ await cloudBackend.updateAppDeployment({
380
+ lastJobId: job.id,
381
+ name: appName,
382
+ status: 'destroying',
383
+ workspace,
384
+ });
385
+ const preDeployResult = await runPreDeployCommand({
386
+ command: parsedHereyaYaml.preDeployCommand,
387
+ directory: rootDir,
388
+ logger,
389
+ });
390
+ if (!preDeployResult.success) {
391
+ await cloudBackend.updateAppDeployment({
392
+ lastJobId: job.id,
393
+ name: appName,
394
+ status: 'failed',
395
+ workspace,
396
+ });
397
+ return { reason: preDeployResult.reason, success: false };
398
+ }
399
+ // Iterate in REVERSE: deploy packages first (depend on regular pkgs), then regular pkgs.
400
+ const reversed = [...deployPackages.reverse(), ...packages.reverse()];
401
+ const workspaceEnvResult = await cloudBackend.getWorkspaceEnv({ workspace });
402
+ const workspaceEnv = workspaceEnvResult.success ? workspaceEnvResult.env : {};
403
+ /* eslint-disable no-await-in-loop */
404
+ for (const pkg of reversed) {
405
+ const result = await appPackageOps.destroyPackage({
406
+ isDeploying: true,
407
+ logger,
408
+ package: pkg.spec,
409
+ parameters,
410
+ projectEnv: workspaceEnv,
411
+ projectRootDir: rootDir,
412
+ workspace,
413
+ });
414
+ if (!result.success) {
415
+ await cloudBackend.updateAppDeployment({
416
+ lastJobId: job.id,
417
+ name: appName,
418
+ status: 'failed',
419
+ workspace,
420
+ });
421
+ return { reason: `Failed to destroy package ${pkg.name}: ${result.reason}`, success: false };
422
+ }
423
+ }
424
+ /* eslint-enable no-await-in-loop */
425
+ const updateResult = await cloudBackend.updateAppDeployment({
426
+ env: {},
427
+ lastJobId: job.id,
428
+ name: appName,
429
+ status: 'destroyed',
430
+ workspace,
431
+ });
432
+ if (!updateResult.success) {
433
+ return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
434
+ }
435
+ return { success: true };
436
+ }
437
+ catch (error) {
438
+ return { reason: `app-destroy failed: ${error.message}`, success: false };
439
+ }
440
+ finally {
441
+ if (cleanup) {
442
+ await cleanup();
443
+ }
444
+ }
445
+ }
159
446
  async executeDeployJob(job, executor, cloudBackend, logger) {
160
447
  const { gitBranch, project, workspace } = job.payload;
161
448
  if (!project || !workspace) {
@@ -418,6 +705,32 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
418
705
  this.log(`Job ${job.id} ${initResult.success ? 'completed' : 'failed'} (init)`);
419
706
  return;
420
707
  }
708
+ if (job.type === 'app-deploy') {
709
+ const appDeployResult = await this.executeAppDeployJob(job, cloudBackend, logger);
710
+ clearInterval(logInterval);
711
+ clearInterval(heartbeatInterval);
712
+ await cloudBackend.updateExecutorJob({
713
+ jobId: job.id,
714
+ logs: logBuffer,
715
+ result: appDeployResult,
716
+ status: appDeployResult.success ? 'completed' : 'failed',
717
+ });
718
+ this.log(`Job ${job.id} ${appDeployResult.success ? 'completed' : 'failed'} (app-deploy)`);
719
+ return;
720
+ }
721
+ if (job.type === 'app-destroy') {
722
+ const appDestroyResult = await this.executeAppDestroyJob(job, cloudBackend, logger);
723
+ clearInterval(logInterval);
724
+ clearInterval(heartbeatInterval);
725
+ await cloudBackend.updateExecutorJob({
726
+ jobId: job.id,
727
+ logs: logBuffer,
728
+ result: appDestroyResult,
729
+ status: appDestroyResult.success ? 'completed' : 'failed',
730
+ });
731
+ this.log(`Job ${job.id} ${appDestroyResult.success ? 'completed' : 'failed'} (app-destroy)`);
732
+ return;
733
+ }
421
734
  const result = job.type === 'provision'
422
735
  ? await executor.provision({
423
736
  ...job.payload,
@@ -450,4 +763,17 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
450
763
  this.warn(`Job ${job.id} failed: ${error.message}`);
451
764
  }
452
765
  }
766
+ async markAppDeploymentFailed(input, cloudBackend) {
767
+ try {
768
+ await cloudBackend.updateAppDeployment({
769
+ lastJobId: input.jobId,
770
+ name: input.appName,
771
+ status: 'failed',
772
+ workspace: input.workspace,
773
+ });
774
+ }
775
+ catch {
776
+ // best-effort; failure of the PATCH must not bubble out
777
+ }
778
+ }
453
779
  }
@@ -1,8 +1,9 @@
1
1
  import { Command } from '@oclif/core';
2
2
  interface HereyarcConfig {
3
- description: string;
4
- iac: string;
5
- infra: string;
3
+ description?: string;
4
+ iac?: string;
5
+ infra?: string;
6
+ kind?: 'app' | 'package';
6
7
  name: string;
7
8
  onDeploy?: {
8
9
  pkg: string;
@@ -28,5 +29,6 @@ export default class Publish extends Command {
28
29
  loadReadme(packageDir: string): string | undefined;
29
30
  run(): Promise<void>;
30
31
  validateConfig(config: HereyarcConfig): void;
32
+ private publishApp;
31
33
  }
32
34
  export {};