@h-ai/cli 0.1.0-alpha.65 → 0.1.0-alpha.67
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.en.md +66 -251
- package/README.ja.md +67 -252
- package/README.md +66 -251
- package/dist/{chunk-47V5OT22.js → chunk-7DSURTJ4.js} +32 -115
- package/dist/chunk-7DSURTJ4.js.map +1 -0
- package/dist/{chunk-IPG3QNYK.js → chunk-AOQ4N657.js} +212 -8
- package/dist/chunk-AOQ4N657.js.map +1 -0
- package/dist/cli-config-templates-6AHNUGBN.js +5 -0
- package/dist/{cli-config-templates-SJ2MBIR7.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
- package/dist/{cli-skill-templates-A5VIR5J6.js → cli-skill-templates-FC3QRKDO.js} +3 -3
- package/dist/{cli-skill-templates-A5VIR5J6.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
- package/dist/index.js +644 -391
- package/dist/index.js.map +1 -1
- package/dist/{src-6PPBKQ6Q.js → src-2V6VP4I5.js} +972 -520
- package/dist/src-2V6VP4I5.js.map +1 -0
- package/package.json +8 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +1 -1
- package/templates/apps/api/.dockerignore.hbs +18 -12
- package/templates/apps/api/.gitignore.hbs +9 -1
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +28 -7
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -3
- package/templates/apps/api/hai.build.json.hbs +1 -0
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +10 -8
- package/templates/apps/fullstack/.dockerignore.hbs +18 -14
- package/templates/apps/fullstack/.gitignore.hbs +9 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -8
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +0 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +3 -3
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +25 -10
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +2 -3
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -10
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +0 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +3 -3
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +12 -1
- package/templates/apps/fullstack/hai.build.json.hbs +7 -0
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +7 -12
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +28 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +0 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +33 -12
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
- package/templates/apps/mobile-app/index.html.hbs +1 -0
- package/templates/apps/mobile-app/public/runtime-config.js +1 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +18 -13
- package/templates/base/.gitignore.hbs +9 -1
- package/templates/base/e2e/app.spec.ts.hbs +10 -0
- package/templates/base/hai.build.json.hbs +1 -0
- package/templates/base/images/app.Dockerfile.hbs +64 -0
- package/templates/base/{docker → images/web}/nginx.conf.hbs +6 -0
- package/templates/base/package.json.hbs +20 -19
- package/templates/base/svelte.config.js.hbs +2 -0
- package/templates/base/vite.config.ts.hbs +9 -3
- package/templates/shared/.gitattributes +11 -0
- package/templates/shared/container-runtime-config.sh +30 -0
- package/templates/shared/run-e2e.mjs +65 -0
- package/templates/shared/vite-runtime-config.ts +54 -0
- package/templates/skills/AGENTS.md +3 -3
- package/templates/skills/bridges/admin/AGENTS.md +1 -1
- package/templates/skills/bridges/api/AGENTS.md +1 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
- package/templates/skills/bridges/h5/AGENTS.md +1 -1
- package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
- package/templates/skills/bridges/website/AGENTS.md +1 -1
- package/templates/skills/hai-api-client/SKILL.md +14 -161
- package/templates/skills/hai-api-client/reference.md +166 -0
- package/templates/skills/hai-api-contract/SKILL.md +15 -273
- package/templates/skills/hai-api-contract/reference.md +278 -0
- package/templates/skills/hai-app-create/SKILL.md +2 -2
- package/templates/skills/hai-app-create/development.md +6 -4
- package/templates/skills/hai-app-create/reference.md +5 -4
- package/templates/skills/hai-app-review/SKILL.md +1 -1
- package/templates/skills/hai-app-tests/SKILL.md +4 -3
- package/templates/skills/hai-app-tests/reference.md +2 -2
- package/templates/skills/hai-app-tests/validation.md +31 -0
- package/templates/skills/hai-audit/SKILL.md +17 -142
- package/templates/skills/hai-audit/reference.md +145 -0
- package/templates/skills/hai-build/SKILL.md +24 -3
- package/templates/skills/hai-build/reference.md +10 -9
- package/templates/skills/hai-cache/SKILL.md +15 -182
- package/templates/skills/hai-cache/reference.md +187 -0
- package/templates/skills/hai-capacitor/SKILL.md +4 -3
- package/templates/skills/hai-capacitor/reference.md +6 -4
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +3 -3
- package/templates/skills/hai-crypto/SKILL.md +16 -252
- package/templates/skills/hai-crypto/reference.md +256 -0
- package/templates/skills/hai-datapipe/SKILL.md +15 -196
- package/templates/skills/hai-datapipe/reference.md +202 -0
- package/templates/skills/hai-deploy/SKILL.md +28 -271
- package/templates/skills/hai-deploy/middleware.md +6 -18
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +1 -3
- package/templates/skills/hai-kit/SKILL.md +12 -167
- package/templates/skills/hai-kit/reference.md +172 -0
- package/templates/skills/hai-payment/SKILL.md +19 -251
- package/templates/skills/hai-payment/reference.md +256 -0
- package/templates/skills/hai-pr-review/SKILL.md +4 -4
- package/templates/skills/hai-reach/SKILL.md +2 -2
- package/templates/skills/hai-reldb/SKILL.md +1 -1
- package/templates/skills/hai-scheduler/SKILL.md +15 -195
- package/templates/skills/hai-scheduler/reference.md +202 -0
- package/templates/skills/hai-serv/SKILL.md +2 -2
- package/templates/skills/hai-spec-implement/SKILL.md +1 -1
- package/templates/skills/hai-spec-implement/workflow.md +3 -3
- package/templates/skills/hai-storage/SKILL.md +17 -238
- package/templates/skills/hai-storage/reference.md +243 -0
- package/templates/skills/hai-ui/SKILL.md +1 -1
- package/templates/skills/hai-ui/reference.md +11 -5
- package/templates/skills/hai-vecdb/SKILL.md +2 -2
- package/dist/chunk-47V5OT22.js.map +0 -1
- package/dist/chunk-IPG3QNYK.js.map +0 -1
- package/dist/cli-config-templates-SJ2MBIR7.js +0 -5
- package/dist/src-6PPBKQ6Q.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -48
- package/templates/apps/fullstack/Dockerfile.hbs +0 -69
- package/templates/base/Dockerfile.hbs +0 -79
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __export } from './chunk-2H7UOFLK.js';
|
|
3
|
-
import {
|
|
4
|
-
import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { createHash, randomBytes } from 'crypto';
|
|
4
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync, readdirSync } from 'fs';
|
|
5
5
|
import { resolve, join, basename, dirname, relative, sep } from 'path';
|
|
6
|
+
import { core, err, ok } from '@h-ai/core';
|
|
6
7
|
import process2 from 'process';
|
|
7
8
|
import { parseEnv } from 'util';
|
|
8
|
-
import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
|
|
9
|
+
import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
|
|
9
10
|
import { homedir } from 'os';
|
|
10
|
-
import {
|
|
11
|
-
import { randomBytes, createHash } from 'crypto';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
12
|
import { Buffer } from 'buffer';
|
|
13
13
|
|
|
14
14
|
// ../../node_modules/zod/v4/classic/external.js
|
|
@@ -1398,10 +1398,10 @@ function defineLazyInternal(inst, key, compute) {
|
|
|
1398
1398
|
Object.defineProperty(this, key, { configurable: true, writable: true, value });
|
|
1399
1399
|
broke = broke || outer;
|
|
1400
1400
|
return value;
|
|
1401
|
-
} catch (
|
|
1401
|
+
} catch (err15) {
|
|
1402
1402
|
delete this[key];
|
|
1403
1403
|
broke = broke || outer;
|
|
1404
|
-
throw
|
|
1404
|
+
throw err15;
|
|
1405
1405
|
}
|
|
1406
1406
|
},
|
|
1407
1407
|
set(value) {
|
|
@@ -5037,7 +5037,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
5037
5037
|
var NONE = 0;
|
|
5038
5038
|
var ASSUMED = 1;
|
|
5039
5039
|
var PROVEN = 2;
|
|
5040
|
-
function isRecursive(inst, stack,
|
|
5040
|
+
function isRecursive(inst, stack, resolve6) {
|
|
5041
5041
|
const cached2 = recursive.get(inst);
|
|
5042
5042
|
if (cached2 !== void 0)
|
|
5043
5043
|
return cached2 ? PROVEN : NONE;
|
|
@@ -5047,7 +5047,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
5047
5047
|
let result = NONE;
|
|
5048
5048
|
const check2 = (child) => {
|
|
5049
5049
|
if (result !== PROVEN && child?._zod) {
|
|
5050
|
-
const answer = isRecursive(child, stack,
|
|
5050
|
+
const answer = isRecursive(child, stack, resolve6);
|
|
5051
5051
|
if (answer > result)
|
|
5052
5052
|
result = answer;
|
|
5053
5053
|
}
|
|
@@ -5058,7 +5058,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
5058
5058
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
5059
5059
|
if (!desc.enumerable)
|
|
5060
5060
|
continue;
|
|
5061
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
5061
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
|
|
5062
5062
|
if (child > answer)
|
|
5063
5063
|
answer = child;
|
|
5064
5064
|
}
|
|
@@ -5122,7 +5122,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
5122
5122
|
break;
|
|
5123
5123
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
5124
5124
|
case "lazy": {
|
|
5125
|
-
const inner = def._cachedInner ?? (
|
|
5125
|
+
const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
|
|
5126
5126
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
5127
5127
|
break;
|
|
5128
5128
|
}
|
|
@@ -12940,9 +12940,9 @@ function compile(schema, options) {
|
|
|
12940
12940
|
const clone2 = withParser(schema, parser);
|
|
12941
12941
|
clone2._zod.bag.validator = compileValidator(schema, parser);
|
|
12942
12942
|
return clone2;
|
|
12943
|
-
} catch (
|
|
12943
|
+
} catch (err15) {
|
|
12944
12944
|
if (options?.strict)
|
|
12945
|
-
throw
|
|
12945
|
+
throw err15;
|
|
12946
12946
|
return schema;
|
|
12947
12947
|
}
|
|
12948
12948
|
}
|
|
@@ -13032,8 +13032,8 @@ ${code}
|
|
|
13032
13032
|
try {
|
|
13033
13033
|
const factory = new F(...constantNames, factoryCode);
|
|
13034
13034
|
fn = factory(...constantValues);
|
|
13035
|
-
} catch (
|
|
13036
|
-
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${
|
|
13035
|
+
} catch (err15) {
|
|
13036
|
+
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err15.message})`);
|
|
13037
13037
|
}
|
|
13038
13038
|
if (options?.debug) {
|
|
13039
13039
|
fn.code = fullCode;
|
|
@@ -13071,9 +13071,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
|
|
|
13071
13071
|
const varCounter = ctx.varCounter;
|
|
13072
13072
|
try {
|
|
13073
13073
|
return generateCheck(doc, ctx, schema, accessor, needsValue);
|
|
13074
|
-
} catch (
|
|
13075
|
-
if (!(
|
|
13076
|
-
throw
|
|
13074
|
+
} catch (err15) {
|
|
13075
|
+
if (!(err15 instanceof ZodCompileUnsupportedError) || !err15.islandable)
|
|
13076
|
+
throw err15;
|
|
13077
13077
|
doc.content.length = contentLen;
|
|
13078
13078
|
if (ctx.constants.size > constantCount) {
|
|
13079
13079
|
const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
|
|
@@ -19668,6 +19668,52 @@ function date4(params) {
|
|
|
19668
19668
|
return _coercedDate(ZodDate, params);
|
|
19669
19669
|
}
|
|
19670
19670
|
|
|
19671
|
+
// ../deploy/src/deploy-artifact.ts
|
|
19672
|
+
var DeploymentArtifactSchema = external_exports.object({
|
|
19673
|
+
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
19674
|
+
version: external_exports.literal(3),
|
|
19675
|
+
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
19676
|
+
appVersion: external_exports.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
|
|
19677
|
+
/** 本次构建标识;同一应用版本允许重复构建。 */
|
|
19678
|
+
buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
|
|
19679
|
+
/** 只包含所选镜像的构建不能用于首次安装。 */
|
|
19680
|
+
partial: external_exports.boolean().default(false),
|
|
19681
|
+
/** API 单独构建时仍保留工程的 Web 拓扑。 */
|
|
19682
|
+
hasWeb: external_exports.boolean().default(false),
|
|
19683
|
+
/** 工程显式配置的应用端口,部分构建未包含 app 镜像时仍保留;缺省由部署配置决定。 */
|
|
19684
|
+
appContainerPort: external_exports.number().int().min(1).max(65535).optional(),
|
|
19685
|
+
/** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
|
|
19686
|
+
webContainerPort: external_exports.number().int().min(1).max(65535).optional(),
|
|
19687
|
+
/** 本次构建选择的镜像归档。 */
|
|
19688
|
+
images: external_exports.array(external_exports.object({
|
|
19689
|
+
/** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
|
|
19690
|
+
name: external_exports.enum(["app", "web"]),
|
|
19691
|
+
/** 相对于清单目录的 tar 文件名。 */
|
|
19692
|
+
file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
|
|
19693
|
+
/** 归档导入后可直接使用的不可变镜像引用。 */
|
|
19694
|
+
imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
|
|
19695
|
+
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
19696
|
+
sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
19697
|
+
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
19698
|
+
containerPort: external_exports.number().int().min(1).max(65535).optional()
|
|
19699
|
+
})).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
|
|
19700
|
+
}).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === "app")).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
|
|
19701
|
+
async function artifactChecksum(file2) {
|
|
19702
|
+
const hash2 = createHash("sha256");
|
|
19703
|
+
for await (const chunk of createReadStream(file2))
|
|
19704
|
+
hash2.update(chunk);
|
|
19705
|
+
return hash2.digest("hex");
|
|
19706
|
+
}
|
|
19707
|
+
async function readDeploymentArtifact(directory) {
|
|
19708
|
+
const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
|
|
19709
|
+
for (const image of artifact.images) {
|
|
19710
|
+
const file2 = resolve(directory, image.file);
|
|
19711
|
+
if (!existsSync(file2) || !lstatSync(file2).isFile() || lstatSync(file2).isSymbolicLink() || await artifactChecksum(file2) !== image.sha256)
|
|
19712
|
+
throw new Error(`Invalid artifact: ${image.file}`);
|
|
19713
|
+
}
|
|
19714
|
+
return artifact;
|
|
19715
|
+
}
|
|
19716
|
+
|
|
19671
19717
|
// ../deploy/messages/en-US.json
|
|
19672
19718
|
var en_US_default = {
|
|
19673
19719
|
deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
|
|
@@ -19678,7 +19724,6 @@ var en_US_default = {
|
|
|
19678
19724
|
deploy_authRequired: "Authentication required",
|
|
19679
19725
|
deploy_authFailed: "Authentication failed: {error}",
|
|
19680
19726
|
deploy_projectCreateFailed: "Project creation failed: {error}",
|
|
19681
|
-
deploy_buildFailed: "Application build failed: {error}",
|
|
19682
19727
|
deploy_uploadFailed: "Build output upload failed: {error}",
|
|
19683
19728
|
deploy_deployFailed: "Deployment failed: {error}",
|
|
19684
19729
|
deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
|
|
@@ -19698,8 +19743,6 @@ var en_US_default = {
|
|
|
19698
19743
|
deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
|
|
19699
19744
|
deploy_commandTimeout: "Command timed out: {command}",
|
|
19700
19745
|
deploy_commandFailed: "Command failed ({command}): {error}",
|
|
19701
|
-
deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
|
|
19702
|
-
deploy_containerBuildFailed: "Container image build failed: {error}",
|
|
19703
19746
|
deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
|
|
19704
19747
|
deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
|
|
19705
19748
|
deploy_imageTransferFailed: "Image transfer failed: {error}",
|
|
@@ -19720,7 +19763,23 @@ var en_US_default = {
|
|
|
19720
19763
|
deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
|
|
19721
19764
|
deploy_purposeConsole: "Messages go to application logs without delivery",
|
|
19722
19765
|
deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
|
|
19723
|
-
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota"
|
|
19766
|
+
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
|
|
19767
|
+
deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
|
|
19768
|
+
deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
|
|
19769
|
+
deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
|
|
19770
|
+
deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) from the adjacent deployment directory.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh /opt/hai/apps/project-environment.\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl and filesystem/container permissions.\nPartial updates require a successful full installation with matching ports/layout. Steps return nonzero on failure; result-DEPLOYMENT_ID.json records the outcome.\nThe bundle contains private runtime configuration; transfer it securely.\nIncluded images:\n{images}",
|
|
19771
|
+
deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
|
|
19772
|
+
deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
|
|
19773
|
+
deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
|
|
19774
|
+
deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
|
|
19775
|
+
deploy_imageVersionRequired: "Specify an image version; latest is not allowed: {image}",
|
|
19776
|
+
deploy_bundleRuntimeRequired: "Start local Podman/Docker to export middleware images, or prepare this complete offline bundle on the build machine.",
|
|
19777
|
+
deploy_bundleFailed: "Package preparation or deployment failed: {reason}",
|
|
19778
|
+
deploy_configFileInvalid: "Invalid YAML in {path}, line {line}. Check indentation, colons and quotes.",
|
|
19779
|
+
deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
|
|
19780
|
+
deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
|
|
19781
|
+
deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
|
|
19782
|
+
deploy_storageArtifactRequired: "Storage initialization requires the application image. Select a complete artifact containing app with --artifact-dir, or run pnpm build --target docker --images app first."
|
|
19724
19783
|
};
|
|
19725
19784
|
|
|
19726
19785
|
// ../deploy/messages/zh-CN.json
|
|
@@ -19733,7 +19792,6 @@ var zh_CN_default2 = {
|
|
|
19733
19792
|
deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
|
|
19734
19793
|
deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
|
|
19735
19794
|
deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
19736
|
-
deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
19737
19795
|
deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
|
|
19738
19796
|
deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
|
|
19739
19797
|
deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
|
|
@@ -19753,8 +19811,6 @@ var zh_CN_default2 = {
|
|
|
19753
19811
|
deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
|
|
19754
19812
|
deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
|
|
19755
19813
|
deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
|
|
19756
|
-
deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
|
|
19757
|
-
deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
19758
19814
|
deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
|
|
19759
19815
|
deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
|
|
19760
19816
|
deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
|
|
@@ -19775,7 +19831,23 @@ var zh_CN_default2 = {
|
|
|
19775
19831
|
deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
|
|
19776
19832
|
deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
|
|
19777
19833
|
deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
|
|
19778
|
-
deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6"
|
|
19834
|
+
deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
|
|
19835
|
+
deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
|
|
19836
|
+
deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target docker\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
|
|
19837
|
+
deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
|
|
19838
|
+
deploy_bundleInstructions: "\u90E8\u7F72\u5305\uFF1A{file}\n\u5305\u4E2D\u5DF2\u5305\u542B\u672C\u6B21\u9009\u62E9\u670D\u52A1\u9700\u8981\u7684\u955C\u50CF\uFF0C\u5305\u62EC\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u3002\n\u6709 SSH \u8FDE\u63A5\u65F6\uFF0C\u5728\u65C1\u8FB9\u7684\u90E8\u7F72\u76EE\u5F55\u8FD0\u884C deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\u3002\n\u5B8C\u5168\u9694\u79BB\u65F6\uFF0C\u590D\u5236 tar \u5230 Linux \u670D\u52A1\u5668\uFF0C\u5728\u72EC\u7ACB\u76EE\u5F55\u89E3\u5305\uFF0C\u518D\u6267\u884C sh apply.sh /opt/hai/apps/\u9879\u76EE\u73AF\u5883\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk \u548C curl\uFF0C\u4EE5\u53CA\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002\n\u90E8\u5206\u66F4\u65B0\u8981\u6C42\u670D\u52A1\u5668\u5DF2\u6709\u6210\u529F\u7684\u5B8C\u6574\u90E8\u7F72\uFF0C\u7AEF\u53E3\u53CA\u5E94\u7528\u5E03\u5C40\u4FDD\u6301\u4E00\u81F4\u3002\u6BCF\u6B65\u5931\u8D25\u4F1A\u8FD4\u56DE\u975E\u96F6\u9000\u51FA\u7801\uFF1B\u7ED3\u679C\u8BB0\u5F55\u5728\u90E8\u7F72\u76EE\u5F55 result-\u90E8\u7F72ID.json\u3002\n\u5305\u5305\u542B\u79C1\u6709\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u4F20\u8F93\u3002\n\u6240\u542B\u955C\u50CF\uFF1A\n{images}",
|
|
19839
|
+
deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
|
|
19840
|
+
deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
|
|
19841
|
+
deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
|
|
19842
|
+
deploy_selectionInvalid: "\u65E0\u6CD5\u90E8\u7F72\u670D\u52A1 {service}\uFF1A\u8BF7\u68C0\u67E5\u670D\u52A1\u540D\u79F0\uFF0C\u5E76\u5148\u6784\u5EFA\u5BF9\u5E94\u955C\u50CF\u3002",
|
|
19843
|
+
deploy_storageArtifactRequired: "\u5B58\u50A8\u521D\u59CB\u5316\u9700\u8981\u5E94\u7528\u955C\u50CF\u3002\u8BF7\u7528 --artifact-dir \u9009\u62E9\u542B app \u955C\u50CF\u7684\u5B8C\u6574\u5236\u54C1\uFF0C\u6216\u5148\u6267\u884C pnpm build --target docker --images app\u3002",
|
|
19844
|
+
deploy_imageVersionRequired: "\u955C\u50CF\u5FC5\u987B\u6307\u5B9A\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 latest\uFF1A{image}",
|
|
19845
|
+
deploy_bundleRuntimeRequired: "\u5B8C\u6574\u79BB\u7EBF\u5305\u9700\u8981\u5BFC\u51FA\u4E2D\u95F4\u4EF6\u955C\u50CF\u3002\u8BF7\u542F\u52A8\u672C\u5730 Podman/Docker\uFF0C\u6216\u5728\u6784\u5EFA\u673A\u6253\u5305\u3002",
|
|
19846
|
+
deploy_bundleFailed: "\u4EA4\u4ED8\u51C6\u5907\u6216\u90E8\u7F72\u5931\u8D25\uFF1A{reason}",
|
|
19847
|
+
deploy_configFileInvalid: "\u914D\u7F6E\u6587\u4EF6 {path} \u7B2C {line} \u884C\u683C\u5F0F\u6709\u8BEF\u3002\u8BF7\u68C0\u67E5\u7F29\u8FDB\u3001\u5192\u53F7\u548C\u5F15\u53F7\u540E\u91CD\u8BD5\u3002",
|
|
19848
|
+
deploy_configFileUnreadable: "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 {path}\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u8DEF\u5F84\u4E0E\u8BFB\u53D6\u6743\u9650\u3002",
|
|
19849
|
+
deploy_bundleNameInvalid: "\u90E8\u7F72\u5305\u6587\u4EF6\u540D\u53EA\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u52A0\u53F7\u3001\u77ED\u6A2A\u7EBF\u548C\u4E0B\u5212\u7EBF\uFF1B\u6240\u5728\u76EE\u5F55\u53EF\u4EE5\u5305\u542B\u7A7A\u683C\u3002",
|
|
19850
|
+
deploy_secretsInvalid: "\u4E2D\u95F4\u4EF6\u51ED\u636E\u6587\u4EF6\u635F\u574F\u6216\u7F3A\u5C11\u5B57\u6BB5\uFF1A{path}\u3002\u8BF7\u4ECE\u5907\u4EFD\u6062\u590D\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u5BC6\u7801\uFF0C\u4EE5\u514D\u5DF2\u6709\u6570\u636E\u65E0\u6CD5\u8BBF\u95EE\u3002"
|
|
19779
19851
|
};
|
|
19780
19852
|
|
|
19781
19853
|
// ../deploy/src/deploy-i18n.ts
|
|
@@ -19791,11 +19863,11 @@ var VercelProviderConfigSchema = external_exports.object({
|
|
|
19791
19863
|
});
|
|
19792
19864
|
var SshConfigSchema = external_exports.object({
|
|
19793
19865
|
/** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
|
|
19794
|
-
host: external_exports.string().
|
|
19866
|
+
host: external_exports.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
|
|
19795
19867
|
/** SSH 端口 */
|
|
19796
19868
|
port: external_exports.number().int().min(1).max(65535).default(22),
|
|
19797
19869
|
/** SSH 用户名 */
|
|
19798
|
-
username: external_exports.string().
|
|
19870
|
+
username: external_exports.string().regex(/^[a-z_][\w-]*$/i),
|
|
19799
19871
|
/** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
|
|
19800
19872
|
identityFile: external_exports.string().min(1).optional(),
|
|
19801
19873
|
/** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
|
|
@@ -19816,11 +19888,11 @@ var ExposeSchema = PortExposeSchema;
|
|
|
19816
19888
|
var DockerSshProviderConfigSchema = external_exports.object({
|
|
19817
19889
|
type: external_exports.literal("docker-ssh"),
|
|
19818
19890
|
/** SSH 连接参数 */
|
|
19819
|
-
ssh: SshConfigSchema,
|
|
19891
|
+
ssh: SshConfigSchema.optional(),
|
|
19820
19892
|
/** 远程主机运行参数 */
|
|
19821
19893
|
remote: external_exports.object({
|
|
19822
19894
|
/** 远程应用根目录 */
|
|
19823
|
-
baseDir: external_exports.string().
|
|
19895
|
+
baseDir: external_exports.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
|
|
19824
19896
|
/** 远程容器运行时(auto 优先 podman) */
|
|
19825
19897
|
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
|
|
19826
19898
|
}).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
|
|
@@ -19870,24 +19942,15 @@ var DeployConfigSchema = external_exports.object({
|
|
|
19870
19942
|
environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
|
|
19871
19943
|
provider: ProviderConfigSchema,
|
|
19872
19944
|
services: ServicesConfigSchema,
|
|
19873
|
-
/**
|
|
19945
|
+
/** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
|
|
19874
19946
|
container: external_exports.object({
|
|
19875
|
-
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto"),
|
|
19876
|
-
/** Podman 构建是否继承代理;容器不能访问宿主机 localhost 代理时设为 false。 */
|
|
19877
|
-
httpProxy: external_exports.boolean().default(true),
|
|
19878
19947
|
/** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
|
|
19879
|
-
s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
|
|
19880
|
-
/** SQLite 预编译包镜像根地址;省略时从官方 GitHub Release 下载,不允许嵌入凭据。 */
|
|
19881
|
-
sqlitePrebuildMirror: external_exports.url().refine((value) => {
|
|
19882
|
-
const url2 = new URL(value);
|
|
19883
|
-
return ["http:", "https:"].includes(url2.protocol) && !url2.username && !url2.password && !url2.search && !url2.hash;
|
|
19884
|
-
}).optional()
|
|
19948
|
+
s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
|
|
19885
19949
|
}).optional()
|
|
19886
19950
|
});
|
|
19887
19951
|
var DeployErrorInfo = {
|
|
19888
19952
|
DEPLOY_FAILED: "001:500",
|
|
19889
19953
|
PROJECT_CREATE_FAILED: "002:500",
|
|
19890
|
-
BUILD_FAILED: "003:500",
|
|
19891
19954
|
UPLOAD_FAILED: "004:500",
|
|
19892
19955
|
AUTH_REQUIRED: "005:401",
|
|
19893
19956
|
AUTH_FAILED: "006:401",
|
|
@@ -19899,8 +19962,6 @@ var DeployErrorInfo = {
|
|
|
19899
19962
|
UNSUPPORTED_TYPE: "012:400",
|
|
19900
19963
|
CONFIG_ERROR: "013:500",
|
|
19901
19964
|
CREDENTIAL_ERROR: "014:500",
|
|
19902
|
-
CONTAINER_RUNTIME_NOT_FOUND: "015:500",
|
|
19903
|
-
CONTAINER_BUILD_FAILED: "016:500",
|
|
19904
19965
|
REMOTE_CONNECT_FAILED: "017:500",
|
|
19905
19966
|
REMOTE_COMMAND_FAILED: "018:500",
|
|
19906
19967
|
IMAGE_TRANSFER_FAILED: "019:500",
|
|
@@ -19909,9 +19970,16 @@ var DeployErrorInfo = {
|
|
|
19909
19970
|
var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
|
|
19910
19971
|
|
|
19911
19972
|
// ../deploy/src/deploy-environment.ts
|
|
19973
|
+
function environmentValue(value) {
|
|
19974
|
+
try {
|
|
19975
|
+
return parse$1(value) ?? value;
|
|
19976
|
+
} catch {
|
|
19977
|
+
return value;
|
|
19978
|
+
}
|
|
19979
|
+
}
|
|
19912
19980
|
function resolveConfig(value, prefix, env) {
|
|
19913
19981
|
if (env[prefix] !== void 0)
|
|
19914
|
-
return
|
|
19982
|
+
return environmentValue(env[prefix]);
|
|
19915
19983
|
if (Array.isArray(value))
|
|
19916
19984
|
return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
|
|
19917
19985
|
if (value && typeof value === "object")
|
|
@@ -19919,14 +19987,20 @@ function resolveConfig(value, prefix, env) {
|
|
|
19919
19987
|
if (typeof value !== "string")
|
|
19920
19988
|
return value;
|
|
19921
19989
|
const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
|
|
19922
|
-
return /^\$\{[^}]+\}$/.test(value) ?
|
|
19990
|
+
return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
|
|
19923
19991
|
}
|
|
19924
19992
|
function loadDeploymentConfig(name, path, values) {
|
|
19925
19993
|
try {
|
|
19926
|
-
const
|
|
19994
|
+
const lineCounter = new LineCounter();
|
|
19995
|
+
const document = parseDocument(readFileSync(path, "utf8"), { lineCounter, prettyErrors: false });
|
|
19996
|
+
if (document.errors.length)
|
|
19997
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
|
|
19998
|
+
const config2 = resolveConfig(document.toJS(), `HAI_${name.toUpperCase()}`, values);
|
|
19999
|
+
if (config2 !== null && (typeof config2 !== "object" || Array.isArray(config2)))
|
|
20000
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
|
|
19927
20001
|
return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
|
|
19928
20002
|
} catch {
|
|
19929
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("
|
|
20003
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
|
|
19930
20004
|
}
|
|
19931
20005
|
}
|
|
19932
20006
|
function loadDeploymentEnvironment(options) {
|
|
@@ -20187,39 +20261,25 @@ function saveCredentials(entries) {
|
|
|
20187
20261
|
);
|
|
20188
20262
|
}
|
|
20189
20263
|
}
|
|
20190
|
-
|
|
20264
|
+
function resolveArtifactDirectory(appDir) {
|
|
20265
|
+
let directory = resolve(appDir);
|
|
20266
|
+
while (true) {
|
|
20267
|
+
if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
|
|
20268
|
+
return join(directory, ".hai/build/docker");
|
|
20269
|
+
const parent = dirname(directory);
|
|
20270
|
+
if (parent === directory)
|
|
20271
|
+
return join(appDir, ".hai/build/docker");
|
|
20272
|
+
directory = parent;
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20191
20275
|
function usesDefaultMiddleware(scan, service, options) {
|
|
20192
20276
|
const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
|
|
20193
20277
|
return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
|
|
20194
20278
|
}
|
|
20195
|
-
function buildApp(appDir, buildCommand, envVars = {}) {
|
|
20196
|
-
logger2.info("Building application", { appDir, buildCommand });
|
|
20197
|
-
try {
|
|
20198
|
-
execSync(buildCommand, {
|
|
20199
|
-
cwd: appDir,
|
|
20200
|
-
stdio: "inherit",
|
|
20201
|
-
env: { ...process2.env, ...envVars }
|
|
20202
|
-
});
|
|
20203
|
-
return ok(void 0);
|
|
20204
|
-
} catch (error62) {
|
|
20205
|
-
return err(
|
|
20206
|
-
HaiDeployError.BUILD_FAILED,
|
|
20207
|
-
deployM("deploy_buildFailed", {
|
|
20208
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
20209
|
-
}),
|
|
20210
|
-
error62
|
|
20211
|
-
);
|
|
20212
|
-
}
|
|
20213
|
-
}
|
|
20214
20279
|
function resolveOutputDir(appDir, providerType) {
|
|
20215
20280
|
switch (providerType) {
|
|
20216
|
-
case "vercel":
|
|
20217
|
-
|
|
20218
|
-
if (existsSync(vercelOutput)) {
|
|
20219
|
-
return vercelOutput;
|
|
20220
|
-
}
|
|
20221
|
-
return join(appDir, "build");
|
|
20222
|
-
}
|
|
20281
|
+
case "vercel":
|
|
20282
|
+
return join(appDir, ".vercel", "output");
|
|
20223
20283
|
default:
|
|
20224
20284
|
return join(appDir, "build");
|
|
20225
20285
|
}
|
|
@@ -20250,7 +20310,7 @@ function recoveryFailure(error62, recovery) {
|
|
|
20250
20310
|
ext: { recovery }
|
|
20251
20311
|
});
|
|
20252
20312
|
}
|
|
20253
|
-
var
|
|
20313
|
+
var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
|
|
20254
20314
|
function record2(value) {
|
|
20255
20315
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20256
20316
|
}
|
|
@@ -20369,26 +20429,522 @@ async function scanApp(inputDir, options) {
|
|
|
20369
20429
|
adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
|
|
20370
20430
|
nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
|
|
20371
20431
|
requiredServices: [...new Set(middlewares.map((item) => item.service))],
|
|
20372
|
-
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
20373
|
-
buildCommand: "pnpm build"
|
|
20432
|
+
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
20374
20433
|
};
|
|
20375
|
-
|
|
20434
|
+
logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
|
|
20376
20435
|
return ok(scan);
|
|
20377
20436
|
} catch {
|
|
20378
20437
|
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
|
|
20379
20438
|
}
|
|
20380
20439
|
}
|
|
20440
|
+
var logger3 = core.logger.child({ module: "deploy", scope: "command" });
|
|
20441
|
+
function maskSecrets(text, secrets) {
|
|
20442
|
+
if (!secrets || secrets.length === 0)
|
|
20443
|
+
return text;
|
|
20444
|
+
let masked = text;
|
|
20445
|
+
for (const secret of secrets) {
|
|
20446
|
+
if (secret && secret.length > 0)
|
|
20447
|
+
masked = masked.split(secret).join("***");
|
|
20448
|
+
}
|
|
20449
|
+
return masked;
|
|
20450
|
+
}
|
|
20451
|
+
function runCommand(command, args, options = {}) {
|
|
20452
|
+
return new Promise((resolve6) => {
|
|
20453
|
+
const child = spawn(command, args, {
|
|
20454
|
+
windowsHide: true,
|
|
20455
|
+
cwd: options.cwd,
|
|
20456
|
+
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
20457
|
+
shell: false
|
|
20458
|
+
});
|
|
20459
|
+
let stdout = "";
|
|
20460
|
+
let stderr = "";
|
|
20461
|
+
let settled = false;
|
|
20462
|
+
let timer;
|
|
20463
|
+
const finish = (result) => {
|
|
20464
|
+
if (settled)
|
|
20465
|
+
return;
|
|
20466
|
+
settled = true;
|
|
20467
|
+
if (timer)
|
|
20468
|
+
clearTimeout(timer);
|
|
20469
|
+
resolve6(result);
|
|
20470
|
+
};
|
|
20471
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
20472
|
+
timer = setTimeout(() => {
|
|
20473
|
+
child.kill("SIGKILL");
|
|
20474
|
+
logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
|
|
20475
|
+
finish(err(
|
|
20476
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20477
|
+
deployM("deploy_commandTimeout", { params: { command } })
|
|
20478
|
+
));
|
|
20479
|
+
}, options.timeoutMs);
|
|
20480
|
+
}
|
|
20481
|
+
child.stdout.on("data", (chunk) => {
|
|
20482
|
+
stdout += chunk.toString("utf-8");
|
|
20483
|
+
options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
|
|
20484
|
+
});
|
|
20485
|
+
child.stderr.on("data", (chunk) => {
|
|
20486
|
+
stderr += chunk.toString("utf-8");
|
|
20487
|
+
});
|
|
20488
|
+
child.on("error", (error62) => {
|
|
20489
|
+
logger3.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
|
|
20490
|
+
finish(err(
|
|
20491
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20492
|
+
deployM("deploy_commandFailed", {
|
|
20493
|
+
params: { command, error: maskSecrets(error62.message, options.secrets) }
|
|
20494
|
+
}),
|
|
20495
|
+
error62
|
|
20496
|
+
));
|
|
20497
|
+
});
|
|
20498
|
+
child.on("close", (code) => {
|
|
20499
|
+
const exitCode = code ?? -1;
|
|
20500
|
+
if (exitCode === 0) {
|
|
20501
|
+
finish(ok({ code: exitCode, stdout, stderr }));
|
|
20502
|
+
return;
|
|
20503
|
+
}
|
|
20504
|
+
logger3.error("Command exited with non-zero code", {
|
|
20505
|
+
command,
|
|
20506
|
+
code: exitCode,
|
|
20507
|
+
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
20508
|
+
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
20509
|
+
});
|
|
20510
|
+
finish(err(
|
|
20511
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20512
|
+
deployM("deploy_commandFailed", {
|
|
20513
|
+
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
20514
|
+
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
20515
|
+
})
|
|
20516
|
+
));
|
|
20517
|
+
});
|
|
20518
|
+
if (options.input !== void 0) {
|
|
20519
|
+
child.stdin.write(options.input);
|
|
20520
|
+
child.stdin.end();
|
|
20521
|
+
} else {
|
|
20522
|
+
child.stdin.end();
|
|
20523
|
+
}
|
|
20524
|
+
});
|
|
20525
|
+
}
|
|
20526
|
+
|
|
20527
|
+
// ../deploy/src/container/deploy-scripts.ts
|
|
20528
|
+
function shellArgument(value) {
|
|
20529
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
20530
|
+
}
|
|
20531
|
+
function createApplyScript(options) {
|
|
20532
|
+
const values = {
|
|
20533
|
+
ID: shellArgument(options.deploymentId),
|
|
20534
|
+
PROJECT: shellArgument(`hai-${options.projectName}`),
|
|
20535
|
+
ENV: shellArgument(options.environment),
|
|
20536
|
+
RUNTIME: shellArgument(options.runtime),
|
|
20537
|
+
PARTIAL: options.partial ? "true" : "false",
|
|
20538
|
+
SELECTED: shellArgument(options.selected.join(" ")),
|
|
20539
|
+
ROOT: shellArgument(options.directory),
|
|
20540
|
+
PORT: String(options.hostPort),
|
|
20541
|
+
HEALTH: options.healthPaths.map(shellArgument).join(" ")
|
|
20542
|
+
};
|
|
20543
|
+
return String.raw`#!/bin/sh
|
|
20544
|
+
set -eu
|
|
20545
|
+
umask 077
|
|
20546
|
+
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
20547
|
+
destination=@@ROOT@@
|
|
20548
|
+
if [ "$#" -gt 0 ]; then destination=$1; fi
|
|
20549
|
+
root=$(mkdir -p -- "$destination" && CDPATH= cd -- "$destination" && pwd)
|
|
20550
|
+
id=@@ID@@
|
|
20551
|
+
project=@@PROJECT@@
|
|
20552
|
+
export HAI_DEPLOY_ENVIRONMENT=@@ENV@@
|
|
20553
|
+
runtime=@@RUNTIME@@
|
|
20554
|
+
partial=@@PARTIAL@@
|
|
20555
|
+
orphan_option=''
|
|
20556
|
+
if [ "$partial" = false ]; then orphan_option=--remove-orphans; fi
|
|
20557
|
+
recreate_option=--force-recreate
|
|
20558
|
+
if [ "$partial" = true ]; then recreate_option=--no-recreate; fi
|
|
20559
|
+
selected=@@SELECTED@@
|
|
20560
|
+
release="$root/releases/$id"
|
|
20561
|
+
case "$release/" in "$source_dir/"*) printf 'Extract the package outside the deployment directory before applying it.\n' >&2; exit 1;; esac
|
|
20562
|
+
stage=''
|
|
20563
|
+
previous=''
|
|
20564
|
+
restore=''
|
|
20565
|
+
changed=false
|
|
20566
|
+
locked=false
|
|
20567
|
+
started=$(date +%s)
|
|
20568
|
+
step_started=$started
|
|
20569
|
+
step() {
|
|
20570
|
+
if [ -n "$stage" ]; then
|
|
20571
|
+
duration=$(( $(date +%s) - step_started ))
|
|
20572
|
+
printf '[PASS] %s (%ss)\n' "$stage" "$duration"
|
|
20573
|
+
printf '{"stage":"%s","status":"passed","durationSeconds":%s}\n' "$stage" "$duration" >> "$root/steps-$id.jsonl"
|
|
20574
|
+
fi
|
|
20575
|
+
stage="$1"
|
|
20576
|
+
step_started=$(date +%s)
|
|
20577
|
+
printf '\n[%s] %s\n' "$stage" "$2"
|
|
20578
|
+
}
|
|
20579
|
+
compose() {
|
|
20580
|
+
arguments_file="$release/compose-files"
|
|
20581
|
+
if [ -n "$restore" ]; then arguments_file="$root/releases/$restore/compose-files"; fi
|
|
20582
|
+
# -f 按顺序合并,由 Compose 保留未选服务、卷和环境文件,不自行拼装配置。
|
|
20583
|
+
command_args="$release/.compose-command"
|
|
20584
|
+
# 文件列表先反向读取,再向参数头部加入,保留原始合并顺序。
|
|
20585
|
+
awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}' "$arguments_file" > "$command_args"
|
|
20586
|
+
while IFS= read -r file; do set -- -f "$file" "$@"; done < "$command_args"
|
|
20587
|
+
"$runtime" compose -p "$project" "$@"
|
|
20588
|
+
}
|
|
20589
|
+
remove_selected() {
|
|
20590
|
+
test "$partial" = true || return 0
|
|
20591
|
+
# 直接替换选中容器,避免 Compose Provider 连带重建依赖它的未选服务;不删除卷。
|
|
20592
|
+
for service in "$@"; do
|
|
20593
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service") || return 1
|
|
20594
|
+
for container in $containers; do
|
|
20595
|
+
"$runtime" rm -f "$container" || return 1
|
|
20596
|
+
done
|
|
20597
|
+
done
|
|
20598
|
+
}
|
|
20599
|
+
finish() {
|
|
20600
|
+
code=$?
|
|
20601
|
+
trap - EXIT HUP INT TERM
|
|
20602
|
+
if [ "$code" -ne 0 ]; then
|
|
20603
|
+
printf '[FAIL] %s (exit %s)\n' "$stage" "$code" >&2
|
|
20604
|
+
if [ "$changed" = true ]; then
|
|
20605
|
+
diagnostics="$root/diagnostics-$id.log"
|
|
20606
|
+
: > "$diagnostics"
|
|
20607
|
+
for service in $selected; do
|
|
20608
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service" || true)
|
|
20609
|
+
for container in $containers; do
|
|
20610
|
+
"$runtime" inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}}' "$container" >> "$diagnostics" 2>&1 || true
|
|
20611
|
+
"$runtime" logs --tail 80 "$container" >> "$diagnostics" 2>&1 || true
|
|
20612
|
+
done
|
|
20613
|
+
done
|
|
20614
|
+
printf 'Startup diagnostics saved to %s (private server file).\n' "$diagnostics" >&2
|
|
20615
|
+
fi
|
|
20616
|
+
if [ -f "$release/rollback-images" ]; then
|
|
20617
|
+
while IFS=' ' read -r image reference; do
|
|
20618
|
+
test -n "$reference" || continue
|
|
20619
|
+
"$runtime" tag "$image" "$reference" || printf '[FAIL] Cannot restore image %s\n' "$reference" >&2
|
|
20620
|
+
done < "$release/rollback-images"
|
|
20621
|
+
fi
|
|
20622
|
+
if [ "$changed" = true ] && [ -n "$previous" ]; then
|
|
20623
|
+
printf '[RECOVER] Restoring previous selected services\n' >&2
|
|
20624
|
+
recover_selected=$selected
|
|
20625
|
+
if [ "$partial" = false ]; then recover_selected=$(grep -v -- '-init$' "$root/releases/$previous/services"); fi
|
|
20626
|
+
restore="$previous"
|
|
20627
|
+
if remove_selected $recover_selected && compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $recover_selected; then
|
|
20628
|
+
case " $selected " in *' app '*)
|
|
20629
|
+
if grep -qx web "$root/releases/$previous/services"; then compose exec -T web nginx -s reload || true; fi
|
|
20630
|
+
;;
|
|
20631
|
+
esac
|
|
20632
|
+
printf '[RECOVER] Previous containers restored; verify application health\n' >&2
|
|
20633
|
+
else
|
|
20634
|
+
printf '[FAIL] Recovery failed. Use the previous release and inspect container logs.\n' >&2
|
|
20635
|
+
fi
|
|
20636
|
+
elif [ "$changed" = true ]; then
|
|
20637
|
+
compose stop $selected || true
|
|
20638
|
+
printf '[RECOVER] Failed first installation stopped; data volumes retained\n' >&2
|
|
20639
|
+
fi
|
|
20640
|
+
fi
|
|
20641
|
+
if [ "$locked" = true ]; then
|
|
20642
|
+
elapsed=$(( $(date +%s) - started ))
|
|
20643
|
+
outcome=passed
|
|
20644
|
+
if [ "$code" -ne 0 ]; then outcome=failed; fi
|
|
20645
|
+
printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
|
|
20646
|
+
printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json"
|
|
20647
|
+
rmdir "$root/.deploy-lock" || true
|
|
20648
|
+
fi
|
|
20649
|
+
exit "$code"
|
|
20650
|
+
}
|
|
20651
|
+
trap finish EXIT
|
|
20652
|
+
trap 'exit 130' HUP INT TERM
|
|
20653
|
+
step preflight 'Checking deployment tools and package'
|
|
20654
|
+
for tool in sha256sum tar awk curl; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
|
|
20655
|
+
if [ "$runtime" = auto ]; then
|
|
20656
|
+
if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
|
|
20657
|
+
fi
|
|
20658
|
+
case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
|
|
20659
|
+
"$runtime" version >/dev/null
|
|
20660
|
+
"$runtime" compose version >/dev/null
|
|
20661
|
+
"$runtime" compose up --help | grep -q never || { printf 'Compose is too old. Install Docker Compose v2 or podman-compose 1.6.0 or newer (supports --pull never).\n' >&2; exit 1; }
|
|
20662
|
+
mkdir "$root/.deploy-lock" 2>/dev/null || { printf 'Another deployment is running. Check %s/.deploy-lock before retrying.\n' "$root" >&2; exit 1; }
|
|
20663
|
+
locked=true
|
|
20664
|
+
cd "$source_dir"
|
|
20665
|
+
sha256sum -c SHA256SUMS
|
|
20666
|
+
if [ -f "$root/current" ]; then
|
|
20667
|
+
previous=$(cat "$root/current")
|
|
20668
|
+
case "$previous" in ''|*[!a-zA-Z0-9._-]*) printf 'Invalid deployment state\n' >&2; exit 1;; esac
|
|
20669
|
+
test -f "$root/releases/$previous/compose-files"
|
|
20670
|
+
fi
|
|
20671
|
+
if [ "$partial" = true ]; then
|
|
20672
|
+
test -n "$previous" || { printf 'Partial deployment requires a successful full installation. Deploy a complete bundle first.\n' >&2; exit 1; }
|
|
20673
|
+
cmp -s layout "$root/releases/$previous/layout" || { printf 'Ports, API routes or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
|
|
20674
|
+
fi
|
|
20675
|
+
test ! -e "$release" || { printf 'This deployment ID was already used. Create a new deployment bundle.\n' >&2; exit 1; }
|
|
20676
|
+
mkdir -p "$release"
|
|
20677
|
+
cp -R "$source_dir/." "$release/"
|
|
20678
|
+
# Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
|
|
20679
|
+
if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
|
|
20680
|
+
: > "$release/compose-files"
|
|
20681
|
+
if [ "$partial" = true ]; then
|
|
20682
|
+
cp "$root/releases/$previous/compose-files" "$release/compose-files"
|
|
20683
|
+
cp "$root/releases/$previous/middleware.env" "$release/middleware.env"
|
|
20684
|
+
for service in $selected; do
|
|
20685
|
+
grep -qx "$service" "$root/releases/$previous/services" || { printf 'Service %s has no installed baseline. Use a full deployment.\n' "$service" >&2; exit 1; }
|
|
20686
|
+
done
|
|
20687
|
+
fi
|
|
20688
|
+
printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
|
|
20689
|
+
compose config >/dev/null
|
|
20690
|
+
compose config --services > "$release/services"
|
|
20691
|
+
: > "$release/rollback-images"
|
|
20692
|
+
if [ -n "$previous" ]; then
|
|
20693
|
+
for service in $selected; do
|
|
20694
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service")
|
|
20695
|
+
for container in $containers; do
|
|
20696
|
+
"$runtime" inspect --format '{{.Image}} {{.Config.Image}}' "$container" >> "$release/rollback-images"
|
|
20697
|
+
done
|
|
20698
|
+
done
|
|
20699
|
+
fi
|
|
20700
|
+
step images 'Loading packaged images (no registry access)'
|
|
20701
|
+
while IFS=' ' read -r archive reference; do
|
|
20702
|
+
test -n "$archive" || continue
|
|
20703
|
+
"$runtime" load -i "$release/images/$archive"
|
|
20704
|
+
"$runtime" image inspect "$reference" >/dev/null
|
|
20705
|
+
architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
|
|
20706
|
+
host_arch=$(uname -m)
|
|
20707
|
+
case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
|
|
20708
|
+
done < "$release/image-list"
|
|
20709
|
+
step dependencies 'Checking dependencies of selected services'
|
|
20710
|
+
if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
|
|
20711
|
+
while IFS= read -r dependency; do
|
|
20712
|
+
test -n "$dependency" || continue
|
|
20713
|
+
case "$dependency" in *-init) continue;; esac
|
|
20714
|
+
case " $selected " in *" $dependency "*) continue;; esac
|
|
20715
|
+
running=$("$runtime" ps -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$dependency")
|
|
20716
|
+
test -n "$running" || { printf 'Dependency %s is not running. Include it in a full deployment.\n' "$dependency" >&2; exit 1; }
|
|
20717
|
+
for container in $running; do
|
|
20718
|
+
test "$("$runtime" inspect --format '{{.State.Running}}' "$container")" = true || { printf 'Dependency %s is stopped. Start it before retrying.\n' "$dependency" >&2; exit 1; }
|
|
20719
|
+
done
|
|
20720
|
+
done < "$release/dependencies"
|
|
20721
|
+
step start 'Updating selected services'
|
|
20722
|
+
changed=true
|
|
20723
|
+
remove_selected $selected
|
|
20724
|
+
if [ -f "$release/initialize-storage" ]; then
|
|
20725
|
+
remove_selected storage-init
|
|
20726
|
+
compose up -d --no-deps --no-build --pull never storage
|
|
20727
|
+
compose up --no-deps --no-build --pull never "$recreate_option" --exit-code-from storage-init storage-init
|
|
20728
|
+
fi
|
|
20729
|
+
compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $selected
|
|
20730
|
+
case " $selected " in
|
|
20731
|
+
*' app '*)
|
|
20732
|
+
case " $selected " in *' web '*) ;; *)
|
|
20733
|
+
if grep -qx web "$release/services"; then compose exec -T web nginx -s reload; fi
|
|
20734
|
+
esac
|
|
20735
|
+
;;
|
|
20736
|
+
esac
|
|
20737
|
+
step health 'Checking HTTP access'
|
|
20738
|
+
for path in @@HEALTH@@; do
|
|
20739
|
+
ready=false
|
|
20740
|
+
attempt=0
|
|
20741
|
+
while [ "$attempt" -lt 40 ]; do
|
|
20742
|
+
if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:@@PORT@@$path" >/dev/null 2>&1; then ready=true; break; fi
|
|
20743
|
+
attempt=$((attempt + 1))
|
|
20744
|
+
sleep 3
|
|
20745
|
+
done
|
|
20746
|
+
test "$ready" = true || { printf 'HTTP check failed: %s. Inspect compose logs for selected services.\n' "$path" >&2; exit 1; }
|
|
20747
|
+
done
|
|
20748
|
+
step complete 'Deployment passed all checks'
|
|
20749
|
+
printf '%s\n' "$id" > "$root/current.next"
|
|
20750
|
+
mv "$root/current.next" "$root/current"
|
|
20751
|
+
printf '[PASS] %s\n' "$id"
|
|
20752
|
+
`.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
|
|
20753
|
+
}
|
|
20754
|
+
function createUploadScripts(config2, bundle, projectName) {
|
|
20755
|
+
const ssh = config2.ssh;
|
|
20756
|
+
const host = ssh?.host ?? "SERVER_HOST";
|
|
20757
|
+
const user = ssh?.username ?? "deploy";
|
|
20758
|
+
const remote = `${config2.remote.baseDir}/${projectName}`;
|
|
20759
|
+
const port = ssh?.port ?? 22;
|
|
20760
|
+
const sh = String.raw`#!/bin/sh
|
|
20761
|
+
set -eu
|
|
20762
|
+
cd "$(dirname "$0")"
|
|
20763
|
+
host=@@HOST@@
|
|
20764
|
+
user=@@USER@@
|
|
20765
|
+
port=@@PORT@@
|
|
20766
|
+
remote=@@REMOTE@@
|
|
20767
|
+
bundle=@@BUNDLE@@
|
|
20768
|
+
if [ "$#" -ge 1 ]; then host=$1; fi
|
|
20769
|
+
if [ "$#" -ge 2 ]; then port=$2; fi
|
|
20770
|
+
if [ "$#" -ge 3 ]; then user=$3; fi
|
|
20771
|
+
identity=$(printenv HAI_DEPLOY_IDENTITY_FILE || true)
|
|
20772
|
+
if [ -z "$identity" ]; then identity=@@IDENTITY@@; fi
|
|
20773
|
+
test "$host" != SERVER_HOST || { printf 'Specify the server: sh deploy.sh SERVER [PORT] [USER].\n' >&2; exit 1; }
|
|
20774
|
+
for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
|
|
20775
|
+
set -- -o BatchMode=yes -o StrictHostKeyChecking=@@STRICT@@ -o ConnectTimeout=10
|
|
20776
|
+
if [ -n "$identity" ]; then set -- "$@" -i "$identity"; fi
|
|
20777
|
+
known_hosts=@@KNOWN@@
|
|
20778
|
+
if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE); then known_hosts=$value; fi
|
|
20779
|
+
if [ -n "$known_hosts" ]; then set -- "$@" -o "UserKnownHostsFile=$known_hosts"; fi
|
|
20780
|
+
printf '[1/3] Connecting to %s\n' "$host"
|
|
20781
|
+
ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
|
|
20782
|
+
printf '[2/3] Uploading %s\n' "$bundle"
|
|
20783
|
+
scp_host=$host
|
|
20784
|
+
case "$host" in *:*) scp_host="[$host]";; esac
|
|
20785
|
+
scp "$@" -P "$port" "$bundle" "$user@$scp_host:$remote/incoming/$bundle"
|
|
20786
|
+
printf '[3/3] Applying package and checking HTTP access\n'
|
|
20787
|
+
ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"
|
|
20788
|
+
`;
|
|
20789
|
+
const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
|
|
20790
|
+
$ErrorActionPreference = 'Stop'
|
|
20791
|
+
Set-Location -LiteralPath $PSScriptRoot
|
|
20792
|
+
$target = Get-Content -LiteralPath './deploy-target.json' -Raw | ConvertFrom-Json
|
|
20793
|
+
if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
|
|
20794
|
+
if ($target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]+$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
|
|
20795
|
+
$options = @('-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'ConnectTimeout=10')
|
|
20796
|
+
if ($target.strictHostKeyChecking -eq $false) { $options[3] = 'StrictHostKeyChecking=no' }
|
|
20797
|
+
if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
|
|
20798
|
+
if ($IdentityFile) { $options += @('-i', $IdentityFile) }
|
|
20799
|
+
if ($target.knownHostsFile) { $options += @('-o', "UserKnownHostsFile=$($target.knownHostsFile)") }
|
|
20800
|
+
$destination = "$($target.username)@$($target.host)"
|
|
20801
|
+
$scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
|
|
20802
|
+
$remote = $target.directory
|
|
20803
|
+
$bundle = $target.bundle
|
|
20804
|
+
function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
|
|
20805
|
+
Write-Host "[$Name]"
|
|
20806
|
+
& $Command @Arguments
|
|
20807
|
+
if ($LASTEXITCODE -ne 0) { throw "$Name failed (exit $LASTEXITCODE)." }
|
|
20808
|
+
}
|
|
20809
|
+
try {
|
|
20810
|
+
foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
|
|
20811
|
+
Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$target.port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
|
|
20812
|
+
Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$target.port, $bundle, "$($scpDestination):$remote/incoming/$bundle"))
|
|
20813
|
+
Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$target.port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
|
|
20814
|
+
} catch { Write-Error $_; exit 1 }
|
|
20815
|
+
`;
|
|
20816
|
+
const values = { HOST: shellArgument(host), USER: shellArgument(user), PORT: String(port), REMOTE: shellArgument(remote), BUNDLE: shellArgument(bundle), IDENTITY: shellArgument(ssh?.identityFile ?? ""), KNOWN: shellArgument(ssh?.knownHostsFile ?? ""), STRICT: ssh?.strictHostKeyChecking === false ? "no" : "accept-new" };
|
|
20817
|
+
return { sh: sh.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]), ps1 };
|
|
20818
|
+
}
|
|
20819
|
+
|
|
20820
|
+
// ../deploy/src/container/deploy-bundle.ts
|
|
20821
|
+
async function createDeploymentBundle(options) {
|
|
20822
|
+
const { directory, artifactDir, artifact, projectName, environment, config: config2, selected } = options;
|
|
20823
|
+
const stem = basename(options.output).replace(/\.tar$/i, "");
|
|
20824
|
+
const suffix = `-${artifact.appVersion}`;
|
|
20825
|
+
const output2 = resolve(dirname(options.output), `${stem.endsWith(suffix) ? stem : `${stem}${suffix}`}.tar`);
|
|
20826
|
+
if (!/^[\w.+-]+\.tar$/.test(basename(output2)))
|
|
20827
|
+
throw new Error(deployM("deploy_bundleNameInvalid"));
|
|
20828
|
+
const staging = mkdtempSync(join(directory, ".bundle-"));
|
|
20829
|
+
const temporaryTar = `${output2}.partial`;
|
|
20830
|
+
mkdirSync(dirname(output2), { recursive: true, mode: 448 });
|
|
20831
|
+
chmodSync(staging, 448);
|
|
20832
|
+
const files = [];
|
|
20833
|
+
const put = (file2, content) => {
|
|
20834
|
+
mkdirSync(dirname(join(staging, file2)), { recursive: true });
|
|
20835
|
+
writeFileSync(join(staging, file2), content, { mode: 384 });
|
|
20836
|
+
files.push(file2);
|
|
20837
|
+
};
|
|
20838
|
+
try {
|
|
20839
|
+
mkdirSync(join(staging, "images"));
|
|
20840
|
+
const compose = parse$1(readFileSync(join(directory, `docker-compose-${artifact.appVersion}.yml`), "utf8"));
|
|
20841
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
20842
|
+
const references = /* @__PURE__ */ new Set();
|
|
20843
|
+
for (const name of selected) {
|
|
20844
|
+
const service = compose.services[name];
|
|
20845
|
+
if (!service)
|
|
20846
|
+
throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
|
|
20847
|
+
if (typeof service.image === "string")
|
|
20848
|
+
references.add(service.image);
|
|
20849
|
+
if (service.depends_on) {
|
|
20850
|
+
for (const dependency of Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on))
|
|
20851
|
+
dependencies.add(dependency);
|
|
20852
|
+
}
|
|
20853
|
+
if (service.env_file)
|
|
20854
|
+
service.env_file = [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`];
|
|
20855
|
+
if (Array.isArray(service.volumes))
|
|
20856
|
+
service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`) : mount);
|
|
20857
|
+
}
|
|
20858
|
+
put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
|
|
20859
|
+
put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
|
|
20860
|
+
let runtime;
|
|
20861
|
+
const imageList = [];
|
|
20862
|
+
for (const reference of references) {
|
|
20863
|
+
if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
|
|
20864
|
+
throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
|
|
20865
|
+
const built = artifact.images.find((image) => image.imageRef === reference);
|
|
20866
|
+
const file2 = built?.file ?? `middleware-${imageList.length + 1}.tar`;
|
|
20867
|
+
if (built) {
|
|
20868
|
+
copyFileSync(join(artifactDir, file2), join(staging, "images", file2));
|
|
20869
|
+
} else {
|
|
20870
|
+
if (!runtime) {
|
|
20871
|
+
const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
|
|
20872
|
+
for (const candidate of candidates) {
|
|
20873
|
+
if ((await runCommand(candidate, ["version"])).success) {
|
|
20874
|
+
runtime = candidate;
|
|
20875
|
+
break;
|
|
20876
|
+
}
|
|
20877
|
+
}
|
|
20878
|
+
if (!runtime)
|
|
20879
|
+
throw new Error(deployM("deploy_bundleRuntimeRequired"));
|
|
20880
|
+
}
|
|
20881
|
+
if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
|
|
20882
|
+
const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
|
|
20883
|
+
if (!pulled.success)
|
|
20884
|
+
throw new Error(pulled.error.message);
|
|
20885
|
+
}
|
|
20886
|
+
const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file2), reference], { timeoutMs: 6e5 });
|
|
20887
|
+
if (!saved.success)
|
|
20888
|
+
throw new Error(saved.error.message);
|
|
20889
|
+
}
|
|
20890
|
+
files.push(`images/${file2}`);
|
|
20891
|
+
imageList.push(`${file2} ${reference}`);
|
|
20892
|
+
}
|
|
20893
|
+
put(".env.runtime", readFileSync(join(directory, ".env.runtime"), "utf8"));
|
|
20894
|
+
put("middleware.env", options.middlewareEnv);
|
|
20895
|
+
if (selected.includes("web"))
|
|
20896
|
+
put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
|
|
20897
|
+
put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
|
|
20898
|
+
`);
|
|
20899
|
+
put("layout", `${config2.expose.hostPort}:${options.containerPort}:${options.hasWeb}:${options.hasWeb ? await artifactChecksum(join(directory, "web.conf")) : ""}
|
|
20900
|
+
`);
|
|
20901
|
+
put("image-list", `${imageList.join("\n")}
|
|
20902
|
+
`);
|
|
20903
|
+
put("dependencies", `${[...dependencies].join("\n")}
|
|
20904
|
+
`);
|
|
20905
|
+
if (selected.includes("storage-init"))
|
|
20906
|
+
put("initialize-storage", "");
|
|
20907
|
+
put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: `${config2.remote.baseDir}/${projectName}`, runtime: config2.remote.runtime, partial: options.partial, selected: selected.filter((name) => name !== "storage-init"), hostPort: config2.expose.hostPort, healthPaths: options.healthPaths }));
|
|
20908
|
+
const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" } });
|
|
20909
|
+
put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
|
|
20910
|
+
${instructions}
|
|
20911
|
+
`);
|
|
20912
|
+
const checksums = [];
|
|
20913
|
+
for (const file2 of files)
|
|
20914
|
+
checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
|
|
20915
|
+
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
20916
|
+
`);
|
|
20917
|
+
const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
|
|
20918
|
+
if (!tar.success)
|
|
20919
|
+
throw new Error(tar.error.message);
|
|
20920
|
+
chmodSync(temporaryTar, 384);
|
|
20921
|
+
renameSync(temporaryTar, output2);
|
|
20922
|
+
const upload = createUploadScripts(config2, basename(output2), projectName);
|
|
20923
|
+
const scripts = `${output2.slice(0, -4)}-deploy`;
|
|
20924
|
+
mkdirSync(scripts, { recursive: true, mode: 448 });
|
|
20925
|
+
writeFileSync(join(scripts, "deploy.sh"), upload.sh.replace('cd "$(dirname "$0")"', 'cd "$(dirname "$0")/.."'), { mode: 448 });
|
|
20926
|
+
writeFileSync(join(scripts, "deploy.ps1"), upload.ps1.replace("Set-Location -LiteralPath $PSScriptRoot", "Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)").replace("'./deploy-target.json'", "(Join-Path $PSScriptRoot 'deploy-target.json')"));
|
|
20927
|
+
writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...config2.ssh, host: config2.ssh?.host ?? "SERVER_HOST", username: config2.ssh?.username ?? "deploy", port: config2.ssh?.port ?? 22, directory: `${config2.remote.baseDir}/${projectName}`, bundle: basename(output2) }, null, 2)}
|
|
20928
|
+
`);
|
|
20929
|
+
writeFileSync(join(scripts, "README.txt"), instructions);
|
|
20930
|
+
return { bundlePath: output2, instructions: `${instructions}
|
|
20931
|
+
${scripts}`, startName: "apply.sh" };
|
|
20932
|
+
} finally {
|
|
20933
|
+
rmSync(staging, { recursive: true, force: true });
|
|
20934
|
+
rmSync(temporaryTar, { force: true });
|
|
20935
|
+
}
|
|
20936
|
+
}
|
|
20381
20937
|
var IMAGES = {
|
|
20382
|
-
postgres: "postgres:16-alpine",
|
|
20383
|
-
mysql: "mysql:8.4",
|
|
20384
|
-
redis: "redis:7-alpine",
|
|
20938
|
+
postgres: "docker.io/library/postgres:16.15-alpine",
|
|
20939
|
+
mysql: "docker.io/library/mysql:8.4.11",
|
|
20940
|
+
redis: "docker.io/library/redis:7.4.11-alpine",
|
|
20385
20941
|
rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
|
|
20386
20942
|
minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
|
|
20387
|
-
qdrant: "qdrant/qdrant:v1.
|
|
20388
|
-
pgvector: "pgvector/pgvector:pg17",
|
|
20389
|
-
chroma: "chromadb/chroma:1.5.5",
|
|
20943
|
+
qdrant: "docker.io/qdrant/qdrant:v1.19.2",
|
|
20944
|
+
pgvector: "docker.io/pgvector/pgvector:0.8.0-pg17",
|
|
20945
|
+
chroma: "docker.io/chromadb/chroma:1.5.5",
|
|
20390
20946
|
ldap: "docker.io/osixia/openldap:1.5.0",
|
|
20391
|
-
smtp: "axllent/mailpit:v1.27.4"
|
|
20947
|
+
smtp: "docker.io/axllent/mailpit:v1.27.4"
|
|
20392
20948
|
};
|
|
20393
20949
|
function generatePassword() {
|
|
20394
20950
|
return randomBytes(24).toString("base64url");
|
|
@@ -20397,8 +20953,12 @@ function resolveSidecarSecrets(appDir, projectName) {
|
|
|
20397
20953
|
const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
|
|
20398
20954
|
if (existsSync(secretFile)) {
|
|
20399
20955
|
try {
|
|
20400
|
-
|
|
20956
|
+
const saved = JSON.parse(readFileSync(secretFile, "utf-8"));
|
|
20957
|
+
if (!saved || !["dbPassword", "storagePassword", "ldapPassword"].every((key) => typeof saved[key] === "string" && saved[key].length > 0))
|
|
20958
|
+
throw new Error("Invalid credentials");
|
|
20959
|
+
return saved;
|
|
20401
20960
|
} catch {
|
|
20961
|
+
throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
|
|
20402
20962
|
}
|
|
20403
20963
|
}
|
|
20404
20964
|
const secrets = {
|
|
@@ -20566,6 +21126,28 @@ function planChromaVecdb(plan, projectName, net) {
|
|
|
20566
21126
|
plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
|
|
20567
21127
|
}
|
|
20568
21128
|
function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
|
|
21129
|
+
const serviceModes = { ...options?.serviceModes };
|
|
21130
|
+
for (const requirement of scan.middlewares ?? []) {
|
|
21131
|
+
if (options?.selectedServices && !options.selectedServices.includes(requirement.service))
|
|
21132
|
+
continue;
|
|
21133
|
+
const mode = serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service];
|
|
21134
|
+
if (mode !== "keep")
|
|
21135
|
+
continue;
|
|
21136
|
+
const endpoint = requirement.config.url ?? requirement.config.endpoint;
|
|
21137
|
+
let host = requirement.config.host;
|
|
21138
|
+
if (typeof endpoint === "string") {
|
|
21139
|
+
try {
|
|
21140
|
+
host = new URL(endpoint).hostname;
|
|
21141
|
+
} catch {
|
|
21142
|
+
continue;
|
|
21143
|
+
}
|
|
21144
|
+
}
|
|
21145
|
+
if (host === requirement.service || ["sqlite", "local", "memory", "lancedb"].includes(requirement.backend)) {
|
|
21146
|
+
serviceModes[requirement.envPrefix] = "default";
|
|
21147
|
+
serviceModes[requirement.service] = "default";
|
|
21148
|
+
}
|
|
21149
|
+
}
|
|
21150
|
+
options = { ...options, serviceModes };
|
|
20569
21151
|
const plan = {
|
|
20570
21152
|
services: [],
|
|
20571
21153
|
composeServices: {},
|
|
@@ -20651,6 +21233,7 @@ function generateComposeFile(options) {
|
|
|
20651
21233
|
const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
|
|
20652
21234
|
const app = {
|
|
20653
21235
|
image: imageRef,
|
|
21236
|
+
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
20654
21237
|
container_name: `hai-${projectName}-app`,
|
|
20655
21238
|
restart: "unless-stopped",
|
|
20656
21239
|
// 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
|
|
@@ -20664,41 +21247,76 @@ function generateComposeFile(options) {
|
|
|
20664
21247
|
if (sidecar.dependsOn.length > 0)
|
|
20665
21248
|
app.depends_on = sidecar.dependsOn;
|
|
20666
21249
|
const services = { app, ...sidecar.composeServices };
|
|
21250
|
+
if (options.web) {
|
|
21251
|
+
delete app.ports;
|
|
21252
|
+
services.web = {
|
|
21253
|
+
image: options.web.imageRef,
|
|
21254
|
+
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
21255
|
+
container_name: `hai-${projectName}-web`,
|
|
21256
|
+
restart: "unless-stopped",
|
|
21257
|
+
pull_policy: "never",
|
|
21258
|
+
ports: [`${hostPort}:${options.web.containerPort}`],
|
|
21259
|
+
environment: { PUBLIC_API_BASE: "" },
|
|
21260
|
+
healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
|
|
21261
|
+
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
21262
|
+
depends_on: ["app"],
|
|
21263
|
+
networks: ["hai-app"]
|
|
21264
|
+
};
|
|
21265
|
+
}
|
|
20667
21266
|
if (services["storage-init"])
|
|
20668
21267
|
services["storage-init"] = { ...services["storage-init"], image: imageRef };
|
|
20669
21268
|
const compose = {
|
|
21269
|
+
"x-hai-version": options.appVersion,
|
|
20670
21270
|
services,
|
|
20671
|
-
networks: {
|
|
21271
|
+
"networks": {
|
|
20672
21272
|
"hai-app": { name: `hai-${projectName}` }
|
|
20673
21273
|
}
|
|
20674
21274
|
};
|
|
20675
21275
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
20676
21276
|
compose.volumes = sidecar.volumes;
|
|
20677
|
-
if (options.previousCompose && options.serviceModes) {
|
|
20678
|
-
const previous = parse$1(options.previousCompose);
|
|
20679
|
-
for (const [service, mode] of Object.entries(options.serviceModes)) {
|
|
20680
|
-
if (mode !== "keep")
|
|
20681
|
-
continue;
|
|
20682
|
-
for (const name of [service, `${service}-init`]) {
|
|
20683
|
-
if (previous.services?.[name]) {
|
|
20684
|
-
services[name] = previous.services[name];
|
|
20685
|
-
app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
|
|
20686
|
-
}
|
|
20687
|
-
}
|
|
20688
|
-
}
|
|
20689
|
-
const previousMounts = previous.services?.app?.volumes;
|
|
20690
|
-
if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
|
|
20691
|
-
app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
|
|
20692
|
-
compose.volumes = { ...previous.volumes, ...sidecar.volumes };
|
|
20693
|
-
}
|
|
20694
21277
|
if (services["storage-init"]) {
|
|
20695
21278
|
app.depends_on = {
|
|
20696
21279
|
...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
|
|
20697
21280
|
"storage-init": { condition: "service_completed_successfully" }
|
|
20698
21281
|
};
|
|
20699
21282
|
}
|
|
21283
|
+
for (const service of Object.values(services))
|
|
21284
|
+
service.pull_policy = "never";
|
|
20700
21285
|
return stringify(compose);
|
|
20701
21286
|
}
|
|
21287
|
+
function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
|
|
21288
|
+
if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
|
|
21289
|
+
throw new Error("Invalid web proxy target");
|
|
21290
|
+
return `server {
|
|
21291
|
+
listen ${webPort};
|
|
21292
|
+
server_name _;
|
|
21293
|
+
root /usr/share/nginx/html;
|
|
21294
|
+
location / {
|
|
21295
|
+
try_files $uri $uri/ /index.html;
|
|
21296
|
+
}
|
|
21297
|
+
location = /runtime-config.js {
|
|
21298
|
+
add_header Cache-Control "no-store";
|
|
21299
|
+
default_type application/javascript;
|
|
21300
|
+
return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
|
|
21301
|
+
}
|
|
21302
|
+
location = /_hai/web-health {
|
|
21303
|
+
access_log off;
|
|
21304
|
+
return 200 'ok';
|
|
21305
|
+
}
|
|
21306
|
+
location = /health {
|
|
21307
|
+
proxy_pass http://app:${port};
|
|
21308
|
+
proxy_set_header Host $http_host;
|
|
21309
|
+
}
|
|
21310
|
+
location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
21311
|
+
proxy_pass http://app:${port};
|
|
21312
|
+
proxy_http_version 1.1;
|
|
21313
|
+
proxy_set_header Host $http_host;
|
|
21314
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
21315
|
+
proxy_buffering off;
|
|
21316
|
+
}
|
|
21317
|
+
}
|
|
21318
|
+
`;
|
|
21319
|
+
}
|
|
20702
21320
|
function serializeEnvFile(env) {
|
|
20703
21321
|
return `${Object.entries(env).map(([key, value]) => {
|
|
20704
21322
|
let literal2 = value;
|
|
@@ -20714,194 +21332,7 @@ function serializeEnvFile(env) {
|
|
|
20714
21332
|
}).join("\n")}
|
|
20715
21333
|
`;
|
|
20716
21334
|
}
|
|
20717
|
-
|
|
20718
|
-
return `**/node_modules
|
|
20719
|
-
**/dist
|
|
20720
|
-
**/build
|
|
20721
|
-
**/.turbo
|
|
20722
|
-
**/.svelte-kit
|
|
20723
|
-
**/coverage
|
|
20724
|
-
**/test-results
|
|
20725
|
-
**/playwright-report
|
|
20726
|
-
**/.hai/deploy
|
|
20727
|
-
**/.env.local
|
|
20728
|
-
**/.env.*.local
|
|
20729
|
-
**/.env.runtime
|
|
20730
|
-
.git
|
|
20731
|
-
`;
|
|
20732
|
-
}
|
|
20733
|
-
function generateFallbackDockerfile(packageName, containerPort, options = {}) {
|
|
20734
|
-
if (!/^@?[a-z0-9][\w./-]*$/i.test(packageName) || !/^[a-z0-9][\w-]*$/i.test(options.environment ?? "prod"))
|
|
20735
|
-
throw new Error("Invalid container build target");
|
|
20736
|
-
const environment = options.environment ?? "prod";
|
|
20737
|
-
const coreEnv = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
20738
|
-
return `# syntax=docker/dockerfile:1.7
|
|
20739
|
-
FROM node:24-bookworm-slim AS build
|
|
20740
|
-
ARG SQLITE_PREBUILD_MIRROR
|
|
20741
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
20742
|
-
ENV PNPM_HOME=/pnpm
|
|
20743
|
-
ENV PATH=$PNPM_HOME:$PATH
|
|
20744
|
-
ENV CI=true
|
|
20745
|
-
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
20746
|
-
ENV HAI_CORE_ENV=${coreEnv}
|
|
20747
|
-
WORKDIR /workspace
|
|
20748
|
-
RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
|
|
20749
|
-
COPY . .
|
|
20750
|
-
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
20751
|
-
RUN pnpm --filter ${packageName}... build
|
|
20752
|
-
RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
|
|
20753
|
-
RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
|
|
20754
|
-
|
|
20755
|
-
FROM node:24-bookworm-slim AS runtime
|
|
20756
|
-
ENV NODE_ENV=production
|
|
20757
|
-
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
20758
|
-
ENV HAI_CORE_ENV=${coreEnv}
|
|
20759
|
-
ENV HOST=0.0.0.0
|
|
20760
|
-
ENV PORT=${containerPort}
|
|
20761
|
-
WORKDIR /app
|
|
20762
|
-
COPY --from=build --chown=node:node /prod/app ./
|
|
20763
|
-
RUN mkdir -p data && chown node:node data
|
|
20764
|
-
USER node
|
|
20765
|
-
EXPOSE ${containerPort}
|
|
20766
|
-
CMD ["node", ${JSON.stringify(options.entrypoint ?? "build")}]
|
|
20767
|
-
`;
|
|
20768
|
-
}
|
|
20769
|
-
var logger4 = core.logger.child({ module: "deploy", scope: "command" });
|
|
20770
|
-
function maskSecrets(text, secrets) {
|
|
20771
|
-
if (!secrets || secrets.length === 0)
|
|
20772
|
-
return text;
|
|
20773
|
-
let masked = text;
|
|
20774
|
-
for (const secret of secrets) {
|
|
20775
|
-
if (secret && secret.length > 0)
|
|
20776
|
-
masked = masked.split(secret).join("***");
|
|
20777
|
-
}
|
|
20778
|
-
return masked;
|
|
20779
|
-
}
|
|
20780
|
-
function runCommand(command, args, options = {}) {
|
|
20781
|
-
return new Promise((resolve3) => {
|
|
20782
|
-
const child = spawn(command, args, {
|
|
20783
|
-
cwd: options.cwd,
|
|
20784
|
-
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
20785
|
-
shell: false
|
|
20786
|
-
});
|
|
20787
|
-
let stdout = "";
|
|
20788
|
-
let stderr = "";
|
|
20789
|
-
let settled = false;
|
|
20790
|
-
let timer;
|
|
20791
|
-
const finish = (result) => {
|
|
20792
|
-
if (settled)
|
|
20793
|
-
return;
|
|
20794
|
-
settled = true;
|
|
20795
|
-
if (timer)
|
|
20796
|
-
clearTimeout(timer);
|
|
20797
|
-
resolve3(result);
|
|
20798
|
-
};
|
|
20799
|
-
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
20800
|
-
timer = setTimeout(() => {
|
|
20801
|
-
child.kill("SIGKILL");
|
|
20802
|
-
logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
|
|
20803
|
-
finish(err(
|
|
20804
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20805
|
-
deployM("deploy_commandTimeout", { params: { command } })
|
|
20806
|
-
));
|
|
20807
|
-
}, options.timeoutMs);
|
|
20808
|
-
}
|
|
20809
|
-
child.stdout.on("data", (chunk) => {
|
|
20810
|
-
stdout += chunk.toString("utf-8");
|
|
20811
|
-
});
|
|
20812
|
-
child.stderr.on("data", (chunk) => {
|
|
20813
|
-
stderr += chunk.toString("utf-8");
|
|
20814
|
-
});
|
|
20815
|
-
child.on("error", (error62) => {
|
|
20816
|
-
logger4.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
|
|
20817
|
-
finish(err(
|
|
20818
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20819
|
-
deployM("deploy_commandFailed", {
|
|
20820
|
-
params: { command, error: maskSecrets(error62.message, options.secrets) }
|
|
20821
|
-
}),
|
|
20822
|
-
error62
|
|
20823
|
-
));
|
|
20824
|
-
});
|
|
20825
|
-
child.on("close", (code) => {
|
|
20826
|
-
const exitCode = code ?? -1;
|
|
20827
|
-
if (exitCode === 0) {
|
|
20828
|
-
finish(ok({ code: exitCode, stdout, stderr }));
|
|
20829
|
-
return;
|
|
20830
|
-
}
|
|
20831
|
-
logger4.error("Command exited with non-zero code", {
|
|
20832
|
-
command,
|
|
20833
|
-
code: exitCode,
|
|
20834
|
-
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
20835
|
-
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
20836
|
-
});
|
|
20837
|
-
finish(err(
|
|
20838
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20839
|
-
deployM("deploy_commandFailed", {
|
|
20840
|
-
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
20841
|
-
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
20842
|
-
})
|
|
20843
|
-
));
|
|
20844
|
-
});
|
|
20845
|
-
if (options.input !== void 0) {
|
|
20846
|
-
child.stdin.write(options.input);
|
|
20847
|
-
child.stdin.end();
|
|
20848
|
-
} else {
|
|
20849
|
-
child.stdin.end();
|
|
20850
|
-
}
|
|
20851
|
-
});
|
|
20852
|
-
}
|
|
20853
|
-
|
|
20854
|
-
// ../deploy/src/container/deploy-container-runtime.ts
|
|
20855
|
-
var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
|
|
20856
|
-
async function detectLocalRuntime(preferred = "auto") {
|
|
20857
|
-
const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
|
|
20858
|
-
for (const runtime of candidates) {
|
|
20859
|
-
const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
|
|
20860
|
-
if (probe.success) {
|
|
20861
|
-
logger5.info("Local container runtime detected", { runtime });
|
|
20862
|
-
return ok(runtime);
|
|
20863
|
-
}
|
|
20864
|
-
}
|
|
20865
|
-
return err(
|
|
20866
|
-
HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
|
|
20867
|
-
deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
|
|
20868
|
-
);
|
|
20869
|
-
}
|
|
20870
|
-
async function buildImage(runtime, options) {
|
|
20871
|
-
const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
|
|
20872
|
-
if (runtime === "podman")
|
|
20873
|
-
args.push("--format", "docker", "--jobs", "1");
|
|
20874
|
-
if (runtime === "podman" && existsSync(`${options.dockerfilePath}.dockerignore`))
|
|
20875
|
-
args.push("--ignorefile", `${options.dockerfilePath}.dockerignore`);
|
|
20876
|
-
if (runtime === "podman" && options.httpProxy === false)
|
|
20877
|
-
args.push("--http-proxy=false");
|
|
20878
|
-
for (const [key, value] of Object.entries(options.buildArgs ?? {}))
|
|
20879
|
-
args.push("--build-arg", `${key}=${value}`);
|
|
20880
|
-
args.push(options.contextDir);
|
|
20881
|
-
logger5.info("Building container image", { runtime, imageRef: options.imageRef });
|
|
20882
|
-
const result = await runCommand(runtime, args, { cwd: options.contextDir });
|
|
20883
|
-
if (!result.success) {
|
|
20884
|
-
return err(
|
|
20885
|
-
HaiDeployError.CONTAINER_BUILD_FAILED,
|
|
20886
|
-
deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
|
|
20887
|
-
result.error
|
|
20888
|
-
);
|
|
20889
|
-
}
|
|
20890
|
-
return ok(void 0);
|
|
20891
|
-
}
|
|
20892
|
-
async function saveImage(runtime, imageRef, tarPath) {
|
|
20893
|
-
logger5.info("Saving container image", { runtime, imageRef, tarPath });
|
|
20894
|
-
const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
|
|
20895
|
-
if (!result.success) {
|
|
20896
|
-
return err(
|
|
20897
|
-
HaiDeployError.CONTAINER_BUILD_FAILED,
|
|
20898
|
-
deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
|
|
20899
|
-
result.error
|
|
20900
|
-
);
|
|
20901
|
-
}
|
|
20902
|
-
return ok(void 0);
|
|
20903
|
-
}
|
|
20904
|
-
var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
|
|
21335
|
+
var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
|
|
20905
21336
|
function buildConnectOptions(config2) {
|
|
20906
21337
|
const args = ["-o", "BatchMode=yes"];
|
|
20907
21338
|
args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
|
|
@@ -20929,14 +21360,15 @@ function buildScpUploadArgs(config2, localPath, remotePath) {
|
|
|
20929
21360
|
"-P",
|
|
20930
21361
|
String(config2.port),
|
|
20931
21362
|
localPath,
|
|
20932
|
-
`${config2.username}@${config2.host}:${remotePath}`
|
|
21363
|
+
`${config2.username}@${config2.host.includes(":") ? `[${config2.host}]` : config2.host}:${remotePath}`
|
|
20933
21364
|
];
|
|
20934
21365
|
}
|
|
20935
21366
|
function createSshExecutor(config2) {
|
|
20936
21367
|
const exec = async (remoteCommand, options) => {
|
|
20937
21368
|
return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
|
|
20938
21369
|
timeoutMs: options?.timeoutMs,
|
|
20939
|
-
secrets: options?.secrets
|
|
21370
|
+
secrets: options?.secrets,
|
|
21371
|
+
onOutput: options?.onOutput
|
|
20940
21372
|
});
|
|
20941
21373
|
};
|
|
20942
21374
|
const upload = async (localPath, remotePath) => {
|
|
@@ -20952,7 +21384,7 @@ function createSshExecutor(config2) {
|
|
|
20952
21384
|
return ok(void 0);
|
|
20953
21385
|
};
|
|
20954
21386
|
const ping = async () => {
|
|
20955
|
-
|
|
21387
|
+
logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
|
|
20956
21388
|
const result = await exec("true", { timeoutMs: 2e4 });
|
|
20957
21389
|
if (!result.success) {
|
|
20958
21390
|
return err(
|
|
@@ -20967,52 +21399,24 @@ function createSshExecutor(config2) {
|
|
|
20967
21399
|
}
|
|
20968
21400
|
|
|
20969
21401
|
// ../deploy/src/providers/deploy-provider-docker-ssh.ts
|
|
20970
|
-
var
|
|
21402
|
+
var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
|
|
20971
21403
|
var HEALTH_POLL_INTERVAL = 3e3;
|
|
20972
21404
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
20973
|
-
function shellArgument(value) {
|
|
20974
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
20975
|
-
}
|
|
20976
21405
|
function makeDeploymentId() {
|
|
20977
21406
|
const now = /* @__PURE__ */ new Date();
|
|
20978
21407
|
const pad = (n) => String(n).padStart(2, "0");
|
|
20979
21408
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
20980
21409
|
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
20981
21410
|
}
|
|
20982
|
-
function parseLoadedImageRef(stdout, fallback) {
|
|
20983
|
-
const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
|
|
20984
|
-
return match ? match[1] : fallback;
|
|
20985
|
-
}
|
|
20986
21411
|
function sleep(ms) {
|
|
20987
|
-
return new Promise((
|
|
20988
|
-
}
|
|
20989
|
-
function readPackageName(appDir) {
|
|
20990
|
-
try {
|
|
20991
|
-
const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
|
|
20992
|
-
return pkg.name ?? "app";
|
|
20993
|
-
} catch {
|
|
20994
|
-
return "app";
|
|
20995
|
-
}
|
|
20996
|
-
}
|
|
20997
|
-
function resolveWorkspaceRoot(appDir) {
|
|
20998
|
-
if (existsSync(join(appDir, "pnpm-workspace.yaml")))
|
|
20999
|
-
return appDir;
|
|
21000
|
-
let current = appDir;
|
|
21001
|
-
let parent = dirname(current);
|
|
21002
|
-
while (parent !== current) {
|
|
21003
|
-
if (existsSync(join(parent, "pnpm-workspace.yaml")))
|
|
21004
|
-
return parent;
|
|
21005
|
-
current = parent;
|
|
21006
|
-
parent = dirname(parent);
|
|
21007
|
-
}
|
|
21008
|
-
return appDir;
|
|
21412
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
21009
21413
|
}
|
|
21010
21414
|
function createDockerSshProvider(config2, container) {
|
|
21011
|
-
const
|
|
21012
|
-
const httpProxy = container?.httpProxy ?? true;
|
|
21013
|
-
const ssh = createSshExecutor(config2.ssh);
|
|
21415
|
+
const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
|
|
21014
21416
|
const baseDir = config2.remote.baseDir;
|
|
21015
21417
|
async function detectRemoteRuntime() {
|
|
21418
|
+
if (!ssh)
|
|
21419
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
21016
21420
|
const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
|
|
21017
21421
|
for (const runtime of candidates) {
|
|
21018
21422
|
const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
|
|
@@ -21020,7 +21424,7 @@ function createDockerSshProvider(config2, container) {
|
|
|
21020
21424
|
continue;
|
|
21021
21425
|
const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
|
|
21022
21426
|
if (composeResult.success) {
|
|
21023
|
-
|
|
21427
|
+
logger5.info("Remote container runtime detected", { runtime });
|
|
21024
21428
|
return ok(runtime);
|
|
21025
21429
|
}
|
|
21026
21430
|
}
|
|
@@ -21044,32 +21448,44 @@ function createDockerSshProvider(config2, container) {
|
|
|
21044
21448
|
const deploy2 = async (context) => {
|
|
21045
21449
|
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
21046
21450
|
const deploymentId = makeDeploymentId();
|
|
21047
|
-
const containerPort = config2.expose.containerPort;
|
|
21048
21451
|
const hostPort = config2.expose.hostPort;
|
|
21049
|
-
const localImageTag = `hai-${projectName}:${deploymentId}`;
|
|
21050
|
-
const runtimeResult = await detectLocalRuntime(localRuntimePref);
|
|
21051
|
-
if (!runtimeResult.success)
|
|
21052
|
-
return err(runtimeResult.error);
|
|
21053
|
-
const localRuntime = runtimeResult.data;
|
|
21054
21452
|
const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
|
|
21055
|
-
mkdirSync(artifactDir, { recursive: true });
|
|
21056
|
-
|
|
21057
|
-
|
|
21058
|
-
|
|
21059
|
-
|
|
21060
|
-
|
|
21061
|
-
|
|
21062
|
-
|
|
21063
|
-
|
|
21064
|
-
|
|
21065
|
-
|
|
21066
|
-
|
|
21067
|
-
|
|
21068
|
-
|
|
21453
|
+
mkdirSync(artifactDir, { recursive: true, mode: 448 });
|
|
21454
|
+
chmodSync(artifactDir, 448);
|
|
21455
|
+
const artifact = context.artifact;
|
|
21456
|
+
const resolvedBuildDir = context.artifactDir;
|
|
21457
|
+
const images = artifact.images;
|
|
21458
|
+
const web = images.find((image) => image.name === "web");
|
|
21459
|
+
const app = images.find((image) => image.name === "app");
|
|
21460
|
+
if (options?.selectedServices) {
|
|
21461
|
+
const available = [...images.map((image) => image.name), ...scan.requiredServices];
|
|
21462
|
+
const invalid = options.selectedServices.find((name) => !available.includes(name));
|
|
21463
|
+
if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
|
|
21464
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
|
|
21465
|
+
}
|
|
21466
|
+
const hasWeb = artifact.hasWeb || !!web;
|
|
21467
|
+
const partial2 = artifact.partial || options?.selectedServices !== void 0;
|
|
21468
|
+
const containerPort = app?.containerPort ?? artifact.appContainerPort ?? config2.expose.containerPort;
|
|
21469
|
+
const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
|
|
21470
|
+
const host = config2.ssh?.host;
|
|
21471
|
+
const url2 = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
21069
21472
|
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
21473
|
+
if (sidecar.composeServices["storage-init"] && !app && options?.selectedServices?.includes("storage"))
|
|
21474
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageArtifactRequired"));
|
|
21475
|
+
let webProxy;
|
|
21476
|
+
if (hasWeb) {
|
|
21477
|
+
const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
|
|
21478
|
+
if (!server.success)
|
|
21479
|
+
return server;
|
|
21480
|
+
const http = server.data.http;
|
|
21481
|
+
const cors = server.data.cors;
|
|
21482
|
+
const apiPrefix = http?.apiPrefix ?? "/api/v1";
|
|
21483
|
+
if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
|
|
21484
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
21485
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
|
|
21486
|
+
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
|
|
21487
|
+
}
|
|
21070
21488
|
const envPath = join(artifactDir, ".env.runtime");
|
|
21071
|
-
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
21072
|
-
Object.assign(env, { ...previousEnv, ...env });
|
|
21073
21489
|
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
21074
21490
|
if (options?.environment) {
|
|
21075
21491
|
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
@@ -21079,88 +21495,83 @@ function createDockerSshProvider(config2, container) {
|
|
|
21079
21495
|
if (!saved.success)
|
|
21080
21496
|
return saved;
|
|
21081
21497
|
}
|
|
21082
|
-
const
|
|
21083
|
-
contextDir,
|
|
21084
|
-
dockerfilePath,
|
|
21085
|
-
imageRef: localImageTag,
|
|
21086
|
-
buildArgs: { HAI_DEPLOY_ENVIRONMENT: options?.environment ?? "prod", HAI_CORE_ENV: env.HAI_CORE_ENV ?? "production", SQLITE_PREBUILD_MIRROR: container?.sqlitePrebuildMirror ?? "" },
|
|
21087
|
-
httpProxy
|
|
21088
|
-
});
|
|
21089
|
-
if (!buildResult.success)
|
|
21090
|
-
return err(buildResult.error);
|
|
21091
|
-
const tarPath = join(artifactDir, `${projectName}.tar`);
|
|
21092
|
-
const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
|
|
21093
|
-
if (!saveResult.success)
|
|
21094
|
-
return err(saveResult.error);
|
|
21095
|
-
const pingResult = await ssh.ping();
|
|
21096
|
-
if (!pingResult.success)
|
|
21097
|
-
return err(pingResult.error);
|
|
21098
|
-
const remoteRuntimeResult = await detectRemoteRuntime();
|
|
21099
|
-
if (!remoteRuntimeResult.success)
|
|
21100
|
-
return err(remoteRuntimeResult.error);
|
|
21101
|
-
const remoteRuntime = remoteRuntimeResult.data;
|
|
21102
|
-
const remoteDir = `${baseDir}/${projectName}`;
|
|
21103
|
-
const prepareResult = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
|
|
21104
|
-
if (!prepareResult.success)
|
|
21105
|
-
return err(prepareResult.error);
|
|
21106
|
-
const remoteTar = `${remoteDir}/image.tar`;
|
|
21107
|
-
const uploadTarResult = await ssh.upload(tarPath, remoteTar);
|
|
21108
|
-
if (!uploadTarResult.success)
|
|
21109
|
-
return err(uploadTarResult.error);
|
|
21110
|
-
const loadResult = await ssh.exec(`${remoteRuntime} load -i ${shellArgument(remoteTar)}`, { timeoutMs: 3e5 });
|
|
21111
|
-
if (!loadResult.success)
|
|
21112
|
-
return err(loadResult.error);
|
|
21113
|
-
const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
|
|
21114
|
-
const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
|
|
21115
|
-
if (loadedRef !== remoteImageRef) {
|
|
21116
|
-
const tagResult = await ssh.exec(`${remoteRuntime} tag ${shellArgument(loadedRef)} ${shellArgument(remoteImageRef)}`);
|
|
21117
|
-
if (!tagResult.success)
|
|
21118
|
-
return err(tagResult.error);
|
|
21119
|
-
}
|
|
21498
|
+
const composeName = `docker-compose-${artifact.appVersion}.yml`;
|
|
21120
21499
|
const composeContent = generateComposeFile({
|
|
21121
21500
|
projectName,
|
|
21122
|
-
|
|
21501
|
+
appVersion: artifact.appVersion,
|
|
21502
|
+
imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
|
|
21123
21503
|
hostPort,
|
|
21124
21504
|
containerPort,
|
|
21125
21505
|
sidecar,
|
|
21126
|
-
|
|
21127
|
-
previousCompose: existsSync(join(artifactDir, "compose.yml")) ? readFileSync(join(artifactDir, "compose.yml"), "utf8") : void 0
|
|
21506
|
+
web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
|
|
21128
21507
|
});
|
|
21129
|
-
const composePath = join(artifactDir,
|
|
21508
|
+
const composePath = join(artifactDir, composeName);
|
|
21130
21509
|
writeFileSync(composePath, composeContent);
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21138
|
-
|
|
21139
|
-
|
|
21140
|
-
|
|
21141
|
-
|
|
21142
|
-
|
|
21143
|
-
|
|
21144
|
-
|
|
21145
|
-
|
|
21146
|
-
|
|
21147
|
-
|
|
21148
|
-
|
|
21149
|
-
|
|
21150
|
-
|
|
21151
|
-
|
|
21152
|
-
|
|
21153
|
-
|
|
21154
|
-
|
|
21155
|
-
|
|
21156
|
-
|
|
21510
|
+
if (webProxy)
|
|
21511
|
+
writeFileSync(join(artifactDir, "web.conf"), webProxy);
|
|
21512
|
+
const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
|
|
21513
|
+
if (!selected.length || selected.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) || new Set(selected).size !== selected.length)
|
|
21514
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
|
|
21515
|
+
for (const name of selected) {
|
|
21516
|
+
if (["app", "web"].includes(name) && !images.some((image) => image.name === name))
|
|
21517
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name } }));
|
|
21518
|
+
}
|
|
21519
|
+
if (selected.includes("storage") && sidecar.composeServices["storage-init"] && !selected.includes("storage-init"))
|
|
21520
|
+
selected.push("storage-init");
|
|
21521
|
+
options?.onProgress?.("packaging");
|
|
21522
|
+
const bundle = await createDeploymentBundle({
|
|
21523
|
+
directory: artifactDir,
|
|
21524
|
+
artifactDir: resolvedBuildDir,
|
|
21525
|
+
artifact,
|
|
21526
|
+
projectName,
|
|
21527
|
+
environment: options?.environment ?? "prod",
|
|
21528
|
+
deploymentId,
|
|
21529
|
+
config: config2,
|
|
21530
|
+
runtime: options?.runtime ?? "auto",
|
|
21531
|
+
selected,
|
|
21532
|
+
partial: partial2,
|
|
21533
|
+
hasWeb,
|
|
21534
|
+
containerPort,
|
|
21535
|
+
middlewareEnv: serializeEnvFile(sidecar.env),
|
|
21536
|
+
healthPaths: hasWeb ? ["/health", "/_hai/web-health", "/"] : [scan.isSvelteKit ? "/" : "/health"],
|
|
21537
|
+
output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
|
|
21538
|
+
});
|
|
21539
|
+
if (options?.bundleOnly)
|
|
21540
|
+
return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
|
|
21541
|
+
if (!ssh)
|
|
21542
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
21543
|
+
const ping = await ssh.ping();
|
|
21544
|
+
if (!ping.success)
|
|
21545
|
+
return ping;
|
|
21546
|
+
const remoteRuntime = await detectRemoteRuntime();
|
|
21547
|
+
if (!remoteRuntime.success)
|
|
21548
|
+
return remoteRuntime;
|
|
21549
|
+
const remoteDir = `${baseDir}/${projectName}`;
|
|
21550
|
+
const remoteTar = `${remoteDir}/incoming/${basename(bundle.bundlePath)}`;
|
|
21551
|
+
const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(`${remoteDir}/incoming`)} && chmod 700 -- ${shellArgument(remoteDir)} ${shellArgument(`${remoteDir}/incoming`)}`);
|
|
21552
|
+
if (!prepared.success)
|
|
21553
|
+
return prepared;
|
|
21554
|
+
const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
|
|
21555
|
+
if (!uploaded.success)
|
|
21556
|
+
return uploaded;
|
|
21557
|
+
let pendingOutput = "";
|
|
21558
|
+
const started = await ssh.exec(
|
|
21559
|
+
`mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)}`,
|
|
21560
|
+
{ timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output2) => {
|
|
21561
|
+
pendingOutput += output2;
|
|
21562
|
+
const lines = pendingOutput.split("\n");
|
|
21563
|
+
pendingOutput = lines.pop() ?? "";
|
|
21564
|
+
for (const line of lines) {
|
|
21565
|
+
const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
|
|
21566
|
+
if (stage)
|
|
21567
|
+
options?.onProgress?.(stage);
|
|
21568
|
+
}
|
|
21569
|
+
} }
|
|
21157
21570
|
);
|
|
21158
|
-
if (!
|
|
21159
|
-
return
|
|
21160
|
-
|
|
21161
|
-
const
|
|
21162
|
-
logger7.info("Running health check", { url: url2 });
|
|
21163
|
-
const healthy = await healthCheck(`${url2}${scan.isSvelteKit ? "/" : "/health"}`);
|
|
21571
|
+
if (!started.success)
|
|
21572
|
+
return started;
|
|
21573
|
+
logger5.info("Running health check", { url: url2 });
|
|
21574
|
+
const healthy = await healthCheck(`${url2}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
|
|
21164
21575
|
if (!healthy) {
|
|
21165
21576
|
return err(
|
|
21166
21577
|
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
@@ -21173,14 +21584,24 @@ exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -
|
|
|
21173
21584
|
status: "ready",
|
|
21174
21585
|
provider: "docker-ssh",
|
|
21175
21586
|
healthCheck: "passed",
|
|
21176
|
-
envVarsSet: Object.keys(env)
|
|
21587
|
+
envVarsSet: Object.keys(env),
|
|
21588
|
+
bundlePath: bundle.bundlePath
|
|
21177
21589
|
};
|
|
21178
|
-
|
|
21590
|
+
logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
|
|
21179
21591
|
return ok(result);
|
|
21180
21592
|
};
|
|
21181
|
-
|
|
21593
|
+
const deploySafely = async (context) => {
|
|
21594
|
+
try {
|
|
21595
|
+
return await deploy2(context);
|
|
21596
|
+
} catch (error62) {
|
|
21597
|
+
logger5.error("Deployment bundle failed", { error: error62 });
|
|
21598
|
+
const reason = error62 instanceof Error ? error62.message : String(error62);
|
|
21599
|
+
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: maskSecrets(reason, Object.values(context.appEnv ?? {}).filter((value) => value.length > 6)) } }), error62);
|
|
21600
|
+
}
|
|
21601
|
+
};
|
|
21602
|
+
return { name: "docker-ssh", deploy: deploySafely };
|
|
21182
21603
|
}
|
|
21183
|
-
var
|
|
21604
|
+
var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
|
|
21184
21605
|
var VERCEL_API = "https://api.vercel.com";
|
|
21185
21606
|
var POLL_INTERVAL = 3e3;
|
|
21186
21607
|
var MAX_POLL_ATTEMPTS = 60;
|
|
@@ -21233,7 +21654,7 @@ function createVercelProvider() {
|
|
|
21233
21654
|
return {
|
|
21234
21655
|
name: "vercel",
|
|
21235
21656
|
async authenticate(apiToken) {
|
|
21236
|
-
|
|
21657
|
+
logger6.debug("Authenticating with Vercel");
|
|
21237
21658
|
try {
|
|
21238
21659
|
const user = await vercelFetch(
|
|
21239
21660
|
apiToken,
|
|
@@ -21241,10 +21662,10 @@ function createVercelProvider() {
|
|
|
21241
21662
|
);
|
|
21242
21663
|
token = apiToken;
|
|
21243
21664
|
const username = user.user.username || user.user.email;
|
|
21244
|
-
|
|
21665
|
+
logger6.info("Vercel authenticated", { user: username });
|
|
21245
21666
|
return ok(username);
|
|
21246
21667
|
} catch (error62) {
|
|
21247
|
-
|
|
21668
|
+
logger6.error("Vercel authentication failed", { error: error62 });
|
|
21248
21669
|
return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
|
|
21249
21670
|
}
|
|
21250
21671
|
},
|
|
@@ -21255,11 +21676,11 @@ function createVercelProvider() {
|
|
|
21255
21676
|
deployM("deploy_authRequired")
|
|
21256
21677
|
);
|
|
21257
21678
|
}
|
|
21258
|
-
|
|
21679
|
+
logger6.debug("Creating Vercel project", { projectName });
|
|
21259
21680
|
try {
|
|
21260
21681
|
const existingId = await findExistingProject(token, projectName);
|
|
21261
21682
|
if (existingId !== null) {
|
|
21262
|
-
|
|
21683
|
+
logger6.info("Vercel project found", { projectName, projectId: existingId });
|
|
21263
21684
|
return ok(existingId);
|
|
21264
21685
|
}
|
|
21265
21686
|
try {
|
|
@@ -21274,13 +21695,13 @@ function createVercelProvider() {
|
|
|
21274
21695
|
})
|
|
21275
21696
|
}
|
|
21276
21697
|
);
|
|
21277
|
-
|
|
21698
|
+
logger6.info("Vercel project created", { projectName, projectId: project.id });
|
|
21278
21699
|
return ok(project.id);
|
|
21279
21700
|
} catch (error62) {
|
|
21280
21701
|
if (error62 instanceof VercelApiError && error62.status === 409) {
|
|
21281
21702
|
const concurrentProjectId = await findExistingProject(token, projectName);
|
|
21282
21703
|
if (concurrentProjectId !== null) {
|
|
21283
|
-
|
|
21704
|
+
logger6.info("Vercel project reused after create conflict", {
|
|
21284
21705
|
projectName,
|
|
21285
21706
|
projectId: concurrentProjectId
|
|
21286
21707
|
});
|
|
@@ -21290,7 +21711,7 @@ function createVercelProvider() {
|
|
|
21290
21711
|
throw error62;
|
|
21291
21712
|
}
|
|
21292
21713
|
} catch (error62) {
|
|
21293
|
-
|
|
21714
|
+
logger6.error("Vercel project creation failed", { projectName, error: error62 });
|
|
21294
21715
|
return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
|
|
21295
21716
|
}
|
|
21296
21717
|
},
|
|
@@ -21301,7 +21722,7 @@ function createVercelProvider() {
|
|
|
21301
21722
|
deployM("deploy_authRequired")
|
|
21302
21723
|
);
|
|
21303
21724
|
}
|
|
21304
|
-
|
|
21725
|
+
logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
|
|
21305
21726
|
try {
|
|
21306
21727
|
const envEntries = Object.entries(envVars).map(([key, value]) => ({
|
|
21307
21728
|
key,
|
|
@@ -21317,10 +21738,10 @@ function createVercelProvider() {
|
|
|
21317
21738
|
body: JSON.stringify(envEntries)
|
|
21318
21739
|
}
|
|
21319
21740
|
);
|
|
21320
|
-
|
|
21741
|
+
logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
|
|
21321
21742
|
return ok(void 0);
|
|
21322
21743
|
} catch (error62) {
|
|
21323
|
-
|
|
21744
|
+
logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
|
|
21324
21745
|
return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
|
|
21325
21746
|
}
|
|
21326
21747
|
},
|
|
@@ -21331,8 +21752,11 @@ function createVercelProvider() {
|
|
|
21331
21752
|
deployM("deploy_authRequired")
|
|
21332
21753
|
);
|
|
21333
21754
|
}
|
|
21334
|
-
|
|
21755
|
+
logger6.debug("Deploying to Vercel", { projectId, outputDir });
|
|
21335
21756
|
try {
|
|
21757
|
+
const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
|
|
21758
|
+
if (outputConfig.version !== 3)
|
|
21759
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
21336
21760
|
const filePaths = collectFiles(outputDir, outputDir);
|
|
21337
21761
|
const files = [];
|
|
21338
21762
|
for (const filePath of filePaths) {
|
|
@@ -21341,18 +21765,20 @@ function createVercelProvider() {
|
|
|
21341
21765
|
const sha = createHash("sha1").update(content).digest("hex");
|
|
21342
21766
|
const size = statSync(fullPath).size;
|
|
21343
21767
|
await uploadFile(token, content, sha);
|
|
21344
|
-
files.push({ file: filePath
|
|
21768
|
+
files.push({ file: `.vercel/output/${filePath}`, sha, size });
|
|
21345
21769
|
}
|
|
21346
|
-
|
|
21770
|
+
logger6.debug("Files uploaded", { count: files.length });
|
|
21347
21771
|
const deployment = await vercelFetch(
|
|
21348
21772
|
token,
|
|
21349
|
-
"/v13/deployments",
|
|
21773
|
+
"/v13/deployments?prebuilt=1",
|
|
21350
21774
|
{
|
|
21351
21775
|
method: "POST",
|
|
21352
21776
|
body: JSON.stringify({
|
|
21353
21777
|
name: projectId,
|
|
21354
21778
|
files,
|
|
21355
|
-
|
|
21779
|
+
version: 2,
|
|
21780
|
+
project: projectId,
|
|
21781
|
+
projectSettings: { framework: null, buildCommand: "", installCommand: "" }
|
|
21356
21782
|
})
|
|
21357
21783
|
}
|
|
21358
21784
|
);
|
|
@@ -21363,14 +21789,14 @@ function createVercelProvider() {
|
|
|
21363
21789
|
status: finalStatus,
|
|
21364
21790
|
envVarsSet: []
|
|
21365
21791
|
};
|
|
21366
|
-
|
|
21792
|
+
logger6.info("Vercel deployment complete", {
|
|
21367
21793
|
deploymentId: deployment.id,
|
|
21368
21794
|
url: result.url,
|
|
21369
21795
|
status: finalStatus
|
|
21370
21796
|
});
|
|
21371
21797
|
return ok(result);
|
|
21372
21798
|
} catch (error62) {
|
|
21373
|
-
|
|
21799
|
+
logger6.error("Vercel deployment failed", { projectId, error: error62 });
|
|
21374
21800
|
return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
|
|
21375
21801
|
}
|
|
21376
21802
|
}
|
|
@@ -21415,11 +21841,11 @@ async function pollDeploymentStatus(token, deploymentId) {
|
|
|
21415
21841
|
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
21416
21842
|
return "error";
|
|
21417
21843
|
}
|
|
21418
|
-
await new Promise((
|
|
21844
|
+
await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
|
|
21419
21845
|
}
|
|
21420
21846
|
return "building";
|
|
21421
21847
|
}
|
|
21422
|
-
var
|
|
21848
|
+
var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
|
|
21423
21849
|
function createAliyunProvisioner() {
|
|
21424
21850
|
let accessKey = null;
|
|
21425
21851
|
let secretKey = null;
|
|
@@ -21428,7 +21854,7 @@ function createAliyunProvisioner() {
|
|
|
21428
21854
|
name: "aliyun",
|
|
21429
21855
|
serviceType: "sms",
|
|
21430
21856
|
async authenticate(credentials) {
|
|
21431
|
-
|
|
21857
|
+
logger7.debug("Authenticating with Aliyun");
|
|
21432
21858
|
try {
|
|
21433
21859
|
const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
|
|
21434
21860
|
const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
|
|
@@ -21438,10 +21864,10 @@ function createAliyunProvisioner() {
|
|
|
21438
21864
|
accessKey = akId;
|
|
21439
21865
|
secretKey = akSecret;
|
|
21440
21866
|
signName = credentials.signName;
|
|
21441
|
-
|
|
21867
|
+
logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
|
|
21442
21868
|
return ok(akId);
|
|
21443
21869
|
} catch (error62) {
|
|
21444
|
-
|
|
21870
|
+
logger7.error("Aliyun authentication failed", { error: error62 });
|
|
21445
21871
|
return err(
|
|
21446
21872
|
HaiDeployError.AUTH_FAILED,
|
|
21447
21873
|
deployM("deploy_authFailed", {
|
|
@@ -21458,7 +21884,7 @@ function createAliyunProvisioner() {
|
|
|
21458
21884
|
deployM("deploy_authRequired")
|
|
21459
21885
|
);
|
|
21460
21886
|
}
|
|
21461
|
-
|
|
21887
|
+
logger7.debug("Provisioning Aliyun SMS service");
|
|
21462
21888
|
return ok({
|
|
21463
21889
|
serviceType: "sms",
|
|
21464
21890
|
provisionerName: "aliyun",
|
|
@@ -21471,7 +21897,7 @@ function createAliyunProvisioner() {
|
|
|
21471
21897
|
}
|
|
21472
21898
|
};
|
|
21473
21899
|
}
|
|
21474
|
-
var
|
|
21900
|
+
var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
|
|
21475
21901
|
var NEON_API = "https://console.neon.tech/api/v2";
|
|
21476
21902
|
var DEFAULT_NEON_DATABASE_NAME = "neondb";
|
|
21477
21903
|
var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
|
|
@@ -21530,7 +21956,7 @@ function createNeonProvisioner() {
|
|
|
21530
21956
|
name: "neon",
|
|
21531
21957
|
serviceType: "db",
|
|
21532
21958
|
async authenticate(credentials) {
|
|
21533
|
-
|
|
21959
|
+
logger8.debug("Authenticating with Neon");
|
|
21534
21960
|
try {
|
|
21535
21961
|
const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
|
|
21536
21962
|
if (!apiToken) {
|
|
@@ -21538,10 +21964,10 @@ function createNeonProvisioner() {
|
|
|
21538
21964
|
}
|
|
21539
21965
|
await listNeonProjects(apiToken);
|
|
21540
21966
|
token = apiToken;
|
|
21541
|
-
|
|
21967
|
+
logger8.info("Neon authenticated");
|
|
21542
21968
|
return ok("neon-user");
|
|
21543
21969
|
} catch (error62) {
|
|
21544
|
-
|
|
21970
|
+
logger8.error("Neon authentication failed", { error: error62 });
|
|
21545
21971
|
return err(
|
|
21546
21972
|
HaiDeployError.AUTH_FAILED,
|
|
21547
21973
|
deployM("deploy_authFailed", {
|
|
@@ -21558,7 +21984,7 @@ function createNeonProvisioner() {
|
|
|
21558
21984
|
deployM("deploy_authRequired")
|
|
21559
21985
|
);
|
|
21560
21986
|
}
|
|
21561
|
-
|
|
21987
|
+
logger8.debug("Provisioning Neon database", { appName });
|
|
21562
21988
|
let resource;
|
|
21563
21989
|
try {
|
|
21564
21990
|
const projectName = `${appName}-db`;
|
|
@@ -21566,7 +21992,7 @@ function createNeonProvisioner() {
|
|
|
21566
21992
|
if (existingProject !== null) {
|
|
21567
21993
|
resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
|
|
21568
21994
|
const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
|
|
21569
|
-
|
|
21995
|
+
logger8.info("Neon project reused", {
|
|
21570
21996
|
projectId: existingProject.id,
|
|
21571
21997
|
projectName
|
|
21572
21998
|
});
|
|
@@ -21588,10 +22014,10 @@ function createNeonProvisioner() {
|
|
|
21588
22014
|
if (!projectId || !connectionUri) {
|
|
21589
22015
|
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
|
|
21590
22016
|
}
|
|
21591
|
-
|
|
22017
|
+
logger8.info("Neon database provisioned", { projectId });
|
|
21592
22018
|
return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
|
|
21593
22019
|
} catch (error62) {
|
|
21594
|
-
|
|
22020
|
+
logger8.error("Neon provisioning failed", { appName });
|
|
21595
22021
|
if (resource) {
|
|
21596
22022
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
21597
22023
|
projectName: appName,
|
|
@@ -21613,7 +22039,7 @@ function createNeonProvisioner() {
|
|
|
21613
22039
|
}
|
|
21614
22040
|
};
|
|
21615
22041
|
}
|
|
21616
|
-
var
|
|
22042
|
+
var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
|
|
21617
22043
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
21618
22044
|
function createR2Provisioner() {
|
|
21619
22045
|
let token = null;
|
|
@@ -21624,7 +22050,7 @@ function createR2Provisioner() {
|
|
|
21624
22050
|
name: "r2",
|
|
21625
22051
|
serviceType: "storage",
|
|
21626
22052
|
async authenticate(credentials) {
|
|
21627
|
-
|
|
22053
|
+
logger9.debug("Authenticating with Cloudflare");
|
|
21628
22054
|
try {
|
|
21629
22055
|
const acctId = credentials.accountId ?? credentials.account_id ?? "";
|
|
21630
22056
|
const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
|
|
@@ -21641,10 +22067,10 @@ function createR2Provisioner() {
|
|
|
21641
22067
|
token = apiTok;
|
|
21642
22068
|
accessKey = credentials.accessKeyId;
|
|
21643
22069
|
secretKey = credentials.secretAccessKey;
|
|
21644
|
-
|
|
22070
|
+
logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
|
|
21645
22071
|
return ok(acctId);
|
|
21646
22072
|
} catch (error62) {
|
|
21647
|
-
|
|
22073
|
+
logger9.error("Cloudflare R2 authentication failed", { error: error62 });
|
|
21648
22074
|
return err(
|
|
21649
22075
|
HaiDeployError.AUTH_FAILED,
|
|
21650
22076
|
deployM("deploy_authFailed", {
|
|
@@ -21662,7 +22088,7 @@ function createR2Provisioner() {
|
|
|
21662
22088
|
);
|
|
21663
22089
|
}
|
|
21664
22090
|
const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
21665
|
-
|
|
22091
|
+
logger9.debug("Provisioning R2 bucket", { appName, bucketName });
|
|
21666
22092
|
try {
|
|
21667
22093
|
const createRes = await fetch(
|
|
21668
22094
|
`${CF_API}/accounts/${accountId}/r2/buckets`,
|
|
@@ -21678,7 +22104,7 @@ function createR2Provisioner() {
|
|
|
21678
22104
|
if (!createRes.ok && createRes.status !== 409) {
|
|
21679
22105
|
throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
|
|
21680
22106
|
}
|
|
21681
|
-
|
|
22107
|
+
logger9.info("R2 bucket provisioned", { bucketName });
|
|
21682
22108
|
return ok({
|
|
21683
22109
|
serviceType: "storage",
|
|
21684
22110
|
provisionerName: "r2",
|
|
@@ -21696,7 +22122,7 @@ function createR2Provisioner() {
|
|
|
21696
22122
|
resourceInfo: `r2-bucket:${bucketName}`
|
|
21697
22123
|
});
|
|
21698
22124
|
} catch (error62) {
|
|
21699
|
-
|
|
22125
|
+
logger9.error("R2 provisioning failed", { appName, error: error62 });
|
|
21700
22126
|
return err(
|
|
21701
22127
|
HaiDeployError.PROVISION_FAILED,
|
|
21702
22128
|
deployM("deploy_provisionFailed", {
|
|
@@ -21711,7 +22137,7 @@ function createR2Provisioner() {
|
|
|
21711
22137
|
}
|
|
21712
22138
|
};
|
|
21713
22139
|
}
|
|
21714
|
-
var
|
|
22140
|
+
var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
|
|
21715
22141
|
var RESEND_API = "https://api.resend.com";
|
|
21716
22142
|
function createResendProvisioner() {
|
|
21717
22143
|
let token = null;
|
|
@@ -21720,7 +22146,7 @@ function createResendProvisioner() {
|
|
|
21720
22146
|
name: "resend",
|
|
21721
22147
|
serviceType: "email",
|
|
21722
22148
|
async authenticate(credentials) {
|
|
21723
|
-
|
|
22149
|
+
logger10.debug("Authenticating with Resend");
|
|
21724
22150
|
try {
|
|
21725
22151
|
const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
21726
22152
|
if (!apiToken || !credentials.from) {
|
|
@@ -21734,10 +22160,10 @@ function createResendProvisioner() {
|
|
|
21734
22160
|
}
|
|
21735
22161
|
token = apiToken;
|
|
21736
22162
|
from = credentials.from;
|
|
21737
|
-
|
|
22163
|
+
logger10.info("Resend authenticated");
|
|
21738
22164
|
return ok("resend-user");
|
|
21739
22165
|
} catch (error62) {
|
|
21740
|
-
|
|
22166
|
+
logger10.error("Resend authentication failed", { error: error62 });
|
|
21741
22167
|
return err(
|
|
21742
22168
|
HaiDeployError.AUTH_FAILED,
|
|
21743
22169
|
deployM("deploy_authFailed", {
|
|
@@ -21754,7 +22180,7 @@ function createResendProvisioner() {
|
|
|
21754
22180
|
deployM("deploy_authRequired")
|
|
21755
22181
|
);
|
|
21756
22182
|
}
|
|
21757
|
-
|
|
22183
|
+
logger10.debug("Provisioning Resend email service");
|
|
21758
22184
|
return ok({
|
|
21759
22185
|
serviceType: "email",
|
|
21760
22186
|
provisionerName: "resend",
|
|
@@ -21767,7 +22193,7 @@ function createResendProvisioner() {
|
|
|
21767
22193
|
}
|
|
21768
22194
|
};
|
|
21769
22195
|
}
|
|
21770
|
-
var
|
|
22196
|
+
var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
|
|
21771
22197
|
var UPSTASH_API = "https://api.upstash.com";
|
|
21772
22198
|
function createUpstashAuthHeader(email3, apiKey) {
|
|
21773
22199
|
return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
|
|
@@ -21818,7 +22244,7 @@ function createUpstashProvisioner() {
|
|
|
21818
22244
|
name: "upstash",
|
|
21819
22245
|
serviceType: "cache",
|
|
21820
22246
|
async authenticate(credentials) {
|
|
21821
|
-
|
|
22247
|
+
logger11.debug("Authenticating with Upstash");
|
|
21822
22248
|
try {
|
|
21823
22249
|
const userEmail = credentials.email ?? "";
|
|
21824
22250
|
const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
@@ -21828,10 +22254,10 @@ function createUpstashProvisioner() {
|
|
|
21828
22254
|
await listUpstashDatabases(userEmail, userKey);
|
|
21829
22255
|
email3 = userEmail;
|
|
21830
22256
|
apiKey = userKey;
|
|
21831
|
-
|
|
22257
|
+
logger11.info("Upstash authenticated", { email: userEmail });
|
|
21832
22258
|
return ok(userEmail);
|
|
21833
22259
|
} catch (error62) {
|
|
21834
|
-
|
|
22260
|
+
logger11.error("Upstash authentication failed", { error: error62 });
|
|
21835
22261
|
return err(
|
|
21836
22262
|
HaiDeployError.AUTH_FAILED,
|
|
21837
22263
|
deployM("deploy_authFailed", {
|
|
@@ -21848,7 +22274,7 @@ function createUpstashProvisioner() {
|
|
|
21848
22274
|
deployM("deploy_authRequired")
|
|
21849
22275
|
);
|
|
21850
22276
|
}
|
|
21851
|
-
|
|
22277
|
+
logger11.debug("Provisioning Upstash Redis", { appName });
|
|
21852
22278
|
let resource;
|
|
21853
22279
|
try {
|
|
21854
22280
|
const databaseName = `${appName}-cache`;
|
|
@@ -21860,7 +22286,7 @@ function createUpstashProvisioner() {
|
|
|
21860
22286
|
}
|
|
21861
22287
|
resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
|
|
21862
22288
|
const detail = await getUpstashDatabase(email3, apiKey, databaseId);
|
|
21863
|
-
|
|
22289
|
+
logger11.info("Upstash Redis reused", { databaseId, databaseName });
|
|
21864
22290
|
return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
|
|
21865
22291
|
}
|
|
21866
22292
|
const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
|
|
@@ -21873,10 +22299,10 @@ function createUpstashProvisioner() {
|
|
|
21873
22299
|
});
|
|
21874
22300
|
if (data.database_id)
|
|
21875
22301
|
resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
|
|
21876
|
-
|
|
22302
|
+
logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
|
|
21877
22303
|
return ok(buildUpstashProvisionResult(data, "created"));
|
|
21878
22304
|
} catch (error62) {
|
|
21879
|
-
|
|
22305
|
+
logger11.error("Upstash provisioning failed", { appName });
|
|
21880
22306
|
if (resource) {
|
|
21881
22307
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
21882
22308
|
projectName: appName,
|
|
@@ -21900,7 +22326,7 @@ function createUpstashProvisioner() {
|
|
|
21900
22326
|
}
|
|
21901
22327
|
|
|
21902
22328
|
// ../deploy/src/deploy-main.ts
|
|
21903
|
-
var
|
|
22329
|
+
var logger12 = core.logger.child({ module: "deploy", scope: "main" });
|
|
21904
22330
|
var currentConfig = null;
|
|
21905
22331
|
var currentProvider = null;
|
|
21906
22332
|
var currentContainerProvider = null;
|
|
@@ -21985,7 +22411,7 @@ var deploy = {
|
|
|
21985
22411
|
credentials: deployCredentials,
|
|
21986
22412
|
async init(config2) {
|
|
21987
22413
|
if (initInProgress) {
|
|
21988
|
-
|
|
22414
|
+
logger12.warn("Deploy init already in progress, skipping concurrent call");
|
|
21989
22415
|
return err(
|
|
21990
22416
|
HaiDeployError.CONFIG_ERROR,
|
|
21991
22417
|
deployM("deploy_configError", { params: { error: "init already in progress" } })
|
|
@@ -21994,13 +22420,13 @@ var deploy = {
|
|
|
21994
22420
|
initInProgress = true;
|
|
21995
22421
|
try {
|
|
21996
22422
|
if (currentProvider || currentContainerProvider) {
|
|
21997
|
-
|
|
22423
|
+
logger12.warn("Deploy module is already initialized, reinitializing");
|
|
21998
22424
|
await deploy.close();
|
|
21999
22425
|
}
|
|
22000
|
-
|
|
22426
|
+
logger12.info("Initializing deploy module");
|
|
22001
22427
|
const parseResult = DeployConfigSchema.safeParse(config2);
|
|
22002
22428
|
if (!parseResult.success) {
|
|
22003
|
-
|
|
22429
|
+
logger12.error("Deploy config validation failed", { error: parseResult.error.message });
|
|
22004
22430
|
return err(
|
|
22005
22431
|
HaiDeployError.CONFIG_ERROR,
|
|
22006
22432
|
deployM("deploy_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -22013,14 +22439,14 @@ var deploy = {
|
|
|
22013
22439
|
let containerProvider = null;
|
|
22014
22440
|
if (parsed.provider.type === "docker-ssh") {
|
|
22015
22441
|
containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
|
|
22016
|
-
|
|
22442
|
+
logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
|
|
22017
22443
|
} else {
|
|
22018
22444
|
provider = createProviderByType(parsed.provider.type);
|
|
22019
22445
|
const authResult = await provider.authenticate(parsed.provider.token);
|
|
22020
22446
|
if (!authResult.success) {
|
|
22021
22447
|
return err(authResult.error);
|
|
22022
22448
|
}
|
|
22023
|
-
|
|
22449
|
+
logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
|
|
22024
22450
|
}
|
|
22025
22451
|
currentProvisioners.clear();
|
|
22026
22452
|
if (parsed.services) {
|
|
@@ -22032,14 +22458,14 @@ var deploy = {
|
|
|
22032
22458
|
const credentials = extractCredentials(serviceConfig);
|
|
22033
22459
|
const provAuthResult = await provisioner.authenticate(credentials);
|
|
22034
22460
|
if (!provAuthResult.success) {
|
|
22035
|
-
|
|
22461
|
+
logger12.error("Provisioner authentication failed", {
|
|
22036
22462
|
service: serviceType,
|
|
22037
22463
|
provisioner: serviceConfig.provisioner
|
|
22038
22464
|
});
|
|
22039
22465
|
currentProvisioners.clear();
|
|
22040
22466
|
return err(provAuthResult.error);
|
|
22041
22467
|
}
|
|
22042
|
-
|
|
22468
|
+
logger12.info("Provisioner authenticated", {
|
|
22043
22469
|
service: serviceType,
|
|
22044
22470
|
user: provAuthResult.data
|
|
22045
22471
|
});
|
|
@@ -22049,7 +22475,7 @@ var deploy = {
|
|
|
22049
22475
|
currentProvider = provider;
|
|
22050
22476
|
currentContainerProvider = containerProvider;
|
|
22051
22477
|
currentConfig = parsed;
|
|
22052
|
-
|
|
22478
|
+
logger12.info("Deploy module initialized", {
|
|
22053
22479
|
provider: parsed.provider.type,
|
|
22054
22480
|
provisioners: [...currentProvisioners.keys()]
|
|
22055
22481
|
});
|
|
@@ -22059,7 +22485,7 @@ var deploy = {
|
|
|
22059
22485
|
currentContainerProvider = null;
|
|
22060
22486
|
currentConfig = null;
|
|
22061
22487
|
currentProvisioners.clear();
|
|
22062
|
-
|
|
22488
|
+
logger12.error("Deploy module initialization failed", { error: error62 });
|
|
22063
22489
|
return err(
|
|
22064
22490
|
HaiDeployError.CONFIG_ERROR,
|
|
22065
22491
|
deployM("deploy_configError", {
|
|
@@ -22074,15 +22500,15 @@ var deploy = {
|
|
|
22074
22500
|
},
|
|
22075
22501
|
async close() {
|
|
22076
22502
|
if (!currentProvider && !currentContainerProvider) {
|
|
22077
|
-
|
|
22503
|
+
logger12.info("Deploy module already closed, skipping");
|
|
22078
22504
|
return;
|
|
22079
22505
|
}
|
|
22080
|
-
|
|
22506
|
+
logger12.info("Closing deploy module");
|
|
22081
22507
|
currentProvider = null;
|
|
22082
22508
|
currentContainerProvider = null;
|
|
22083
22509
|
currentConfig = null;
|
|
22084
22510
|
currentProvisioners.clear();
|
|
22085
|
-
|
|
22511
|
+
logger12.info("Deploy module closed");
|
|
22086
22512
|
},
|
|
22087
22513
|
get config() {
|
|
22088
22514
|
return currentConfig;
|
|
@@ -22091,22 +22517,22 @@ var deploy = {
|
|
|
22091
22517
|
return currentProvider !== null || currentContainerProvider !== null;
|
|
22092
22518
|
},
|
|
22093
22519
|
async scan(appDir, options) {
|
|
22094
|
-
|
|
22520
|
+
logger12.debug("Scanning application", { appDir });
|
|
22095
22521
|
return scanApp(appDir, options);
|
|
22096
22522
|
},
|
|
22097
22523
|
async provisionAll(projectName, services) {
|
|
22098
22524
|
if (!currentProvider && !currentContainerProvider) {
|
|
22099
22525
|
return notInitialized.result();
|
|
22100
22526
|
}
|
|
22101
|
-
|
|
22527
|
+
logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
|
|
22102
22528
|
const results = [];
|
|
22103
22529
|
for (const [serviceType, provisioner] of currentProvisioners) {
|
|
22104
22530
|
if (services && !services.includes(serviceType))
|
|
22105
22531
|
continue;
|
|
22106
|
-
|
|
22532
|
+
logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
|
|
22107
22533
|
const provResult = await provisioner.provision(projectName);
|
|
22108
22534
|
if (!provResult.success) {
|
|
22109
|
-
|
|
22535
|
+
logger12.error("Service provisioning failed", {
|
|
22110
22536
|
serviceType,
|
|
22111
22537
|
provisioner: provisioner.name,
|
|
22112
22538
|
code: provResult.error.code
|
|
@@ -22123,7 +22549,7 @@ var deploy = {
|
|
|
22123
22549
|
});
|
|
22124
22550
|
}
|
|
22125
22551
|
results.push(provResult.data);
|
|
22126
|
-
|
|
22552
|
+
logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
|
|
22127
22553
|
}
|
|
22128
22554
|
return ok(results);
|
|
22129
22555
|
},
|
|
@@ -22131,7 +22557,7 @@ var deploy = {
|
|
|
22131
22557
|
if (!currentProvider && !currentContainerProvider) {
|
|
22132
22558
|
return notInitialized.result();
|
|
22133
22559
|
}
|
|
22134
|
-
|
|
22560
|
+
logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
|
|
22135
22561
|
const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
|
|
22136
22562
|
const scanResult = await scanApp(appDir, scanOptions);
|
|
22137
22563
|
if (!scanResult.success) {
|
|
@@ -22147,11 +22573,21 @@ var deploy = {
|
|
|
22147
22573
|
options = {
|
|
22148
22574
|
...options,
|
|
22149
22575
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
22150
|
-
...options?.
|
|
22576
|
+
...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
|
|
22151
22577
|
};
|
|
22578
|
+
if (options.selectedServices) {
|
|
22579
|
+
options.serviceModes = {
|
|
22580
|
+
...options.serviceModes,
|
|
22581
|
+
...Object.fromEntries(scan.requiredServices.filter((service) => !options.selectedServices.includes(service)).map((service) => [service, "keep"]))
|
|
22582
|
+
};
|
|
22583
|
+
}
|
|
22152
22584
|
if (currentContainerProvider) {
|
|
22153
22585
|
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
22154
22586
|
}
|
|
22587
|
+
if (options?.bundleOnly)
|
|
22588
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
|
|
22589
|
+
if (options.selectedServices)
|
|
22590
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
|
|
22155
22591
|
if (!currentProvider) {
|
|
22156
22592
|
return notInitialized.result();
|
|
22157
22593
|
}
|
|
@@ -22163,9 +22599,11 @@ var deploy = {
|
|
|
22163
22599
|
})
|
|
22164
22600
|
);
|
|
22165
22601
|
}
|
|
22602
|
+
if (!existsSync(resolveOutputDir(appDir, "vercel")))
|
|
22603
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
22166
22604
|
let allEnvVars = appEnv;
|
|
22167
22605
|
const recovery = { projectName, stage: "provision", resources: [] };
|
|
22168
|
-
if (!options?.
|
|
22606
|
+
if (!options?.skipMiddleware) {
|
|
22169
22607
|
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
22170
22608
|
if (!provResults.success) {
|
|
22171
22609
|
return err(provResults.error);
|
|
@@ -22190,12 +22628,6 @@ var deploy = {
|
|
|
22190
22628
|
return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
|
|
22191
22629
|
}
|
|
22192
22630
|
}
|
|
22193
|
-
if (!options?.skipBuild) {
|
|
22194
|
-
const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
|
|
22195
|
-
if (!buildResult.success) {
|
|
22196
|
-
return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
|
|
22197
|
-
}
|
|
22198
|
-
}
|
|
22199
22631
|
const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
|
|
22200
22632
|
const deployResult = await currentProvider.deploy(projectId, outputDir);
|
|
22201
22633
|
if (!deployResult.success) {
|
|
@@ -22205,7 +22637,7 @@ var deploy = {
|
|
|
22205
22637
|
...deployResult.data,
|
|
22206
22638
|
envVarsSet: Object.keys(allEnvVars)
|
|
22207
22639
|
};
|
|
22208
|
-
|
|
22640
|
+
logger12.info("Deployment completed", {
|
|
22209
22641
|
url: result.url,
|
|
22210
22642
|
deploymentId: result.deploymentId,
|
|
22211
22643
|
status: result.status
|
|
@@ -22215,21 +22647,41 @@ var deploy = {
|
|
|
22215
22647
|
};
|
|
22216
22648
|
async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
|
|
22217
22649
|
const projectName = normalizeProjectName(rawProjectName);
|
|
22218
|
-
|
|
22219
|
-
|
|
22220
|
-
|
|
22221
|
-
|
|
22222
|
-
|
|
22650
|
+
let artifact;
|
|
22651
|
+
let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
|
|
22652
|
+
try {
|
|
22653
|
+
if (existsSync(join(artifactDir, "latest"))) {
|
|
22654
|
+
const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
|
|
22655
|
+
if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
|
|
22656
|
+
throw new Error("Invalid artifact pointer");
|
|
22657
|
+
artifactDir = join(artifactDir, latest);
|
|
22658
|
+
}
|
|
22659
|
+
artifact = await readDeploymentArtifact(artifactDir);
|
|
22660
|
+
} catch {
|
|
22661
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
22662
|
+
}
|
|
22663
|
+
if (artifact.partial && !options?.selectedServices) {
|
|
22664
|
+
const selectedServices = artifact.images.map((image) => image.name);
|
|
22665
|
+
options = {
|
|
22666
|
+
...options,
|
|
22667
|
+
selectedServices,
|
|
22668
|
+
serviceModes: {
|
|
22669
|
+
...options?.serviceModes,
|
|
22670
|
+
...Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
|
|
22671
|
+
}
|
|
22672
|
+
};
|
|
22223
22673
|
}
|
|
22674
|
+
if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
|
|
22675
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
22224
22676
|
let provisionEnv = {};
|
|
22225
|
-
if (!options?.
|
|
22677
|
+
if (!options?.skipMiddleware) {
|
|
22226
22678
|
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
22227
22679
|
if (!provResults.success) {
|
|
22228
22680
|
return err(provResults.error);
|
|
22229
22681
|
}
|
|
22230
22682
|
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
22231
22683
|
}
|
|
22232
|
-
return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
|
|
22684
|
+
return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
|
|
22233
22685
|
}
|
|
22234
22686
|
|
|
22235
22687
|
// ../deploy/src/deploy-middleware.ts
|
|
@@ -22277,6 +22729,6 @@ function describeMiddlewareBackend(backend) {
|
|
|
22277
22729
|
return deployM(backend.purpose);
|
|
22278
22730
|
}
|
|
22279
22731
|
|
|
22280
|
-
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, saveDeploymentEnvironment };
|
|
22281
|
-
//# sourceMappingURL=src-
|
|
22282
|
-
//# sourceMappingURL=src-
|
|
22732
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
|
|
22733
|
+
//# sourceMappingURL=src-2V6VP4I5.js.map
|
|
22734
|
+
//# sourceMappingURL=src-2V6VP4I5.js.map
|