hereya-cli 0.89.1 → 0.91.0

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.
Files changed (67) hide show
  1. package/README.md +88 -75
  2. package/dist/backend/cloud/cloud-backend/apps-deploy.d.ts +69 -0
  3. package/dist/backend/cloud/cloud-backend/apps-deploy.js +94 -0
  4. package/dist/backend/cloud/cloud-backend/apps-versions.d.ts +69 -0
  5. package/dist/backend/cloud/cloud-backend/apps-versions.js +113 -0
  6. package/dist/backend/cloud/cloud-backend/executor-broker.d.ts +25 -0
  7. package/dist/backend/cloud/cloud-backend/executor-broker.js +38 -0
  8. package/dist/backend/cloud/cloud-backend/executor-jobs.d.ts +75 -0
  9. package/dist/backend/cloud/cloud-backend/executor-jobs.js +110 -0
  10. package/dist/backend/cloud/cloud-backend/misc.d.ts +5 -0
  11. package/dist/backend/cloud/cloud-backend/misc.js +78 -0
  12. package/dist/backend/cloud/cloud-backend/packages-publish.d.ts +3 -0
  13. package/dist/backend/cloud/cloud-backend/packages-publish.js +99 -0
  14. package/dist/backend/cloud/cloud-backend/packages-registry.d.ts +6 -0
  15. package/dist/backend/cloud/cloud-backend/packages-registry.js +146 -0
  16. package/dist/backend/cloud/cloud-backend/packages-workspace.d.ts +4 -0
  17. package/dist/backend/cloud/cloud-backend/packages-workspace.js +50 -0
  18. package/dist/backend/cloud/cloud-backend/projects.d.ts +7 -0
  19. package/dist/backend/cloud/cloud-backend/projects.js +122 -0
  20. package/dist/backend/cloud/cloud-backend/state.d.ts +6 -0
  21. package/dist/backend/cloud/cloud-backend/state.js +86 -0
  22. package/dist/backend/cloud/cloud-backend/utils.d.ts +55 -0
  23. package/dist/backend/cloud/cloud-backend/utils.js +56 -0
  24. package/dist/backend/cloud/cloud-backend/workspace-env.d.ts +5 -0
  25. package/dist/backend/cloud/cloud-backend/workspace-env.js +63 -0
  26. package/dist/backend/cloud/cloud-backend/workspaces.d.ts +7 -0
  27. package/dist/backend/cloud/cloud-backend/workspaces.js +124 -0
  28. package/dist/backend/cloud/cloud-backend.d.ts +59 -126
  29. package/dist/backend/cloud/cloud-backend.js +100 -1087
  30. package/dist/backend/cloud/cloud-backend.test.setup.d.ts +13 -0
  31. package/dist/backend/cloud/cloud-backend.test.setup.js +14 -0
  32. package/dist/backend/common.d.ts +2 -2
  33. package/dist/backend/local.setup.d.ts +10 -0
  34. package/dist/backend/local.setup.js +20 -0
  35. package/dist/commands/executor/start/index.d.ts +4 -11
  36. package/dist/commands/executor/start/index.js +118 -520
  37. package/dist/commands/workspace/executor/install/index.d.ts +3 -0
  38. package/dist/commands/workspace/executor/install/index.js +210 -76
  39. package/dist/commands/workspace/executor/uninstall/index.d.ts +1 -0
  40. package/dist/commands/workspace/executor/uninstall/index.js +30 -4
  41. package/dist/executor/local.d.ts +0 -1
  42. package/dist/executor/local.js +10 -62
  43. package/dist/executor/resolve-env.d.ts +88 -0
  44. package/dist/executor/resolve-env.js +77 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +7 -0
  47. package/dist/infrastructure/index.d.ts +12 -16
  48. package/dist/infrastructure/index.js +18 -25
  49. package/dist/infrastructure/registry.d.ts +38 -0
  50. package/dist/infrastructure/registry.js +61 -0
  51. package/dist/lib/env/test.setup.d.ts +7 -0
  52. package/dist/lib/env/test.setup.js +18 -0
  53. package/dist/lib/executor-start/auth.d.ts +2 -0
  54. package/dist/lib/executor-start/auth.js +21 -0
  55. package/dist/lib/executor-start/execute-app-job.d.ts +13 -0
  56. package/dist/lib/executor-start/execute-app-job.js +146 -0
  57. package/dist/lib/executor-start/execute-deploy-job.d.ts +14 -0
  58. package/dist/lib/executor-start/execute-deploy-job.js +64 -0
  59. package/dist/lib/executor-start/execute-init-job.d.ts +14 -0
  60. package/dist/lib/executor-start/execute-init-job.js +135 -0
  61. package/dist/lib/executor-start/format.d.ts +13 -0
  62. package/dist/lib/executor-start/format.js +22 -0
  63. package/dist/lib/executor-start/job-dispatch.d.ts +15 -0
  64. package/dist/lib/executor-start/job-dispatch.js +89 -0
  65. package/dist/lib/package/index.d.ts +25 -25
  66. package/oclif.manifest.json +43 -3
  67. package/package.json +1 -1
@@ -4,7 +4,10 @@ export default class WorkspaceExecutorInstall extends Command {
4
4
  static flags: {
5
5
  debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
6
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ mode: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
9
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
10
  };
9
11
  run(): Promise<void>;
12
+ private parseParameterFlag;
10
13
  }
@@ -5,12 +5,27 @@ import { getBackend } from '../../../../backend/index.js';
5
5
  import { getExecutor } from '../../../../executor/index.js';
6
6
  import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
7
7
  import { delay } from '../../../../lib/shell.js';
8
- const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
8
+ const ALWAYS_ON_PACKAGE = 'hereya/remote-executor-aws';
9
+ const EPHEMERAL_PACKAGE = 'hereya/aws-executor-broker';
9
10
  export default class WorkspaceExecutorInstall extends Command {
10
- static description = 'Install a remote executor into a workspace';
11
+ static description = `Install a remote executor into a workspace.
12
+
13
+ Two modes are supported:
14
+ - ephemeral (default): provisions hereya/aws-executor-broker (Lambda + on-demand EC2).
15
+ - always-on: provisions hereya/remote-executor-aws (legacy always-on EC2).`;
11
16
  static flags = {
12
17
  debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
13
18
  force: Flags.boolean({ char: 'f', default: false, description: 'force reinstall even if executor is already installed' }),
19
+ mode: Flags.string({
20
+ default: 'ephemeral',
21
+ description: 'executor mode: ephemeral (Lambda + on-demand EC2) or always-on (legacy)',
22
+ options: ['ephemeral', 'always-on'],
23
+ }),
24
+ parameter: Flags.string({
25
+ char: 'p',
26
+ description: "parameter for the package, in the form of 'key=value'. Can be specified multiple times.",
27
+ multiple: true,
28
+ }),
14
29
  workspace: Flags.string({
15
30
  char: 'w',
16
31
  description: 'name of the workspace',
@@ -21,84 +36,184 @@ export default class WorkspaceExecutorInstall extends Command {
21
36
  const { flags } = await this.parse(WorkspaceExecutorInstall);
22
37
  setDebug(flags.debug);
23
38
  const myLogger = new ListrLogger({ useIcons: false });
39
+ const isEphemeral = flags.mode === 'ephemeral';
40
+ const executorPackage = isEphemeral ? EPHEMERAL_PACKAGE : ALWAYS_ON_PACKAGE;
41
+ const userParameters = this.parseParameterFlag(flags.parameter);
42
+ const reservedKeys = ['workspaceId', 'workspaceName'];
43
+ for (const reserved of reservedKeys) {
44
+ if (Object.hasOwn(userParameters, reserved)) {
45
+ this.error(`--parameter ${reserved}=... is not allowed: this value is auto-inferred from the workspace and overriding it would corrupt the broker registration`);
46
+ }
47
+ }
48
+ const validateTask = {
49
+ async task(ctx) {
50
+ const backend = await getBackend();
51
+ if (!(backend instanceof CloudBackend)) {
52
+ throw new TypeError('Remote executor requires cloud backend. Run `hereya login` first.');
53
+ }
54
+ const workspace$ = await backend.getWorkspace(flags.workspace);
55
+ if (!workspace$.found) {
56
+ throw new Error(`Workspace ${flags.workspace} not found`);
57
+ }
58
+ if (workspace$.hasError) {
59
+ throw new Error(workspace$.error);
60
+ }
61
+ if (workspace$.workspace.hasExecutor && !flags.force) {
62
+ throw new Error(`Workspace ${flags.workspace} already has an executor installed. Use --force to reinstall.`);
63
+ }
64
+ ctx.workspaceId = workspace$.workspace.id;
65
+ await delay(500);
66
+ },
67
+ title: 'Validating workspace',
68
+ };
69
+ const provisionAlwaysOnTasks = [
70
+ {
71
+ async task(ctx) {
72
+ const backend = await getBackend();
73
+ const tokenResult = await backend.generateExecutorToken({
74
+ workspace: flags.workspace,
75
+ });
76
+ if (!tokenResult.success) {
77
+ throw new Error(`Failed to generate executor token: ${tokenResult.reason}`);
78
+ }
79
+ ctx.executorToken = tokenResult.token;
80
+ await delay(500);
81
+ },
82
+ title: 'Generating executor token',
83
+ },
84
+ {
85
+ rendererOptions: {
86
+ persistentOutput: isDebug(),
87
+ },
88
+ async task(ctx, task) {
89
+ const executor$ = getExecutor();
90
+ if (!executor$.success) {
91
+ throw new Error(executor$.reason);
92
+ }
93
+ const { executor } = executor$;
94
+ // Merge precedence (lowest -> highest):
95
+ // 1. --parameter key=value (catch-all for any remote-executor-aws inputs)
96
+ // 2. auto-inferred (EXECUTOR_TOKEN, WORKSPACE) — cloud-derived, must not be overridden
97
+ const parameters = {
98
+ ...userParameters,
99
+ EXECUTOR_TOKEN: ctx.executorToken,
100
+ WORKSPACE: flags.workspace,
101
+ };
102
+ const provisionOutput = await executor.provision({
103
+ logger: getLogger(task),
104
+ package: executorPackage,
105
+ parameters,
106
+ skipDeploy: true,
107
+ workspace: flags.workspace,
108
+ });
109
+ if (!provisionOutput.success) {
110
+ throw new Error(provisionOutput.reason);
111
+ }
112
+ },
113
+ title: 'Provisioning remote executor infrastructure',
114
+ },
115
+ {
116
+ async task() {
117
+ const backend = await getBackend();
118
+ await backend.updateWorkspace({
119
+ hasExecutor: true,
120
+ name: flags.workspace,
121
+ });
122
+ await delay(500);
123
+ },
124
+ title: 'Registering executor on workspace',
125
+ },
126
+ ];
127
+ const provisionEphemeralTasks = [
128
+ {
129
+ rendererOptions: {
130
+ persistentOutput: isDebug(),
131
+ },
132
+ async task(ctx, task) {
133
+ const executor$ = getExecutor();
134
+ if (!executor$.success) {
135
+ throw new Error(executor$.reason);
136
+ }
137
+ const { executor } = executor$;
138
+ const hereyaCloudUrl = process.env.HEREYA_CLOUD_URL || 'https://cloud.hereya.dev';
139
+ // Merge precedence (lowest -> highest):
140
+ // 1. auto-inferred (hereyaCloudUrl, workspaceId, workspaceName)
141
+ // 2. --parameter key=value (can override hereyaCloudUrl; workspaceId/workspaceName are
142
+ // rejected up-front, so we re-pin them here defensively)
143
+ const parameters = {
144
+ hereyaCloudUrl,
145
+ ...userParameters,
146
+ workspaceId: ctx.workspaceId,
147
+ workspaceName: flags.workspace,
148
+ };
149
+ const provisionOutput = await executor.provision({
150
+ logger: getLogger(task),
151
+ package: executorPackage,
152
+ parameters,
153
+ skipDeploy: true,
154
+ workspace: flags.workspace,
155
+ });
156
+ if (!provisionOutput.success) {
157
+ throw new Error(provisionOutput.reason);
158
+ }
159
+ ctx.brokerOutputs = provisionOutput.env;
160
+ },
161
+ title: 'Provisioning ephemeral executor broker infrastructure',
162
+ },
163
+ {
164
+ async task(ctx) {
165
+ const outputs = ctx.brokerOutputs ?? {};
166
+ const { brokerVersion, brokerWebhookUrl } = outputs;
167
+ if (!brokerWebhookUrl) {
168
+ throw new Error('Executor broker package did not return brokerWebhookUrl output');
169
+ }
170
+ if (!brokerVersion) {
171
+ throw new Error('Executor broker package did not return brokerVersion output');
172
+ }
173
+ const metadata = {
174
+ awsAccountId: outputs.awsAccountId,
175
+ brokerLambdaArn: outputs.brokerLambdaArn,
176
+ ec2InstanceId: outputs.ec2InstanceId,
177
+ ec2LaunchTemplateId: outputs.ec2LaunchTemplateId,
178
+ invokerRoleArn: outputs.invokerRoleArn,
179
+ region: outputs.region,
180
+ };
181
+ const backend = await getBackend();
182
+ const result = await backend.registerExecutorBroker({
183
+ brokerVersion,
184
+ brokerWebhookUrl,
185
+ metadata,
186
+ provider: 'aws',
187
+ workspace: flags.workspace,
188
+ });
189
+ if (!result.success) {
190
+ throw new Error(`Failed to register executor broker: ${result.reason}`);
191
+ }
192
+ await delay(500);
193
+ },
194
+ title: 'Registering executor broker with hereya cloud',
195
+ },
196
+ {
197
+ async task() {
198
+ const backend = await getBackend();
199
+ await backend.updateWorkspace({
200
+ hasExecutor: true,
201
+ name: flags.workspace,
202
+ });
203
+ await delay(500);
204
+ },
205
+ title: 'Marking workspace executor-enabled',
206
+ },
207
+ ];
208
+ const subTasks = isEphemeral
209
+ ? [validateTask, ...provisionEphemeralTasks]
210
+ : [validateTask, ...provisionAlwaysOnTasks];
24
211
  const task = new Listr([
25
212
  {
26
213
  async task(_ctx, task) {
27
- return task.newListr([
28
- {
29
- async task() {
30
- const backend = await getBackend();
31
- if (!(backend instanceof CloudBackend)) {
32
- throw new TypeError('Remote executor requires cloud backend. Run `hereya login` first.');
33
- }
34
- const workspace$ = await backend.getWorkspace(flags.workspace);
35
- if (!workspace$.found) {
36
- throw new Error(`Workspace ${flags.workspace} not found`);
37
- }
38
- if (workspace$.hasError) {
39
- throw new Error(workspace$.error);
40
- }
41
- if (workspace$.workspace.hasExecutor && !flags.force) {
42
- throw new Error(`Workspace ${flags.workspace} already has an executor installed. Use --force to reinstall.`);
43
- }
44
- await delay(500);
45
- },
46
- title: 'Validating workspace',
47
- },
48
- {
49
- async task(ctx) {
50
- const backend = await getBackend();
51
- const tokenResult = await backend.generateExecutorToken({
52
- workspace: flags.workspace,
53
- });
54
- if (!tokenResult.success) {
55
- throw new Error(`Failed to generate executor token: ${tokenResult.reason}`);
56
- }
57
- ctx.executorToken = tokenResult.token;
58
- await delay(500);
59
- },
60
- title: 'Generating executor token',
61
- },
62
- {
63
- rendererOptions: {
64
- persistentOutput: isDebug(),
65
- },
66
- async task(ctx, task) {
67
- const executor$ = getExecutor();
68
- if (!executor$.success) {
69
- throw new Error(executor$.reason);
70
- }
71
- const { executor } = executor$;
72
- const provisionOutput = await executor.provision({
73
- logger: getLogger(task),
74
- package: DEFAULT_EXECUTOR_PACKAGE,
75
- parameters: {
76
- EXECUTOR_TOKEN: ctx.executorToken,
77
- WORKSPACE: flags.workspace,
78
- },
79
- skipDeploy: true,
80
- workspace: flags.workspace,
81
- });
82
- if (!provisionOutput.success) {
83
- throw new Error(provisionOutput.reason);
84
- }
85
- },
86
- title: 'Provisioning remote executor infrastructure',
87
- },
88
- {
89
- async task() {
90
- const backend = await getBackend();
91
- await backend.updateWorkspace({
92
- hasExecutor: true,
93
- name: flags.workspace,
94
- });
95
- await delay(500);
96
- },
97
- title: 'Registering executor on workspace',
98
- },
99
- ], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
214
+ return task.newListr(subTasks, { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
100
215
  },
101
- title: `Installing executor on workspace ${flags.workspace}`,
216
+ title: `Installing executor on workspace ${flags.workspace} (mode: ${flags.mode})`,
102
217
  },
103
218
  ], { concurrent: false });
104
219
  try {
@@ -109,4 +224,23 @@ export default class WorkspaceExecutorInstall extends Command {
109
224
  this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
110
225
  }
111
226
  }
227
+ parseParameterFlag(values) {
228
+ const result = {};
229
+ if (!values || values.length === 0) {
230
+ return result;
231
+ }
232
+ for (const raw of values) {
233
+ const eqIdx = raw.indexOf('=');
234
+ if (eqIdx === -1) {
235
+ this.error(`Invalid --parameter value '${raw}': expected format 'key=value'`);
236
+ }
237
+ const key = raw.slice(0, eqIdx);
238
+ const value = raw.slice(eqIdx + 1);
239
+ if (key.length === 0) {
240
+ this.error(`Invalid --parameter value '${raw}': key must not be empty`);
241
+ }
242
+ result[key] = value;
243
+ }
244
+ return result;
245
+ }
112
246
  }
@@ -3,6 +3,7 @@ export default class WorkspaceExecutorUninstall extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
5
  debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ mode: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
7
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
8
  };
8
9
  run(): Promise<void>;
@@ -5,11 +5,19 @@ import { getBackend } from '../../../../backend/index.js';
5
5
  import { getExecutor } from '../../../../executor/index.js';
6
6
  import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
7
7
  import { delay } from '../../../../lib/shell.js';
8
- const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
8
+ const ALWAYS_ON_PACKAGE = 'hereya/remote-executor-aws';
9
+ const EPHEMERAL_PACKAGE = 'hereya/aws-executor-broker';
9
10
  export default class WorkspaceExecutorUninstall extends Command {
10
- static description = 'Uninstall the remote executor from a workspace';
11
+ static description = `Uninstall the remote executor from a workspace.
12
+
13
+ The --mode flag must match the mode used at install time (default: ephemeral).`;
11
14
  static flags = {
12
15
  debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
16
+ mode: Flags.string({
17
+ default: 'ephemeral',
18
+ description: 'executor mode used at install time',
19
+ options: ['ephemeral', 'always-on'],
20
+ }),
13
21
  workspace: Flags.string({
14
22
  char: 'w',
15
23
  description: 'name of the workspace',
@@ -20,6 +28,8 @@ export default class WorkspaceExecutorUninstall extends Command {
20
28
  const { flags } = await this.parse(WorkspaceExecutorUninstall);
21
29
  setDebug(flags.debug);
22
30
  const myLogger = new ListrLogger({ useIcons: false });
31
+ const isEphemeral = flags.mode === 'ephemeral';
32
+ const executorPackage = isEphemeral ? EPHEMERAL_PACKAGE : ALWAYS_ON_PACKAGE;
23
33
  const task = new Listr([
24
34
  {
25
35
  async task(_ctx, task) {
@@ -56,7 +66,7 @@ export default class WorkspaceExecutorUninstall extends Command {
56
66
  const { executor } = executor$;
57
67
  const destroyOutput = await executor.destroy({
58
68
  logger: getLogger(task),
59
- package: DEFAULT_EXECUTOR_PACKAGE,
69
+ package: executorPackage,
60
70
  skipDeploy: true,
61
71
  workspace: flags.workspace,
62
72
  });
@@ -76,6 +86,22 @@ export default class WorkspaceExecutorUninstall extends Command {
76
86
  },
77
87
  title: 'Revoking executor token',
78
88
  },
89
+ {
90
+ async task() {
91
+ const backend = await getBackend();
92
+ // Always attempt to unregister the broker. The endpoint
93
+ // gracefully no-ops on 404 so this is safe for legacy
94
+ // always-on installs that never registered a broker.
95
+ const result = await backend.unregisterExecutorBroker({
96
+ workspace: flags.workspace,
97
+ });
98
+ if (!result.success) {
99
+ throw new Error(`Failed to unregister executor broker: ${result.reason}`);
100
+ }
101
+ await delay(500);
102
+ },
103
+ title: 'Unregistering executor broker',
104
+ },
79
105
  {
80
106
  async task() {
81
107
  const backend = await getBackend();
@@ -89,7 +115,7 @@ export default class WorkspaceExecutorUninstall extends Command {
89
115
  },
90
116
  ], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
91
117
  },
92
- title: `Uninstalling executor from workspace ${flags.workspace}`,
118
+ title: `Uninstalling executor from workspace ${flags.workspace} (mode: ${flags.mode})`,
93
119
  },
94
120
  ], { concurrent: false });
95
121
  try {
@@ -7,5 +7,4 @@ export declare class LocalExecutor implements Executor {
7
7
  setEnvVar(input: ExecutorSetEnvVarInput): Promise<ExecutorSetEnvVarOutput>;
8
8
  unsetEnvVar(input: ExecutorUnsetEnvVarInput): Promise<ExecutorUnsetEnvVarOutput>;
9
9
  private getWorkspaceEnv;
10
- private resolveGithubAppMarkers;
11
10
  }
@@ -2,6 +2,7 @@ import { getBackend } from '../backend/index.js';
2
2
  import { destroyPackage, getInfrastructure, getProvisioningLogicalId, provisionPackage, resolveEnvValues as resolveEnvValuesSimple } from '../infrastructure/index.js';
3
3
  import { mintInstallationToken } from '../lib/github-app.js';
4
4
  import { resolvePackage } from '../lib/package/index.js';
5
+ import { resolveEnvValues as resolveEnvValuesPure } from './resolve-env.js';
5
6
  export class LocalExecutor {
6
7
  async destroy(input) {
7
8
  // For apps, we use `app` as the project-name source for workspace env resolution
@@ -70,28 +71,15 @@ export class LocalExecutor {
70
71
  });
71
72
  }
72
73
  async resolveEnvValues(input) {
73
- const resolved = Object.fromEntries(await Promise.all(Object.entries(input.env).map(async ([key, value]) => {
74
- // Check if value has infra prefix (contains ':' and first part is valid infra)
75
- const colonIndex = value.indexOf(':');
76
- if (colonIndex === -1) {
77
- // No colon - plain value, return as-is
78
- return [key, value];
79
- }
80
- const infraType = value.slice(0, colonIndex);
81
- const infra$ = getInfrastructure({ type: infraType });
82
- if (!infra$.supported) {
83
- // Unknown infra type - treat as plain value, return as-is
84
- return [key, value];
85
- }
86
- const { infrastructure } = infra$;
87
- const valueWithoutInfra = value.slice(colonIndex + 1);
88
- const { isSecret, value: resolvedValue } = await infrastructure.resolveEnv({
89
- value: valueWithoutInfra,
90
- });
91
- const finalValue = input.markSecret && isSecret ? `secret://${resolvedValue}` : resolvedValue;
92
- return [key, finalValue];
93
- })));
94
- return this.resolveGithubAppMarkers(resolved, input);
74
+ return resolveEnvValuesPure(input, {
75
+ getInfrastructure,
76
+ async getWorkspaceEnv(args) {
77
+ const backend = await getBackend();
78
+ return backend.getWorkspaceEnv(args);
79
+ },
80
+ mintInstallationToken,
81
+ resolveSimpleEnv: resolveEnvValuesSimple,
82
+ });
95
83
  }
96
84
  async setEnvVar(input) {
97
85
  const backend = await getBackend();
@@ -187,44 +175,4 @@ export class LocalExecutor {
187
175
  success: true,
188
176
  };
189
177
  }
190
- async resolveGithubAppMarkers(resolved, input) {
191
- const needsToken = Object.values(resolved).some((v) => typeof v === 'string' && v.startsWith('github-app:'));
192
- if (!needsToken)
193
- return resolved;
194
- if (!input.workspace || !input.project)
195
- return resolved;
196
- const backend = await getBackend();
197
- const wsEnv$ = await backend.getWorkspaceEnv({
198
- project: input.project,
199
- workspace: input.workspace,
200
- });
201
- if (!wsEnv$.success)
202
- return resolved;
203
- // Use the simple infrastructure resolver - resolves aws: prefixes only and
204
- // does NOT recurse into github-app: markers, so no circularity.
205
- const wsResolved = await resolveEnvValuesSimple(wsEnv$.env);
206
- const appId = wsResolved.hereyaGithubAppId;
207
- const installationIdFromEnv = wsResolved.hereyaGithubAppInstallationId;
208
- const privateKey = wsResolved.hereyaGithubAppPrivateKey;
209
- if (!appId || !privateKey) {
210
- console.warn('hereya: github-app marker present but workspace env missing hereyaGithubAppId / hereyaGithubAppPrivateKey; leaving marker unresolved');
211
- return resolved;
212
- }
213
- const out = { ...resolved };
214
- for (const [k, v] of Object.entries(resolved)) {
215
- if (typeof v !== 'string' || !v.startsWith('github-app:'))
216
- continue;
217
- const installationId = v.slice('github-app:'.length) || installationIdFromEnv;
218
- if (!installationId)
219
- continue;
220
- try {
221
- // eslint-disable-next-line no-await-in-loop
222
- out[k] = await mintInstallationToken({ appId, installationId, privateKey });
223
- }
224
- catch (error) {
225
- console.warn(`hereya: failed to mint github-app installation token: ${error.message}`);
226
- }
227
- }
228
- return out;
229
- }
230
178
  }
@@ -0,0 +1,88 @@
1
+ import type { GetInfrastructureOutput } from '../infrastructure/registry.js';
2
+ /**
3
+ * Pure-function env-value resolver, factored out of `LocalExecutor.resolveEnvValues`.
4
+ *
5
+ * The functions in this module are deliberately free of any class state
6
+ * (`this`) or hard-coded global lookups so they can be invoked from contexts
7
+ * that do not include the full CLI runtime — e.g. the broker Lambda that
8
+ * imports `hereya-cli` as a published npm dependency.
9
+ *
10
+ * Dependencies that the resolver needs (the infrastructure-provider lookup,
11
+ * the workspace-env lookup, and the GitHub-app installation-token minter) are
12
+ * all injected via the `providers` argument so callers can supply only the
13
+ * pieces they have available.
14
+ */
15
+ import { InfrastructureType } from '../infrastructure/common.js';
16
+ export type ResolveEnvValuesInput = {
17
+ env: {
18
+ [key: string]: string;
19
+ };
20
+ markSecret?: boolean;
21
+ project?: string;
22
+ workspace?: string;
23
+ };
24
+ export type ResolveEnvValuesOutput = {
25
+ [key: string]: string;
26
+ };
27
+ /**
28
+ * Look up an infrastructure provider by type. The Lambda passes a thin wrapper
29
+ * over its registered `aws` provider; the CLI passes the full registry.
30
+ */
31
+ export type GetInfrastructureFn = (input: {
32
+ type: InfrastructureType;
33
+ }) => GetInfrastructureOutput;
34
+ /**
35
+ * Fetch a workspace's stored env values (used to find the GitHub-app
36
+ * configuration when resolving `github-app:` markers).
37
+ */
38
+ export type GetWorkspaceEnvFn = (input: {
39
+ project: string;
40
+ workspace: string;
41
+ }) => Promise<{
42
+ env: {
43
+ [key: string]: string;
44
+ };
45
+ success: true;
46
+ } | {
47
+ reason: string;
48
+ success: false;
49
+ }>;
50
+ /**
51
+ * Resolve a workspace env map without recursing into github-app markers.
52
+ * Used to dereference the `aws:` (or other infra) values for the GitHub-app
53
+ * config keys.
54
+ */
55
+ export type ResolveSimpleEnvFn = (env: Record<string, string>) => Promise<Record<string, string>>;
56
+ /**
57
+ * Mint a GitHub-app installation token.
58
+ */
59
+ export type MintInstallationTokenFn = (input: {
60
+ appId: string;
61
+ installationId: string;
62
+ privateKey: string;
63
+ }) => Promise<string>;
64
+ export interface ResolveEnvProviders {
65
+ /** Infrastructure-provider lookup — required. */
66
+ getInfrastructure: GetInfrastructureFn;
67
+ /**
68
+ * Optional GitHub-app dependencies. If any of them is missing, `github-app:`
69
+ * markers are left unresolved (the Lambda path can choose not to wire
70
+ * GitHub-app support at all).
71
+ */
72
+ getWorkspaceEnv?: GetWorkspaceEnvFn;
73
+ mintInstallationToken?: MintInstallationTokenFn;
74
+ resolveSimpleEnv?: ResolveSimpleEnvFn;
75
+ }
76
+ /**
77
+ * Resolve `infra:` prefixes and `github-app:` markers in an env map. Pure
78
+ * function — no module-level state, no `this`, side effects are limited to
79
+ * what the injected providers do.
80
+ */
81
+ export declare function resolveEnvValues(input: ResolveEnvValuesInput, providers: ResolveEnvProviders): Promise<ResolveEnvValuesOutput>;
82
+ /**
83
+ * Walk the resolved env, replace any `github-app:<installationId?>` marker
84
+ * with a freshly-minted installation token. Reads the GitHub-app config from
85
+ * the workspace env (`hereyaGithubAppId`, `hereyaGithubAppPrivateKey`,
86
+ * optional `hereyaGithubAppInstallationId`).
87
+ */
88
+ export declare function resolveGithubAppMarkers(resolved: Record<string, string>, input: ResolveEnvValuesInput, providers: ResolveEnvProviders): Promise<Record<string, string>>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Resolve `infra:` prefixes and `github-app:` markers in an env map. Pure
3
+ * function — no module-level state, no `this`, side effects are limited to
4
+ * what the injected providers do.
5
+ */
6
+ export async function resolveEnvValues(input, providers) {
7
+ const resolved = Object.fromEntries(await Promise.all(Object.entries(input.env).map(async ([key, value]) => {
8
+ // Check if value has infra prefix (contains ':' and first part is valid infra)
9
+ const colonIndex = value.indexOf(':');
10
+ if (colonIndex === -1) {
11
+ // No colon - plain value, return as-is
12
+ return [key, value];
13
+ }
14
+ const infraType = value.slice(0, colonIndex);
15
+ const infra$ = providers.getInfrastructure({ type: infraType });
16
+ if (!infra$.supported) {
17
+ // Unknown infra type - treat as plain value, return as-is
18
+ return [key, value];
19
+ }
20
+ const { infrastructure } = infra$;
21
+ const valueWithoutInfra = value.slice(colonIndex + 1);
22
+ const { isSecret, value: resolvedValue } = await infrastructure.resolveEnv({
23
+ value: valueWithoutInfra,
24
+ });
25
+ const finalValue = input.markSecret && isSecret ? `secret://${resolvedValue}` : resolvedValue;
26
+ return [key, finalValue];
27
+ })));
28
+ return resolveGithubAppMarkers(resolved, input, providers);
29
+ }
30
+ /**
31
+ * Walk the resolved env, replace any `github-app:<installationId?>` marker
32
+ * with a freshly-minted installation token. Reads the GitHub-app config from
33
+ * the workspace env (`hereyaGithubAppId`, `hereyaGithubAppPrivateKey`,
34
+ * optional `hereyaGithubAppInstallationId`).
35
+ */
36
+ export async function resolveGithubAppMarkers(resolved, input, providers) {
37
+ const needsToken = Object.values(resolved).some((v) => typeof v === 'string' && v.startsWith('github-app:'));
38
+ if (!needsToken)
39
+ return resolved;
40
+ if (!input.workspace || !input.project)
41
+ return resolved;
42
+ const { getWorkspaceEnv, mintInstallationToken, resolveSimpleEnv } = providers;
43
+ if (!getWorkspaceEnv || !mintInstallationToken || !resolveSimpleEnv)
44
+ return resolved;
45
+ const wsEnv$ = await getWorkspaceEnv({
46
+ project: input.project,
47
+ workspace: input.workspace,
48
+ });
49
+ if (!wsEnv$.success)
50
+ return resolved;
51
+ // Use the simple infrastructure resolver - resolves aws: prefixes only and
52
+ // does NOT recurse into github-app: markers, so no circularity.
53
+ const wsResolved = await resolveSimpleEnv(wsEnv$.env);
54
+ const appId = wsResolved.hereyaGithubAppId;
55
+ const installationIdFromEnv = wsResolved.hereyaGithubAppInstallationId;
56
+ const privateKey = wsResolved.hereyaGithubAppPrivateKey;
57
+ if (!appId || !privateKey) {
58
+ console.warn('hereya: github-app marker present but workspace env missing hereyaGithubAppId / hereyaGithubAppPrivateKey; leaving marker unresolved');
59
+ return resolved;
60
+ }
61
+ const out = { ...resolved };
62
+ for (const [k, v] of Object.entries(resolved)) {
63
+ if (typeof v !== 'string' || !v.startsWith('github-app:'))
64
+ continue;
65
+ const installationId = v.slice('github-app:'.length) || installationIdFromEnv;
66
+ if (!installationId)
67
+ continue;
68
+ try {
69
+ // eslint-disable-next-line no-await-in-loop
70
+ out[k] = await mintInstallationToken({ appId, installationId, privateKey });
71
+ }
72
+ catch (error) {
73
+ console.warn(`hereya: failed to mint github-app installation token: ${error.message}`);
74
+ }
75
+ }
76
+ return out;
77
+ }