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.
@@ -1,10 +1,66 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { ListrLogger, ListrLogLevels } from 'listr2';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import * as yaml from 'yaml';
3
6
  import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
4
7
  import { getBackend } from '../../../backend/index.js';
5
- import { resolveAppParameters } from '../../../lib/app-parameters.js';
8
+ import { LocalExecutor } from '../../../executor/local.js';
9
+ import { resolveAndSubstituteAppParams, resolveAppParameters } from '../../../lib/app-parameters.js';
10
+ import { getEnvManager } from '../../../lib/env/index.js';
6
11
  import { arrayOfStringToObject } from '../../../lib/object-utils.js';
12
+ import { stripOrgPrefix } from '../../../lib/org-utils.js';
13
+ import { getProfileFromWorkspace } from '../../../lib/profile-utils.js';
7
14
  import { pollExecutorJob } from '../../../lib/remote-job-utils.js';
15
+ import { shellUtils } from '../../../lib/shell.js';
16
+ function normalizePackageList(raw) {
17
+ if (!raw)
18
+ return [];
19
+ if (Array.isArray(raw)) {
20
+ return raw
21
+ .filter((entry) => typeof entry === 'string' && entry.length > 0)
22
+ .map((entry) => {
23
+ const [name, version] = entry.split('@');
24
+ return {
25
+ name,
26
+ spec: entry,
27
+ version,
28
+ };
29
+ });
30
+ }
31
+ if (typeof raw === 'object') {
32
+ return Object.entries(raw).map(([name, info]) => {
33
+ const version = info?.version;
34
+ return {
35
+ name,
36
+ spec: version ? `${name}@${version}` : name,
37
+ version,
38
+ };
39
+ });
40
+ }
41
+ return [];
42
+ }
43
+ async function getAnyExisting(...candidates) {
44
+ for (const candidate of candidates) {
45
+ try {
46
+ // eslint-disable-next-line no-await-in-loop
47
+ await fs.access(candidate);
48
+ return candidate;
49
+ }
50
+ catch {
51
+ // try next
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Workspaces look like `org/name`, which is not filesystem-safe. The
58
+ * corresponding read-side in the executor wrapper MUST use the same
59
+ * sanitization — see `executeAppJob` in `commands/executor/start/index.ts`.
60
+ */
61
+ function sanitizeWorkspaceForFilename(workspace) {
62
+ return workspace.replaceAll('/', '-');
63
+ }
8
64
  export default class AppDeploy extends Command {
9
65
  static args = {
10
66
  name: Args.string({ description: 'app name in org/name format', required: true }),
@@ -16,6 +72,16 @@ export default class AppDeploy extends Command {
16
72
  '<%= config.bin %> <%= command.id %> my-org/my-app -w prod -p organizationId=org-123',
17
73
  ];
18
74
  static flags = {
75
+ chdir: Flags.string({
76
+ description: `Directory where the command will be executed.
77
+ If not specified, it defaults to the current working directory.
78
+ Alternatively, you can define the project root by setting the HEREYA_PROJECT_ROOT_DIR environment variable.`,
79
+ required: false,
80
+ }),
81
+ local: Flags.boolean({
82
+ default: false,
83
+ description: 'force local execution (skip remote executor)',
84
+ }),
19
85
  parameter: Flags.string({
20
86
  char: 'p',
21
87
  default: [],
@@ -33,6 +99,145 @@ export default class AppDeploy extends Command {
33
99
  };
34
100
  async run() {
35
101
  const { args, flags } = await this.parse(AppDeploy);
102
+ const forceLocal = flags.local || process.env.HEREYA_LOCAL_EXECUTION === 'true';
103
+ if (forceLocal) {
104
+ await this.runLocal({ appName: args.name, flags });
105
+ return;
106
+ }
107
+ await this.runRemote({ appName: args.name, flags });
108
+ }
109
+ async runLocal(input) {
110
+ const { appName, flags } = input;
111
+ const rootDir = path.resolve(flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR || process.cwd());
112
+ const myLogger = new ListrLogger({ useIcons: false });
113
+ const logger = {
114
+ debug: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
115
+ error: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
116
+ info: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
117
+ };
118
+ // Read hereya.yaml
119
+ const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
120
+ if (!hereyaYamlPath) {
121
+ this.error(`No hereya.yaml found in ${rootDir}`);
122
+ }
123
+ const parsedHereyaYaml = (yaml.parse(await fs.readFile(hereyaYamlPath, 'utf8')) ?? {});
124
+ const packages = normalizePackageList(parsedHereyaYaml.packages);
125
+ const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
126
+ // Parse user-provided parameters
127
+ const provided = arrayOfStringToObject(flags.parameter);
128
+ // Resolve params + substitute hereyavars files
129
+ const paramsResult = await resolveAndSubstituteAppParams({
130
+ logger,
131
+ provided,
132
+ rootDir,
133
+ });
134
+ if (!paramsResult.success) {
135
+ this.error(paramsResult.reason);
136
+ }
137
+ const { parameters: resolvedParameters } = paramsResult;
138
+ // Run preDeployCommand if any
139
+ if (parsedHereyaYaml.preDeployCommand) {
140
+ logger.info('Running pre-deploy command...');
141
+ try {
142
+ await shellUtils.runShell(parsedHereyaYaml.preDeployCommand, [], { directory: rootDir, logger });
143
+ }
144
+ catch (error) {
145
+ this.error(`pre-deploy command failed: ${error.message}`);
146
+ }
147
+ }
148
+ const backend = await getBackend();
149
+ const { profile } = await getProfileFromWorkspace(backend, flags.workspace, appName);
150
+ const executor = new LocalExecutor();
151
+ const envMgr = getEnvManager();
152
+ const deployEnv = {};
153
+ // Mirror project-deploy env machinery exactly:
154
+ // 1. Each regular pkg is provisioned in isolation. LocalExecutor.provision
155
+ // auto-loads workspace env; pkg outputs are persisted via
156
+ // envManager.addProjectEnv to .hereya/env.<workspace>.yaml.
157
+ // 2. projectEnv for deploy pkgs is built from envManager.getProjectEnv
158
+ // (.hereya/env.<workspace>.yaml + .env + hereyaconfig/hereyastaticenv)
159
+ // — explicitly NOT including workspace env. That keeps the deploy
160
+ // Lambda env block under AWS's 4 KB limit.
161
+ // 3. deployEnv accumulates only deploy-package outputs and is the
162
+ // AppDeployment's public env surface (cloud UI + `hereya app env`).
163
+ /* eslint-disable no-await-in-loop */
164
+ for (const pkg of packages) {
165
+ myLogger.log(ListrLogLevels.STARTED, `Provisioning ${pkg.name}...`);
166
+ const result = await executor.provision({
167
+ app: appName,
168
+ isDeploying: true,
169
+ logger,
170
+ package: pkg.spec,
171
+ parameters: resolvedParameters,
172
+ projectRootDir: rootDir,
173
+ workspace: flags.workspace,
174
+ });
175
+ if (!result.success) {
176
+ this.error(`Failed to provision package ${pkg.name}: ${result.reason}`);
177
+ }
178
+ await envMgr.addProjectEnv({
179
+ env: result.env,
180
+ infra: result.metadata.originalInfra ?? result.metadata.infra,
181
+ pkg: result.pkgName,
182
+ projectRootDir: rootDir,
183
+ snakeCase: result.metadata.snakeCase,
184
+ workspace: flags.workspace,
185
+ });
186
+ }
187
+ // Build projectEnv for deploy packages from saved per-package outputs.
188
+ const projectEnvResult = await envMgr.getProjectEnv({
189
+ markSecret: true,
190
+ profile: profile ?? stripOrgPrefix(flags.workspace),
191
+ project: appName,
192
+ projectRootDir: rootDir,
193
+ workspace: flags.workspace,
194
+ });
195
+ if (!projectEnvResult.success) {
196
+ this.error(`Failed to build projectEnv: ${projectEnvResult.reason}`);
197
+ }
198
+ const projectEnv = projectEnvResult.env;
199
+ for (const pkg of deployPackages) {
200
+ myLogger.log(ListrLogLevels.STARTED, `Provisioning deploy package ${pkg.name}...`);
201
+ const result = await executor.provision({
202
+ app: appName,
203
+ isDeploying: true,
204
+ logger,
205
+ package: pkg.spec,
206
+ parameters: resolvedParameters,
207
+ projectEnv,
208
+ projectRootDir: rootDir,
209
+ workspace: flags.workspace,
210
+ });
211
+ if (!result.success) {
212
+ this.error(`Failed to provision deploy package ${pkg.name}: ${result.reason}`);
213
+ }
214
+ Object.assign(deployEnv, result.env);
215
+ }
216
+ /* eslint-enable no-await-in-loop */
217
+ // Write deploy env file for executor wrapper to PATCH back to the cloud.
218
+ const hereyaDir = path.join(rootDir, '.hereya');
219
+ await fs.mkdir(hereyaDir, { recursive: true });
220
+ const deployEnvFile = path.join(hereyaDir, `deploy-env.${sanitizeWorkspaceForFilename(flags.workspace)}.json`);
221
+ const state = {
222
+ deployPackages: deployPackages.map((p) => ({ name: p.name, version: p.version })),
223
+ packages: packages.map((p) => ({ name: p.name, version: p.version })),
224
+ version: flags.version,
225
+ };
226
+ await fs.writeFile(deployEnvFile, JSON.stringify({ env: deployEnv, state }, null, 2), 'utf8');
227
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${appName} deployed to ${flags.workspace}`);
228
+ this.log(`\nStatus: deployed`);
229
+ if (flags.version) {
230
+ this.log(`Version: ${flags.version}`);
231
+ }
232
+ if (Object.keys(deployEnv).length > 0) {
233
+ this.log('\nEnv outputs:');
234
+ for (const [key, value] of Object.entries(deployEnv)) {
235
+ this.log(` ${key}=${value}`);
236
+ }
237
+ }
238
+ }
239
+ async runRemote(input) {
240
+ const { appName, flags } = input;
36
241
  const backend = await getBackend();
37
242
  if (!(backend instanceof CloudBackend)) {
38
243
  this.error('App deployment requires the cloud backend. Run `hereya login` first.');
@@ -43,7 +248,7 @@ export default class AppDeploy extends Command {
43
248
  const provided = arrayOfStringToObject(flags.parameter);
44
249
  // Fetch the target version's declared parameters before submitting any cloud job.
45
250
  const versionResult = await cloudBackend.getAppVersion({
46
- name: args.name,
251
+ name: appName,
47
252
  version: flags.version,
48
253
  });
49
254
  if (!versionResult.success) {
@@ -54,9 +259,9 @@ export default class AppDeploy extends Command {
54
259
  if (errors.length > 0) {
55
260
  this.error(['Cannot deploy app due to parameter errors:', ...errors.map((e) => ` - ${e}`)].join('\n'));
56
261
  }
57
- myLogger.log(ListrLogLevels.STARTED, `Deploying ${args.name}${flags.version ? `@${flags.version}` : ''} to workspace ${flags.workspace}...`);
262
+ myLogger.log(ListrLogLevels.STARTED, `Deploying ${appName}${flags.version ? `@${flags.version}` : ''} to workspace ${flags.workspace}...`);
58
263
  const deployResult = await cloudBackend.deployApp({
59
- name: args.name,
264
+ name: appName,
60
265
  parameters: Object.keys(parameters).length > 0 ? parameters : undefined,
61
266
  version: flags.version,
62
267
  workspace: flags.workspace,
@@ -80,9 +285,9 @@ export default class AppDeploy extends Command {
80
285
  if (!pollResult.success) {
81
286
  this.error(pollResult.reason);
82
287
  }
83
- myLogger.log(ListrLogLevels.COMPLETED, `App ${args.name} deployed to ${flags.workspace}`);
288
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${appName} deployed to ${flags.workspace}`);
84
289
  // Print final state
85
- const status = await cloudBackend.getAppDeployment({ name: args.name, workspace: flags.workspace });
290
+ const status = await cloudBackend.getAppDeployment({ name: appName, workspace: flags.workspace });
86
291
  if (status.success) {
87
292
  this.log(`\nStatus: ${status.deployment.status}`);
88
293
  if (status.deployment.version) {
@@ -6,7 +6,12 @@ export default class AppDestroy extends Command {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
+ chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
9
12
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
13
  };
11
14
  run(): Promise<void>;
15
+ private runLocal;
16
+ private runRemote;
12
17
  }
@@ -1,8 +1,58 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { ListrLogger, ListrLogLevels } from 'listr2';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import * as yaml from 'yaml';
3
6
  import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
4
7
  import { getBackend } from '../../../backend/index.js';
8
+ import { LocalExecutor } from '../../../executor/local.js';
9
+ import { resolveAndSubstituteAppParams } from '../../../lib/app-parameters.js';
10
+ import { getEnvManager } from '../../../lib/env/index.js';
11
+ import { arrayOfStringToObject } from '../../../lib/object-utils.js';
12
+ import { stripOrgPrefix } from '../../../lib/org-utils.js';
13
+ import { getProfileFromWorkspace } from '../../../lib/profile-utils.js';
5
14
  import { pollExecutorJob } from '../../../lib/remote-job-utils.js';
15
+ import { shellUtils } from '../../../lib/shell.js';
16
+ function normalizePackageList(raw) {
17
+ if (!raw)
18
+ return [];
19
+ if (Array.isArray(raw)) {
20
+ return raw
21
+ .filter((entry) => typeof entry === 'string' && entry.length > 0)
22
+ .map((entry) => {
23
+ const [name, version] = entry.split('@');
24
+ return {
25
+ name,
26
+ spec: entry,
27
+ version,
28
+ };
29
+ });
30
+ }
31
+ if (typeof raw === 'object') {
32
+ return Object.entries(raw).map(([name, info]) => {
33
+ const version = info?.version;
34
+ return {
35
+ name,
36
+ spec: version ? `${name}@${version}` : name,
37
+ version,
38
+ };
39
+ });
40
+ }
41
+ return [];
42
+ }
43
+ async function getAnyExisting(...candidates) {
44
+ for (const candidate of candidates) {
45
+ try {
46
+ // eslint-disable-next-line no-await-in-loop
47
+ await fs.access(candidate);
48
+ return candidate;
49
+ }
50
+ catch {
51
+ // try next
52
+ }
53
+ }
54
+ return undefined;
55
+ }
6
56
  export default class AppDestroy extends Command {
7
57
  static args = {
8
58
  name: Args.string({ description: 'app name in org/name format', required: true }),
@@ -10,6 +60,22 @@ export default class AppDestroy extends Command {
10
60
  static description = 'Destroy a hereya-app deployment from a workspace.';
11
61
  static examples = ['<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace'];
12
62
  static flags = {
63
+ chdir: Flags.string({
64
+ description: `Directory where the command will be executed.
65
+ If not specified, it defaults to the current working directory.
66
+ Alternatively, you can define the project root by setting the HEREYA_PROJECT_ROOT_DIR environment variable.`,
67
+ required: false,
68
+ }),
69
+ local: Flags.boolean({
70
+ default: false,
71
+ description: 'force local execution (skip remote executor)',
72
+ }),
73
+ parameter: Flags.string({
74
+ char: 'p',
75
+ default: [],
76
+ description: 'parameter for the app deployment, in the form of key=value (repeatable)',
77
+ multiple: true,
78
+ }),
13
79
  workspace: Flags.string({
14
80
  char: 'w',
15
81
  description: 'workspace where the app is currently deployed',
@@ -18,15 +84,98 @@ export default class AppDestroy extends Command {
18
84
  };
19
85
  async run() {
20
86
  const { args, flags } = await this.parse(AppDestroy);
87
+ const forceLocal = flags.local || process.env.HEREYA_LOCAL_EXECUTION === 'true';
88
+ if (forceLocal) {
89
+ await this.runLocal({ appName: args.name, flags });
90
+ return;
91
+ }
92
+ await this.runRemote({ appName: args.name, flags });
93
+ }
94
+ async runLocal(input) {
95
+ const { appName, flags } = input;
96
+ const rootDir = path.resolve(flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR || process.cwd());
97
+ const myLogger = new ListrLogger({ useIcons: false });
98
+ const logger = {
99
+ debug: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
100
+ error: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
101
+ info: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
102
+ };
103
+ // Read hereya.yaml
104
+ const hereyaYamlPath = await getAnyExisting(path.join(rootDir, 'hereya.yaml'), path.join(rootDir, 'hereya.yml'));
105
+ if (!hereyaYamlPath) {
106
+ this.error(`No hereya.yaml found in ${rootDir}`);
107
+ }
108
+ const parsedHereyaYaml = (yaml.parse(await fs.readFile(hereyaYamlPath, 'utf8')) ?? {});
109
+ const packages = normalizePackageList(parsedHereyaYaml.packages);
110
+ const deployPackages = normalizePackageList(parsedHereyaYaml.deployPackages ?? parsedHereyaYaml.deploy);
111
+ // Parse user-provided parameters
112
+ const provided = arrayOfStringToObject(flags.parameter);
113
+ // Resolve params + substitute hereyavars files
114
+ const paramsResult = await resolveAndSubstituteAppParams({
115
+ logger,
116
+ provided,
117
+ rootDir,
118
+ });
119
+ if (!paramsResult.success) {
120
+ this.error(paramsResult.reason);
121
+ }
122
+ const { parameters: resolvedParameters } = paramsResult;
123
+ // Run preDeployCommand if any (matches project undeploy + current executor behavior)
124
+ if (parsedHereyaYaml.preDeployCommand) {
125
+ logger.info('Running pre-deploy command...');
126
+ try {
127
+ await shellUtils.runShell(parsedHereyaYaml.preDeployCommand, [], { directory: rootDir, logger });
128
+ }
129
+ catch (error) {
130
+ this.error(`pre-deploy command failed: ${error.message}`);
131
+ }
132
+ }
133
+ const backend = await getBackend();
134
+ const { profile } = await getProfileFromWorkspace(backend, flags.workspace, appName);
135
+ const executor = new LocalExecutor();
136
+ const envMgr = getEnvManager();
137
+ // Build projectEnv from saved per-package outputs.
138
+ const projectEnvResult = await envMgr.getProjectEnv({
139
+ markSecret: true,
140
+ profile: profile ?? stripOrgPrefix(flags.workspace),
141
+ project: appName,
142
+ projectRootDir: rootDir,
143
+ workspace: flags.workspace,
144
+ });
145
+ const projectEnv = projectEnvResult.success ? projectEnvResult.env : {};
146
+ // Iterate in declaration order: deploy packages first, then regular packages.
147
+ const allPackages = [...deployPackages, ...packages];
148
+ /* eslint-disable no-await-in-loop */
149
+ for (const pkg of allPackages) {
150
+ myLogger.log(ListrLogLevels.STARTED, `Destroying ${pkg.name}...`);
151
+ const result = await executor.destroy({
152
+ app: appName,
153
+ isDeploying: true,
154
+ logger,
155
+ package: pkg.spec,
156
+ parameters: resolvedParameters,
157
+ projectEnv,
158
+ projectRootDir: rootDir,
159
+ workspace: flags.workspace,
160
+ });
161
+ if (!result.success) {
162
+ this.error(`Failed to destroy package ${pkg.name}: ${result.reason}`);
163
+ }
164
+ }
165
+ /* eslint-enable no-await-in-loop */
166
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${appName} destroyed from ${flags.workspace}`);
167
+ }
168
+ async runRemote(input) {
169
+ const { appName, flags } = input;
21
170
  const backend = await getBackend();
22
171
  if (!(backend instanceof CloudBackend)) {
23
172
  this.error('App destroy requires the cloud backend. Run `hereya login` first.');
24
173
  }
25
174
  const cloudBackend = backend;
26
175
  const myLogger = new ListrLogger({ useIcons: false });
27
- myLogger.log(ListrLogLevels.STARTED, `Destroying ${args.name} on workspace ${flags.workspace}...`);
176
+ myLogger.log(ListrLogLevels.STARTED, `Destroying ${appName} on workspace ${flags.workspace}...`);
28
177
  const destroyResult = await cloudBackend.destroyApp({
29
- name: args.name,
178
+ name: appName,
30
179
  workspace: flags.workspace,
31
180
  });
32
181
  if (!destroyResult.success) {
@@ -47,6 +196,6 @@ export default class AppDestroy extends Command {
47
196
  if (!pollResult.success) {
48
197
  this.error(pollResult.reason);
49
198
  }
50
- myLogger.log(ListrLogLevels.COMPLETED, `App ${args.name} destroyed from ${flags.workspace}`);
199
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${appName} destroyed from ${flags.workspace}`);
51
200
  }
52
201
  }
@@ -14,12 +14,10 @@ export default class ExecutorStart extends Command {
14
14
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
15
  };
16
16
  run(): Promise<void>;
17
- private executeAppDeployJob;
18
- private executeAppDestroyJob;
17
+ private executeAppJob;
19
18
  private executeDeployJob;
20
19
  private executeInitJob;
21
20
  private executeJob;
22
21
  private markAppDeploymentFailed;
23
- private readDeclaredParameters;
24
- private resolveAndSubstituteParams;
22
+ private patchAppFinalState;
25
23
  }