hereya-cli 0.100.5 → 0.101.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.
- package/README.md +66 -66
- package/dist/commands/executor/start/index.js +2 -2
- package/dist/iac/cdk.js +15 -5
- package/dist/iac/terraform.js +9 -9
- package/dist/lib/app-source.d.ts +24 -0
- package/dist/lib/app-source.js +44 -7
- package/dist/lib/executor-start/execute-app-job.d.ts +2 -1
- package/dist/lib/executor-start/execute-app-job.js +55 -2
- package/dist/lib/executor-start/job-dispatch.js +1 -1
- package/dist/lib/object-utils.d.ts +20 -0
- package/dist/lib/object-utils.js +30 -0
- package/oclif.manifest.json +585 -585
- package/package.json +1 -1
package/dist/iac/terraform.js
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { Extract as decompress } from 'unzip-stream';
|
|
7
|
-
import { mapObject } from '../lib/object-utils.js';
|
|
7
|
+
import { mapObject, paramToEnvString } from '../lib/object-utils.js';
|
|
8
8
|
import { runShell } from '../lib/shell.js';
|
|
9
9
|
export class Terraform {
|
|
10
10
|
async apply(input) {
|
|
@@ -46,12 +46,12 @@ export class Terraform {
|
|
|
46
46
|
env: {
|
|
47
47
|
...mapObject(input.env ?? {}, (key, value) => [
|
|
48
48
|
`TF_VAR_${key}`,
|
|
49
|
-
|
|
49
|
+
paramToEnvString(value),
|
|
50
50
|
]),
|
|
51
51
|
...input.env,
|
|
52
52
|
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
53
53
|
`TF_VAR_${key}`,
|
|
54
|
-
|
|
54
|
+
paramToEnvString(value),
|
|
55
55
|
]),
|
|
56
56
|
},
|
|
57
57
|
logger: input.logger,
|
|
@@ -61,12 +61,12 @@ export class Terraform {
|
|
|
61
61
|
env: {
|
|
62
62
|
...mapObject(input.env ?? {}, (key, value) => [
|
|
63
63
|
`TF_VAR_${key}`,
|
|
64
|
-
|
|
64
|
+
paramToEnvString(value),
|
|
65
65
|
]),
|
|
66
66
|
...input.env,
|
|
67
67
|
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
68
68
|
`TF_VAR_${key}`,
|
|
69
|
-
|
|
69
|
+
paramToEnvString(value),
|
|
70
70
|
]),
|
|
71
71
|
},
|
|
72
72
|
logger: input.logger,
|
|
@@ -124,12 +124,12 @@ export class Terraform {
|
|
|
124
124
|
env: {
|
|
125
125
|
...mapObject(input.env ?? {}, (key, value) => [
|
|
126
126
|
`TF_VAR_${key}`,
|
|
127
|
-
|
|
127
|
+
paramToEnvString(value),
|
|
128
128
|
]),
|
|
129
129
|
...input.env,
|
|
130
130
|
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
131
131
|
`TF_VAR_${key}`,
|
|
132
|
-
|
|
132
|
+
paramToEnvString(value),
|
|
133
133
|
]),
|
|
134
134
|
},
|
|
135
135
|
logger: input.logger,
|
|
@@ -140,12 +140,12 @@ export class Terraform {
|
|
|
140
140
|
env: {
|
|
141
141
|
...mapObject(input.env ?? {}, (key, value) => [
|
|
142
142
|
`TF_VAR_${key}`,
|
|
143
|
-
|
|
143
|
+
paramToEnvString(value),
|
|
144
144
|
]),
|
|
145
145
|
...input.env,
|
|
146
146
|
...mapObject(input.parameters ?? {}, (key, value) => [
|
|
147
147
|
`TF_VAR_${key}`,
|
|
148
|
-
|
|
148
|
+
paramToEnvString(value),
|
|
149
149
|
]),
|
|
150
150
|
},
|
|
151
151
|
logger: input.logger,
|
package/dist/lib/app-source.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { Logger } from './log.js';
|
|
2
2
|
export interface DownloadAppSourceInput {
|
|
3
|
+
/**
|
|
4
|
+
* Optional git credentials resolved via the hereya GitHub-App credential
|
|
5
|
+
* mechanism. When `password` (and optionally `username`) are provided, the
|
|
6
|
+
* clone is performed through the hereya credential helper so PRIVATE app
|
|
7
|
+
* repositories can be cloned on a headless executor. When omitted, an
|
|
8
|
+
* unauthenticated clone is performed (public-repo path preserved).
|
|
9
|
+
*/
|
|
10
|
+
auth?: {
|
|
11
|
+
/** Absolute path to the hereya binary used as the credential helper. */
|
|
12
|
+
hereyaBinPath: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
username?: string;
|
|
15
|
+
};
|
|
3
16
|
commit?: string;
|
|
4
17
|
logger?: Logger;
|
|
5
18
|
repository: string;
|
|
@@ -9,6 +22,17 @@ export interface DownloadAppSourceOutput {
|
|
|
9
22
|
cleanup: () => Promise<void>;
|
|
10
23
|
rootDir: string;
|
|
11
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Minimal surface of `simple-git` used by this module. Kept narrow so tests can
|
|
27
|
+
* supply a lightweight fake.
|
|
28
|
+
*/
|
|
29
|
+
export interface SimpleGitLike {
|
|
30
|
+
checkout(commit: string): Promise<unknown>;
|
|
31
|
+
clone(repo: string, target: string, options?: string[]): Promise<unknown>;
|
|
32
|
+
raw(args: string[]): Promise<Buffer | string>;
|
|
33
|
+
}
|
|
34
|
+
export type SimpleGitFactory = (baseDir?: string) => SimpleGitLike;
|
|
35
|
+
export declare function setSimpleGitFactory(factory: SimpleGitFactory | undefined): void;
|
|
12
36
|
/**
|
|
13
37
|
* Clone an app's source repository (by repo URL + commit SHA) into a fresh
|
|
14
38
|
* temp dir under ~/.hereya/downloads/<id>. Returns the rootDir path along
|
package/dist/lib/app-source.js
CHANGED
|
@@ -2,6 +2,19 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { gitUtils } from './git-utils.js';
|
|
6
|
+
// DI seam for tests. Defaults to the real `simple-git` (loaded lazily so the
|
|
7
|
+
// module stays cheap to import). Tests replace this via `setSimpleGitFactory`.
|
|
8
|
+
let simpleGitFactory;
|
|
9
|
+
export function setSimpleGitFactory(factory) {
|
|
10
|
+
simpleGitFactory = factory;
|
|
11
|
+
}
|
|
12
|
+
async function getSimpleGit() {
|
|
13
|
+
if (simpleGitFactory)
|
|
14
|
+
return simpleGitFactory;
|
|
15
|
+
const { simpleGit } = await import('simple-git');
|
|
16
|
+
return simpleGit;
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* Clone an app's source repository (by repo URL + commit SHA) into a fresh
|
|
7
20
|
* temp dir under ~/.hereya/downloads/<id>. Returns the rootDir path along
|
|
@@ -24,16 +37,40 @@ export const appSource = {
|
|
|
24
37
|
}
|
|
25
38
|
};
|
|
26
39
|
try {
|
|
27
|
-
const
|
|
28
|
-
const git = simpleGit();
|
|
40
|
+
const simpleGit = await getSimpleGit();
|
|
29
41
|
input.logger?.debug?.(`Cloning app source from ${input.repository}${input.commit ? `@${input.commit}` : ''} into ${rootDir}`);
|
|
30
|
-
if (input.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
if (input.auth?.password) {
|
|
43
|
+
// Authenticated clone through the hereya credential helper. We always
|
|
44
|
+
// do a full clone here (no shallow/no-checkout optimization) because
|
|
45
|
+
// the credential helper path uses a plain `git clone`; the optional
|
|
46
|
+
// commit checkout is performed afterwards.
|
|
47
|
+
input.logger?.debug?.('Using hereya credential helper for authenticated app-source clone');
|
|
48
|
+
const cloneResult = await gitUtils.cloneWithCredentialHelper({
|
|
49
|
+
gitUrl: input.repository,
|
|
50
|
+
hereyaBinPath: input.auth.hereyaBinPath,
|
|
51
|
+
password: input.auth.password,
|
|
52
|
+
targetDir: rootDir,
|
|
53
|
+
username: input.auth.username,
|
|
54
|
+
});
|
|
55
|
+
if (!cloneResult.success) {
|
|
56
|
+
throw new Error(cloneResult.reason);
|
|
57
|
+
}
|
|
58
|
+
if (input.commit) {
|
|
59
|
+
const repoGit = simpleGit(rootDir);
|
|
60
|
+
await repoGit.checkout(input.commit);
|
|
61
|
+
}
|
|
34
62
|
}
|
|
35
63
|
else {
|
|
36
|
-
|
|
64
|
+
// Unauthenticated clone (public-repo path).
|
|
65
|
+
const git = simpleGit();
|
|
66
|
+
if (input.commit) {
|
|
67
|
+
await git.clone(input.repository, rootDir, ['--no-checkout']);
|
|
68
|
+
const repoGit = simpleGit(rootDir);
|
|
69
|
+
await repoGit.checkout(input.commit);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await git.clone(input.repository, rootDir, ['--depth=1']);
|
|
73
|
+
}
|
|
37
74
|
}
|
|
38
75
|
if (input.sha256) {
|
|
39
76
|
const repoGit = simpleGit(rootDir);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { LocalExecutor } from '../../executor/local.js';
|
|
2
3
|
import { Logger } from '../../lib/log.js';
|
|
3
4
|
export type AppJobResult = {
|
|
4
5
|
reason: string;
|
|
@@ -10,4 +11,4 @@ export declare function executeAppJob(job: {
|
|
|
10
11
|
id: string;
|
|
11
12
|
payload: any;
|
|
12
13
|
type: string;
|
|
13
|
-
}, cloudBackend: CloudBackend, logger: Logger): Promise<AppJobResult>;
|
|
14
|
+
}, cloudBackend: CloudBackend, logger: Logger, executor: LocalExecutor): Promise<AppJobResult>;
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import * as appSourceLib from '../../lib/app-source.js';
|
|
4
4
|
import { shellUtils } from '../../lib/shell.js';
|
|
5
5
|
import { sanitizeWorkspaceForFilename } from './format.js';
|
|
6
|
-
export async function executeAppJob(job, cloudBackend, logger) {
|
|
6
|
+
export async function executeAppJob(job, cloudBackend, logger, executor) {
|
|
7
7
|
const isDeploy = job.type === 'app-deploy';
|
|
8
8
|
const subcommand = isDeploy ? 'deploy' : 'destroy';
|
|
9
9
|
const payload = job.payload;
|
|
@@ -17,7 +17,8 @@ export async function executeAppJob(job, cloudBackend, logger) {
|
|
|
17
17
|
let cleanup;
|
|
18
18
|
try {
|
|
19
19
|
logger.info(`Downloading app source ${repository}${commit ? `@${commit}` : ''}...`);
|
|
20
|
-
const
|
|
20
|
+
const auth = await resolveAppRepoCredentials({ appName, executor, logger, repository, workspace });
|
|
21
|
+
const downloadResult = await appSourceLib.appSource.download({ auth, commit, logger, repository, sha256 });
|
|
21
22
|
cleanup = downloadResult.cleanup;
|
|
22
23
|
const { rootDir } = downloadResult;
|
|
23
24
|
await cloudBackend.updateAppDeployment({
|
|
@@ -74,6 +75,58 @@ export async function executeAppJob(job, cloudBackend, logger) {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve git credentials for cloning the app's source repository, reusing the
|
|
80
|
+
* SAME hereya GitHub-App credential mechanism used for package/project clones.
|
|
81
|
+
*
|
|
82
|
+
* We build a synthetic git env map (`hereyaGitRemoteUrl` + a `github-app:`
|
|
83
|
+
* marker for the password) and run it through `executor.resolveEnvValues`,
|
|
84
|
+
* which:
|
|
85
|
+
* 1. reads the GitHub-App config (`hereyaGithubAppId`,
|
|
86
|
+
* `hereyaGithubAppPrivateKey`, optional `hereyaGithubAppInstallationId`)
|
|
87
|
+
* from the workspace env, and
|
|
88
|
+
* 2. mints an installation token scoped to the repo named by
|
|
89
|
+
* `hereyaGitRemoteUrl`.
|
|
90
|
+
*
|
|
91
|
+
* If the GitHub-App config is not available (token not minted, marker left
|
|
92
|
+
* unresolved), we return `undefined` so the caller falls back to an
|
|
93
|
+
* unauthenticated clone (public-repo path preserved).
|
|
94
|
+
*/
|
|
95
|
+
async function resolveAppRepoCredentials(input) {
|
|
96
|
+
const { appName, executor, logger, repository, workspace } = input;
|
|
97
|
+
try {
|
|
98
|
+
const gitEnvToResolve = {
|
|
99
|
+
// Empty installation id => resolveGithubAppMarkers falls back to
|
|
100
|
+
// hereyaGithubAppInstallationId from the workspace env.
|
|
101
|
+
hereyaGitPassword: 'github-app:',
|
|
102
|
+
// Peer URL used to scope the minted installation token to this repo.
|
|
103
|
+
hereyaGitRemoteUrl: repository,
|
|
104
|
+
hereyaGitUsername: 'x-access-token',
|
|
105
|
+
};
|
|
106
|
+
// For apps, workspace env is keyed by the app name (see LocalExecutor.getWorkspaceEnv).
|
|
107
|
+
const resolved = await executor.resolveEnvValues({
|
|
108
|
+
env: gitEnvToResolve,
|
|
109
|
+
project: appName,
|
|
110
|
+
workspace,
|
|
111
|
+
});
|
|
112
|
+
const password = resolved.hereyaGitPassword;
|
|
113
|
+
// If the marker was left unresolved (no GitHub-App config available), the
|
|
114
|
+
// value still starts with `github-app:` — treat that as "no auth".
|
|
115
|
+
if (!password || password.startsWith('github-app:')) {
|
|
116
|
+
logger.debug?.('No hereya GitHub-App credentials resolved for app source; falling back to unauthenticated clone');
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
hereyaBinPath: process.argv[1],
|
|
121
|
+
password,
|
|
122
|
+
username: resolved.hereyaGitUsername || 'x-access-token',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logger.debug?.(`Failed to resolve hereya GitHub-App credentials for app source (${error?.message ?? error}); falling back to unauthenticated clone`);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
77
130
|
async function runAppSubcommand(input) {
|
|
78
131
|
const { appName, logger, paramFlags, rootDir, subcommand, versionFlags, workspace } = input;
|
|
79
132
|
let runResult;
|
|
@@ -90,7 +90,7 @@ export async function dispatchJob(job, executor, cloudBackend, hooks) {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
if (job.type === 'app-deploy' || job.type === 'app-destroy') {
|
|
93
|
-
const appJobResult = await executeAppJob(job, cloudBackend, logger);
|
|
93
|
+
const appJobResult = await executeAppJob(job, cloudBackend, logger, executor);
|
|
94
94
|
await finalize(appJobResult, job.type);
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
export declare function mapObject(obj: object, fn: (key: string, value: any) => [string, any]): object;
|
|
2
|
+
/**
|
|
3
|
+
* Coerce a parameter value (as parsed from a hereyavars YAML file or
|
|
4
|
+
* forwarded through the executor) into the string form expected by the
|
|
5
|
+
* package's CDK/Terraform process when injected as an environment variable
|
|
6
|
+
* or CLI argument.
|
|
7
|
+
*
|
|
8
|
+
* - Strings (including pre-stringified JSON) pass through unchanged so the
|
|
9
|
+
* existing "stringify it yourself" workaround keeps working.
|
|
10
|
+
* - Numbers and booleans become their plain string representation
|
|
11
|
+
* (matching the previous behavior of `String(value)`).
|
|
12
|
+
* - Arrays and objects are JSON-encoded so the receiving package can
|
|
13
|
+
* `JSON.parse(process.env.foo)` instead of receiving "[object Object]"
|
|
14
|
+
* or a comma-joined `Array.prototype.toString()`.
|
|
15
|
+
* - `null` and `undefined` become an empty string.
|
|
16
|
+
*
|
|
17
|
+
* Intended to be applied at the single boundary where the env map for the
|
|
18
|
+
* package process is built — not earlier in the pipeline, so the parsed
|
|
19
|
+
* YAML can stay structured everywhere else in the CLI.
|
|
20
|
+
*/
|
|
21
|
+
export declare function paramToEnvString(value: unknown): string;
|
|
2
22
|
export declare function arrayOfStringToObject(arr: string[], keyValueSeparator?: string): {
|
|
3
23
|
[k: string]: string;
|
|
4
24
|
};
|
package/dist/lib/object-utils.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
export function mapObject(obj, fn) {
|
|
2
2
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => fn(key, value)));
|
|
3
3
|
}
|
|
4
|
+
/**
|
|
5
|
+
* Coerce a parameter value (as parsed from a hereyavars YAML file or
|
|
6
|
+
* forwarded through the executor) into the string form expected by the
|
|
7
|
+
* package's CDK/Terraform process when injected as an environment variable
|
|
8
|
+
* or CLI argument.
|
|
9
|
+
*
|
|
10
|
+
* - Strings (including pre-stringified JSON) pass through unchanged so the
|
|
11
|
+
* existing "stringify it yourself" workaround keeps working.
|
|
12
|
+
* - Numbers and booleans become their plain string representation
|
|
13
|
+
* (matching the previous behavior of `String(value)`).
|
|
14
|
+
* - Arrays and objects are JSON-encoded so the receiving package can
|
|
15
|
+
* `JSON.parse(process.env.foo)` instead of receiving "[object Object]"
|
|
16
|
+
* or a comma-joined `Array.prototype.toString()`.
|
|
17
|
+
* - `null` and `undefined` become an empty string.
|
|
18
|
+
*
|
|
19
|
+
* Intended to be applied at the single boundary where the env map for the
|
|
20
|
+
* package process is built — not earlier in the pipeline, so the parsed
|
|
21
|
+
* YAML can stay structured everywhere else in the CLI.
|
|
22
|
+
*/
|
|
23
|
+
export function paramToEnvString(value) {
|
|
24
|
+
if (value === null || value === undefined)
|
|
25
|
+
return '';
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
// Objects/arrays -> JSON. Round-trippable and what package authors expect.
|
|
32
|
+
return JSON.stringify(value);
|
|
33
|
+
}
|
|
4
34
|
export function arrayOfStringToObject(arr, keyValueSeparator = '=') {
|
|
5
35
|
return Object.fromEntries(arr.map((item) => {
|
|
6
36
|
const [key, value] = item.split(keyValueSeparator);
|