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,18 @@
|
|
|
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
|
+
import { EnvManager } from './index.js';
|
|
7
|
+
export async function setupEnvManagerTest() {
|
|
8
|
+
const tempDir = path.join(os.tmpdir(), 'hereya-test-env', randomUUID());
|
|
9
|
+
await fs.mkdir(path.join(tempDir, '.hereya'), { recursive: true });
|
|
10
|
+
process.env.HEREYA_PROJECT_ROOT_DIR = tempDir;
|
|
11
|
+
const envManager = new EnvManager();
|
|
12
|
+
return { envManager, tempDir };
|
|
13
|
+
}
|
|
14
|
+
export async function teardownEnvManagerTest(context) {
|
|
15
|
+
sinon.restore();
|
|
16
|
+
await fs.rm(context.tempDir, { force: true, recursive: true });
|
|
17
|
+
delete process.env.HEREYA_PROJECT_ROOT_DIR;
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { loginWithToken } from '../../backend/cloud/login.js';
|
|
3
|
+
import { saveCloudCredentials } from '../../backend/config.js';
|
|
4
|
+
export async function createAuthenticatedBackend(hereyaToken, cloudUrl) {
|
|
5
|
+
const loginResult = await loginWithToken(cloudUrl, hereyaToken);
|
|
6
|
+
if (!loginResult.success) {
|
|
7
|
+
throw new Error(`Failed to authenticate: ${loginResult.error}`);
|
|
8
|
+
}
|
|
9
|
+
await saveCloudCredentials({
|
|
10
|
+
accessToken: loginResult.accessToken,
|
|
11
|
+
clientId: loginResult.clientId,
|
|
12
|
+
refreshToken: loginResult.refreshToken,
|
|
13
|
+
url: cloudUrl,
|
|
14
|
+
});
|
|
15
|
+
return new CloudBackend({
|
|
16
|
+
accessToken: loginResult.accessToken,
|
|
17
|
+
clientId: loginResult.clientId,
|
|
18
|
+
refreshToken: loginResult.refreshToken,
|
|
19
|
+
url: cloudUrl,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { Logger } from '../../lib/log.js';
|
|
3
|
+
export type AppJobResult = {
|
|
4
|
+
reason: string;
|
|
5
|
+
success: false;
|
|
6
|
+
} | {
|
|
7
|
+
success: true;
|
|
8
|
+
};
|
|
9
|
+
export declare function executeAppJob(job: {
|
|
10
|
+
id: string;
|
|
11
|
+
payload: any;
|
|
12
|
+
type: string;
|
|
13
|
+
}, cloudBackend: CloudBackend, logger: Logger): Promise<AppJobResult>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as appSourceLib from '../../lib/app-source.js';
|
|
4
|
+
import { shellUtils } from '../../lib/shell.js';
|
|
5
|
+
import { sanitizeWorkspaceForFilename } from './format.js';
|
|
6
|
+
export async function executeAppJob(job, cloudBackend, logger) {
|
|
7
|
+
const isDeploy = job.type === 'app-deploy';
|
|
8
|
+
const subcommand = isDeploy ? 'deploy' : 'destroy';
|
|
9
|
+
const payload = job.payload;
|
|
10
|
+
const { appName, commit, parameters: providedParameters, repository, sha256, version, workspace } = payload;
|
|
11
|
+
if (!appName || !workspace || !repository) {
|
|
12
|
+
return {
|
|
13
|
+
reason: `Missing appName, workspace, or repository in ${job.type} job payload`,
|
|
14
|
+
success: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
let cleanup;
|
|
18
|
+
try {
|
|
19
|
+
logger.info(`Downloading app source ${repository}${commit ? `@${commit}` : ''}...`);
|
|
20
|
+
const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
|
|
21
|
+
cleanup = downloadResult.cleanup;
|
|
22
|
+
const { rootDir } = downloadResult;
|
|
23
|
+
await cloudBackend.updateAppDeployment({
|
|
24
|
+
lastJobId: job.id,
|
|
25
|
+
name: appName,
|
|
26
|
+
status: isDeploy ? 'deploying' : 'destroying',
|
|
27
|
+
workspace,
|
|
28
|
+
});
|
|
29
|
+
const parameters = providedParameters ?? {};
|
|
30
|
+
const paramFlags = [];
|
|
31
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
32
|
+
paramFlags.push('-p', `${key}=${value}`);
|
|
33
|
+
}
|
|
34
|
+
const versionFlags = isDeploy && version ? ['--version', version] : [];
|
|
35
|
+
logger.info(`Running hereya app ${subcommand} ${appName} -w ${workspace} --local...`);
|
|
36
|
+
const runOutcome = await runAppSubcommand({
|
|
37
|
+
appName,
|
|
38
|
+
logger,
|
|
39
|
+
paramFlags,
|
|
40
|
+
rootDir,
|
|
41
|
+
subcommand,
|
|
42
|
+
versionFlags,
|
|
43
|
+
workspace,
|
|
44
|
+
});
|
|
45
|
+
if (!runOutcome.success) {
|
|
46
|
+
await markAppDeploymentFailed({ appName, jobId: job.id, reason: runOutcome.reason, workspace }, cloudBackend);
|
|
47
|
+
return { reason: runOutcome.reason, success: false };
|
|
48
|
+
}
|
|
49
|
+
const patchResult = await patchAppFinalState({
|
|
50
|
+
appName,
|
|
51
|
+
cloudBackend,
|
|
52
|
+
isDeploy,
|
|
53
|
+
jobId: job.id,
|
|
54
|
+
rootDir,
|
|
55
|
+
workspace,
|
|
56
|
+
});
|
|
57
|
+
if (!patchResult.success) {
|
|
58
|
+
return patchResult;
|
|
59
|
+
}
|
|
60
|
+
return { success: true };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
try {
|
|
64
|
+
await markAppDeploymentFailed({ appName, jobId: job.id, reason: error.message, workspace }, cloudBackend);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// best-effort
|
|
68
|
+
}
|
|
69
|
+
return { reason: `app-${subcommand} failed: ${error.message}`, success: false };
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (cleanup) {
|
|
73
|
+
await cleanup();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function runAppSubcommand(input) {
|
|
78
|
+
const { appName, logger, paramFlags, rootDir, subcommand, versionFlags, workspace } = input;
|
|
79
|
+
let runResult;
|
|
80
|
+
try {
|
|
81
|
+
runResult = await shellUtils.runShell('hereya', ['app', subcommand, appName, '-w', workspace, ...paramFlags, ...versionFlags, '--local'], {
|
|
82
|
+
directory: rootDir,
|
|
83
|
+
env: { ...process.env, HEREYA_LOCAL_EXECUTION: 'true', HEREYA_PROJECT_ROOT_DIR: rootDir },
|
|
84
|
+
logger,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return { reason: `hereya app ${subcommand} failed: ${error.message}`, success: false };
|
|
89
|
+
}
|
|
90
|
+
if (runResult && runResult.status !== 0) {
|
|
91
|
+
return { reason: `hereya app ${subcommand} exited with code ${runResult.status}`, success: false };
|
|
92
|
+
}
|
|
93
|
+
return { success: true };
|
|
94
|
+
}
|
|
95
|
+
async function markAppDeploymentFailed(input, cloudBackend) {
|
|
96
|
+
try {
|
|
97
|
+
await cloudBackend.updateAppDeployment({
|
|
98
|
+
lastJobId: input.jobId,
|
|
99
|
+
name: input.appName,
|
|
100
|
+
status: 'failed',
|
|
101
|
+
workspace: input.workspace,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// best-effort; failure of the PATCH must not bubble out
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function patchAppFinalState(input) {
|
|
109
|
+
const { appName, cloudBackend, isDeploy, jobId, rootDir, workspace } = input;
|
|
110
|
+
if (!isDeploy) {
|
|
111
|
+
const updateResult = await cloudBackend.updateAppDeployment({
|
|
112
|
+
env: {},
|
|
113
|
+
lastJobId: jobId,
|
|
114
|
+
name: appName,
|
|
115
|
+
status: 'destroyed',
|
|
116
|
+
workspace,
|
|
117
|
+
});
|
|
118
|
+
if (!updateResult.success) {
|
|
119
|
+
return { reason: `Destroy succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
120
|
+
}
|
|
121
|
+
return { success: true };
|
|
122
|
+
}
|
|
123
|
+
const deployEnvFile = path.join(rootDir, '.hereya', `deploy-env.${sanitizeWorkspaceForFilename(workspace)}.json`);
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
const content = await fs.readFile(deployEnvFile, 'utf8');
|
|
127
|
+
parsed = JSON.parse(content);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const reason = `Failed to read deploy env file at ${deployEnvFile}: ${error.message}`;
|
|
131
|
+
await markAppDeploymentFailed({ appName, jobId, reason, workspace }, cloudBackend);
|
|
132
|
+
return { reason, success: false };
|
|
133
|
+
}
|
|
134
|
+
const updateResult = await cloudBackend.updateAppDeployment({
|
|
135
|
+
env: parsed.env ?? {},
|
|
136
|
+
lastJobId: jobId,
|
|
137
|
+
name: appName,
|
|
138
|
+
state: parsed.state,
|
|
139
|
+
status: 'deployed',
|
|
140
|
+
workspace,
|
|
141
|
+
});
|
|
142
|
+
if (!updateResult.success) {
|
|
143
|
+
return { reason: `Provisioning succeeded but PATCH to deployment failed: ${updateResult.reason}`, success: false };
|
|
144
|
+
}
|
|
145
|
+
return { success: true };
|
|
146
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { LocalExecutor } from '../../executor/local.js';
|
|
3
|
+
import { Logger } from '../../lib/log.js';
|
|
4
|
+
export type DeployJobResult = {
|
|
5
|
+
reason: string;
|
|
6
|
+
success: false;
|
|
7
|
+
} | {
|
|
8
|
+
success: true;
|
|
9
|
+
};
|
|
10
|
+
export declare function executeDeployJob(job: {
|
|
11
|
+
id: string;
|
|
12
|
+
payload: any;
|
|
13
|
+
type: string;
|
|
14
|
+
}, executor: LocalExecutor, cloudBackend: CloudBackend, logger: Logger): Promise<DeployJobResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { cloneWithCredentialHelper } from '../../lib/git-utils.js';
|
|
6
|
+
import { runShell } from '../../lib/shell.js';
|
|
7
|
+
export async function executeDeployJob(job, executor, cloudBackend, logger) {
|
|
8
|
+
const { gitBranch, project, workspace } = job.payload;
|
|
9
|
+
if (!project || !workspace) {
|
|
10
|
+
return { reason: 'Missing project or workspace in job payload', success: false };
|
|
11
|
+
}
|
|
12
|
+
const metadata$ = await cloudBackend.getProjectMetadata({ project });
|
|
13
|
+
if (!metadata$.found) {
|
|
14
|
+
return { reason: `No metadata found for project ${project}`, success: false };
|
|
15
|
+
}
|
|
16
|
+
const { metadata } = metadata$;
|
|
17
|
+
if (!metadata.env?.hereyaGitRemoteUrl) {
|
|
18
|
+
return { reason: 'Project metadata missing hereyaGitRemoteUrl', success: false };
|
|
19
|
+
}
|
|
20
|
+
const gitEnvToResolve = {};
|
|
21
|
+
for (const [key, value] of Object.entries(metadata.env)) {
|
|
22
|
+
if (key.startsWith('hereyaGit'))
|
|
23
|
+
gitEnvToResolve[key] = value;
|
|
24
|
+
}
|
|
25
|
+
const resolvedGitEnv = await executor.resolveEnvValues({ env: gitEnvToResolve, project, workspace });
|
|
26
|
+
const tempDir = path.join(os.tmpdir(), `hereya-deploy-${randomUUID()}`);
|
|
27
|
+
try {
|
|
28
|
+
const branch = gitBranch || 'main';
|
|
29
|
+
logger.info(`Cloning ${resolvedGitEnv.hereyaGitRemoteUrl} (branch: ${branch})...`);
|
|
30
|
+
const cloneResult = await cloneWithCredentialHelper({
|
|
31
|
+
gitUrl: resolvedGitEnv.hereyaGitRemoteUrl,
|
|
32
|
+
hereyaBinPath: process.argv[1],
|
|
33
|
+
password: resolvedGitEnv.hereyaGitPassword,
|
|
34
|
+
targetDir: tempDir,
|
|
35
|
+
username: resolvedGitEnv.hereyaGitUsername,
|
|
36
|
+
});
|
|
37
|
+
if (!cloneResult.success) {
|
|
38
|
+
return { reason: `Git clone failed: ${cloneResult.reason}`, success: false };
|
|
39
|
+
}
|
|
40
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
41
|
+
await runShell('git', ['checkout', branch], { directory: tempDir, logger });
|
|
42
|
+
}
|
|
43
|
+
const command = job.type === 'deploy' ? 'deploy' : 'undeploy';
|
|
44
|
+
logger.info(`Running hereya ${command} -w ${workspace} --local...`);
|
|
45
|
+
const result = await runShell('hereya', [command, '-w', workspace, '--local'], {
|
|
46
|
+
directory: tempDir,
|
|
47
|
+
env: { ...process.env, HEREYA_LOCAL_EXECUTION: 'true', HEREYA_PROJECT_ROOT_DIR: tempDir },
|
|
48
|
+
logger,
|
|
49
|
+
});
|
|
50
|
+
logger.info(`hereya ${command} exited with code ${result.status}`);
|
|
51
|
+
if (result.status !== 0) {
|
|
52
|
+
return { reason: `hereya ${command} failed (exit code ${result.status})`, success: false };
|
|
53
|
+
}
|
|
54
|
+
return { success: true };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
try {
|
|
58
|
+
await fs.rm(tempDir, { force: true, recursive: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore cleanup errors
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LocalExecutor } from '../../executor/local.js';
|
|
2
|
+
import { Logger } from '../../lib/log.js';
|
|
3
|
+
export type InitJobResult = {
|
|
4
|
+
hereyaGitRemoteUrl: string;
|
|
5
|
+
success: true;
|
|
6
|
+
} | {
|
|
7
|
+
reason: string;
|
|
8
|
+
success: false;
|
|
9
|
+
};
|
|
10
|
+
export declare function executeInitJob(job: {
|
|
11
|
+
id: string;
|
|
12
|
+
payload: any;
|
|
13
|
+
type: string;
|
|
14
|
+
}, executor: LocalExecutor, logger: Logger): Promise<InitJobResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { SimpleConfigManager } from '../../lib/config/simple.js';
|
|
5
|
+
import { gitUtils } from '../../lib/git-utils.js';
|
|
6
|
+
import { shellUtils } from '../../lib/shell.js';
|
|
7
|
+
import { redactCredentials } from './format.js';
|
|
8
|
+
export async function executeInitJob(job, executor, logger) {
|
|
9
|
+
const payload = job.payload;
|
|
10
|
+
const { deployWorkspace, parameters, projectName, template, workspace } = payload;
|
|
11
|
+
if (!projectName || !workspace || !template || !deployWorkspace) {
|
|
12
|
+
return {
|
|
13
|
+
reason: 'Missing projectName, workspace, template, or deployWorkspace in job payload',
|
|
14
|
+
success: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
let password;
|
|
18
|
+
const redactingLogger = {
|
|
19
|
+
debug(msg) {
|
|
20
|
+
logger.debug(redactCredentials(msg, password));
|
|
21
|
+
},
|
|
22
|
+
error(msg) {
|
|
23
|
+
logger.error(redactCredentials(msg, password));
|
|
24
|
+
},
|
|
25
|
+
info(msg) {
|
|
26
|
+
logger.info(redactCredentials(msg, password));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const scratchDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hereya-init-'));
|
|
30
|
+
try {
|
|
31
|
+
redactingLogger.info(`Provisioning template ${template}...`);
|
|
32
|
+
const provisionResult = await executor.provision({
|
|
33
|
+
logger: redactingLogger,
|
|
34
|
+
package: template,
|
|
35
|
+
parameters: {
|
|
36
|
+
...parameters,
|
|
37
|
+
deployWorkspace,
|
|
38
|
+
projectName,
|
|
39
|
+
workspace,
|
|
40
|
+
},
|
|
41
|
+
project: projectName,
|
|
42
|
+
workspace,
|
|
43
|
+
});
|
|
44
|
+
if (!provisionResult.success) {
|
|
45
|
+
return { reason: `Template provisioning failed: ${provisionResult.reason}`, success: false };
|
|
46
|
+
}
|
|
47
|
+
const cloneResult = await provisionAndClone(provisionResult, scratchDir, redactingLogger, (pwd) => {
|
|
48
|
+
password = pwd;
|
|
49
|
+
});
|
|
50
|
+
if (!cloneResult.success) {
|
|
51
|
+
return cloneResult;
|
|
52
|
+
}
|
|
53
|
+
const { hereyaGitPassword, hereyaGitRemoteUrl, hereyaGitUsername, repoDir } = cloneResult;
|
|
54
|
+
redactingLogger.info('Writing hereya.yaml...');
|
|
55
|
+
const config = {
|
|
56
|
+
project: projectName,
|
|
57
|
+
workspace,
|
|
58
|
+
};
|
|
59
|
+
await new SimpleConfigManager().saveConfig({ config, projectRootDir: repoDir });
|
|
60
|
+
redactingLogger.info('Committing and pushing initial project config...');
|
|
61
|
+
const commitResult = await commitAndPush(repoDir, hereyaGitUsername, hereyaGitPassword, redactingLogger);
|
|
62
|
+
if (!commitResult.success) {
|
|
63
|
+
return {
|
|
64
|
+
reason: `Git commit/push failed: ${redactCredentials(commitResult.reason, password)}`,
|
|
65
|
+
success: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { hereyaGitRemoteUrl, success: true };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
reason: `Init failed: ${redactCredentials(error.message ?? String(error), password)}`,
|
|
73
|
+
success: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
try {
|
|
78
|
+
await fs.rm(scratchDir, { force: true, recursive: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore cleanup errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function provisionAndClone(provisionResult, scratchDir, logger, setPassword) {
|
|
86
|
+
const { env } = provisionResult;
|
|
87
|
+
const { hereyaGitPassword, hereyaGitRemoteUrl, hereyaGitUsername } = env;
|
|
88
|
+
setPassword(hereyaGitPassword);
|
|
89
|
+
if (!hereyaGitRemoteUrl) {
|
|
90
|
+
return { reason: 'Template did not export hereyaGitRemoteUrl', success: false };
|
|
91
|
+
}
|
|
92
|
+
logger.info(`Cloning ${hereyaGitRemoteUrl}...`);
|
|
93
|
+
const repoDir = path.join(scratchDir, 'repo');
|
|
94
|
+
const cloneResult = await gitUtils.cloneWithCredentialHelper({
|
|
95
|
+
gitUrl: hereyaGitRemoteUrl,
|
|
96
|
+
hereyaBinPath: process.argv[1],
|
|
97
|
+
password: hereyaGitPassword,
|
|
98
|
+
targetDir: repoDir,
|
|
99
|
+
username: hereyaGitUsername,
|
|
100
|
+
});
|
|
101
|
+
if (!cloneResult.success) {
|
|
102
|
+
return { reason: `Git clone failed: ${cloneResult.reason}`, success: false };
|
|
103
|
+
}
|
|
104
|
+
return { hereyaGitPassword, hereyaGitRemoteUrl, hereyaGitUsername, repoDir, success: true };
|
|
105
|
+
}
|
|
106
|
+
async function commitAndPush(repoDir, hereyaGitUsername, hereyaGitPassword, logger) {
|
|
107
|
+
const gitEnv = {
|
|
108
|
+
...process.env,
|
|
109
|
+
GIT_AUTHOR_EMAIL: 'executor@hereya.dev',
|
|
110
|
+
GIT_AUTHOR_NAME: 'hereya-executor',
|
|
111
|
+
GIT_COMMITTER_EMAIL: 'executor@hereya.dev',
|
|
112
|
+
GIT_COMMITTER_NAME: 'hereya-executor',
|
|
113
|
+
};
|
|
114
|
+
if (hereyaGitUsername)
|
|
115
|
+
gitEnv.hereyaGitUsername = hereyaGitUsername;
|
|
116
|
+
if (hereyaGitPassword)
|
|
117
|
+
gitEnv.hereyaGitPassword = hereyaGitPassword;
|
|
118
|
+
try {
|
|
119
|
+
await shellUtils.runShell('git', ['add', '.'], { directory: repoDir, env: gitEnv, logger });
|
|
120
|
+
await shellUtils.runShell('git', ['commit', '-m', '"chore: hereya init"'], {
|
|
121
|
+
directory: repoDir,
|
|
122
|
+
env: gitEnv,
|
|
123
|
+
logger,
|
|
124
|
+
});
|
|
125
|
+
await shellUtils.runShell('git', ['push', 'origin', 'HEAD'], {
|
|
126
|
+
directory: repoDir,
|
|
127
|
+
env: gitEnv,
|
|
128
|
+
logger,
|
|
129
|
+
});
|
|
130
|
+
return { success: true };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { reason: error.message ?? String(error), success: false };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspaces look like `org/name`, which is not filesystem-safe. The
|
|
3
|
+
* write-side in `app deploy --local` (commands/app/deploy/index.ts) MUST use
|
|
4
|
+
* the same sanitization.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sanitizeWorkspaceForFilename(workspace: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Strip credentials from log strings before flushing them to the cloud.
|
|
9
|
+
* Removes:
|
|
10
|
+
* - any occurrence of the actual hereyaGitPassword value
|
|
11
|
+
* - userinfo from URLs (https://user:pass@host -> https://host)
|
|
12
|
+
*/
|
|
13
|
+
export declare function redactCredentials(str: string, password?: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspaces look like `org/name`, which is not filesystem-safe. The
|
|
3
|
+
* write-side in `app deploy --local` (commands/app/deploy/index.ts) MUST use
|
|
4
|
+
* the same sanitization.
|
|
5
|
+
*/
|
|
6
|
+
export function sanitizeWorkspaceForFilename(workspace) {
|
|
7
|
+
return workspace.replaceAll('/', '-');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Strip credentials from log strings before flushing them to the cloud.
|
|
11
|
+
* Removes:
|
|
12
|
+
* - any occurrence of the actual hereyaGitPassword value
|
|
13
|
+
* - userinfo from URLs (https://user:pass@host -> https://host)
|
|
14
|
+
*/
|
|
15
|
+
export function redactCredentials(str, password) {
|
|
16
|
+
let out = str;
|
|
17
|
+
if (password) {
|
|
18
|
+
out = out.split(password).join('[REDACTED]');
|
|
19
|
+
}
|
|
20
|
+
out = out.replaceAll(/(https?:\/\/|git:\/\/)([^/\s@]+@)/g, '$1');
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { LocalExecutor } from '../../executor/local.js';
|
|
3
|
+
export interface JobDispatchHooks {
|
|
4
|
+
log(msg: string): void;
|
|
5
|
+
warn(msg: string): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Dispatches a single job to the right handler, with periodic log flushing
|
|
9
|
+
* and heartbeating to the cloud. Errors are caught and reported as failed jobs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dispatchJob(job: {
|
|
12
|
+
id: string;
|
|
13
|
+
payload: any;
|
|
14
|
+
type: string;
|
|
15
|
+
}, executor: LocalExecutor, cloudBackend: CloudBackend, hooks: JobDispatchHooks): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { executeAppJob } from './execute-app-job.js';
|
|
2
|
+
import { executeDeployJob } from './execute-deploy-job.js';
|
|
3
|
+
import { executeInitJob } from './execute-init-job.js';
|
|
4
|
+
/**
|
|
5
|
+
* Dispatches a single job to the right handler, with periodic log flushing
|
|
6
|
+
* and heartbeating to the cloud. Errors are caught and reported as failed jobs.
|
|
7
|
+
*/
|
|
8
|
+
export async function dispatchJob(job, executor, cloudBackend, hooks) {
|
|
9
|
+
let logBuffer = '';
|
|
10
|
+
const logInterval = setInterval(async () => {
|
|
11
|
+
if (logBuffer) {
|
|
12
|
+
const logs = logBuffer;
|
|
13
|
+
logBuffer = '';
|
|
14
|
+
try {
|
|
15
|
+
await cloudBackend.updateExecutorJob({ jobId: job.id, logs });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// log flush errors are non-fatal
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}, 10_000);
|
|
22
|
+
const heartbeatInterval = setInterval(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await cloudBackend.updateExecutorJob({ jobId: job.id });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// heartbeat errors are non-fatal
|
|
28
|
+
}
|
|
29
|
+
}, 30_000);
|
|
30
|
+
const logger = {
|
|
31
|
+
debug(msg) {
|
|
32
|
+
logBuffer += msg + '\n';
|
|
33
|
+
},
|
|
34
|
+
error(msg) {
|
|
35
|
+
logBuffer += msg + '\n';
|
|
36
|
+
},
|
|
37
|
+
info(msg) {
|
|
38
|
+
logBuffer += msg + '\n';
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const finalize = async (result, label) => {
|
|
42
|
+
clearInterval(logInterval);
|
|
43
|
+
clearInterval(heartbeatInterval);
|
|
44
|
+
await cloudBackend.updateExecutorJob({
|
|
45
|
+
jobId: job.id,
|
|
46
|
+
logs: logBuffer,
|
|
47
|
+
result,
|
|
48
|
+
status: result.success ? 'completed' : 'failed',
|
|
49
|
+
});
|
|
50
|
+
hooks.log(`Job ${job.id} ${result.success ? 'completed' : 'failed'}${label ? ` (${label})` : ''}`);
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
if (job.type === 'resolve-env') {
|
|
54
|
+
const resolved = await executor.resolveEnvValues(job.payload);
|
|
55
|
+
await finalize({ env: resolved, success: true }, 'resolve-env');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (job.type === 'deploy' || job.type === 'undeploy') {
|
|
59
|
+
const deployResult = await executeDeployJob(job, executor, cloudBackend, logger);
|
|
60
|
+
await finalize(deployResult, job.type);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (job.type === 'init') {
|
|
64
|
+
const initResult = await executeInitJob(job, executor, logger);
|
|
65
|
+
await finalize(initResult, 'init');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (job.type === 'app-deploy' || job.type === 'app-destroy') {
|
|
69
|
+
const appJobResult = await executeAppJob(job, cloudBackend, logger);
|
|
70
|
+
await finalize(appJobResult, job.type);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const result = job.type === 'provision'
|
|
74
|
+
? await executor.provision({ ...job.payload, logger })
|
|
75
|
+
: await executor.destroy({ ...job.payload, logger });
|
|
76
|
+
await finalize(result, '');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
clearInterval(logInterval);
|
|
80
|
+
clearInterval(heartbeatInterval);
|
|
81
|
+
await cloudBackend.updateExecutorJob({
|
|
82
|
+
jobId: job.id,
|
|
83
|
+
logs: logBuffer + `\nError: ${error.message}\n`,
|
|
84
|
+
result: { reason: error.message, success: false },
|
|
85
|
+
status: 'failed',
|
|
86
|
+
});
|
|
87
|
+
hooks.warn(`Job ${job.id} failed: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -105,10 +105,10 @@ export declare const PackageMetadata: z.ZodObject<{
|
|
|
105
105
|
}> | undefined>;
|
|
106
106
|
snakeCase: z.ZodOptional<z.ZodBoolean>;
|
|
107
107
|
}, "strip", z.ZodTypeAny, {
|
|
108
|
-
infra: InfrastructureType;
|
|
109
108
|
iac: IacType;
|
|
109
|
+
infra: InfrastructureType;
|
|
110
110
|
dependencies?: Record<string, string> | undefined;
|
|
111
|
-
kind?: "
|
|
111
|
+
kind?: "app" | "package" | undefined;
|
|
112
112
|
parameters?: Record<string, {
|
|
113
113
|
description?: string | undefined;
|
|
114
114
|
default?: string | undefined;
|
|
@@ -125,10 +125,10 @@ export declare const PackageMetadata: z.ZodObject<{
|
|
|
125
125
|
outputs?: Record<string, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">> | undefined;
|
|
126
126
|
snakeCase?: boolean | undefined;
|
|
127
127
|
}, {
|
|
128
|
-
infra: InfrastructureType;
|
|
129
128
|
iac: IacType;
|
|
129
|
+
infra: InfrastructureType;
|
|
130
130
|
dependencies?: Record<string, string> | undefined;
|
|
131
|
-
kind?: "
|
|
131
|
+
kind?: "app" | "package" | undefined;
|
|
132
132
|
parameters?: Record<string, {
|
|
133
133
|
description?: string | undefined;
|
|
134
134
|
default?: string | undefined;
|