hereya-cli 0.89.0 → 0.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -65
- package/dist/backend/cloud/cloud-backend/apps-deploy.d.ts +69 -0
- package/dist/backend/cloud/cloud-backend/apps-deploy.js +94 -0
- package/dist/backend/cloud/cloud-backend/apps-versions.d.ts +69 -0
- package/dist/backend/cloud/cloud-backend/apps-versions.js +113 -0
- package/dist/backend/cloud/cloud-backend/executor-jobs.d.ts +75 -0
- package/dist/backend/cloud/cloud-backend/executor-jobs.js +110 -0
- package/dist/backend/cloud/cloud-backend/misc.d.ts +5 -0
- package/dist/backend/cloud/cloud-backend/misc.js +78 -0
- package/dist/backend/cloud/cloud-backend/packages-publish.d.ts +3 -0
- package/dist/backend/cloud/cloud-backend/packages-publish.js +99 -0
- package/dist/backend/cloud/cloud-backend/packages-registry.d.ts +6 -0
- package/dist/backend/cloud/cloud-backend/packages-registry.js +146 -0
- package/dist/backend/cloud/cloud-backend/packages-workspace.d.ts +4 -0
- package/dist/backend/cloud/cloud-backend/packages-workspace.js +50 -0
- package/dist/backend/cloud/cloud-backend/projects.d.ts +7 -0
- package/dist/backend/cloud/cloud-backend/projects.js +122 -0
- package/dist/backend/cloud/cloud-backend/state.d.ts +6 -0
- package/dist/backend/cloud/cloud-backend/state.js +86 -0
- package/dist/backend/cloud/cloud-backend/utils.d.ts +55 -0
- package/dist/backend/cloud/cloud-backend/utils.js +56 -0
- package/dist/backend/cloud/cloud-backend/workspace-env.d.ts +5 -0
- package/dist/backend/cloud/cloud-backend/workspace-env.js +63 -0
- package/dist/backend/cloud/cloud-backend/workspaces.d.ts +7 -0
- package/dist/backend/cloud/cloud-backend/workspaces.js +124 -0
- package/dist/backend/cloud/cloud-backend.d.ts +56 -126
- package/dist/backend/cloud/cloud-backend.js +95 -1089
- package/dist/backend/cloud/cloud-backend.test.setup.d.ts +13 -0
- package/dist/backend/cloud/cloud-backend.test.setup.js +14 -0
- package/dist/backend/local.setup.d.ts +10 -0
- package/dist/backend/local.setup.js +20 -0
- package/dist/commands/executor/start/index.d.ts +1 -11
- package/dist/commands/executor/start/index.js +13 -498
- package/dist/commands/import-repo/index.js +2 -2
- package/dist/lib/env/test.setup.d.ts +7 -0
- package/dist/lib/env/test.setup.js +18 -0
- package/dist/lib/executor-start/auth.d.ts +2 -0
- package/dist/lib/executor-start/auth.js +21 -0
- package/dist/lib/executor-start/execute-app-job.d.ts +13 -0
- package/dist/lib/executor-start/execute-app-job.js +146 -0
- package/dist/lib/executor-start/execute-deploy-job.d.ts +14 -0
- package/dist/lib/executor-start/execute-deploy-job.js +64 -0
- package/dist/lib/executor-start/execute-init-job.d.ts +14 -0
- package/dist/lib/executor-start/execute-init-job.js +135 -0
- package/dist/lib/executor-start/format.d.ts +13 -0
- package/dist/lib/executor-start/format.js +22 -0
- package/dist/lib/executor-start/job-dispatch.d.ts +15 -0
- package/dist/lib/executor-start/job-dispatch.js +89 -0
- package/dist/lib/package/index.d.ts +4 -4
- package/oclif.manifest.json +52 -52
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import nock from 'nock';
|
|
2
|
+
import { CloudBackend } from './cloud-backend.js';
|
|
3
|
+
export declare const cloudConfig: {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
export interface CloudBackendTestContext {
|
|
10
|
+
backend: CloudBackend;
|
|
11
|
+
scope: nock.Scope;
|
|
12
|
+
}
|
|
13
|
+
export declare function setupCloudBackendTest(): CloudBackendTestContext;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import nock from 'nock';
|
|
2
|
+
import { CloudBackend } from './cloud-backend.js';
|
|
3
|
+
export const cloudConfig = {
|
|
4
|
+
accessToken: 'test-token',
|
|
5
|
+
clientId: 'test-client-id',
|
|
6
|
+
refreshToken: 'test-refresh-token',
|
|
7
|
+
url: 'http://test.com',
|
|
8
|
+
};
|
|
9
|
+
export function setupCloudBackendTest() {
|
|
10
|
+
return {
|
|
11
|
+
backend: new CloudBackend(cloudConfig),
|
|
12
|
+
scope: nock(cloudConfig.url),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface LocalBackendTestContext {
|
|
2
|
+
homeDir: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Shared setup helpers for LocalFileBackend tests. Each split test file calls
|
|
6
|
+
* `setupLocalBackendTest()` from its `beforeEach` and uses the returned context
|
|
7
|
+
* to access the temp homeDir. Stubbing of `os.homedir` is restored by sinon.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setupLocalBackendTest(): Promise<LocalBackendTestContext>;
|
|
10
|
+
export declare function teardownLocalBackendTest(context: LocalBackendTestContext): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import * as sinon from 'sinon';
|
|
6
|
+
/**
|
|
7
|
+
* Shared setup helpers for LocalFileBackend tests. Each split test file calls
|
|
8
|
+
* `setupLocalBackendTest()` from its `beforeEach` and uses the returned context
|
|
9
|
+
* to access the temp homeDir. Stubbing of `os.homedir` is restored by sinon.
|
|
10
|
+
*/
|
|
11
|
+
export async function setupLocalBackendTest() {
|
|
12
|
+
const homeDir = path.join(os.tmpdir(), 'hereya-test', randomUUID());
|
|
13
|
+
sinon.stub(os, 'homedir').returns(homeDir);
|
|
14
|
+
await fs.mkdir(path.join(homeDir, '.hereya', 'state', 'workspaces'), { recursive: true });
|
|
15
|
+
return { homeDir };
|
|
16
|
+
}
|
|
17
|
+
export async function teardownLocalBackendTest(context) {
|
|
18
|
+
sinon.restore();
|
|
19
|
+
await fs.rm(context.homeDir, { force: true, recursive: true });
|
|
20
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
|
|
3
|
-
* Strip credentials from log strings before flushing them to the cloud.
|
|
4
|
-
* Removes:
|
|
5
|
-
* - any occurrence of the actual hereyaGitPassword value
|
|
6
|
-
* - userinfo from URLs (https://user:pass@host -> https://host)
|
|
7
|
-
*/
|
|
8
|
-
export declare function redactCredentials(str: string, password?: string): string;
|
|
2
|
+
export { redactCredentials } from '../../../lib/executor-start/format.js';
|
|
9
3
|
export default class ExecutorStart extends Command {
|
|
10
4
|
static description: string;
|
|
11
5
|
static examples: string[];
|
|
@@ -15,9 +9,5 @@ export default class ExecutorStart extends Command {
|
|
|
15
9
|
};
|
|
16
10
|
run(): Promise<void>;
|
|
17
11
|
private executeAppJob;
|
|
18
|
-
private executeDeployJob;
|
|
19
12
|
private executeInitJob;
|
|
20
|
-
private executeJob;
|
|
21
|
-
private markAppDeploymentFailed;
|
|
22
|
-
private patchAppFinalState;
|
|
23
13
|
}
|
|
@@ -1,59 +1,14 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
3
|
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
4
|
import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
|
|
7
|
-
import { loginWithToken } from '../../../backend/cloud/login.js';
|
|
8
|
-
import { saveCloudCredentials } from '../../../backend/config.js';
|
|
9
5
|
import { clearBackend, getBackend } from '../../../backend/index.js';
|
|
10
6
|
import { LocalExecutor } from '../../../executor/local.js';
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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('/', '-');
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Strip credentials from log strings before flushing them to the cloud.
|
|
25
|
-
* Removes:
|
|
26
|
-
* - any occurrence of the actual hereyaGitPassword value
|
|
27
|
-
* - userinfo from URLs (https://user:pass@host -> https://host)
|
|
28
|
-
*/
|
|
29
|
-
export function redactCredentials(str, password) {
|
|
30
|
-
let out = str;
|
|
31
|
-
if (password) {
|
|
32
|
-
// Replace every exact occurrence of the password with a placeholder.
|
|
33
|
-
out = out.split(password).join('[REDACTED]');
|
|
34
|
-
}
|
|
35
|
-
// Strip userinfo from URLs (http/https/git).
|
|
36
|
-
out = out.replaceAll(/(https?:\/\/|git:\/\/)([^/\s@]+@)/g, '$1');
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
async function createAuthenticatedBackend(hereyaToken, cloudUrl) {
|
|
40
|
-
const loginResult = await loginWithToken(cloudUrl, hereyaToken);
|
|
41
|
-
if (!loginResult.success) {
|
|
42
|
-
throw new Error(`Failed to authenticate: ${loginResult.error}`);
|
|
43
|
-
}
|
|
44
|
-
await saveCloudCredentials({
|
|
45
|
-
accessToken: loginResult.accessToken,
|
|
46
|
-
clientId: loginResult.clientId,
|
|
47
|
-
refreshToken: loginResult.refreshToken,
|
|
48
|
-
url: cloudUrl,
|
|
49
|
-
});
|
|
50
|
-
return new CloudBackend({
|
|
51
|
-
accessToken: loginResult.accessToken,
|
|
52
|
-
clientId: loginResult.clientId,
|
|
53
|
-
refreshToken: loginResult.refreshToken,
|
|
54
|
-
url: cloudUrl,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
7
|
+
import { createAuthenticatedBackend } from '../../../lib/executor-start/auth.js';
|
|
8
|
+
import { executeAppJob } from '../../../lib/executor-start/execute-app-job.js';
|
|
9
|
+
import { executeInitJob } from '../../../lib/executor-start/execute-init-job.js';
|
|
10
|
+
import { dispatchJob } from '../../../lib/executor-start/job-dispatch.js';
|
|
11
|
+
export { redactCredentials } from '../../../lib/executor-start/format.js';
|
|
57
12
|
export default class ExecutorStart extends Command {
|
|
58
13
|
static description = `Start the remote executor process (polls for jobs from hereya cloud).
|
|
59
14
|
|
|
@@ -100,7 +55,6 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
100
55
|
const executorId = `${os.hostname()}-${randomUUID().slice(0, 8)}`;
|
|
101
56
|
this.log(`Starting executor ${executorId} for workspace: ${flags.workspace} (concurrency: ${concurrency})`);
|
|
102
57
|
this.log('Polling for jobs...');
|
|
103
|
-
// Handle graceful shutdown
|
|
104
58
|
const state = { shuttingDown: false };
|
|
105
59
|
const shutdown = () => {
|
|
106
60
|
this.log('Shutting down executor...');
|
|
@@ -109,9 +63,9 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
109
63
|
process.on('SIGINT', shutdown);
|
|
110
64
|
process.on('SIGTERM', shutdown);
|
|
111
65
|
const activeJobs = new Set();
|
|
66
|
+
const hooks = { log: (m) => this.log(m), warn: (m) => this.warn(m) };
|
|
112
67
|
while (!state.shuttingDown) {
|
|
113
68
|
try {
|
|
114
|
-
// Wait for a slot to open up if at concurrency limit
|
|
115
69
|
if (activeJobs.size >= concurrency) {
|
|
116
70
|
// eslint-disable-next-line no-await-in-loop
|
|
117
71
|
await Promise.race(activeJobs);
|
|
@@ -138,474 +92,35 @@ Set the HEREYA_CLOUD_URL environment variable to target a specific hereya cloud
|
|
|
138
92
|
continue;
|
|
139
93
|
}
|
|
140
94
|
if (!pollResult.job) {
|
|
141
|
-
// No job available, continue polling
|
|
142
95
|
continue;
|
|
143
96
|
}
|
|
144
97
|
const { job } = pollResult;
|
|
145
98
|
this.log(`Received job ${job.id} (${job.type}) [${activeJobs.size + 1}/${concurrency} slots used]`);
|
|
146
|
-
|
|
147
|
-
const jobPromise = this.executeJob(job, executor, cloudBackend).finally(() => {
|
|
99
|
+
const jobPromise = dispatchJob(job, executor, cloudBackend, hooks).finally(() => {
|
|
148
100
|
activeJobs.delete(jobPromise);
|
|
149
101
|
});
|
|
150
102
|
activeJobs.add(jobPromise);
|
|
151
103
|
}
|
|
152
104
|
catch (error) {
|
|
153
105
|
this.warn(`Executor error: ${error.message}`);
|
|
154
|
-
// Wait before retrying
|
|
155
106
|
// eslint-disable-next-line no-await-in-loop
|
|
156
107
|
await new Promise(resolve => {
|
|
157
108
|
setTimeout(resolve, 5000);
|
|
158
109
|
});
|
|
159
110
|
}
|
|
160
111
|
}
|
|
161
|
-
// Wait for all active jobs to complete before exiting
|
|
162
112
|
if (activeJobs.size > 0) {
|
|
163
113
|
this.log(`Waiting for ${activeJobs.size} active job(s) to complete...`);
|
|
164
114
|
await Promise.allSettled(activeJobs);
|
|
165
115
|
}
|
|
166
116
|
this.log('Executor stopped.');
|
|
167
117
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const { appName, commit, parameters: providedParameters, repository, sha256, version, workspace } = payload;
|
|
173
|
-
if (!appName || !workspace || !repository) {
|
|
174
|
-
return {
|
|
175
|
-
reason: `Missing appName, workspace, or repository in ${job.type} job payload`,
|
|
176
|
-
success: false,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
let cleanup;
|
|
180
|
-
try {
|
|
181
|
-
logger.info(`Downloading app source ${repository}${commit ? `@${commit}` : ''}...`);
|
|
182
|
-
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
183
|
-
cleanup = downloadResult.cleanup;
|
|
184
|
-
const { rootDir } = downloadResult;
|
|
185
|
-
// Mark deployment as deploying / destroying.
|
|
186
|
-
await cloudBackend.updateAppDeployment({
|
|
187
|
-
lastJobId: job.id,
|
|
188
|
-
name: appName,
|
|
189
|
-
status: isDeploy ? 'deploying' : 'destroying',
|
|
190
|
-
workspace,
|
|
191
|
-
});
|
|
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 },
|
|
209
|
-
logger,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
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,
|
|
228
|
-
workspace,
|
|
229
|
-
});
|
|
230
|
-
if (!patchResult.success) {
|
|
231
|
-
return patchResult;
|
|
232
|
-
}
|
|
233
|
-
return { success: true };
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
try {
|
|
237
|
-
await this.markAppDeploymentFailed({ appName, jobId: job.id, reason: error.message, workspace }, cloudBackend);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
// best-effort
|
|
241
|
-
}
|
|
242
|
-
return { reason: `app-${subcommand} failed: ${error.message}`, success: false };
|
|
243
|
-
}
|
|
244
|
-
finally {
|
|
245
|
-
if (cleanup) {
|
|
246
|
-
await cleanup();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async executeDeployJob(job, executor, cloudBackend, logger) {
|
|
251
|
-
const { gitBranch, project, workspace } = job.payload;
|
|
252
|
-
if (!project || !workspace) {
|
|
253
|
-
return { reason: 'Missing project or workspace in job payload', success: false };
|
|
254
|
-
}
|
|
255
|
-
// 1. Get project metadata to find git credentials
|
|
256
|
-
const metadata$ = await cloudBackend.getProjectMetadata({ project });
|
|
257
|
-
if (!metadata$.found) {
|
|
258
|
-
return { reason: `No metadata found for project ${project}`, success: false };
|
|
259
|
-
}
|
|
260
|
-
const { metadata } = metadata$;
|
|
261
|
-
if (!metadata.env?.hereyaGitRemoteUrl) {
|
|
262
|
-
return { reason: 'Project metadata missing hereyaGitRemoteUrl', success: false };
|
|
263
|
-
}
|
|
264
|
-
// 2. Resolve git credentials using the local executor
|
|
265
|
-
const gitEnvToResolve = {};
|
|
266
|
-
for (const [key, value] of Object.entries(metadata.env)) {
|
|
267
|
-
if (key.startsWith('hereyaGit'))
|
|
268
|
-
gitEnvToResolve[key] = value;
|
|
269
|
-
}
|
|
270
|
-
const resolvedGitEnv = await executor.resolveEnvValues({ env: gitEnvToResolve, project, workspace });
|
|
271
|
-
// 3. Clone repo at specified branch into temp dir
|
|
272
|
-
const tempDir = path.join(os.tmpdir(), `hereya-deploy-${randomUUID()}`);
|
|
273
|
-
try {
|
|
274
|
-
const branch = gitBranch || 'main';
|
|
275
|
-
logger.info(`Cloning ${resolvedGitEnv.hereyaGitRemoteUrl} (branch: ${branch})...`);
|
|
276
|
-
const cloneResult = await cloneWithCredentialHelper({
|
|
277
|
-
gitUrl: resolvedGitEnv.hereyaGitRemoteUrl,
|
|
278
|
-
hereyaBinPath: process.argv[1],
|
|
279
|
-
password: resolvedGitEnv.hereyaGitPassword,
|
|
280
|
-
targetDir: tempDir,
|
|
281
|
-
username: resolvedGitEnv.hereyaGitUsername,
|
|
282
|
-
});
|
|
283
|
-
if (!cloneResult.success) {
|
|
284
|
-
return { reason: `Git clone failed: ${cloneResult.reason}`, success: false };
|
|
285
|
-
}
|
|
286
|
-
// Checkout the correct branch if not main/master
|
|
287
|
-
if (branch !== 'main' && branch !== 'master') {
|
|
288
|
-
await runShell('git', ['checkout', branch], { directory: tempDir, logger });
|
|
289
|
-
}
|
|
290
|
-
// 4. Run hereya deploy/undeploy --local inside the cloned repo
|
|
291
|
-
const command = job.type === 'deploy' ? 'deploy' : 'undeploy';
|
|
292
|
-
logger.info(`Running hereya ${command} -w ${workspace} --local...`);
|
|
293
|
-
const result = await runShell('hereya', [command, '-w', workspace, '--local'], {
|
|
294
|
-
directory: tempDir,
|
|
295
|
-
env: { ...process.env, HEREYA_LOCAL_EXECUTION: 'true', HEREYA_PROJECT_ROOT_DIR: tempDir },
|
|
296
|
-
logger,
|
|
297
|
-
});
|
|
298
|
-
logger.info(`hereya ${command} exited with code ${result.status}`);
|
|
299
|
-
if (result.status !== 0) {
|
|
300
|
-
return { reason: `hereya ${command} failed (exit code ${result.status})`, success: false };
|
|
301
|
-
}
|
|
302
|
-
return { success: true };
|
|
303
|
-
}
|
|
304
|
-
finally {
|
|
305
|
-
// 5. Clean up temp dir
|
|
306
|
-
try {
|
|
307
|
-
await fs.rm(tempDir, { force: true, recursive: true });
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// Ignore cleanup errors
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async executeInitJob(job, executor, logger) {
|
|
315
|
-
const payload = job.payload;
|
|
316
|
-
const { deployWorkspace, parameters, projectName, template, workspace } = payload;
|
|
317
|
-
if (!projectName || !workspace || !template || !deployWorkspace) {
|
|
318
|
-
return {
|
|
319
|
-
reason: 'Missing projectName, workspace, template, or deployWorkspace in job payload',
|
|
320
|
-
success: false,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
// Track the password here (captured after provision) so the redacting logger knows what
|
|
324
|
-
// to strip. Until we have it, redaction still runs for URL userinfo.
|
|
325
|
-
let password;
|
|
326
|
-
const redactingLogger = {
|
|
327
|
-
debug(msg) {
|
|
328
|
-
logger.debug(redactCredentials(msg, password));
|
|
329
|
-
},
|
|
330
|
-
error(msg) {
|
|
331
|
-
logger.error(redactCredentials(msg, password));
|
|
332
|
-
},
|
|
333
|
-
info(msg) {
|
|
334
|
-
logger.info(redactCredentials(msg, password));
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
const scratchDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hereya-init-'));
|
|
338
|
-
try {
|
|
339
|
-
// 1. Provision template
|
|
340
|
-
redactingLogger.info(`Provisioning template ${template}...`);
|
|
341
|
-
const provisionResult = await executor.provision({
|
|
342
|
-
logger: redactingLogger,
|
|
343
|
-
package: template,
|
|
344
|
-
parameters: {
|
|
345
|
-
...parameters,
|
|
346
|
-
deployWorkspace,
|
|
347
|
-
projectName,
|
|
348
|
-
workspace,
|
|
349
|
-
},
|
|
350
|
-
project: projectName,
|
|
351
|
-
workspace,
|
|
352
|
-
});
|
|
353
|
-
if (!provisionResult.success) {
|
|
354
|
-
return { reason: `Template provisioning failed: ${provisionResult.reason}`, success: false };
|
|
355
|
-
}
|
|
356
|
-
const { env } = provisionResult;
|
|
357
|
-
const { hereyaGitPassword, hereyaGitRemoteUrl, hereyaGitUsername } = env;
|
|
358
|
-
password = hereyaGitPassword;
|
|
359
|
-
if (!hereyaGitRemoteUrl) {
|
|
360
|
-
return { reason: 'Template did not export hereyaGitRemoteUrl', success: false };
|
|
361
|
-
}
|
|
362
|
-
// 2. Clone repo into scratch dir.
|
|
363
|
-
redactingLogger.info(`Cloning ${hereyaGitRemoteUrl}...`);
|
|
364
|
-
// Clone into a subdirectory so we control the target exactly.
|
|
365
|
-
const repoDir = path.join(scratchDir, 'repo');
|
|
366
|
-
const cloneResult = await gitUtils.cloneWithCredentialHelper({
|
|
367
|
-
gitUrl: hereyaGitRemoteUrl,
|
|
368
|
-
hereyaBinPath: process.argv[1],
|
|
369
|
-
password: hereyaGitPassword,
|
|
370
|
-
targetDir: repoDir,
|
|
371
|
-
username: hereyaGitUsername,
|
|
372
|
-
});
|
|
373
|
-
if (!cloneResult.success) {
|
|
374
|
-
return { reason: `Git clone failed: ${cloneResult.reason}`, success: false };
|
|
375
|
-
}
|
|
376
|
-
// 3. Write hereya.yaml
|
|
377
|
-
redactingLogger.info('Writing hereya.yaml...');
|
|
378
|
-
const config = {
|
|
379
|
-
project: projectName,
|
|
380
|
-
workspace,
|
|
381
|
-
};
|
|
382
|
-
await new SimpleConfigManager().saveConfig({ config, projectRootDir: repoDir });
|
|
383
|
-
// 4. Commit + push
|
|
384
|
-
redactingLogger.info('Committing and pushing initial project config...');
|
|
385
|
-
const gitEnv = {
|
|
386
|
-
...process.env,
|
|
387
|
-
GIT_AUTHOR_EMAIL: 'executor@hereya.dev',
|
|
388
|
-
GIT_AUTHOR_NAME: 'hereya-executor',
|
|
389
|
-
GIT_COMMITTER_EMAIL: 'executor@hereya.dev',
|
|
390
|
-
GIT_COMMITTER_NAME: 'hereya-executor',
|
|
391
|
-
};
|
|
392
|
-
if (hereyaGitUsername)
|
|
393
|
-
gitEnv.hereyaGitUsername = hereyaGitUsername;
|
|
394
|
-
if (hereyaGitPassword)
|
|
395
|
-
gitEnv.hereyaGitPassword = hereyaGitPassword;
|
|
396
|
-
try {
|
|
397
|
-
await shellUtils.runShell('git', ['add', '.'], { directory: repoDir, env: gitEnv, logger: redactingLogger });
|
|
398
|
-
await shellUtils.runShell('git', ['commit', '-m', '"chore: hereya init"'], {
|
|
399
|
-
directory: repoDir,
|
|
400
|
-
env: gitEnv,
|
|
401
|
-
logger: redactingLogger,
|
|
402
|
-
});
|
|
403
|
-
await shellUtils.runShell('git', ['push', 'origin', 'HEAD'], {
|
|
404
|
-
directory: repoDir,
|
|
405
|
-
env: gitEnv,
|
|
406
|
-
logger: redactingLogger,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
return {
|
|
411
|
-
reason: `Git commit/push failed: ${redactCredentials(error.message ?? String(error), password)}`,
|
|
412
|
-
success: false,
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
return { hereyaGitRemoteUrl, success: true };
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
return {
|
|
419
|
-
reason: `Init failed: ${redactCredentials(error.message ?? String(error), password)}`,
|
|
420
|
-
success: false,
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
finally {
|
|
424
|
-
try {
|
|
425
|
-
await fs.rm(scratchDir, { force: true, recursive: true });
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// Ignore cleanup errors
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
async executeJob(job, executor, cloudBackend) {
|
|
433
|
-
// Set up buffered logging
|
|
434
|
-
let logBuffer = '';
|
|
435
|
-
const logInterval = setInterval(async () => {
|
|
436
|
-
if (logBuffer) {
|
|
437
|
-
const logs = logBuffer;
|
|
438
|
-
logBuffer = '';
|
|
439
|
-
try {
|
|
440
|
-
await cloudBackend.updateExecutorJob({
|
|
441
|
-
jobId: job.id,
|
|
442
|
-
logs,
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
// Log flush errors are non-fatal
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}, 10_000);
|
|
450
|
-
// Heartbeat every 30 seconds (independent of log flushing)
|
|
451
|
-
const heartbeatInterval = setInterval(async () => {
|
|
452
|
-
try {
|
|
453
|
-
await cloudBackend.updateExecutorJob({ jobId: job.id });
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
// Heartbeat errors are non-fatal
|
|
457
|
-
}
|
|
458
|
-
}, 30_000);
|
|
459
|
-
const logger = {
|
|
460
|
-
debug(msg) {
|
|
461
|
-
logBuffer += msg + '\n';
|
|
462
|
-
},
|
|
463
|
-
error(msg) {
|
|
464
|
-
logBuffer += msg + '\n';
|
|
465
|
-
},
|
|
466
|
-
info(msg) {
|
|
467
|
-
logBuffer += msg + '\n';
|
|
468
|
-
},
|
|
469
|
-
};
|
|
470
|
-
try {
|
|
471
|
-
if (job.type === 'resolve-env') {
|
|
472
|
-
// resolve-env job: resolve env values using local executor
|
|
473
|
-
const resolved = await executor.resolveEnvValues(job.payload);
|
|
474
|
-
// Send resolved env as result
|
|
475
|
-
clearInterval(logInterval);
|
|
476
|
-
clearInterval(heartbeatInterval);
|
|
477
|
-
await cloudBackend.updateExecutorJob({
|
|
478
|
-
jobId: job.id,
|
|
479
|
-
logs: logBuffer,
|
|
480
|
-
result: { env: resolved, success: true },
|
|
481
|
-
status: 'completed',
|
|
482
|
-
});
|
|
483
|
-
this.log(`Job ${job.id} completed (resolve-env)`);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
if (job.type === 'deploy' || job.type === 'undeploy') {
|
|
487
|
-
const deployResult = await this.executeDeployJob(job, executor, cloudBackend, logger);
|
|
488
|
-
clearInterval(logInterval);
|
|
489
|
-
clearInterval(heartbeatInterval);
|
|
490
|
-
await cloudBackend.updateExecutorJob({
|
|
491
|
-
jobId: job.id,
|
|
492
|
-
logs: logBuffer,
|
|
493
|
-
result: deployResult,
|
|
494
|
-
status: deployResult.success ? 'completed' : 'failed',
|
|
495
|
-
});
|
|
496
|
-
this.log(`Job ${job.id} ${deployResult.success ? 'completed' : 'failed'} (${job.type})`);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (job.type === 'init') {
|
|
500
|
-
const initResult = await this.executeInitJob(job, executor, logger);
|
|
501
|
-
clearInterval(logInterval);
|
|
502
|
-
clearInterval(heartbeatInterval);
|
|
503
|
-
await cloudBackend.updateExecutorJob({
|
|
504
|
-
jobId: job.id,
|
|
505
|
-
logs: logBuffer,
|
|
506
|
-
result: initResult,
|
|
507
|
-
status: initResult.success ? 'completed' : 'failed',
|
|
508
|
-
});
|
|
509
|
-
this.log(`Job ${job.id} ${initResult.success ? 'completed' : 'failed'} (init)`);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (job.type === 'app-deploy' || job.type === 'app-destroy') {
|
|
513
|
-
const appJobResult = await this.executeAppJob(job, cloudBackend, logger);
|
|
514
|
-
clearInterval(logInterval);
|
|
515
|
-
clearInterval(heartbeatInterval);
|
|
516
|
-
await cloudBackend.updateExecutorJob({
|
|
517
|
-
jobId: job.id,
|
|
518
|
-
logs: logBuffer,
|
|
519
|
-
result: appJobResult,
|
|
520
|
-
status: appJobResult.success ? 'completed' : 'failed',
|
|
521
|
-
});
|
|
522
|
-
this.log(`Job ${job.id} ${appJobResult.success ? 'completed' : 'failed'} (${job.type})`);
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
const result = job.type === 'provision'
|
|
526
|
-
? await executor.provision({
|
|
527
|
-
...job.payload,
|
|
528
|
-
logger,
|
|
529
|
-
})
|
|
530
|
-
: await executor.destroy({
|
|
531
|
-
...job.payload,
|
|
532
|
-
logger,
|
|
533
|
-
});
|
|
534
|
-
// Flush remaining logs and send result
|
|
535
|
-
clearInterval(logInterval);
|
|
536
|
-
clearInterval(heartbeatInterval);
|
|
537
|
-
await cloudBackend.updateExecutorJob({
|
|
538
|
-
jobId: job.id,
|
|
539
|
-
logs: logBuffer,
|
|
540
|
-
result,
|
|
541
|
-
status: result.success ? 'completed' : 'failed',
|
|
542
|
-
});
|
|
543
|
-
this.log(`Job ${job.id} ${result.success ? 'completed' : 'failed'}`);
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
clearInterval(logInterval);
|
|
547
|
-
clearInterval(heartbeatInterval);
|
|
548
|
-
await cloudBackend.updateExecutorJob({
|
|
549
|
-
jobId: job.id,
|
|
550
|
-
logs: logBuffer + `\nError: ${error.message}\n`,
|
|
551
|
-
result: { reason: error.message, success: false },
|
|
552
|
-
status: 'failed',
|
|
553
|
-
});
|
|
554
|
-
this.warn(`Job ${job.id} failed: ${error.message}`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
async markAppDeploymentFailed(input, cloudBackend) {
|
|
558
|
-
try {
|
|
559
|
-
await cloudBackend.updateAppDeployment({
|
|
560
|
-
lastJobId: input.jobId,
|
|
561
|
-
name: input.appName,
|
|
562
|
-
status: 'failed',
|
|
563
|
-
workspace: input.workspace,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
// best-effort; failure of the PATCH must not bubble out
|
|
568
|
-
}
|
|
118
|
+
// Test-friendly delegators kept on the class for backwards compatibility with
|
|
119
|
+
// tests that call `(cmd as any).executeInitJob(...)` / `executeAppJob(...)`.
|
|
120
|
+
executeAppJob(job, cloudBackend, logger) {
|
|
121
|
+
return executeAppJob(job, cloudBackend, logger);
|
|
569
122
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (!isDeploy) {
|
|
573
|
-
const updateResult = await cloudBackend.updateAppDeployment({
|
|
574
|
-
env: {},
|
|
575
|
-
lastJobId: jobId,
|
|
576
|
-
name: appName,
|
|
577
|
-
status: 'destroyed',
|
|
578
|
-
workspace,
|
|
579
|
-
});
|
|
580
|
-
if (!updateResult.success) {
|
|
581
|
-
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
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);
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
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 };
|
|
608
|
-
}
|
|
609
|
-
return { success: true };
|
|
123
|
+
executeInitJob(job, executor, logger) {
|
|
124
|
+
return executeInitJob(job, executor, logger);
|
|
610
125
|
}
|
|
611
126
|
}
|
|
@@ -91,8 +91,8 @@ export default class ImportRepo extends Command {
|
|
|
91
91
|
},
|
|
92
92
|
...cloneAndConfigureProjectTasks({
|
|
93
93
|
getEnv: (ctx) => ctx.metadata.env || {},
|
|
94
|
-
getProject: (
|
|
95
|
-
getWorkspace: (
|
|
94
|
+
getProject: () => args.project,
|
|
95
|
+
getWorkspace: () => flags.workspace,
|
|
96
96
|
hereyaBinPath: process.argv[1],
|
|
97
97
|
missingRemoteUrlError: `Project metadata does not contain hereyaGitRemoteUrl for ${args.project}. Did the import populate the workspace github-app config?`,
|
|
98
98
|
targetDir,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EnvManager } from './index.js';
|
|
2
|
+
export interface EnvManagerTestContext {
|
|
3
|
+
envManager: EnvManager;
|
|
4
|
+
tempDir: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function setupEnvManagerTest(): Promise<EnvManagerTestContext>;
|
|
7
|
+
export declare function teardownEnvManagerTest(context: EnvManagerTestContext): Promise<void>;
|