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.
- package/README.md +88 -75
- 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-broker.d.ts +25 -0
- package/dist/backend/cloud/cloud-backend/executor-broker.js +38 -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 +59 -126
- package/dist/backend/cloud/cloud-backend.js +100 -1087
- 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/common.d.ts +2 -2
- 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 +4 -11
- package/dist/commands/executor/start/index.js +118 -520
- package/dist/commands/workspace/executor/install/index.d.ts +3 -0
- package/dist/commands/workspace/executor/install/index.js +210 -76
- package/dist/commands/workspace/executor/uninstall/index.d.ts +1 -0
- package/dist/commands/workspace/executor/uninstall/index.js +30 -4
- package/dist/executor/local.d.ts +0 -1
- package/dist/executor/local.js +10 -62
- package/dist/executor/resolve-env.d.ts +88 -0
- package/dist/executor/resolve-env.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/infrastructure/index.d.ts +12 -16
- package/dist/infrastructure/index.js +18 -25
- package/dist/infrastructure/registry.d.ts +38 -0
- package/dist/infrastructure/registry.js +61 -0
- 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 +25 -25
- package/oclif.manifest.json +43 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
+
export { type GetInfrastructureFn, type GetWorkspaceEnvFn, type MintInstallationTokenFn, type ResolveEnvProviders, resolveEnvValues, type ResolveEnvValuesInput, type ResolveEnvValuesOutput, resolveGithubAppMarkers, type ResolveSimpleEnvFn, } from './executor/resolve-env.js';
|
|
2
|
+
export { InfrastructureType } from './infrastructure/common.js';
|
|
3
|
+
export { awsProviderFactory, getInfrastructure, type GetInfrastructureInput, type GetInfrastructureOutput, type InfrastructureProviderFactory, registerInfrastructureProvider, resetInfrastructureProviders, } from './infrastructure/registry.js';
|
|
1
4
|
export { run } from '@oclif/core';
|
package/dist/index.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
+
// Public API for external consumers (e.g. the broker Lambda) that import
|
|
2
|
+
// `hereya-cli` as a published npm dependency. None of these re-exports pull
|
|
3
|
+
// in the `local` infrastructure provider or the rest of the CLI runtime — the
|
|
4
|
+
// Lambda bundle stays slim.
|
|
5
|
+
export { resolveEnvValues, resolveGithubAppMarkers, } from './executor/resolve-env.js';
|
|
6
|
+
export { InfrastructureType } from './infrastructure/common.js';
|
|
7
|
+
export { awsProviderFactory, getInfrastructure, registerInfrastructureProvider, resetInfrastructureProviders, } from './infrastructure/registry.js';
|
|
1
8
|
export { run } from '@oclif/core';
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { Logger } from '../lib/log.js';
|
|
3
3
|
import { PackageMetadata } from '../lib/package/index.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { Infrastructure } from './common.js';
|
|
5
|
+
import { type InfrastructureProviderFactory } from './registry.js';
|
|
6
|
+
export { awsProviderFactory, getInfrastructure, type GetInfrastructureInput, type GetInfrastructureOutput, type InfrastructureProviderFactory, registerInfrastructureProvider, resetInfrastructureProviders, } from './registry.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default factory for the local-filesystem provider. Registered eagerly when
|
|
9
|
+
* this module is imported (the CLI's standard entry point). The broker
|
|
10
|
+
* Lambda never imports this module, so `LocalInfrastructure` and its
|
|
11
|
+
* filesystem deps are tree-shaken out of the Lambda bundle.
|
|
12
|
+
*/
|
|
13
|
+
export declare const localProviderFactory: InfrastructureProviderFactory;
|
|
14
|
+
export declare const awsInfrastructure: Infrastructure;
|
|
15
|
+
export declare const localInfrastructure: Infrastructure;
|
|
9
16
|
export declare function resolveEnvValues(env: Record<string, string>): Promise<Record<string, string>>;
|
|
10
|
-
export declare function getInfrastructure(input: GetInfrastructureInput): GetInfrastructureOutput;
|
|
11
17
|
export declare function destroyPackage(input: DestroyPackageInput): Promise<DestroyPackageOutput>;
|
|
12
18
|
export declare function provisionPackage(input: ProvisionPackageInput): Promise<ProvisionPackageOutput>;
|
|
13
19
|
export declare function getProvisioningLogicalId({ app, pkg, project, workspace }: {
|
|
@@ -49,14 +55,4 @@ export type ProvisionPackageOutput = {
|
|
|
49
55
|
reason: string;
|
|
50
56
|
success: false;
|
|
51
57
|
};
|
|
52
|
-
export type GetInfrastructureInput = {
|
|
53
|
-
type: InfrastructureType;
|
|
54
|
-
};
|
|
55
|
-
export type GetInfrastructureOutput = {
|
|
56
|
-
infrastructure: Infrastructure;
|
|
57
|
-
supported: true;
|
|
58
|
-
} | {
|
|
59
|
-
reason: string;
|
|
60
|
-
supported: false;
|
|
61
|
-
};
|
|
62
58
|
export type PackageMetadata = z.infer<typeof PackageMetadata>;
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { getBackend } from '../backend/index.js';
|
|
2
2
|
import { stripOrgPrefix } from '../lib/org-utils.js';
|
|
3
3
|
import { resolvePackage } from '../lib/package/index.js';
|
|
4
|
-
import { AwsInfrastructure } from './aws.js';
|
|
5
4
|
import { InfrastructureType } from './common.js';
|
|
6
5
|
import { LocalInfrastructure } from './local.js';
|
|
7
|
-
|
|
8
|
-
export
|
|
6
|
+
import { getInfrastructure, getOrCreateProvider, registerInfrastructureProvider, } from './registry.js';
|
|
7
|
+
// Re-export the registry surface so existing imports
|
|
8
|
+
// (`from '../infrastructure/index.js'`) keep working.
|
|
9
|
+
export { awsProviderFactory, getInfrastructure, registerInfrastructureProvider, resetInfrastructureProviders, } from './registry.js';
|
|
10
|
+
/**
|
|
11
|
+
* Default factory for the local-filesystem provider. Registered eagerly when
|
|
12
|
+
* this module is imported (the CLI's standard entry point). The broker
|
|
13
|
+
* Lambda never imports this module, so `LocalInfrastructure` and its
|
|
14
|
+
* filesystem deps are tree-shaken out of the Lambda bundle.
|
|
15
|
+
*/
|
|
16
|
+
export const localProviderFactory = () => new LocalInfrastructure();
|
|
17
|
+
registerInfrastructureProvider(InfrastructureType.local, localProviderFactory);
|
|
18
|
+
// Backwards-compatible singleton exports. These remain stable references
|
|
19
|
+
// (`sinon.stub(awsInfrastructure, 'foo')` continues to work) because the
|
|
20
|
+
// default registrations above run at import time and the cached instances
|
|
21
|
+
// never change identity for the lifetime of the CLI process.
|
|
22
|
+
export const awsInfrastructure = getOrCreateProvider(InfrastructureType.aws);
|
|
23
|
+
export const localInfrastructure = getOrCreateProvider(InfrastructureType.local);
|
|
9
24
|
export async function resolveEnvValues(env) {
|
|
10
25
|
return Object.fromEntries(await Promise.all(Object.entries(env).map(async ([key, value]) => {
|
|
11
26
|
const colonIndex = value.indexOf(':');
|
|
@@ -21,28 +36,6 @@ export async function resolveEnvValues(env) {
|
|
|
21
36
|
return [key, resolvedValue];
|
|
22
37
|
})));
|
|
23
38
|
}
|
|
24
|
-
export function getInfrastructure(input) {
|
|
25
|
-
switch (input.type) {
|
|
26
|
-
case InfrastructureType.aws: {
|
|
27
|
-
return {
|
|
28
|
-
infrastructure: awsInfrastructure,
|
|
29
|
-
supported: true,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
case InfrastructureType.local: {
|
|
33
|
-
return {
|
|
34
|
-
infrastructure: localInfrastructure,
|
|
35
|
-
supported: true,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
default: {
|
|
39
|
-
return {
|
|
40
|
-
reason: `Invalid infrastructure type: ${input.type}. Valid options are: aws, local`,
|
|
41
|
-
supported: false,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
39
|
/**
|
|
47
40
|
* When `input.app` is set, scope the provisioning row by `app` (workspace-scoped).
|
|
48
41
|
* Otherwise, scope by `project`. Apps and projects are mutually exclusive scopes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Infrastructure, InfrastructureType } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* A factory that lazily constructs an `Infrastructure` provider singleton.
|
|
4
|
+
* Registered factories are invoked at most once per type — the resulting
|
|
5
|
+
* instance is cached so external consumers (and tests that stub methods on
|
|
6
|
+
* `awsInfrastructure` / `localInfrastructure`) see the same reference every
|
|
7
|
+
* time.
|
|
8
|
+
*/
|
|
9
|
+
export type InfrastructureProviderFactory = () => Infrastructure;
|
|
10
|
+
/**
|
|
11
|
+
* Default factory for the AWS provider. Both the CLI and the broker Lambda
|
|
12
|
+
* register this; it is the only factory the Lambda bundle pulls in.
|
|
13
|
+
*/
|
|
14
|
+
export declare const awsProviderFactory: InfrastructureProviderFactory;
|
|
15
|
+
/**
|
|
16
|
+
* Register (or replace) the factory for a given infrastructure type. Resets
|
|
17
|
+
* any cached instance for that type so the next `getInfrastructure` call
|
|
18
|
+
* rebuilds it from the new factory.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerInfrastructureProvider(type: InfrastructureType, factory: InfrastructureProviderFactory): void;
|
|
21
|
+
/**
|
|
22
|
+
* Clear all registered providers and cached instances. Primarily useful for
|
|
23
|
+
* tests and for the Lambda bundle that wants to start with a clean slate
|
|
24
|
+
* before registering only `aws`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resetInfrastructureProviders(): void;
|
|
27
|
+
export declare function getOrCreateProvider(type: InfrastructureType): Infrastructure;
|
|
28
|
+
export declare function getInfrastructure(input: GetInfrastructureInput): GetInfrastructureOutput;
|
|
29
|
+
export type GetInfrastructureInput = {
|
|
30
|
+
type: InfrastructureType;
|
|
31
|
+
};
|
|
32
|
+
export type GetInfrastructureOutput = {
|
|
33
|
+
infrastructure: Infrastructure;
|
|
34
|
+
supported: true;
|
|
35
|
+
} | {
|
|
36
|
+
reason: string;
|
|
37
|
+
supported: false;
|
|
38
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure provider registry. This module is the slim, Lambda-friendly
|
|
3
|
+
* entry point: it never imports the local-filesystem provider. The CLI's
|
|
4
|
+
* `./index.ts` extends this registry with `LocalInfrastructure`; the broker
|
|
5
|
+
* Lambda imports only this registry (via the `hereya-cli` package root) and
|
|
6
|
+
* registers `aws` only.
|
|
7
|
+
*/
|
|
8
|
+
import { AwsInfrastructure } from './aws.js';
|
|
9
|
+
import { InfrastructureType } from './common.js';
|
|
10
|
+
/**
|
|
11
|
+
* Default factory for the AWS provider. Both the CLI and the broker Lambda
|
|
12
|
+
* register this; it is the only factory the Lambda bundle pulls in.
|
|
13
|
+
*/
|
|
14
|
+
export const awsProviderFactory = () => new AwsInfrastructure();
|
|
15
|
+
const factories = new Map();
|
|
16
|
+
const instances = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Register (or replace) the factory for a given infrastructure type. Resets
|
|
19
|
+
* any cached instance for that type so the next `getInfrastructure` call
|
|
20
|
+
* rebuilds it from the new factory.
|
|
21
|
+
*/
|
|
22
|
+
export function registerInfrastructureProvider(type, factory) {
|
|
23
|
+
factories.set(type, factory);
|
|
24
|
+
instances.delete(type);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Clear all registered providers and cached instances. Primarily useful for
|
|
28
|
+
* tests and for the Lambda bundle that wants to start with a clean slate
|
|
29
|
+
* before registering only `aws`.
|
|
30
|
+
*/
|
|
31
|
+
export function resetInfrastructureProviders() {
|
|
32
|
+
factories.clear();
|
|
33
|
+
instances.clear();
|
|
34
|
+
}
|
|
35
|
+
export function getOrCreateProvider(type) {
|
|
36
|
+
const cached = instances.get(type);
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
const factory = factories.get(type);
|
|
40
|
+
if (!factory) {
|
|
41
|
+
throw new Error(`Infrastructure provider not registered: ${type}. Call registerInfrastructureProvider(${type}, factory) first.`);
|
|
42
|
+
}
|
|
43
|
+
const instance = factory();
|
|
44
|
+
instances.set(type, instance);
|
|
45
|
+
return instance;
|
|
46
|
+
}
|
|
47
|
+
export function getInfrastructure(input) {
|
|
48
|
+
if (!factories.has(input.type)) {
|
|
49
|
+
return {
|
|
50
|
+
reason: `Invalid infrastructure type: ${input.type}. Valid options are: ${[...factories.keys()].join(', ') || '<none registered>'}`,
|
|
51
|
+
supported: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
infrastructure: getOrCreateProvider(input.type),
|
|
56
|
+
supported: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Register the AWS provider eagerly. The CLI's `infrastructure/index.ts`
|
|
60
|
+
// additionally registers the `local` provider; the broker Lambda does not.
|
|
61
|
+
registerInfrastructureProvider(InfrastructureType.aws, awsProviderFactory);
|
|
@@ -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>;
|
|
@@ -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
|
+
}
|