@h-ai/cli 0.1.0-alpha.66 → 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 -257
- package/README.ja.md +66 -257
- package/README.md +66 -257
- package/dist/{chunk-QYX6V7H2.js → chunk-7DSURTJ4.js} +22 -79
- package/dist/chunk-7DSURTJ4.js.map +1 -0
- package/dist/{chunk-FWN5B54L.js → chunk-AOQ4N657.js} +182 -10
- package/dist/chunk-AOQ4N657.js.map +1 -0
- package/dist/cli-config-templates-6AHNUGBN.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-FC3QRKDO.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
- package/dist/index.js +435 -247
- package/dist/index.js.map +1 -1
- package/dist/{src-RDQ2EIBT.js → src-2V6VP4I5.js} +553 -119
- 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 +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- 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 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +5 -0
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- 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 +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +22 -4
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +11 -1
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- 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 +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +27 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +32 -11
- 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/src/App.svelte.hbs +2 -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 +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +10 -0
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +8 -3
- package/templates/base/vite.config.ts.hbs +3 -1
- package/templates/shared/.gitattributes +6 -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 +4 -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 +14 -8
- 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 +20 -55
- package/templates/skills/hai-deploy/middleware.md +5 -11
- 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 +9 -5
- package/templates/skills/hai-vecdb/SKILL.md +2 -2
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /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
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __export } from './chunk-2H7UOFLK.js';
|
|
3
3
|
import { createHash, randomBytes } from 'crypto';
|
|
4
|
-
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
|
|
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,
|
|
6
|
+
import { core, err, ok } from '@h-ai/core';
|
|
7
7
|
import process2 from 'process';
|
|
8
8
|
import { parseEnv } from 'util';
|
|
9
|
-
import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
|
|
9
|
+
import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
12
|
import { Buffer } from 'buffer';
|
|
@@ -1856,18 +1856,18 @@ var validateAsync = async (schema, value, _ctx) => {
|
|
|
1856
1856
|
return result.issues.length === 0;
|
|
1857
1857
|
};
|
|
1858
1858
|
var _encode = (_Err) => {
|
|
1859
|
-
const
|
|
1859
|
+
const parse6 = _parse(_Err);
|
|
1860
1860
|
const fn = (schema, value, _ctx, _params) => {
|
|
1861
1861
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
1862
|
-
return
|
|
1862
|
+
return parse6(schema, value, ctx, finalizeParams(fn, _params));
|
|
1863
1863
|
};
|
|
1864
1864
|
return fn;
|
|
1865
1865
|
};
|
|
1866
1866
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
1867
1867
|
var _decode = (_Err) => {
|
|
1868
|
-
const
|
|
1868
|
+
const parse6 = _parse(_Err);
|
|
1869
1869
|
const fn = (schema, value, _ctx, _params) => {
|
|
1870
|
-
return
|
|
1870
|
+
return parse6(schema, value, _ctx, finalizeParams(fn, _params));
|
|
1871
1871
|
};
|
|
1872
1872
|
return fn;
|
|
1873
1873
|
};
|
|
@@ -19671,25 +19671,33 @@ function date4(params) {
|
|
|
19671
19671
|
// ../deploy/src/deploy-artifact.ts
|
|
19672
19672
|
var DeploymentArtifactSchema = external_exports.object({
|
|
19673
19673
|
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
19674
|
-
version: external_exports.literal(
|
|
19674
|
+
version: external_exports.literal(3),
|
|
19675
19675
|
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
19676
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
|
-
/**
|
|
19677
|
+
/** 本次构建标识;同一应用版本允许重复构建。 */
|
|
19678
19678
|
buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
|
|
19679
|
-
/**
|
|
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
|
+
/** 本次构建选择的镜像归档。 */
|
|
19680
19688
|
images: external_exports.array(external_exports.object({
|
|
19681
19689
|
/** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
|
|
19682
19690
|
name: external_exports.enum(["app", "web"]),
|
|
19683
19691
|
/** 相对于清单目录的 tar 文件名。 */
|
|
19684
19692
|
file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
|
|
19685
19693
|
/** 归档导入后可直接使用的不可变镜像引用。 */
|
|
19686
|
-
imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]
|
|
19694
|
+
imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
|
|
19687
19695
|
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
19688
19696
|
sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
19689
19697
|
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
19690
19698
|
containerPort: external_exports.number().int().min(1).max(65535).optional()
|
|
19691
|
-
})).min(1).max(2).refine((images) =>
|
|
19692
|
-
}).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
|
|
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"));
|
|
19693
19701
|
async function artifactChecksum(file2) {
|
|
19694
19702
|
const hash2 = createHash("sha256");
|
|
19695
19703
|
for await (const chunk of createReadStream(file2))
|
|
@@ -19757,11 +19765,21 @@ var en_US_default = {
|
|
|
19757
19765
|
deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
|
|
19758
19766
|
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
|
|
19759
19767
|
deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
|
|
19760
|
-
deploy_artifactInvalid: "
|
|
19768
|
+
deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
|
|
19761
19769
|
deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
|
|
19762
|
-
deploy_bundleInstructions: "
|
|
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}",
|
|
19763
19771
|
deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
|
|
19764
|
-
deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
|
|
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."
|
|
19765
19783
|
};
|
|
19766
19784
|
|
|
19767
19785
|
// ../deploy/messages/zh-CN.json
|
|
@@ -19815,11 +19833,21 @@ var zh_CN_default2 = {
|
|
|
19815
19833
|
deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
|
|
19816
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",
|
|
19817
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",
|
|
19818
|
-
deploy_artifactInvalid: "\
|
|
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",
|
|
19819
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",
|
|
19820
|
-
deploy_bundleInstructions: "\
|
|
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}",
|
|
19821
19839
|
deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
|
|
19822
|
-
deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
|
|
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"
|
|
19823
19851
|
};
|
|
19824
19852
|
|
|
19825
19853
|
// ../deploy/src/deploy-i18n.ts
|
|
@@ -19835,11 +19863,11 @@ var VercelProviderConfigSchema = external_exports.object({
|
|
|
19835
19863
|
});
|
|
19836
19864
|
var SshConfigSchema = external_exports.object({
|
|
19837
19865
|
/** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
|
|
19838
|
-
host: external_exports.string().
|
|
19866
|
+
host: external_exports.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
|
|
19839
19867
|
/** SSH 端口 */
|
|
19840
19868
|
port: external_exports.number().int().min(1).max(65535).default(22),
|
|
19841
19869
|
/** SSH 用户名 */
|
|
19842
|
-
username: external_exports.string().
|
|
19870
|
+
username: external_exports.string().regex(/^[a-z_][\w-]*$/i),
|
|
19843
19871
|
/** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
|
|
19844
19872
|
identityFile: external_exports.string().min(1).optional(),
|
|
19845
19873
|
/** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
|
|
@@ -19864,7 +19892,7 @@ var DockerSshProviderConfigSchema = external_exports.object({
|
|
|
19864
19892
|
/** 远程主机运行参数 */
|
|
19865
19893
|
remote: external_exports.object({
|
|
19866
19894
|
/** 远程应用根目录 */
|
|
19867
|
-
baseDir: external_exports.string().
|
|
19895
|
+
baseDir: external_exports.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
|
|
19868
19896
|
/** 远程容器运行时(auto 优先 podman) */
|
|
19869
19897
|
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
|
|
19870
19898
|
}).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
|
|
@@ -19963,10 +19991,16 @@ function resolveConfig(value, prefix, env) {
|
|
|
19963
19991
|
}
|
|
19964
19992
|
function loadDeploymentConfig(name, path, values) {
|
|
19965
19993
|
try {
|
|
19966
|
-
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" } }));
|
|
19967
20001
|
return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
|
|
19968
20002
|
} catch {
|
|
19969
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("
|
|
20003
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
|
|
19970
20004
|
}
|
|
19971
20005
|
}
|
|
19972
20006
|
function loadDeploymentEnvironment(options) {
|
|
@@ -20417,6 +20451,7 @@ function maskSecrets(text, secrets) {
|
|
|
20417
20451
|
function runCommand(command, args, options = {}) {
|
|
20418
20452
|
return new Promise((resolve6) => {
|
|
20419
20453
|
const child = spawn(command, args, {
|
|
20454
|
+
windowsHide: true,
|
|
20420
20455
|
cwd: options.cwd,
|
|
20421
20456
|
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
20422
20457
|
shell: false
|
|
@@ -20445,6 +20480,7 @@ function runCommand(command, args, options = {}) {
|
|
|
20445
20480
|
}
|
|
20446
20481
|
child.stdout.on("data", (chunk) => {
|
|
20447
20482
|
stdout += chunk.toString("utf-8");
|
|
20483
|
+
options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
|
|
20448
20484
|
});
|
|
20449
20485
|
child.stderr.on("data", (chunk) => {
|
|
20450
20486
|
stderr += chunk.toString("utf-8");
|
|
@@ -20488,62 +20524,393 @@ ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
|
20488
20524
|
});
|
|
20489
20525
|
}
|
|
20490
20526
|
|
|
20491
|
-
// ../deploy/src/container/deploy-
|
|
20527
|
+
// ../deploy/src/container/deploy-scripts.ts
|
|
20492
20528
|
function shellArgument(value) {
|
|
20493
20529
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
20494
20530
|
}
|
|
20495
|
-
|
|
20496
|
-
const
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
for (const image of artifact.images)
|
|
20509
|
-
copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
|
|
20510
|
-
for (const file2 of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
|
|
20511
|
-
copyFileSync(join(directory, file2), join(staging, file2));
|
|
20512
|
-
const compose = parse$1(readFileSync(join(directory, composeName), "utf8"));
|
|
20513
|
-
const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
|
|
20514
|
-
const start = `#!/bin/sh
|
|
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
|
|
20515
20544
|
set -eu
|
|
20516
|
-
cd "$(dirname "$0")"
|
|
20517
20545
|
umask 077
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
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
|
|
20522
20655
|
if [ "$runtime" = auto ]; then
|
|
20523
20656
|
if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
|
|
20524
20657
|
fi
|
|
20525
|
-
case "$runtime" in docker|podman) ;; *)
|
|
20658
|
+
case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
|
|
20526
20659
|
"$runtime" version >/dev/null
|
|
20527
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"
|
|
20528
20665
|
sha256sum -c SHA256SUMS
|
|
20529
|
-
|
|
20530
|
-
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
|
|
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; }
|
|
20534
20747
|
done
|
|
20535
|
-
|
|
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 }
|
|
20536
20815
|
`;
|
|
20537
|
-
|
|
20538
|
-
|
|
20539
|
-
|
|
20540
|
-
|
|
20541
|
-
|
|
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" } })}
|
|
20542
20910
|
${instructions}
|
|
20543
20911
|
`);
|
|
20544
|
-
copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
|
|
20545
20912
|
const checksums = [];
|
|
20546
|
-
for (const file2 of
|
|
20913
|
+
for (const file2 of files)
|
|
20547
20914
|
checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
|
|
20548
20915
|
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
20549
20916
|
`);
|
|
@@ -20551,25 +20918,33 @@ ${instructions}
|
|
|
20551
20918
|
if (!tar.success)
|
|
20552
20919
|
throw new Error(tar.error.message);
|
|
20553
20920
|
chmodSync(temporaryTar, 384);
|
|
20554
|
-
const { renameSync } = await import('fs');
|
|
20555
20921
|
renameSync(temporaryTar, output2);
|
|
20556
|
-
|
|
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" };
|
|
20557
20932
|
} finally {
|
|
20558
20933
|
rmSync(staging, { recursive: true, force: true });
|
|
20559
20934
|
rmSync(temporaryTar, { force: true });
|
|
20560
20935
|
}
|
|
20561
20936
|
}
|
|
20562
20937
|
var IMAGES = {
|
|
20563
|
-
postgres: "postgres:16-alpine",
|
|
20564
|
-
mysql: "mysql:8.4",
|
|
20565
|
-
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",
|
|
20566
20941
|
rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
|
|
20567
20942
|
minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
|
|
20568
|
-
qdrant: "qdrant/qdrant:v1.
|
|
20569
|
-
pgvector: "pgvector/pgvector:pg17",
|
|
20570
|
-
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",
|
|
20571
20946
|
ldap: "docker.io/osixia/openldap:1.5.0",
|
|
20572
|
-
smtp: "axllent/mailpit:v1.27.4"
|
|
20947
|
+
smtp: "docker.io/axllent/mailpit:v1.27.4"
|
|
20573
20948
|
};
|
|
20574
20949
|
function generatePassword() {
|
|
20575
20950
|
return randomBytes(24).toString("base64url");
|
|
@@ -20578,8 +20953,12 @@ function resolveSidecarSecrets(appDir, projectName) {
|
|
|
20578
20953
|
const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
|
|
20579
20954
|
if (existsSync(secretFile)) {
|
|
20580
20955
|
try {
|
|
20581
|
-
|
|
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;
|
|
20582
20960
|
} catch {
|
|
20961
|
+
throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
|
|
20583
20962
|
}
|
|
20584
20963
|
}
|
|
20585
20964
|
const secrets = {
|
|
@@ -20747,6 +21126,28 @@ function planChromaVecdb(plan, projectName, net) {
|
|
|
20747
21126
|
plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
|
|
20748
21127
|
}
|
|
20749
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 };
|
|
20750
21151
|
const plan = {
|
|
20751
21152
|
services: [],
|
|
20752
21153
|
composeServices: {},
|
|
@@ -20873,23 +21274,6 @@ function generateComposeFile(options) {
|
|
|
20873
21274
|
};
|
|
20874
21275
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
20875
21276
|
compose.volumes = sidecar.volumes;
|
|
20876
|
-
if (options.previousCompose && options.serviceModes) {
|
|
20877
|
-
const previous = parse$1(options.previousCompose);
|
|
20878
|
-
for (const [service, mode] of Object.entries(options.serviceModes)) {
|
|
20879
|
-
if (mode !== "keep")
|
|
20880
|
-
continue;
|
|
20881
|
-
for (const name of [service, `${service}-init`]) {
|
|
20882
|
-
if (previous.services?.[name]) {
|
|
20883
|
-
services[name] = previous.services[name];
|
|
20884
|
-
app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
|
|
20885
|
-
}
|
|
20886
|
-
}
|
|
20887
|
-
}
|
|
20888
|
-
const previousMounts = previous.services?.app?.volumes;
|
|
20889
|
-
if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
|
|
20890
|
-
app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
|
|
20891
|
-
compose.volumes = { ...previous.volumes, ...sidecar.volumes };
|
|
20892
|
-
}
|
|
20893
21277
|
if (services["storage-init"]) {
|
|
20894
21278
|
app.depends_on = {
|
|
20895
21279
|
...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
|
|
@@ -20976,14 +21360,15 @@ function buildScpUploadArgs(config2, localPath, remotePath) {
|
|
|
20976
21360
|
"-P",
|
|
20977
21361
|
String(config2.port),
|
|
20978
21362
|
localPath,
|
|
20979
|
-
`${config2.username}@${config2.host}:${remotePath}`
|
|
21363
|
+
`${config2.username}@${config2.host.includes(":") ? `[${config2.host}]` : config2.host}:${remotePath}`
|
|
20980
21364
|
];
|
|
20981
21365
|
}
|
|
20982
21366
|
function createSshExecutor(config2) {
|
|
20983
21367
|
const exec = async (remoteCommand, options) => {
|
|
20984
21368
|
return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
|
|
20985
21369
|
timeoutMs: options?.timeoutMs,
|
|
20986
|
-
secrets: options?.secrets
|
|
21370
|
+
secrets: options?.secrets,
|
|
21371
|
+
onOutput: options?.onOutput
|
|
20987
21372
|
});
|
|
20988
21373
|
};
|
|
20989
21374
|
const upload = async (localPath, remotePath) => {
|
|
@@ -21072,11 +21457,23 @@ function createDockerSshProvider(config2, container) {
|
|
|
21072
21457
|
const images = artifact.images;
|
|
21073
21458
|
const web = images.find((image) => image.name === "web");
|
|
21074
21459
|
const app = images.find((image) => image.name === "app");
|
|
21075
|
-
|
|
21076
|
-
|
|
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}`;
|
|
21077
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"));
|
|
21078
21475
|
let webProxy;
|
|
21079
|
-
if (
|
|
21476
|
+
if (hasWeb) {
|
|
21080
21477
|
const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
|
|
21081
21478
|
if (!server.success)
|
|
21082
21479
|
return server;
|
|
@@ -21085,12 +21482,10 @@ function createDockerSshProvider(config2, container) {
|
|
|
21085
21482
|
const apiPrefix = http?.apiPrefix ?? "/api/v1";
|
|
21086
21483
|
if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
|
|
21087
21484
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
21088
|
-
webProxy = generateWebProxyConfig(containerPort, apiPrefix,
|
|
21485
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
|
|
21089
21486
|
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
|
|
21090
21487
|
}
|
|
21091
21488
|
const envPath = join(artifactDir, ".env.runtime");
|
|
21092
|
-
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
21093
|
-
Object.assign(env, { ...previousEnv, ...env });
|
|
21094
21489
|
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
21095
21490
|
if (options?.environment) {
|
|
21096
21491
|
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
@@ -21101,36 +21496,46 @@ function createDockerSshProvider(config2, container) {
|
|
|
21101
21496
|
return saved;
|
|
21102
21497
|
}
|
|
21103
21498
|
const composeName = `docker-compose-${artifact.appVersion}.yml`;
|
|
21104
|
-
const latestPath = join(artifactDir, "latest");
|
|
21105
|
-
const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
|
|
21106
|
-
const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
|
|
21107
21499
|
const composeContent = generateComposeFile({
|
|
21108
21500
|
projectName,
|
|
21109
21501
|
appVersion: artifact.appVersion,
|
|
21110
|
-
imageRef: app
|
|
21502
|
+
imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
|
|
21111
21503
|
hostPort,
|
|
21112
21504
|
containerPort,
|
|
21113
21505
|
sidecar,
|
|
21114
|
-
web:
|
|
21115
|
-
serviceModes: options?.serviceModes,
|
|
21116
|
-
previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
|
|
21506
|
+
web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
|
|
21117
21507
|
});
|
|
21118
21508
|
const composePath = join(artifactDir, composeName);
|
|
21119
21509
|
writeFileSync(composePath, composeContent);
|
|
21120
21510
|
if (webProxy)
|
|
21121
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");
|
|
21122
21522
|
const bundle = await createDeploymentBundle({
|
|
21123
21523
|
directory: artifactDir,
|
|
21124
21524
|
artifactDir: resolvedBuildDir,
|
|
21125
21525
|
artifact,
|
|
21126
21526
|
projectName,
|
|
21127
21527
|
environment: options?.environment ?? "prod",
|
|
21128
|
-
|
|
21129
|
-
|
|
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"],
|
|
21130
21537
|
output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
|
|
21131
21538
|
});
|
|
21132
|
-
writeFileSync(latestPath, `${composeName}
|
|
21133
|
-
`);
|
|
21134
21539
|
if (options?.bundleOnly)
|
|
21135
21540
|
return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
|
|
21136
21541
|
if (!ssh)
|
|
@@ -21142,22 +21547,31 @@ function createDockerSshProvider(config2, container) {
|
|
|
21142
21547
|
if (!remoteRuntime.success)
|
|
21143
21548
|
return remoteRuntime;
|
|
21144
21549
|
const remoteDir = `${baseDir}/${projectName}`;
|
|
21145
|
-
const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
|
|
21146
|
-
const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
|
|
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`)}`);
|
|
21147
21552
|
if (!prepared.success)
|
|
21148
21553
|
return prepared;
|
|
21149
21554
|
const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
|
|
21150
21555
|
if (!uploaded.success)
|
|
21151
21556
|
return uploaded;
|
|
21557
|
+
let pendingOutput = "";
|
|
21152
21558
|
const started = await ssh.exec(
|
|
21153
|
-
`
|
|
21154
|
-
{ timeoutMs:
|
|
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
|
+
} }
|
|
21155
21570
|
);
|
|
21156
21571
|
if (!started.success)
|
|
21157
21572
|
return started;
|
|
21158
|
-
await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
|
|
21159
21573
|
logger5.info("Running health check", { url: url2 });
|
|
21160
|
-
const healthy = await healthCheck(`${url2}${!
|
|
21574
|
+
const healthy = await healthCheck(`${url2}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
|
|
21161
21575
|
if (!healthy) {
|
|
21162
21576
|
return err(
|
|
21163
21577
|
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
@@ -21181,7 +21595,8 @@ function createDockerSshProvider(config2, container) {
|
|
|
21181
21595
|
return await deploy2(context);
|
|
21182
21596
|
} catch (error62) {
|
|
21183
21597
|
logger5.error("Deployment bundle failed", { error: error62 });
|
|
21184
|
-
|
|
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);
|
|
21185
21600
|
}
|
|
21186
21601
|
};
|
|
21187
21602
|
return { name: "docker-ssh", deploy: deploySafely };
|
|
@@ -22160,11 +22575,19 @@ var deploy = {
|
|
|
22160
22575
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
22161
22576
|
...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
|
|
22162
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
|
+
}
|
|
22163
22584
|
if (currentContainerProvider) {
|
|
22164
22585
|
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
22165
22586
|
}
|
|
22166
22587
|
if (options?.bundleOnly)
|
|
22167
22588
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
|
|
22589
|
+
if (options.selectedServices)
|
|
22590
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
|
|
22168
22591
|
if (!currentProvider) {
|
|
22169
22592
|
return notInitialized.result();
|
|
22170
22593
|
}
|
|
@@ -22237,6 +22660,17 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
|
|
|
22237
22660
|
} catch {
|
|
22238
22661
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
22239
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
|
+
};
|
|
22673
|
+
}
|
|
22240
22674
|
if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
|
|
22241
22675
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
22242
22676
|
let provisionEnv = {};
|
|
@@ -22296,5 +22730,5 @@ function describeMiddlewareBackend(backend) {
|
|
|
22296
22730
|
}
|
|
22297
22731
|
|
|
22298
22732
|
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
|
|
22299
|
-
//# sourceMappingURL=src-
|
|
22300
|
-
//# sourceMappingURL=src-
|
|
22733
|
+
//# sourceMappingURL=src-2V6VP4I5.js.map
|
|
22734
|
+
//# sourceMappingURL=src-2V6VP4I5.js.map
|