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.
@@ -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
- async function getAnyExisting(...candidates) {
20
- for (const candidate of candidates) {
21
- try {
22
- // eslint-disable-next-line no-await-in-loop
23
- await fs.access(candidate);
24
- return candidate;
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 executeAppDeployJob(job, cloudBackend, logger) {
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: 'Missing appName, workspace, or repository in app-deploy job payload',
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
- // Re-read declarations + substitute hereyavars before any package work.
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
- if (!projectEnvResult.success) {
316
- await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: projectEnvResult.reason, workspace }, cloudBackend);
317
- return { reason: `Failed to build projectEnv: ${projectEnvResult.reason}`, success: false };
318
- }
319
- const projectEnv = projectEnvResult.env;
320
- for (const pkg of deployPackages) {
321
- const result = await executor.provision({
322
- 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 },
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
- /* eslint-enable no-await-in-loop */
338
- // 3) PATCH AppDeployment with final state only deploy package outputs
339
- const updateResult = await cloudBackend.updateAppDeployment({
340
- env: deployEnv,
341
- lastJobId: job.id,
342
- name: appName,
343
- state: {
344
- deployPackages: deployPackages.map((p) => ({ name: p.name, version: p.version })),
345
- packages: packages.map((p) => ({ name: p.name, version: p.version })),
346
- version,
347
- },
348
- status: 'deployed',
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 (!updateResult.success) {
352
- return { reason: `Provisioning succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
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-deploy failed: ${error.message}`, success: false };
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 appDeployResult = await this.executeAppDeployJob(job, cloudBackend, logger);
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: appDestroyResult,
774
- status: appDestroyResult.success ? 'completed' : 'failed',
519
+ result: appJobResult,
520
+ status: appJobResult.success ? 'completed' : 'failed',
775
521
  });
776
- this.log(`Job ${job.id} ${appDestroyResult.success ? 'completed' : 'failed'} (app-destroy)`);
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 readDeclaredParameters(rootDir) {
825
- const hereyarcPath = await getAnyExisting(path.join(rootDir, 'hereyarc.yaml'), path.join(rootDir, 'hereyarc.yml'));
826
- if (!hereyarcPath) {
827
- return {};
828
- }
829
- const content = await fs.readFile(hereyarcPath, 'utf8');
830
- const parsed = (yaml.parse(content) ?? {});
831
- return parsed.parameters ?? {};
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 (filesWritten.length > 0) {
846
- input.logger.info(`Substituted parameters in ${filesWritten.length} hereyavars file(s).`);
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
- return { reason: `Failed to substitute hereyavars parameters: ${error.message}`, success: false };
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 { parameters: resolved.parameters, success: true };
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;
@@ -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
- const projectName = project ? stripOrgPrefix(project) : project;
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, projectName].filter(Boolean).join('.');
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>;