@h-ai/deploy 0.1.0-alpha.66 → 0.1.0-alpha.68
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 +119 -36
- package/README.ja.md +119 -36
- package/README.md +122 -76
- package/dist/index.d.ts +477 -62
- package/dist/index.js +1418 -212
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'crypto';
|
|
2
|
-
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
2
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync, readdirSync } from 'fs';
|
|
3
3
|
import { resolve, join, basename, dirname, relative, sep } from 'path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { core,
|
|
6
|
-
import
|
|
5
|
+
import { core, err, ok } from '@h-ai/core';
|
|
6
|
+
import process3 from 'process';
|
|
7
7
|
import { parseEnv } from 'util';
|
|
8
|
-
import { parse, stringify, parseAllDocuments } from 'yaml';
|
|
8
|
+
import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
|
+
import { readdir, readFile } from 'fs/promises';
|
|
12
|
+
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
11
13
|
import { Buffer } from 'buffer';
|
|
12
14
|
|
|
13
15
|
// src/deploy-artifact.ts
|
|
14
16
|
var DeploymentArtifactSchema = z.object({
|
|
15
17
|
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
16
|
-
version: z.literal(
|
|
18
|
+
version: z.literal(3),
|
|
17
19
|
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
18
20
|
appVersion: z.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),
|
|
19
|
-
/**
|
|
21
|
+
/** 本次构建标识;同一应用版本允许重复构建。 */
|
|
20
22
|
buildId: z.string().regex(/^[a-z0-9][\w.+-]*$/i),
|
|
21
|
-
/**
|
|
23
|
+
/** 只包含所选镜像的构建不能用于首次安装。 */
|
|
24
|
+
partial: z.boolean().default(false),
|
|
25
|
+
/** API 单独构建时仍保留工程的 Web 拓扑。 */
|
|
26
|
+
hasWeb: z.boolean().default(false),
|
|
27
|
+
/** 工程显式配置的应用端口,部分构建未包含 app 镜像时仍保留;缺省由部署配置决定。 */
|
|
28
|
+
appContainerPort: z.number().int().min(1).max(65535).optional(),
|
|
29
|
+
/** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
|
|
30
|
+
webContainerPort: z.number().int().min(1).max(65535).optional(),
|
|
31
|
+
/** 本次构建选择的镜像归档。 */
|
|
22
32
|
images: z.array(z.object({
|
|
23
33
|
/** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
|
|
24
34
|
name: z.enum(["app", "web"]),
|
|
25
35
|
/** 相对于清单目录的 tar 文件名。 */
|
|
26
36
|
file: z.string().regex(/^[\w.+-]+\.tar$/),
|
|
27
37
|
/** 归档导入后可直接使用的不可变镜像引用。 */
|
|
28
|
-
imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]
|
|
38
|
+
imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
|
|
29
39
|
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
30
40
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
31
41
|
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
32
42
|
containerPort: z.number().int().min(1).max(65535).optional()
|
|
33
|
-
})).min(1).max(2).refine((images) =>
|
|
34
|
-
}).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
|
|
43
|
+
})).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
|
|
44
|
+
}).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"));
|
|
35
45
|
async function artifactChecksum(file) {
|
|
36
46
|
const hash = createHash("sha256");
|
|
37
47
|
for await (const chunk of createReadStream(file))
|
|
@@ -99,11 +109,125 @@ var en_US_default = {
|
|
|
99
109
|
deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
|
|
100
110
|
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
|
|
101
111
|
deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
|
|
102
|
-
deploy_artifactInvalid: "
|
|
112
|
+
deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
|
|
103
113
|
deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
|
|
104
|
-
deploy_bundleInstructions: "
|
|
114
|
+
deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, first edit deploy-target.json in the adjacent deployment directory using its _comments, then run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) there. Both read this JSON; the sh entry also requires local awk.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh (uses the configured target directory).\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl, util-linux flock and filesystem/container permissions on a filesystem supporting flock.\nNever delete or replace .deploy.lock; its lock releases after every holder exits or the server restarts. Inspect execution and recovery state after forced termination.\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}",
|
|
105
115
|
deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
|
|
106
|
-
|
|
116
|
+
deploy_targetHost: "Required: Linux server address or SSH alias. Replace SERVER_HOST before running either upload script.",
|
|
117
|
+
deploy_targetPort: "SSH port, integer 1-65535; default 22.",
|
|
118
|
+
deploy_targetUsername: "SSH login username; default deploy. Requires write access to directory and container runtime permissions.",
|
|
119
|
+
deploy_targetIdentityFile: "Optional private key path on the computer running the upload script. Prefer an absolute path; empty uses ssh-agent/default keys. HAI_DEPLOY_IDENTITY_FILE overrides it.",
|
|
120
|
+
deploy_targetKnownHostsFile: "Optional known_hosts path on the computer running the upload script. Prefer an absolute path; empty uses OpenSSH defaults. HAI_DEPLOY_KNOWN_HOSTS_FILE overrides it.",
|
|
121
|
+
deploy_targetStrictHostKeyChecking: "Boolean, default true: trust new host keys and reject changed keys. false disables host key checking.",
|
|
122
|
+
deploy_targetConnectTimeoutSeconds: "SSH connection timeout in seconds, integer 1-2147483647; default 10.",
|
|
123
|
+
deploy_targetDirectory: "Required absolute application directory on the Linux server; contains incoming packages, releases and successful current state. Parent-directory traversal is forbidden.",
|
|
124
|
+
deploy_targetBundle: "Required tar filename in the parent of this deployment directory. Keep the tar and this entire directory together when moving them.",
|
|
125
|
+
deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
|
|
126
|
+
deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
|
|
127
|
+
deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
|
|
128
|
+
deploy_imageVersionRequired: "Specify an image version; latest is not allowed: {image}",
|
|
129
|
+
deploy_bundleRuntimeRequired: "Start local Podman/Docker to export middleware images, or prepare this complete offline bundle on the build machine.",
|
|
130
|
+
deploy_bundleFailed: "Package preparation or deployment failed: {reason}",
|
|
131
|
+
deploy_configFileInvalid: "Invalid YAML in {path}, line {line}. Check indentation, colons and quotes.",
|
|
132
|
+
deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
|
|
133
|
+
deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
|
|
134
|
+
deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
|
|
135
|
+
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.",
|
|
136
|
+
deploy_localStopped: "The local command process tree has been confirmed exited.",
|
|
137
|
+
deploy_localUnknown: "Local process tree termination is unconfirmed; inspect local execution state.",
|
|
138
|
+
deploy_localNotStarted: "The local command was not started.",
|
|
139
|
+
deploy_commandOutputFailed: "Command output or callback failed",
|
|
140
|
+
deploy_commandInputFailed: "Command input failed",
|
|
141
|
+
deploy_commandTreeActive: "Child processes remain active after the main command exited",
|
|
142
|
+
deploy_commandExecutionFailed: "Command execution failed",
|
|
143
|
+
deploy_remoteUnknown: "Remote execution is unknown. Local SSH termination does not prove remote deployment stopped; inspect the server result and actual containers.",
|
|
144
|
+
deploy_remoteFinished: "A matching server result confirms the deployment script and recovery finished: stage {stage}, exit {code}. Verify actual service health."
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// messages/ja-JP.json
|
|
148
|
+
var ja_JP_default = {
|
|
149
|
+
deploy_notInitialized: "\u30C7\u30D7\u30ED\u30A4\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Bdeploy.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
150
|
+
deploy_initFailed: "\u30C7\u30D7\u30ED\u30A4\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
151
|
+
deploy_unsupportedProviderType: "\u672A\u5BFE\u5FDC\u306E\u30C7\u30D7\u30ED\u30A4\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u7A2E\u985E\u3067\u3059: {type}",
|
|
152
|
+
deploy_unsupportedProvisionerType: "\u672A\u5BFE\u5FDC\u306E\u30B5\u30FC\u30D3\u30B9\u30D7\u30ED\u30D3\u30B8\u30E7\u30CA\u30FC\u306E\u7A2E\u985E\u3067\u3059: {type}",
|
|
153
|
+
deploy_configTokenRequired: "\u30C7\u30D7\u30ED\u30A4\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u30C8\u30FC\u30AF\u30F3\u306F\u5FC5\u9808\u3067\u3059",
|
|
154
|
+
deploy_authRequired: "\u8A8D\u8A3C\u304C\u5FC5\u8981\u3067\u3059",
|
|
155
|
+
deploy_authFailed: "\u8A8D\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
156
|
+
deploy_projectCreateFailed: "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
157
|
+
deploy_uploadFailed: "\u30D3\u30EB\u30C9\u6210\u679C\u7269\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
158
|
+
deploy_deployFailed: "\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
159
|
+
deploy_provisionFailed: "\u30B5\u30FC\u30D3\u30B9\u306E\u6E96\u5099\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {service} - {error}",
|
|
160
|
+
deploy_adapterMissing: "\u5BFE\u8C61\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0\u306E\u30A2\u30C0\u30D7\u30BF\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002{adapter}\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
161
|
+
deploy_scanFailed: "\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u30B9\u30AD\u30E3\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
162
|
+
deploy_envVarFailed: "\u74B0\u5883\u5909\u6570\u306E\u8A2D\u5B9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
163
|
+
deploy_credentialError: "\u8A8D\u8A3C\u60C5\u5831\u306E\u8AAD\u307F\u66F8\u304D\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
164
|
+
deploy_credentialInvalidKey: "\u672A\u5BFE\u5FDC\u306E\u8A8D\u8A3C\u60C5\u5831\u30AD\u30FC\u3067\u3059: {key}\u3002HAI_DEPLOY_*\u30AD\u30FC\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059",
|
|
165
|
+
deploy_credentialSaved: "\u8A8D\u8A3C\u60C5\u5831\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F",
|
|
166
|
+
deploy_credentialLoaded: "\u8A8D\u8A3C\u60C5\u5831\u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3057\u305F",
|
|
167
|
+
deploy_configError: "\u30C7\u30D7\u30ED\u30A4\u8A2D\u5B9A\u30A8\u30E9\u30FC: {error}",
|
|
168
|
+
deploy_credentialMissing: "\u8A8D\u8A3C\u60C5\u5831\u306E\u9805\u76EE\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: {fields}",
|
|
169
|
+
deploy_apiError: "{service} API\u30A8\u30E9\u30FC\uFF08{status}\uFF09",
|
|
170
|
+
deploy_fileUploadFailed: "\u30D5\u30A1\u30A4\u30EB\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08{status}\uFF09",
|
|
171
|
+
deploy_provisionNoResult: "{service}\u304B\u3089\u63A5\u7D9A\u60C5\u5831\u304C\u8FD4\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F",
|
|
172
|
+
deploy_recoveryFailure: "{stage}\u3067\u30C7\u30D7\u30ED\u30A4\u3092\u505C\u6B62\u3057\u307E\u3057\u305F\u3002\u5FA9\u65E7\u60C5\u5831\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
173
|
+
deploy_recoverySuggestion: "\u540C\u3058\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D\u3067\u518D\u8A66\u884C\u3059\u308B\u524D\u306B\u3001\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u30B3\u30F3\u30BD\u30FC\u30EB\u3067\u8A18\u8F09\u3055\u308C\u305F\u30EA\u30BD\u30FC\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u300C\u4E0D\u660E\u300D\u306F\u3001\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u3088\u3063\u3066\u30EA\u30BD\u30FC\u30B9\u304C\u4F5C\u6210\u3055\u308C\u305F\u53EF\u80FD\u6027\u3092\u793A\u3057\u307E\u3059\u3002\u518D\u5229\u7528\u3057\u305F\u30EA\u30BD\u30FC\u30B9\u306F\u81EA\u52D5\u524A\u9664\u3055\u308C\u307E\u305B\u3093\u3002\u65B0\u898F\u30EA\u30BD\u30FC\u30B9\u306F\u4F7F\u7528\u72B6\u6CC1\u3092\u78BA\u8A8D\u3057\u3066\u304B\u3089\u624B\u52D5\u3067\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
174
|
+
deploy_commandTimeout: "\u30B3\u30DE\u30F3\u30C9\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F: {command}",
|
|
175
|
+
deploy_commandFailed: "\u30B3\u30DE\u30F3\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08{command}\uFF09: {error}",
|
|
176
|
+
deploy_remoteConnectFailed: "\u30EA\u30E2\u30FC\u30C8\u30DB\u30B9\u30C8{host}\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093: {error}",
|
|
177
|
+
deploy_remoteRuntimeMissing: "\u30EA\u30E2\u30FC\u30C8\u30DB\u30B9\u30C8\u306B\u30B3\u30F3\u30C6\u30CA\u30FC\u30E9\u30F3\u30BF\u30A4\u30E0\u307E\u305F\u306Fcompose\u304C\u3042\u308A\u307E\u305B\u3093: {runtime}",
|
|
178
|
+
deploy_imageTransferFailed: "\u30A4\u30E1\u30FC\u30B8\u306E\u8EE2\u9001\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
179
|
+
deploy_healthCheckFailed: "\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002{url}\u3067\u30A2\u30D7\u30EA\u306E\u6E96\u5099\u304C\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093",
|
|
180
|
+
deploy_invalidEnvironment: "\u74B0\u5883\u540D\u306B\u306F\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002local/example\u306F\u4E88\u7D04\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
|
|
181
|
+
deploy_environmentMissing: "\u74B0\u5883\u30D5\u30A1\u30A4\u30EB\u304C\u3042\u308A\u307E\u305B\u3093: {path}\u3002\u30C7\u30D7\u30ED\u30A4\u524D\u306B\u4F5C\u6210\u3057\u3066\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
182
|
+
deploy_environmentReadFailed: "\u30C7\u30D7\u30ED\u30A4\u7528\u306E\u74B0\u5883\u30D5\u30A1\u30A4\u30EB\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\u3002\u69CB\u6587\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
183
|
+
deploy_environmentWriteFailed: "\u30C7\u30D7\u30ED\u30A4\u8A2D\u5B9A\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3002\u30D5\u30A1\u30A4\u30EB\u306E\u69CB\u6587\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
184
|
+
deploy_multipleApplications: "\u30B5\u30FC\u30D3\u30B9\u30D1\u30C3\u30B1\u30FC\u30B8\u304C\u8907\u6570\u898B\u3064\u304B\u308A\u307E\u3057\u305F\u3002\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
185
|
+
deploy_purposeSqlite: "\u30DC\u30EA\u30E5\u30FC\u30E0\u306B\u6C38\u7D9A\u5316\u3059\u308B\u30ED\u30FC\u30AB\u30EB\u30EA\u30EC\u30FC\u30B7\u30E7\u30CA\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9",
|
|
186
|
+
deploy_purposeDatabase: "\u696D\u52D9\u30C7\u30FC\u30BF\u7528\u306E\u30EA\u30EC\u30FC\u30B7\u30E7\u30CA\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u30B5\u30FC\u30D3\u30B9",
|
|
187
|
+
deploy_purposeMemory: "\u30D7\u30ED\u30BB\u30B9\u5358\u4F4D\u306E\u30E1\u30E2\u30EA\u30FC\u30AD\u30E3\u30C3\u30B7\u30E5\u3002\u518D\u8D77\u52D5\u6642\u306B\u6D88\u53BB\u3055\u308C\u3001\u5916\u90E8\u30B5\u30FC\u30D3\u30B9\u306F\u4F7F\u7528\u3057\u307E\u305B\u3093",
|
|
188
|
+
deploy_purposeLocal: "\u30DC\u30EA\u30E5\u30FC\u30E0\u306B\u6C38\u7D9A\u5316\u3059\u308B\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u30B9\u30C8\u30EC\u30FC\u30B8",
|
|
189
|
+
deploy_purposeCache: "\u5171\u6709\u30AD\u30E3\u30C3\u30B7\u30E5\u3001\u30BB\u30C3\u30B7\u30E7\u30F3\u3001\u5206\u6563\u30ED\u30C3\u30AF",
|
|
190
|
+
deploy_purposeStorage: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30D5\u30A1\u30A4\u30EB\u7528\u306ES3\u4E92\u63DB\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u30B9\u30C8\u30EC\u30FC\u30B8",
|
|
191
|
+
deploy_purposeVector: "\u30D9\u30AF\u30C8\u30EB\u306E\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u4F5C\u6210\u3068\u985E\u4F3C\u691C\u7D22",
|
|
192
|
+
deploy_purposeDisabled: "LDAP\u30ED\u30B0\u30A4\u30F3\u306F\u7121\u52B9\u3067\u3059\u3002\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30B5\u30FC\u30D3\u30B9\u306F\u4F5C\u6210\u3057\u307E\u305B\u3093",
|
|
193
|
+
deploy_purposeLdap: "\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30A2\u30AB\u30A6\u30F3\u30C8\u8A8D\u8A3C\u3002\u65E2\u5B9A\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u306F\u696D\u52D9\u30E6\u30FC\u30B6\u30FC\u306F\u3044\u307E\u305B\u3093",
|
|
194
|
+
deploy_purposeConsole: "\u914D\u4FE1\u305B\u305A\u3001\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30ED\u30B0\u306B\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u51FA\u529B\u3057\u307E\u3059",
|
|
195
|
+
deploy_purposeEmail: "\u30E1\u30FC\u30EB/HTTP\u914D\u4FE1\u3002\u65E2\u5B9A\u306ESMTP\u30B3\u30F3\u30C6\u30CA\u30FC\u306F\u30C6\u30B9\u30C8\u30E1\u30C3\u30BB\u30FC\u30B8\u306E\u307F\u53D7\u4FE1\u3057\u307E\u3059",
|
|
196
|
+
deploy_purposeSms: "Aliyun\u306E\u8A8D\u8A3C\u60C5\u5831\u306B\u3088\u308BSMS\u3002\u65E2\u5B58\u306E\u7F72\u540D\u3068\u5229\u7528\u67A0\u304C\u5FC5\u8981\u3067\u3059",
|
|
197
|
+
deploy_webProxyInvalid: "API\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3001\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30B9\u30E9\u30C3\u30B7\u30E5\u306E\u307F\u3092\u542B\u3080\u7D76\u5BFE\u30D1\u30B9\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
198
|
+
deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target docker \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001--artifact-dir \u3067\u6709\u52B9\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001pnpm run deploy --build \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
199
|
+
deploy_sshRequired: "\u81EA\u52D5\u30C7\u30D7\u30ED\u30A4\u306B\u306FSSH\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u306A\u3044\u5834\u5408\u306F--bundle-only\u3067\u624B\u52D5\u8EE2\u9001\u7528\u306E\u30D0\u30F3\u30C9\u30EB\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
200
|
+
deploy_bundleInstructions: "\u30D0\u30F3\u30C9\u30EB\uFF1A{file}\n\u9078\u629E\u3057\u305F\u30B5\u30FC\u30D3\u30B9\u306B\u5FC5\u8981\u306A\u3059\u3079\u3066\u306E\u30A4\u30E1\u30FC\u30B8\u3092\u542B\u307F\u307E\u3059\uFF08\u30BB\u30EB\u30D5\u30DB\u30B9\u30C8\u306E\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u3092\u542B\u3080\uFF09\u3002\nSSH \u3092\u4F7F\u7528\u3059\u308B\u5834\u5408\u3001\u96A3\u63A5\u3059\u308B\u30C7\u30D7\u30ED\u30A4\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E deploy-target.json \u3092 _comments \u306B\u5F93\u3063\u3066\u7DE8\u96C6\u3057\u3001\u540C\u3058\u5834\u6240\u3067 deploy.ps1\uFF08Windows\uFF09\u307E\u305F\u306F deploy.sh\uFF08Linux/macOS\uFF09\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4E21\u65B9\u3068\u3082\u3053\u306E JSON \u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3059\u3002sh \u306E\u5B9F\u884C\u306B\u306F\u30ED\u30FC\u30AB\u30EB\u306E awk \u3082\u5FC5\u8981\u3067\u3059\u3002\n\u9694\u96E2\u3055\u308C\u305F\u30B5\u30FC\u30D0\u30FC\u3067\u306F\u3001tar \u3092\u5225\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u30B3\u30D4\u30FC\u3057\u3066\u5C55\u958B\u3057\u3001sh apply.sh \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u8A2D\u5B9A\u3055\u308C\u305F\u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u4F7F\u7528\u3057\u307E\u3059\uFF09\u3002\nLinux \u306B\u306F Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk\u3001curl\u3001util-linux \u306E flock \u304C\u5FC5\u8981\u3067\u3059\u3002flock \u306B\u5BFE\u5FDC\u3057\u305F\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u4E0A\u3067\u3001\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u3068\u30B3\u30F3\u30C6\u30CA\u30FC\u306E\u64CD\u4F5C\u6A29\u9650\u3082\u5FC5\u8981\u3067\u3059\u3002\n.deploy.lock \u306F\u524A\u9664\u3082\u7F6E\u63DB\u3082\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002\u3059\u3079\u3066\u306E\u4FDD\u6301\u30D7\u30ED\u30BB\u30B9\u304C\u7D42\u4E86\u3059\u308B\u304B\u3001\u30B5\u30FC\u30D0\u30FC\u304C\u518D\u8D77\u52D5\u3059\u308B\u3068\u30ED\u30C3\u30AF\u304C\u89E3\u653E\u3055\u308C\u307E\u3059\u3002\u5F37\u5236\u7D42\u4E86\u5F8C\u306F\u5B9F\u884C\u72B6\u614B\u3068\u5FA9\u65E7\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\n\u90E8\u5206\u66F4\u65B0\u306B\u306F\u30DD\u30FC\u30C8\u3068\u69CB\u6210\u304C\u4E00\u81F4\u3059\u308B\u6B63\u5E38\u306A\u5B8C\u5168\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u304C\u5FC5\u8981\u3067\u3059\u3002\u5404\u624B\u9806\u306F\u5931\u6557\u6642\u306B\u975E\u30BC\u30ED\u3067\u7D42\u4E86\u3057\u3001result-DEPLOYMENT_ID.json \u306B\u7D50\u679C\u3092\u8A18\u9332\u3057\u307E\u3059\u3002\n\u30D0\u30F3\u30C9\u30EB\u306B\u306F\u975E\u516C\u958B\u306E\u5B9F\u884C\u6642\u8A2D\u5B9A\u304C\u542B\u307E\u308C\u307E\u3059\u3002\u5B89\u5168\u306B\u8EE2\u9001\u3057\u3066\u304F\u3060\u3055\u3044\u3002\n\u540C\u68B1\u30A4\u30E1\u30FC\u30B8\uFF1A\n{images}",
|
|
201
|
+
deploy_bundleMetadata: "\u30A2\u30D7\u30EA\u306E\u30D0\u30FC\u30B8\u30E7\u30F3: {version}\n\u30D3\u30EB\u30C9ID: {buildId}\nCompose\u30D5\u30A1\u30A4\u30EB: {compose}",
|
|
202
|
+
deploy_targetHost: "\u5FC5\u9808\uFF1ALinux \u30B5\u30FC\u30D0\u30FC\u306E\u30A2\u30C9\u30EC\u30B9\u307E\u305F\u306F SSH \u30A8\u30A4\u30EA\u30A2\u30B9\u3002\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u524D\u306B SERVER_HOST \u3092\u7F6E\u304D\u63DB\u3048\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
203
|
+
deploy_targetPort: "SSH \u30DD\u30FC\u30C8\u30021\u301C65535\u306E\u6574\u6570\u3002\u65E2\u5B9A\u5024\u306F22\u3067\u3059\u3002",
|
|
204
|
+
deploy_targetUsername: "SSH \u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u540D\u3002\u65E2\u5B9A\u5024\u306F deploy \u3067\u3059\u3002\u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u306E\u66F8\u304D\u8FBC\u307F\u6A29\u9650\u3068\u30B3\u30F3\u30C6\u30CA\u30FC\u30E9\u30F3\u30BF\u30A4\u30E0\u306E\u64CD\u4F5C\u6A29\u9650\u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
205
|
+
deploy_targetIdentityFile: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u7AEF\u672B\u4E0A\u306E\u79D8\u5BC6\u9375\u30D1\u30B9\uFF08\u7701\u7565\u53EF\uFF09\u3002\u7D76\u5BFE\u30D1\u30B9\u3092\u63A8\u5968\u3057\u307E\u3059\u3002\u7A7A\u6B04\u3067\u306F ssh-agent \u307E\u305F\u306F\u65E2\u5B9A\u306E\u9375\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002HAI_DEPLOY_IDENTITY_FILE \u3067\u4E0A\u66F8\u304D\u3067\u304D\u307E\u3059\u3002",
|
|
206
|
+
deploy_targetKnownHostsFile: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u7AEF\u672B\u4E0A\u306E known_hosts \u30D1\u30B9\uFF08\u7701\u7565\u53EF\uFF09\u3002\u7D76\u5BFE\u30D1\u30B9\u3092\u63A8\u5968\u3057\u307E\u3059\u3002\u7A7A\u6B04\u3067\u306F OpenSSH \u306E\u65E2\u5B9A\u5024\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002HAI_DEPLOY_KNOWN_HOSTS_FILE \u3067\u4E0A\u66F8\u304D\u3067\u304D\u307E\u3059\u3002",
|
|
207
|
+
deploy_targetStrictHostKeyChecking: "\u771F\u507D\u5024\u3002\u65E2\u5B9A\u5024\u306E true \u306F\u65B0\u3057\u3044\u30DB\u30B9\u30C8\u9375\u3092\u4FE1\u983C\u3057\u3001\u5909\u66F4\u3055\u308C\u305F\u9375\u3092\u62D2\u5426\u3057\u307E\u3059\u3002false \u306F\u30DB\u30B9\u30C8\u9375\u306E\u691C\u8A3C\u3092\u7121\u52B9\u306B\u3057\u307E\u3059\u3002",
|
|
208
|
+
deploy_targetConnectTimeoutSeconds: "SSH \u63A5\u7D9A\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\uFF08\u79D2\uFF09\u30021\u301C2147483647\u306E\u6574\u6570\u3002\u65E2\u5B9A\u5024\u306F10\u3067\u3059\u3002",
|
|
209
|
+
deploy_targetDirectory: "\u5FC5\u9808\uFF1ALinux \u30B5\u30FC\u30D0\u30FC\u4E0A\u306E\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u7D76\u5BFE\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3002\u53D7\u4FE1\u30D1\u30C3\u30B1\u30FC\u30B8\u3001\u30EA\u30EA\u30FC\u30B9\u3001\u6210\u529F\u6642\u306E current \u72B6\u614B\u3092\u4FDD\u5B58\u3057\u307E\u3059\u3002\u89AA\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u306E\u79FB\u52D5\u306F\u8A31\u53EF\u3055\u308C\u307E\u305B\u3093\u3002",
|
|
210
|
+
deploy_targetBundle: "\u5FC5\u9808\uFF1A\u3053\u306E\u30C7\u30D7\u30ED\u30A4\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u89AA\u306B\u3042\u308B tar \u30D5\u30A1\u30A4\u30EB\u540D\u3002\u79FB\u52D5\u6642\u306F tar \u3068\u3053\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u5168\u4F53\u3092\u4E00\u7DD2\u306B\u79FB\u52D5\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
211
|
+
deploy_bundleOnlyContainer: "Compose\u30D0\u30F3\u30C9\u30EB\u306E\u751F\u6210\u306Fdocker-ssh\u306E\u307F\u3067\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u307E\u3059\u3002",
|
|
212
|
+
deploy_selectedContainerOnly: "\u30B5\u30FC\u30D3\u30B9\u3092\u9078\u629E\u3057\u305F\u30C7\u30D7\u30ED\u30A4\u306B\u306F docker-ssh Compose \u306E\u30C7\u30D7\u30ED\u30A4\u5148\u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
213
|
+
deploy_selectionInvalid: "{service} \u3092\u30C7\u30D7\u30ED\u30A4\u3067\u304D\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D3\u30B9\u540D\u3092\u78BA\u8A8D\u3057\u3001\u5148\u306B\u30A4\u30E1\u30FC\u30B8\u3092\u30D3\u30EB\u30C9\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
214
|
+
deploy_imageVersionRequired: "\u30A4\u30E1\u30FC\u30B8\u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002latest \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF1A{image}",
|
|
215
|
+
deploy_bundleRuntimeRequired: "\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u30A4\u30E1\u30FC\u30B8\u3092\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3059\u308B\u306B\u306F\u30ED\u30FC\u30AB\u30EB\u306E Podman/Docker \u3092\u8D77\u52D5\u3059\u308B\u304B\u3001\u30D3\u30EB\u30C9\u7AEF\u672B\u3067\u5B8C\u5168\u306A\u30AA\u30D5\u30E9\u30A4\u30F3\u30D0\u30F3\u30C9\u30EB\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
216
|
+
deploy_bundleFailed: "\u30D1\u30C3\u30B1\u30FC\u30B8\u306E\u6E96\u5099\u307E\u305F\u306F\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF1A{reason}",
|
|
217
|
+
deploy_configFileInvalid: "{path} \u306E {line} \u884C\u76EE\u306E YAML \u304C\u7121\u52B9\u3067\u3059\u3002\u30A4\u30F3\u30C7\u30F3\u30C8\u3001\u30B3\u30ED\u30F3\u3001\u5F15\u7528\u7B26\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
218
|
+
deploy_configFileUnreadable: "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB {path} \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3002\u30D1\u30B9\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
219
|
+
deploy_bundleNameInvalid: "\u30D0\u30F3\u30C9\u30EB\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u306F\u82F1\u6570\u5B57\u3001\u30C9\u30C3\u30C8\u3001\u30D7\u30E9\u30B9\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3092\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002\u89AA\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u306F\u7A7A\u767D\u3092\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002",
|
|
220
|
+
deploy_secretsInvalid: "\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u306E\u8A8D\u8A3C\u60C5\u5831\u304C\u7834\u640D\u3057\u3066\u3044\u308B\u304B\u3001\u4E0D\u5B8C\u5168\u3067\u3059\uFF1A{path}\u3002\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u3092\u5FA9\u5143\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u65E2\u5B58\u30C7\u30FC\u30BF\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u306A\u304F\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u3001\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u518D\u751F\u6210\u3057\u307E\u305B\u3093\u3002",
|
|
221
|
+
deploy_storageArtifactRequired: "\u30B9\u30C8\u30EC\u30FC\u30B8\u306E\u521D\u671F\u5316\u306B\u306F\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30A4\u30E1\u30FC\u30B8\u304C\u5FC5\u8981\u3067\u3059\u3002--artifact-dir \u3067 app \u3092\u542B\u3080\u5B8C\u5168\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001\u5148\u306B pnpm build --target docker --images app \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
222
|
+
deploy_localStopped: "\u30ED\u30FC\u30AB\u30EB\u30B3\u30DE\u30F3\u30C9\u306E\u30D7\u30ED\u30BB\u30B9\u30C4\u30EA\u30FC\u306E\u7D42\u4E86\u3092\u78BA\u8A8D\u3057\u307E\u3057\u305F\u3002",
|
|
223
|
+
deploy_localUnknown: "\u30ED\u30FC\u30AB\u30EB\u30D7\u30ED\u30BB\u30B9\u30C4\u30EA\u30FC\u306E\u7D42\u4E86\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB\u306E\u5B9F\u884C\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
224
|
+
deploy_localNotStarted: "\u30ED\u30FC\u30AB\u30EB\u30B3\u30DE\u30F3\u30C9\u306F\u8D77\u52D5\u3057\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
225
|
+
deploy_commandOutputFailed: "\u30B3\u30DE\u30F3\u30C9\u51FA\u529B\u307E\u305F\u306F\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
226
|
+
deploy_commandInputFailed: "\u30B3\u30DE\u30F3\u30C9\u5165\u529B\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
227
|
+
deploy_commandTreeActive: "\u30E1\u30A4\u30F3\u30B3\u30DE\u30F3\u30C9\u7D42\u4E86\u5F8C\u3082\u5B50\u30D7\u30ED\u30BB\u30B9\u304C\u52D5\u4F5C\u3057\u3066\u3044\u307E\u3059",
|
|
228
|
+
deploy_commandExecutionFailed: "\u30B3\u30DE\u30F3\u30C9\u306E\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
229
|
+
deploy_remoteUnknown: "\u30EA\u30E2\u30FC\u30C8\u306E\u5B9F\u884C\u72B6\u614B\u306F\u4E0D\u660E\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB SSH \u306E\u7D42\u4E86\u306F\u30EA\u30E2\u30FC\u30C8\u30C7\u30D7\u30ED\u30A4\u306E\u505C\u6B62\u3092\u610F\u5473\u3057\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D0\u30FC\u306E\u7D50\u679C\u3068\u5B9F\u969B\u306E\u30B3\u30F3\u30C6\u30CA\u30FC\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
230
|
+
deploy_remoteFinished: "\u5BFE\u5FDC\u3059\u308B\u30B5\u30FC\u30D0\u30FC\u306E\u7D50\u679C\u306B\u3088\u308A\u3001\u30C7\u30D7\u30ED\u30A4\u30B9\u30AF\u30EA\u30D7\u30C8\u3068\u5FA9\u65E7\u51E6\u7406\u306E\u7D42\u4E86\u3092\u78BA\u8A8D\u3057\u307E\u3057\u305F\uFF1A\u6BB5\u968E {stage}\u3001\u7D42\u4E86\u30B3\u30FC\u30C9 {code}\u3002\u5B9F\u969B\u306E\u30B5\u30FC\u30D3\u30B9\u306E\u7A3C\u50CD\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
107
231
|
};
|
|
108
232
|
|
|
109
233
|
// messages/zh-CN.json
|
|
@@ -157,17 +281,46 @@ var zh_CN_default = {
|
|
|
157
281
|
deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
|
|
158
282
|
deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
|
|
159
283
|
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",
|
|
160
|
-
deploy_artifactInvalid: "\
|
|
284
|
+
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",
|
|
161
285
|
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",
|
|
162
|
-
deploy_bundleInstructions: "\
|
|
286
|
+
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\u5148\u6309 _comments \u8BF4\u660E\u4FEE\u6539\u65C1\u8FB9\u90E8\u7F72\u76EE\u5F55\u4E2D\u7684 deploy-target.json\uFF0C\u518D\u8FD0\u884C\u540C\u76EE\u5F55\u7684 deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\uFF1B\u4E24\u8005\u5747\u8BFB\u53D6\u6B64 JSON\uFF0Csh \u5165\u53E3\u8FD8\u9700\u672C\u673A\u5B89\u88C5 awk\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\uFF08\u4F7F\u7528\u5305\u4E2D\u914D\u7F6E\u7684\u76EE\u6807\u76EE\u5F55\uFF09\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk\u3001curl\u3001util-linux flock\uFF0C\u4EE5\u53CA\u652F\u6301 flock \u7684\u6587\u4EF6\u7CFB\u7EDF\u548C\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002\n\u56FA\u5B9A\u9501\u6587\u4EF6 .deploy.lock \u7981\u6B62\u5220\u9664\u6216\u66FF\u6362\uFF1B\u5168\u90E8\u6301\u6709\u8FDB\u7A0B\u9000\u51FA\u6216\u91CD\u542F\u540E\u81EA\u52A8\u91CA\u653E\u3002\u5F3A\u6740\u540E\u7684\u6267\u884C\u4E0E\u6062\u590D\u72B6\u6001\u987B\u4EBA\u5DE5\u6838\u5BF9\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}",
|
|
163
287
|
deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
|
|
164
|
-
|
|
288
|
+
deploy_targetHost: "\u5FC5\u586B\uFF1ALinux \u670D\u52A1\u5668\u5730\u5740\u6216 SSH \u522B\u540D\u3002\u6267\u884C\u4EFB\u4E00\u4E0A\u4F20\u811A\u672C\u524D\uFF0C\u5C06 SERVER_HOST \u6539\u4E3A\u5B9E\u9645\u5730\u5740\u3002",
|
|
289
|
+
deploy_targetPort: "SSH \u7AEF\u53E3\uFF0C1-65535 \u7684\u6574\u6570\uFF0C\u9ED8\u8BA4 22\u3002",
|
|
290
|
+
deploy_targetUsername: "SSH \u767B\u5F55\u7528\u6237\u540D\uFF0C\u9ED8\u8BA4 deploy\uFF1B\u9700\u8981\u76EE\u6807\u76EE\u5F55\u5199\u5165\u6743\u9650\u53CA\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002",
|
|
291
|
+
deploy_targetIdentityFile: "\u53EF\u9009\uFF1A\u8FD0\u884C\u4E0A\u4F20\u811A\u672C\u7684\u7535\u8111\u4E0A\u7684\u79C1\u94A5\u8DEF\u5F84\uFF0C\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7559\u7A7A\u4F7F\u7528 ssh-agent \u6216\u9ED8\u8BA4\u5BC6\u94A5\u3002HAI_DEPLOY_IDENTITY_FILE \u53EF\u8986\u76D6\u3002",
|
|
292
|
+
deploy_targetKnownHostsFile: "\u53EF\u9009\uFF1A\u8FD0\u884C\u4E0A\u4F20\u811A\u672C\u7684\u7535\u8111\u4E0A\u7684 known_hosts \u8DEF\u5F84\uFF0C\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7559\u7A7A\u4F7F\u7528 OpenSSH \u9ED8\u8BA4\u6587\u4EF6\u3002HAI_DEPLOY_KNOWN_HOSTS_FILE \u53EF\u8986\u76D6\u3002",
|
|
293
|
+
deploy_targetStrictHostKeyChecking: "\u5E03\u5C14\u503C\uFF0C\u9ED8\u8BA4 true\uFF1A\u9996\u6B21\u4FE1\u4EFB\u65B0\u4E3B\u673A\u5BC6\u94A5\uFF0C\u62D2\u7EDD\u5DF2\u53D8\u66F4\u5BC6\u94A5\uFF1Bfalse \u5173\u95ED\u4E3B\u673A\u5BC6\u94A5\u68C0\u67E5\u3002",
|
|
294
|
+
deploy_targetConnectTimeoutSeconds: "SSH \u8FDE\u63A5\u8D85\u65F6\u79D2\u6570\uFF0C1-2147483647 \u7684\u6574\u6570\uFF0C\u9ED8\u8BA4 10\u3002",
|
|
295
|
+
deploy_targetDirectory: "\u5FC5\u586B\uFF1ALinux \u670D\u52A1\u5668\u4E0A\u7684\u5E94\u7528\u7EDD\u5BF9\u76EE\u5F55\uFF0C\u4FDD\u5B58 incoming \u90E8\u7F72\u5305\u3001releases \u53CA\u6210\u529F\u72B6\u6001 current\uFF1B\u7981\u6B62 .. \u7236\u76EE\u5F55\u8DF3\u8F6C\u3002",
|
|
296
|
+
deploy_targetBundle: "\u5FC5\u586B\uFF1A\u672C\u90E8\u7F72\u64CD\u4F5C\u76EE\u5F55\u4E0A\u4E00\u7EA7\u7684 tar \u6587\u4EF6\u540D\uFF1B\u642C\u8FD0\u65F6\u8BF7\u5C06 tar \u548C\u672C\u76EE\u5F55\u5B8C\u6574\u653E\u5728\u4E00\u8D77\u3002",
|
|
297
|
+
deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
|
|
298
|
+
deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
|
|
299
|
+
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",
|
|
300
|
+
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",
|
|
301
|
+
deploy_imageVersionRequired: "\u955C\u50CF\u5FC5\u987B\u6307\u5B9A\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 latest\uFF1A{image}",
|
|
302
|
+
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",
|
|
303
|
+
deploy_bundleFailed: "\u4EA4\u4ED8\u51C6\u5907\u6216\u90E8\u7F72\u5931\u8D25\uFF1A{reason}",
|
|
304
|
+
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",
|
|
305
|
+
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",
|
|
306
|
+
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",
|
|
307
|
+
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",
|
|
308
|
+
deploy_localStopped: "\u5DF2\u786E\u8BA4\u672C\u5730\u547D\u4EE4\u8FDB\u7A0B\u6811\u5168\u90E8\u9000\u51FA\u3002",
|
|
309
|
+
deploy_localUnknown: "\u65E0\u6CD5\u786E\u8BA4\u672C\u5730\u547D\u4EE4\u8FDB\u7A0B\u6811\u5DF2\u5168\u90E8\u9000\u51FA\uFF1B\u8BF7\u68C0\u67E5\u672C\u673A\u6267\u884C\u72B6\u6001\u3002",
|
|
310
|
+
deploy_localNotStarted: "\u672C\u5730\u547D\u4EE4\u5C1A\u672A\u542F\u52A8\u3002",
|
|
311
|
+
deploy_commandOutputFailed: "\u547D\u4EE4\u8F93\u51FA\u6216\u56DE\u8C03\u5931\u8D25",
|
|
312
|
+
deploy_commandInputFailed: "\u547D\u4EE4\u8F93\u5165\u5931\u8D25",
|
|
313
|
+
deploy_commandTreeActive: "\u4E3B\u547D\u4EE4\u9000\u51FA\u540E\u4ECD\u6709\u6D3B\u8DC3\u7684\u5B50\u8FDB\u7A0B",
|
|
314
|
+
deploy_commandExecutionFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25",
|
|
315
|
+
deploy_remoteUnknown: "\u8FDC\u7AEF\u6267\u884C\u72B6\u6001\u672A\u77E5\uFF1B\u672C\u5730 SSH \u7ED3\u675F\u4E0D\u8BC1\u660E\u8FDC\u7AEF\u90E8\u7F72\u5DF2\u505C\u6B62\uFF0C\u8BF7\u6838\u5BF9\u670D\u52A1\u5668\u7ED3\u679C\u4E0E\u5B9E\u9645\u5BB9\u5668\u72B6\u6001\u3002",
|
|
316
|
+
deploy_remoteFinished: "\u5339\u914D\u7684\u670D\u52A1\u5668\u7ED3\u679C\u786E\u8BA4\u90E8\u7F72\u811A\u672C\u53CA\u6062\u590D\u5DF2\u7ED3\u675F\uFF1A\u9636\u6BB5 {stage}\uFF0C\u9000\u51FA\u7801 {code}\u3002\u8BF7\u6838\u5BF9\u5B9E\u9645\u670D\u52A1\u5065\u5EB7\u72B6\u6001\u3002"
|
|
165
317
|
};
|
|
166
318
|
|
|
167
319
|
// src/deploy-i18n.ts
|
|
168
320
|
var deployM = core.i18n.createMessageGetter({
|
|
169
321
|
"zh-CN": zh_CN_default,
|
|
170
|
-
"en-US": en_US_default
|
|
322
|
+
"en-US": en_US_default,
|
|
323
|
+
"ja-JP": ja_JP_default
|
|
171
324
|
});
|
|
172
325
|
|
|
173
326
|
// src/deploy-config.ts
|
|
@@ -177,11 +330,11 @@ var VercelProviderConfigSchema = z.object({
|
|
|
177
330
|
});
|
|
178
331
|
var SshConfigSchema = z.object({
|
|
179
332
|
/** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
|
|
180
|
-
host: z.string().
|
|
333
|
+
host: z.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
|
|
181
334
|
/** SSH 端口 */
|
|
182
335
|
port: z.number().int().min(1).max(65535).default(22),
|
|
183
336
|
/** SSH 用户名 */
|
|
184
|
-
username: z.string().
|
|
337
|
+
username: z.string().regex(/^[a-z_][\w-]*$/i),
|
|
185
338
|
/** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
|
|
186
339
|
identityFile: z.string().min(1).optional(),
|
|
187
340
|
/** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
|
|
@@ -206,7 +359,7 @@ var DockerSshProviderConfigSchema = z.object({
|
|
|
206
359
|
/** 远程主机运行参数 */
|
|
207
360
|
remote: z.object({
|
|
208
361
|
/** 远程应用根目录 */
|
|
209
|
-
baseDir: z.string().
|
|
362
|
+
baseDir: z.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
|
|
210
363
|
/** 远程容器运行时(auto 优先 podman) */
|
|
211
364
|
runtime: z.enum(["auto", "docker", "podman"]).default("auto")
|
|
212
365
|
}).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
|
|
@@ -305,10 +458,16 @@ function resolveConfig(value, prefix, env) {
|
|
|
305
458
|
}
|
|
306
459
|
function loadDeploymentConfig(name, path, values) {
|
|
307
460
|
try {
|
|
308
|
-
const
|
|
461
|
+
const lineCounter = new LineCounter();
|
|
462
|
+
const document = parseDocument(readFileSync(path, "utf8"), { lineCounter, prettyErrors: false });
|
|
463
|
+
if (document.errors.length)
|
|
464
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
|
|
465
|
+
const config = resolveConfig(document.toJS(), `HAI_${name.toUpperCase()}`, values);
|
|
466
|
+
if (config !== null && (typeof config !== "object" || Array.isArray(config)))
|
|
467
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
|
|
309
468
|
return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
|
|
310
469
|
} catch {
|
|
311
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("
|
|
470
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
|
|
312
471
|
}
|
|
313
472
|
}
|
|
314
473
|
function loadDeploymentEnvironment(options) {
|
|
@@ -324,7 +483,7 @@ function loadDeploymentEnvironment(options) {
|
|
|
324
483
|
return loaded;
|
|
325
484
|
try {
|
|
326
485
|
const values = { ...loaded.data };
|
|
327
|
-
for (const [name, value] of Object.entries(
|
|
486
|
+
for (const [name, value] of Object.entries(process3.env)) {
|
|
328
487
|
if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
|
|
329
488
|
values[name] = value;
|
|
330
489
|
}
|
|
@@ -512,7 +671,7 @@ function loadCredentials() {
|
|
|
512
671
|
const data = readCredentialsFile();
|
|
513
672
|
const keys = Object.keys(data);
|
|
514
673
|
for (const key of keys) {
|
|
515
|
-
|
|
674
|
+
process3.env[key] = String(data[key]);
|
|
516
675
|
}
|
|
517
676
|
logger.debug("Credentials loaded", { count: keys.length });
|
|
518
677
|
return ok(keys);
|
|
@@ -533,7 +692,7 @@ function saveCredential(key, value) {
|
|
|
533
692
|
const data = readCredentialsFile();
|
|
534
693
|
data[key] = value;
|
|
535
694
|
writeCredentialsFile(data);
|
|
536
|
-
|
|
695
|
+
process3.env[key] = value;
|
|
537
696
|
logger.debug("Credential saved", { key });
|
|
538
697
|
return ok(void 0);
|
|
539
698
|
} catch (error) {
|
|
@@ -553,7 +712,7 @@ function saveCredentials(entries) {
|
|
|
553
712
|
const data = readCredentialsFile();
|
|
554
713
|
for (const [key, value] of Object.entries(entries)) {
|
|
555
714
|
data[key] = value;
|
|
556
|
-
|
|
715
|
+
process3.env[key] = value;
|
|
557
716
|
}
|
|
558
717
|
writeCredentialsFile(data);
|
|
559
718
|
logger.debug("Credentials saved", { count: Object.keys(entries).length });
|
|
@@ -569,17 +728,20 @@ function saveCredentials(entries) {
|
|
|
569
728
|
);
|
|
570
729
|
}
|
|
571
730
|
}
|
|
572
|
-
function
|
|
731
|
+
function resolveProjectDirectory(appDir) {
|
|
573
732
|
let directory = resolve(appDir);
|
|
574
733
|
while (true) {
|
|
575
734
|
if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
|
|
576
|
-
return
|
|
735
|
+
return directory;
|
|
577
736
|
const parent = dirname(directory);
|
|
578
737
|
if (parent === directory)
|
|
579
|
-
return
|
|
738
|
+
return resolve(appDir);
|
|
580
739
|
directory = parent;
|
|
581
740
|
}
|
|
582
741
|
}
|
|
742
|
+
function resolveArtifactDirectory(appDir) {
|
|
743
|
+
return join(resolveProjectDirectory(appDir), ".hai/build/docker");
|
|
744
|
+
}
|
|
583
745
|
function usesDefaultMiddleware(scan, service, options) {
|
|
584
746
|
const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
|
|
585
747
|
return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
|
|
@@ -603,6 +765,19 @@ var RecoverySchema = z.object({
|
|
|
603
765
|
resourceStatus: z.enum(["created", "reused", "unknown"])
|
|
604
766
|
}))
|
|
605
767
|
});
|
|
768
|
+
var CommandStateSchema = z.object({
|
|
769
|
+
reason: z.enum(["timeout", "output", "input", "spawn", "exit", "configuration"]),
|
|
770
|
+
localTermination: z.enum(["confirmed", "unknown", "not-started"]),
|
|
771
|
+
remoteExecution: z.enum(["unknown", "finished"]).optional(),
|
|
772
|
+
deploymentId: z.string().optional(),
|
|
773
|
+
resultFile: z.string().optional(),
|
|
774
|
+
remoteExitCode: z.number().int().optional(),
|
|
775
|
+
remoteStage: z.string().optional()
|
|
776
|
+
});
|
|
777
|
+
function getDeployCommandState(error) {
|
|
778
|
+
const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
|
|
779
|
+
return parsed.success ? parsed.data : void 0;
|
|
780
|
+
}
|
|
606
781
|
function getDeployRecovery(error) {
|
|
607
782
|
const parsed = RecoverySchema.safeParse(error.ext?.recovery);
|
|
608
783
|
return parsed.success ? parsed.data : void 0;
|
|
@@ -719,7 +894,7 @@ async function scanApp(inputDir, options) {
|
|
|
719
894
|
for (const [index, item] of providers.entries()) {
|
|
720
895
|
const provider = record(item);
|
|
721
896
|
const type = String(provider.type ?? "");
|
|
722
|
-
const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
|
|
897
|
+
const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
|
|
723
898
|
if (service)
|
|
724
899
|
add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
|
|
725
900
|
}
|
|
@@ -745,147 +920,1045 @@ async function scanApp(inputDir, options) {
|
|
|
745
920
|
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
|
|
746
921
|
}
|
|
747
922
|
}
|
|
748
|
-
var
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
923
|
+
var nativeJob = String.raw`
|
|
924
|
+
using System;
|
|
925
|
+
using System.ComponentModel;
|
|
926
|
+
using System.Runtime.InteropServices;
|
|
927
|
+
using System.Text;
|
|
928
|
+
using System.Threading;
|
|
929
|
+
|
|
930
|
+
public static class HaiCommandJob {
|
|
931
|
+
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
|
|
932
|
+
public long ProcessTime, JobTime;
|
|
933
|
+
public uint Flags;
|
|
934
|
+
public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
|
|
935
|
+
public uint ActiveProcessLimit;
|
|
936
|
+
public UIntPtr Affinity;
|
|
937
|
+
public uint PriorityClass, SchedulingClass;
|
|
938
|
+
}
|
|
939
|
+
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
|
|
940
|
+
public ulong ReadOperations, WriteOperations, OtherOperations, ReadBytes, WriteBytes, OtherBytes;
|
|
941
|
+
}
|
|
942
|
+
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
|
|
943
|
+
public BasicLimits Basic;
|
|
944
|
+
public IoCounters Io;
|
|
945
|
+
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
|
|
946
|
+
}
|
|
947
|
+
[StructLayout(LayoutKind.Sequential)] struct Accounting {
|
|
948
|
+
public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
|
|
949
|
+
public uint Faults, Total, Active, Terminated;
|
|
950
|
+
}
|
|
951
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Startup {
|
|
952
|
+
public uint Size;
|
|
953
|
+
public string Reserved, Desktop, Title;
|
|
954
|
+
public uint X, Y, Width, Height, XChars, YChars, Fill, Flags;
|
|
955
|
+
public ushort Show, ReservedSize;
|
|
956
|
+
public IntPtr ReservedData, Input, Output, Error;
|
|
957
|
+
}
|
|
958
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
|
|
959
|
+
public IntPtr Process, Thread;
|
|
960
|
+
public uint ProcessId, ThreadId;
|
|
961
|
+
}
|
|
962
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr CreateJobObject(IntPtr attributes, string name);
|
|
963
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr OpenJobObject(uint access, bool inherit, string name);
|
|
964
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits information, uint size);
|
|
965
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting information, uint size, IntPtr length);
|
|
966
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
967
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CreateProcess(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref Startup startup, out ProcessInfo information);
|
|
968
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
|
|
969
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint code);
|
|
970
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
971
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
|
972
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
|
|
973
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
|
|
974
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int kind);
|
|
975
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
|
|
976
|
+
|
|
977
|
+
static void Check(bool success) { if (!success) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
978
|
+
static uint Active(IntPtr job) {
|
|
979
|
+
Accounting information;
|
|
980
|
+
Check(QueryInformationJobObject(job, 1, out information, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
|
|
981
|
+
return information.Active;
|
|
982
|
+
}
|
|
983
|
+
// Windows argv 编码:引号前与结尾的反斜杠加倍,其余字符原样传递,不经过命令 shell。
|
|
984
|
+
static string Quote(string value) {
|
|
985
|
+
StringBuilder result = new StringBuilder("\"");
|
|
986
|
+
int slashes = 0;
|
|
987
|
+
foreach (char current in value) {
|
|
988
|
+
if (current == '\\') { slashes++; continue; }
|
|
989
|
+
if (current == '"') result.Append('\\', slashes * 2 + 1);
|
|
990
|
+
else result.Append('\\', slashes);
|
|
991
|
+
result.Append(current);
|
|
992
|
+
slashes = 0;
|
|
993
|
+
}
|
|
994
|
+
result.Append('\\', slashes * 2).Append('"');
|
|
995
|
+
return result.ToString();
|
|
996
|
+
}
|
|
997
|
+
public static int Run(string name, string command, string[] arguments) {
|
|
998
|
+
IntPtr job = CreateJobObject(IntPtr.Zero, name);
|
|
999
|
+
if (job == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1000
|
+
ProcessInfo process = new ProcessInfo();
|
|
1001
|
+
bool assigned = false;
|
|
1002
|
+
try {
|
|
1003
|
+
ExtendedLimits limits = new ExtendedLimits();
|
|
1004
|
+
limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;不允许 breakaway。
|
|
1005
|
+
Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits))));
|
|
1006
|
+
StringBuilder line = new StringBuilder(Quote(command));
|
|
1007
|
+
foreach (string argument in arguments) line.Append(' ').Append(Quote(argument));
|
|
1008
|
+
Startup startup = new Startup();
|
|
1009
|
+
startup.Size = (uint)Marshal.SizeOf(typeof(Startup));
|
|
1010
|
+
startup.Flags = 0x100;
|
|
1011
|
+
startup.Input = GetStdHandle(-10); startup.Output = GetStdHandle(-11); startup.Error = GetStdHandle(-12);
|
|
1012
|
+
Check(SetHandleInformation(startup.Input, 1, 1));
|
|
1013
|
+
Check(SetHandleInformation(startup.Output, 1, 1));
|
|
1014
|
+
Check(SetHandleInformation(startup.Error, 1, 1));
|
|
1015
|
+
Check(CreateProcess(null, line, IntPtr.Zero, IntPtr.Zero, true, 0x08000004, IntPtr.Zero, null, ref startup, out process));
|
|
1016
|
+
Check(AssignProcessToJobObject(job, process.Process));
|
|
1017
|
+
assigned = true;
|
|
1018
|
+
if (ResumeThread(process.Thread) == 0xFFFFFFFF) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1019
|
+
if (WaitForSingleObject(process.Process, 0xFFFFFFFF) != 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1020
|
+
uint code;
|
|
1021
|
+
Check(GetExitCodeProcess(process.Process, out code));
|
|
1022
|
+
while (Active(job) != 0) Thread.Sleep(20);
|
|
1023
|
+
return unchecked((int)code);
|
|
1024
|
+
}
|
|
1025
|
+
finally {
|
|
1026
|
+
if (process.Process != IntPtr.Zero) {
|
|
1027
|
+
// 分配失败时进程仍挂起,直接终止,不允许裸进程恢复;Job 在其他异常时同样清空。
|
|
1028
|
+
if (!assigned) TerminateProcess(process.Process, 125);
|
|
1029
|
+
else TerminateJobObject(job, 125);
|
|
1030
|
+
WaitForSingleObject(process.Process, 5000);
|
|
1031
|
+
}
|
|
1032
|
+
if (process.Thread != IntPtr.Zero) CloseHandle(process.Thread);
|
|
1033
|
+
if (process.Process != IntPtr.Zero) CloseHandle(process.Process);
|
|
1034
|
+
CloseHandle(job);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
public static bool Empty(string name) {
|
|
1038
|
+
IntPtr job = OpenJobObject(4, false, name); // JOB_OBJECT_QUERY
|
|
1039
|
+
if (job == IntPtr.Zero) {
|
|
1040
|
+
if (Marshal.GetLastWin32Error() == 2) return true;
|
|
1041
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
1042
|
+
}
|
|
1043
|
+
try { return Active(job) == 0; }
|
|
1044
|
+
finally { CloseHandle(job); }
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
`;
|
|
1048
|
+
function powershell(script) {
|
|
1049
|
+
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
|
|
1050
|
+
}
|
|
1051
|
+
function windowsCommand(command, args) {
|
|
1052
|
+
const name = `Local\\hai-deploy-${randomUUID()}`;
|
|
1053
|
+
const key = `__HAI_DEPLOY_JOB_${randomUUID().replaceAll("-", "")}`;
|
|
1054
|
+
const script = `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1055
|
+
try {
|
|
1056
|
+
Add-Type -TypeDefinition @'
|
|
1057
|
+
${nativeJob}
|
|
1058
|
+
'@
|
|
1059
|
+
$inputConfig = [Environment]::GetEnvironmentVariable('${key}') | ConvertFrom-Json
|
|
1060
|
+
[Environment]::SetEnvironmentVariable('${key}', $null)
|
|
1061
|
+
exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
|
|
1062
|
+
} catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
|
|
1063
|
+
return { name, args: powershell(script), env: { [key]: JSON.stringify({ name, command, args }) } };
|
|
1064
|
+
}
|
|
1065
|
+
function windowsJobProbe(name) {
|
|
1066
|
+
return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
1067
|
+
try {
|
|
1068
|
+
Add-Type -TypeDefinition @'
|
|
1069
|
+
${nativeJob}
|
|
1070
|
+
'@
|
|
1071
|
+
if ([HaiCommandJob]::Empty('${name}')) { exit 0 } else { exit 1 }
|
|
1072
|
+
} catch { exit 2 }`);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/container/deploy-command-process.ts
|
|
1076
|
+
async function groupExited(pid) {
|
|
1077
|
+
try {
|
|
1078
|
+
process3.kill(-pid, 0);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
1081
|
+
}
|
|
1082
|
+
if (process3.platform !== "linux")
|
|
1083
|
+
return false;
|
|
1084
|
+
try {
|
|
1085
|
+
for (const entry of await readdir("/proc")) {
|
|
1086
|
+
if (!/^\d+$/.test(entry))
|
|
1087
|
+
continue;
|
|
1088
|
+
let stat;
|
|
1089
|
+
try {
|
|
1090
|
+
stat = await readFile(`/proc/${entry}/stat`, "utf8");
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
1093
|
+
continue;
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
1097
|
+
if (Number(fields[2]) === pid && fields[0] !== "Z" && fields[0] !== "X")
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
return true;
|
|
1101
|
+
} catch {
|
|
1102
|
+
return false;
|
|
756
1103
|
}
|
|
757
|
-
return masked;
|
|
758
1104
|
}
|
|
759
|
-
function
|
|
1105
|
+
async function helper(command, args, timeoutMs) {
|
|
1106
|
+
if (timeoutMs <= 0)
|
|
1107
|
+
return false;
|
|
760
1108
|
return new Promise((resolve6) => {
|
|
761
|
-
const child = spawn(command, args, {
|
|
762
|
-
cwd: options.cwd,
|
|
763
|
-
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
764
|
-
shell: false
|
|
765
|
-
});
|
|
766
|
-
let stdout = "";
|
|
767
|
-
let stderr = "";
|
|
1109
|
+
const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
|
|
768
1110
|
let settled = false;
|
|
769
1111
|
let timer;
|
|
770
|
-
const finish = (
|
|
1112
|
+
const finish = (success) => {
|
|
771
1113
|
if (settled)
|
|
772
1114
|
return;
|
|
773
1115
|
settled = true;
|
|
774
1116
|
if (timer)
|
|
775
1117
|
clearTimeout(timer);
|
|
776
|
-
resolve6(
|
|
1118
|
+
resolve6(success);
|
|
777
1119
|
};
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1120
|
+
timer = globalThis.setTimeout(() => {
|
|
1121
|
+
child.kill("SIGKILL");
|
|
1122
|
+
child.unref();
|
|
1123
|
+
finish(false);
|
|
1124
|
+
}, timeoutMs);
|
|
1125
|
+
child.once("error", () => finish(false));
|
|
1126
|
+
child.once("close", (code) => finish(code === 0));
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function spawnCommand(command, args, options) {
|
|
1130
|
+
const job = process3.platform === "win32" ? windowsCommand(command, args) : void 0;
|
|
1131
|
+
const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
|
|
1132
|
+
...options,
|
|
1133
|
+
env: job ? { ...options.env, ...job.env } : options.env,
|
|
1134
|
+
windowsHide: true,
|
|
1135
|
+
shell: false,
|
|
1136
|
+
detached: !job
|
|
1137
|
+
});
|
|
1138
|
+
let exited = false;
|
|
1139
|
+
child.once("exit", () => {
|
|
1140
|
+
exited = true;
|
|
1141
|
+
});
|
|
1142
|
+
child.once("close", () => {
|
|
1143
|
+
exited = true;
|
|
1144
|
+
});
|
|
1145
|
+
let termination;
|
|
1146
|
+
const treeExited = async () => {
|
|
1147
|
+
if (!child.pid)
|
|
1148
|
+
return true;
|
|
1149
|
+
if (job)
|
|
1150
|
+
return exited && helper("powershell.exe", windowsJobProbe(job.name), 5e3);
|
|
1151
|
+
return groupExited(child.pid);
|
|
1152
|
+
};
|
|
1153
|
+
const terminate = (timeoutMs) => {
|
|
1154
|
+
termination ??= (async () => {
|
|
1155
|
+
const deadline = performance.now() + timeoutMs;
|
|
1156
|
+
let windowsKillConfirmed = !job || exited;
|
|
1157
|
+
if (child.pid) {
|
|
1158
|
+
if (job) {
|
|
1159
|
+
if (!exited) {
|
|
1160
|
+
windowsKillConfirmed = await helper("taskkill", ["/pid", String(child.pid), "/t", "/f"], Math.max(1, deadline - performance.now()));
|
|
1161
|
+
child.kill("SIGKILL");
|
|
1162
|
+
}
|
|
1163
|
+
} else {
|
|
1164
|
+
try {
|
|
1165
|
+
process3.kill(-child.pid, "SIGKILL");
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
while (performance.now() < deadline) {
|
|
1171
|
+
if (exited)
|
|
1172
|
+
break;
|
|
1173
|
+
await setTimeout$1(20);
|
|
1174
|
+
}
|
|
1175
|
+
let verified = false;
|
|
1176
|
+
if (exited && job) {
|
|
1177
|
+
verified = await helper("powershell.exe", windowsJobProbe(job.name), deadline - performance.now());
|
|
1178
|
+
verified &&= windowsKillConfirmed || child.exitCode !== null;
|
|
1179
|
+
} else if (exited) {
|
|
1180
|
+
while (performance.now() < deadline) {
|
|
1181
|
+
if (await treeExited()) {
|
|
1182
|
+
verified = true;
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
await setTimeout$1(20);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
child.stdin.destroy();
|
|
1189
|
+
child.stdout.destroy();
|
|
1190
|
+
child.stderr.destroy();
|
|
1191
|
+
if (!verified)
|
|
1192
|
+
child.unref();
|
|
1193
|
+
return verified ? "confirmed" : "unknown";
|
|
1194
|
+
})();
|
|
1195
|
+
return termination;
|
|
1196
|
+
};
|
|
1197
|
+
return { child, treeExited, terminate };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/container/deploy-secret-masker.ts
|
|
1201
|
+
function createSecretMasker(secrets = []) {
|
|
1202
|
+
const values = [...new Set(secrets.filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
1203
|
+
if (values.reduce((total, value) => total + value.length, 0) > 1048576)
|
|
1204
|
+
throw new RangeError("Secret redaction input is too large");
|
|
1205
|
+
const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
|
|
1206
|
+
const tailLength = Math.max(0, (values[0]?.length ?? 1) - 1);
|
|
1207
|
+
let pending = "";
|
|
1208
|
+
let ended = false;
|
|
1209
|
+
function drain(final) {
|
|
1210
|
+
let end = final ? pending.length : Math.max(0, pending.length - tailLength);
|
|
1211
|
+
if (end > 0 && end < pending.length && /[\uD800-\uDBFF]/.test(pending[end - 1]) && /[\uDC00-\uDFFF]/.test(pending[end]))
|
|
1212
|
+
end--;
|
|
1213
|
+
if (end === 0)
|
|
1214
|
+
return "";
|
|
1215
|
+
let cursor = 0;
|
|
1216
|
+
let output = "";
|
|
1217
|
+
if (pattern) {
|
|
1218
|
+
pattern.lastIndex = 0;
|
|
1219
|
+
let match = pattern.exec(pending);
|
|
1220
|
+
while (match && match.index < end) {
|
|
1221
|
+
output += `${pending.slice(cursor, match.index)}***`;
|
|
1222
|
+
cursor = pattern.lastIndex;
|
|
1223
|
+
end = Math.max(end, cursor);
|
|
1224
|
+
match = pattern.exec(pending);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
output += pending.slice(cursor, end);
|
|
1228
|
+
pending = pending.slice(end);
|
|
1229
|
+
return output;
|
|
1230
|
+
}
|
|
1231
|
+
return {
|
|
1232
|
+
write(text) {
|
|
1233
|
+
if (ended)
|
|
1234
|
+
throw new Error("Secret masker has ended");
|
|
1235
|
+
pending += text;
|
|
1236
|
+
return drain(false);
|
|
1237
|
+
},
|
|
1238
|
+
end() {
|
|
1239
|
+
if (ended)
|
|
1240
|
+
return "";
|
|
1241
|
+
ended = true;
|
|
1242
|
+
return drain(true);
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// src/container/deploy-command.ts
|
|
1248
|
+
var logger3 = core.logger.child({ module: "deploy", scope: "command" });
|
|
1249
|
+
function maskSecrets(text, secrets = []) {
|
|
1250
|
+
const masker = createSecretMasker(secrets);
|
|
1251
|
+
return masker.write(text) + masker.end();
|
|
1252
|
+
}
|
|
1253
|
+
async function runCommand(command, args, options = {}) {
|
|
1254
|
+
const failure = (message, state) => err({
|
|
1255
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
1256
|
+
message: `${message}
|
|
1257
|
+
${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state.localTermination === "not-started" ? "deploy_localNotStarted" : "deploy_localUnknown")}`,
|
|
1258
|
+
ext: { commandExecution: state }
|
|
1259
|
+
});
|
|
1260
|
+
try {
|
|
1261
|
+
return await new Promise((resolve6) => {
|
|
1262
|
+
if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
|
|
1263
|
+
throw new TypeError("Invalid command arguments");
|
|
1264
|
+
if (options.input !== void 0 && typeof options.input !== "string")
|
|
1265
|
+
throw new TypeError("Invalid command input");
|
|
1266
|
+
const maxCapture = options.maxCaptureChars ?? 1048576;
|
|
1267
|
+
if (!Number.isSafeInteger(maxCapture) || maxCapture <= 0 || maxCapture > 16777216)
|
|
1268
|
+
throw new RangeError("Invalid command capture limit");
|
|
1269
|
+
const terminationTimeout = options.terminationTimeoutMs ?? 1e4;
|
|
1270
|
+
if (!Number.isSafeInteger(terminationTimeout) || terminationTimeout <= 0 || terminationTimeout > 6e4)
|
|
1271
|
+
throw new RangeError("Invalid command termination timeout");
|
|
1272
|
+
if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
|
|
1273
|
+
throw new RangeError("Invalid command timeout");
|
|
1274
|
+
const secrets = [...options.secrets ?? []];
|
|
1275
|
+
const safeCommand = maskSecrets(command, secrets);
|
|
1276
|
+
const outputMasker = createSecretMasker(secrets);
|
|
1277
|
+
const errorMasker = createSecretMasker(secrets);
|
|
1278
|
+
const control = spawnCommand(command, args, { cwd: options.cwd, env: options.env ? { ...process3.env, ...options.env } : process3.env });
|
|
1279
|
+
const child = control.child;
|
|
1280
|
+
child.stdout.setEncoding("utf8");
|
|
1281
|
+
child.stderr.setEncoding("utf8");
|
|
1282
|
+
let stdout = "";
|
|
1283
|
+
let stderr = "";
|
|
1284
|
+
let stdoutTruncated = false;
|
|
1285
|
+
let stderrTruncated = false;
|
|
1286
|
+
let settled = false;
|
|
1287
|
+
let interrupted = false;
|
|
1288
|
+
let interruption;
|
|
1289
|
+
let timer;
|
|
1290
|
+
const finish = (result) => {
|
|
1291
|
+
if (settled)
|
|
1292
|
+
return;
|
|
1293
|
+
settled = true;
|
|
1294
|
+
if (timer)
|
|
1295
|
+
clearTimeout(timer);
|
|
1296
|
+
resolve6(result);
|
|
1297
|
+
};
|
|
1298
|
+
const interrupt = (reason, message) => {
|
|
1299
|
+
interrupted = true;
|
|
1300
|
+
interruption ??= (async () => {
|
|
1301
|
+
if (timer)
|
|
1302
|
+
clearTimeout(timer);
|
|
1303
|
+
const localTermination = child.pid ? await control.terminate(terminationTimeout) : "not-started";
|
|
1304
|
+
logger3.error("Command termination checked", { command: safeCommand, reason, localTermination });
|
|
1305
|
+
finish(failure(message, { reason, localTermination }));
|
|
1306
|
+
})();
|
|
1307
|
+
return interruption;
|
|
1308
|
+
};
|
|
1309
|
+
const publish = async (text) => {
|
|
1310
|
+
if (!text || settled || interrupted)
|
|
1311
|
+
return;
|
|
1312
|
+
stdoutTruncated ||= stdout.length + text.length > maxCapture;
|
|
1313
|
+
stdout = (stdout + text).slice(-maxCapture);
|
|
1314
|
+
try {
|
|
1315
|
+
await options.onOutput?.(text);
|
|
1316
|
+
} catch {
|
|
1317
|
+
await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
const captureError = (text) => {
|
|
1321
|
+
stderrTruncated ||= stderr.length + text.length > maxCapture;
|
|
1322
|
+
stderr = (stderr + text).slice(-maxCapture);
|
|
1323
|
+
};
|
|
1324
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
1325
|
+
timer = setTimeout(() => {
|
|
1326
|
+
logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
|
|
1327
|
+
void interrupt("timeout", deployM("deploy_commandTimeout", { params: { command: safeCommand } }));
|
|
1328
|
+
}, options.timeoutMs);
|
|
809
1329
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1330
|
+
const outputTask = (async () => {
|
|
1331
|
+
try {
|
|
1332
|
+
for await (const chunk of child.stdout) {
|
|
1333
|
+
if (settled || interrupted)
|
|
1334
|
+
return;
|
|
1335
|
+
if (typeof chunk !== "string")
|
|
1336
|
+
throw new TypeError("Command output must be UTF-8 decoded");
|
|
1337
|
+
await publish(outputMasker.write(chunk));
|
|
1338
|
+
}
|
|
1339
|
+
if (!settled && !interrupted)
|
|
1340
|
+
await publish(outputMasker.end());
|
|
1341
|
+
} catch {
|
|
1342
|
+
await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
|
|
1343
|
+
}
|
|
1344
|
+
})();
|
|
1345
|
+
const errorTask = (async () => {
|
|
1346
|
+
try {
|
|
1347
|
+
for await (const chunk of child.stderr) {
|
|
1348
|
+
if (settled || interrupted)
|
|
1349
|
+
return;
|
|
1350
|
+
if (typeof chunk !== "string")
|
|
1351
|
+
throw new TypeError("Command error output must be UTF-8 decoded");
|
|
1352
|
+
captureError(errorMasker.write(chunk));
|
|
1353
|
+
}
|
|
1354
|
+
if (!settled && !interrupted)
|
|
1355
|
+
captureError(errorMasker.end());
|
|
1356
|
+
} catch {
|
|
1357
|
+
await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
|
|
1358
|
+
}
|
|
1359
|
+
})();
|
|
1360
|
+
child.stdin.on("error", () => {
|
|
1361
|
+
void interrupt("input", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandInputFailed") } }));
|
|
815
1362
|
});
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1363
|
+
child.on("error", (error) => {
|
|
1364
|
+
const safeError = maskSecrets(error.message, secrets);
|
|
1365
|
+
logger3.error("Command failed to start", { command: safeCommand, error: safeError });
|
|
1366
|
+
void interrupt("spawn", deployM("deploy_commandFailed", { params: { command: safeCommand, error: safeError } }));
|
|
1367
|
+
});
|
|
1368
|
+
child.on("close", async (code) => {
|
|
1369
|
+
await Promise.all([outputTask, errorTask]);
|
|
1370
|
+
if (settled || interrupted)
|
|
1371
|
+
return;
|
|
1372
|
+
if (timer)
|
|
1373
|
+
clearTimeout(timer);
|
|
1374
|
+
if (!await control.treeExited()) {
|
|
1375
|
+
await interrupt("exit", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandTreeActive") } }));
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const exitCode = code ?? -1;
|
|
1379
|
+
if (exitCode === 0) {
|
|
1380
|
+
finish(ok({ code: exitCode, stdout, stderr, stdoutTruncated, stderrTruncated }));
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
logger3.error("Command exited with non-zero code", { command: safeCommand, code: exitCode, stderr: maskSecrets(stderr, secrets).slice(-500), stdout: maskSecrets(stdout, secrets).slice(-3e3), stdoutTruncated, stderrTruncated });
|
|
1384
|
+
finish(failure(deployM("deploy_commandFailed", { params: { command: safeCommand, error: maskSecrets(`${stdout.slice(-2500)}
|
|
1385
|
+
${stderr || `exit ${exitCode}`}`, secrets).slice(-3e3) } }), { reason: "exit", localTermination: "confirmed" }));
|
|
1386
|
+
});
|
|
1387
|
+
if (options.input !== void 0)
|
|
1388
|
+
child.stdin.write(options.input);
|
|
828
1389
|
child.stdin.end();
|
|
1390
|
+
});
|
|
1391
|
+
} catch {
|
|
1392
|
+
return failure(deployM("deploy_commandFailed", { params: { command: "<command>", error: deployM("deploy_commandExecutionFailed") } }), { reason: "configuration", localTermination: "not-started" });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/container/deploy-target.ts
|
|
1397
|
+
function createTargetReader() {
|
|
1398
|
+
return String.raw`function fail(message) {
|
|
1399
|
+
print "Invalid deploy-target.json: " message > "/dev/stderr"
|
|
1400
|
+
exit 2
|
|
1401
|
+
}
|
|
1402
|
+
function whitespace() {
|
|
1403
|
+
while (substr(input, position, 1) ~ /^[ \t\r\n]$/) position++
|
|
1404
|
+
}
|
|
1405
|
+
function hexadecimal(text, result, offset, digit) {
|
|
1406
|
+
result = 0
|
|
1407
|
+
if (length(text) != 4) fail("invalid Unicode escape")
|
|
1408
|
+
for (offset = 1; offset <= 4; offset++) {
|
|
1409
|
+
digit = index("0123456789abcdef", tolower(substr(text, offset, 1))) - 1
|
|
1410
|
+
if (digit < 0) fail("invalid Unicode escape")
|
|
1411
|
+
result = result * 16 + digit
|
|
1412
|
+
}
|
|
1413
|
+
return result
|
|
1414
|
+
}
|
|
1415
|
+
function utf8(code) {
|
|
1416
|
+
if (code < 128) return sprintf("%c", code)
|
|
1417
|
+
if (code < 2048) return sprintf("%c%c", 192 + int(code / 64), 128 + code % 64)
|
|
1418
|
+
if (code < 65536) return sprintf("%c%c%c", 224 + int(code / 4096), 128 + int(code / 64) % 64, 128 + code % 64)
|
|
1419
|
+
return sprintf("%c%c%c%c", 240 + int(code / 262144), 128 + int(code / 4096) % 64, 128 + int(code / 64) % 64, 128 + code % 64)
|
|
1420
|
+
}
|
|
1421
|
+
function string_value( result, character, escaped, code, low) {
|
|
1422
|
+
if (substr(input, position++, 1) != "\"") fail("expected string")
|
|
1423
|
+
result = ""
|
|
1424
|
+
while (position <= length(input)) {
|
|
1425
|
+
character = substr(input, position++, 1)
|
|
1426
|
+
if (character == "\"") return result
|
|
1427
|
+
if (character ~ /[[:cntrl:]]/) fail("control character in string")
|
|
1428
|
+
if (character != "\\") { result = result character; continue }
|
|
1429
|
+
escaped = substr(input, position++, 1)
|
|
1430
|
+
if (escaped == "\"" || escaped == "\\" || escaped == "/") result = result escaped
|
|
1431
|
+
else if (escaped == "b") result = result sprintf("%c", 8)
|
|
1432
|
+
else if (escaped == "f") result = result sprintf("%c", 12)
|
|
1433
|
+
else if (escaped == "n") result = result "\n"
|
|
1434
|
+
else if (escaped == "r") result = result "\r"
|
|
1435
|
+
else if (escaped == "t") result = result "\t"
|
|
1436
|
+
else if (escaped == "u") {
|
|
1437
|
+
code = hexadecimal(substr(input, position, 4)); position += 4
|
|
1438
|
+
if (code >= 55296 && code <= 56319) {
|
|
1439
|
+
if (substr(input, position, 2) != "\\u") fail("missing low surrogate")
|
|
1440
|
+
position += 2
|
|
1441
|
+
low = hexadecimal(substr(input, position, 4)); position += 4
|
|
1442
|
+
if (low < 56320 || low > 57343) fail("invalid low surrogate")
|
|
1443
|
+
code = 65536 + (code - 55296) * 1024 + low - 56320
|
|
1444
|
+
} else if (code >= 56320 && code <= 57343) fail("unexpected low surrogate")
|
|
1445
|
+
if (code == 0) fail("NUL is not allowed")
|
|
1446
|
+
result = result utf8(code)
|
|
1447
|
+
} else fail("invalid escape")
|
|
1448
|
+
}
|
|
1449
|
+
fail("unterminated string")
|
|
1450
|
+
}
|
|
1451
|
+
function object_value(depth, key, character, value, kind) {
|
|
1452
|
+
if (depth > 16) fail("nesting is too deep")
|
|
1453
|
+
position++
|
|
1454
|
+
whitespace()
|
|
1455
|
+
if (substr(input, position, 1) == "}") { position++; return }
|
|
1456
|
+
while (position <= length(input)) {
|
|
1457
|
+
whitespace(); key = string_value(); whitespace()
|
|
1458
|
+
if (substr(input, position++, 1) != ":") fail("expected colon")
|
|
1459
|
+
whitespace(); character = substr(input, position, 1)
|
|
1460
|
+
if (character == "{") { object_value(depth + 1); value = ""; kind = "object" }
|
|
1461
|
+
else if (character == "\"") { value = string_value(); kind = "string" }
|
|
1462
|
+
else if (substr(input, position, 4) == "true") { value = "true"; kind = "boolean"; position += 4 }
|
|
1463
|
+
else if (substr(input, position, 5) == "false") { value = "false"; kind = "boolean"; position += 5 }
|
|
1464
|
+
else if (substr(input, position, 4) == "null") { value = ""; kind = "null"; position += 4 }
|
|
1465
|
+
else if (match(substr(input, position), /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/)) {
|
|
1466
|
+
value = substr(input, position, RLENGTH); position += RLENGTH; kind = "number"
|
|
1467
|
+
} else fail("unsupported value")
|
|
1468
|
+
if (depth == 1) {
|
|
1469
|
+
if (key in values) fail("duplicate field: " key)
|
|
1470
|
+
values[key] = value; kinds[key] = kind
|
|
829
1471
|
}
|
|
830
|
-
|
|
1472
|
+
whitespace(); character = substr(input, position++, 1)
|
|
1473
|
+
if (character == "}") return
|
|
1474
|
+
if (character != ",") fail("expected comma or closing brace")
|
|
1475
|
+
}
|
|
1476
|
+
fail("unterminated object")
|
|
1477
|
+
}
|
|
1478
|
+
{ input = input $0 "\n" }
|
|
1479
|
+
END {
|
|
1480
|
+
position = 1
|
|
1481
|
+
if (substr(input, 1, 3) == sprintf("%c%c%c", 239, 187, 191)) position = 4
|
|
1482
|
+
whitespace()
|
|
1483
|
+
if (substr(input, position, 1) != "{") fail("expected object")
|
|
1484
|
+
object_value(1); whitespace()
|
|
1485
|
+
if (position <= length(input)) fail("trailing content")
|
|
1486
|
+
if (!(field in values) || kinds[field] == "null") { print fallback; exit }
|
|
1487
|
+
if (kinds[field] != expected) fail("wrong type for " field)
|
|
1488
|
+
if (values[field] ~ /[[:cntrl:]]/) fail("control character in " field)
|
|
1489
|
+
print values[field]
|
|
1490
|
+
}
|
|
1491
|
+
`;
|
|
831
1492
|
}
|
|
832
1493
|
|
|
833
|
-
// src/container/deploy-
|
|
1494
|
+
// src/container/deploy-scripts.ts
|
|
834
1495
|
function shellArgument(value) {
|
|
835
1496
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
836
1497
|
}
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
for (const image of artifact.images)
|
|
851
|
-
copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
|
|
852
|
-
for (const file of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
|
|
853
|
-
copyFileSync(join(directory, file), join(staging, file));
|
|
854
|
-
const compose = parse(readFileSync(join(directory, composeName), "utf8"));
|
|
855
|
-
const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
|
|
856
|
-
const start = `#!/bin/sh
|
|
1498
|
+
function createApplyScript(options) {
|
|
1499
|
+
const values = {
|
|
1500
|
+
ID: shellArgument(options.deploymentId),
|
|
1501
|
+
PROJECT: shellArgument(`hai-${options.projectName}`),
|
|
1502
|
+
ENV: shellArgument(options.environment),
|
|
1503
|
+
RUNTIME: shellArgument(options.runtime),
|
|
1504
|
+
PARTIAL: options.partial ? "true" : "false",
|
|
1505
|
+
SELECTED: shellArgument(options.selected.join(" ")),
|
|
1506
|
+
ROOT: shellArgument(options.directory),
|
|
1507
|
+
PORT: String(options.hostPort),
|
|
1508
|
+
HEALTH: options.healthPaths.map(shellArgument).join(" ")
|
|
1509
|
+
};
|
|
1510
|
+
return String.raw`#!/bin/sh
|
|
857
1511
|
set -eu
|
|
858
|
-
cd "$(dirname "$0")"
|
|
859
1512
|
umask 077
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1513
|
+
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
1514
|
+
destination=@@ROOT@@
|
|
1515
|
+
if [ "$#" -gt 0 ]; then destination=$1; fi
|
|
1516
|
+
root=$(mkdir -p -- "$destination" && CDPATH= cd -- "$destination" && pwd)
|
|
1517
|
+
id=@@ID@@
|
|
1518
|
+
project=@@PROJECT@@
|
|
1519
|
+
export HAI_DEPLOY_ENVIRONMENT=@@ENV@@
|
|
1520
|
+
runtime=@@RUNTIME@@
|
|
1521
|
+
partial=@@PARTIAL@@
|
|
1522
|
+
orphan_option=''
|
|
1523
|
+
if [ "$partial" = false ]; then orphan_option=--remove-orphans; fi
|
|
1524
|
+
recreate_option=--force-recreate
|
|
1525
|
+
if [ "$partial" = true ]; then recreate_option=--no-recreate; fi
|
|
1526
|
+
selected=@@SELECTED@@
|
|
1527
|
+
release="$root/releases/$id"
|
|
1528
|
+
case "$release/" in "$source_dir/"*) printf 'Extract the package outside the deployment directory before applying it.\n' >&2; exit 1;; esac
|
|
1529
|
+
stage=''
|
|
1530
|
+
previous=''
|
|
1531
|
+
restore=''
|
|
1532
|
+
changed=false
|
|
1533
|
+
locked=false
|
|
1534
|
+
interrupted=false
|
|
1535
|
+
started=$(date +%s)
|
|
1536
|
+
step_started=$started
|
|
1537
|
+
step() {
|
|
1538
|
+
if [ -n "$stage" ]; then
|
|
1539
|
+
duration=$(( $(date +%s) - step_started ))
|
|
1540
|
+
printf '[PASS] %s (%ss)\n' "$stage" "$duration"
|
|
1541
|
+
printf '{"stage":"%s","status":"passed","durationSeconds":%s}\n' "$stage" "$duration" >> "$root/steps-$id.jsonl"
|
|
1542
|
+
fi
|
|
1543
|
+
stage="$1"
|
|
1544
|
+
step_started=$(date +%s)
|
|
1545
|
+
printf '\n[%s] %s\n' "$stage" "$2"
|
|
1546
|
+
}
|
|
1547
|
+
compose() {
|
|
1548
|
+
arguments_file="$release/compose-files"
|
|
1549
|
+
if [ -n "$restore" ]; then arguments_file="$root/releases/$restore/compose-files"; fi
|
|
1550
|
+
# -f 按顺序合并,由 Compose 保留未选服务、卷和环境文件,不自行拼装配置。
|
|
1551
|
+
command_args="$release/.compose-command"
|
|
1552
|
+
# 文件列表先反向读取,再向参数头部加入,保留原始合并顺序。
|
|
1553
|
+
awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}' "$arguments_file" > "$command_args"
|
|
1554
|
+
while IFS= read -r file; do set -- -f "$file" "$@"; done < "$command_args"
|
|
1555
|
+
"$runtime" compose -p "$project" "$@"
|
|
1556
|
+
}
|
|
1557
|
+
remove_selected() {
|
|
1558
|
+
test "$partial" = true || return 0
|
|
1559
|
+
# 直接替换选中容器,避免 Compose Provider 连带重建依赖它的未选服务;不删除卷。
|
|
1560
|
+
for service in "$@"; do
|
|
1561
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service") || return 1
|
|
1562
|
+
for container in $containers; do
|
|
1563
|
+
"$runtime" rm -f "$container" || return 1
|
|
1564
|
+
done
|
|
1565
|
+
done
|
|
1566
|
+
}
|
|
1567
|
+
finish() {
|
|
1568
|
+
code=$?
|
|
1569
|
+
trap - EXIT HUP INT TERM
|
|
1570
|
+
if [ "$code" -ne 0 ]; then
|
|
1571
|
+
printf '[FAIL] %s (exit %s)\n' "$stage" "$code" >&2
|
|
1572
|
+
if [ "$changed" = true ]; then
|
|
1573
|
+
diagnostics="$root/diagnostics-$id.log"
|
|
1574
|
+
: > "$diagnostics"
|
|
1575
|
+
for service in $selected; do
|
|
1576
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service" || true)
|
|
1577
|
+
for container in $containers; do
|
|
1578
|
+
"$runtime" inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}}' "$container" >> "$diagnostics" 2>&1 || true
|
|
1579
|
+
"$runtime" logs --tail 80 "$container" >> "$diagnostics" 2>&1 || true
|
|
1580
|
+
done
|
|
1581
|
+
done
|
|
1582
|
+
printf 'Startup diagnostics saved to %s (private server file).\n' "$diagnostics" >&2
|
|
1583
|
+
fi
|
|
1584
|
+
if [ -f "$release/rollback-images" ]; then
|
|
1585
|
+
while IFS=' ' read -r image reference; do
|
|
1586
|
+
test -n "$reference" || continue
|
|
1587
|
+
"$runtime" tag "$image" "$reference" || printf '[FAIL] Cannot restore image %s\n' "$reference" >&2
|
|
1588
|
+
done < "$release/rollback-images"
|
|
1589
|
+
fi
|
|
1590
|
+
if [ "$changed" = true ] && [ -n "$previous" ]; then
|
|
1591
|
+
printf '[RECOVER] Restoring previous selected services\n' >&2
|
|
1592
|
+
recover_selected=$selected
|
|
1593
|
+
if [ "$partial" = false ]; then recover_selected=$(grep -v -- '-init$' "$root/releases/$previous/services"); fi
|
|
1594
|
+
restore="$previous"
|
|
1595
|
+
if remove_selected $recover_selected && compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $recover_selected; then
|
|
1596
|
+
case " $selected " in *' app '*)
|
|
1597
|
+
if grep -qx web "$root/releases/$previous/services"; then compose exec -T web nginx -s reload || true; fi
|
|
1598
|
+
;;
|
|
1599
|
+
esac
|
|
1600
|
+
printf '[RECOVER] Previous containers restored; verify application health\n' >&2
|
|
1601
|
+
else
|
|
1602
|
+
printf '[FAIL] Recovery failed. Use the previous release and inspect container logs.\n' >&2
|
|
1603
|
+
fi
|
|
1604
|
+
elif [ "$changed" = true ]; then
|
|
1605
|
+
compose stop $selected || true
|
|
1606
|
+
printf '[RECOVER] Failed first installation stopped; data volumes retained\n' >&2
|
|
1607
|
+
fi
|
|
1608
|
+
fi
|
|
1609
|
+
if [ "$locked" = true ]; then
|
|
1610
|
+
elapsed=$(( $(date +%s) - started ))
|
|
1611
|
+
outcome=passed
|
|
1612
|
+
if [ "$code" -ne 0 ]; then outcome=failed; fi
|
|
1613
|
+
printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
|
|
1614
|
+
# 先完整写入再同目录原子发布,SSH 重连核对不能读取到半份终态结果。
|
|
1615
|
+
printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json.next"
|
|
1616
|
+
mv "$root/result-$id.json.next" "$root/result-$id.json"
|
|
1617
|
+
# 前台命令及同步恢复已结束;后台服务可继续运行,但不能使成功终态永久占锁。
|
|
1618
|
+
# 中断时仍可能有活跃子命令,必须由全部描述符持有者退出后释放。
|
|
1619
|
+
if [ "$interrupted" = false ]; then
|
|
1620
|
+
flock -u 9
|
|
1621
|
+
exec 9>&-
|
|
1622
|
+
fi
|
|
1623
|
+
fi
|
|
1624
|
+
exit "$code"
|
|
1625
|
+
}
|
|
1626
|
+
trap finish EXIT
|
|
1627
|
+
trap 'interrupted=true; exit 130' HUP INT TERM
|
|
1628
|
+
step preflight 'Checking deployment tools and package'
|
|
1629
|
+
for tool in sha256sum tar awk curl flock; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
|
|
864
1630
|
if [ "$runtime" = auto ]; then
|
|
865
1631
|
if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
|
|
866
1632
|
fi
|
|
867
|
-
case "$runtime" in docker|podman) ;; *)
|
|
1633
|
+
case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
|
|
868
1634
|
"$runtime" version >/dev/null
|
|
869
1635
|
"$runtime" compose version >/dev/null
|
|
1636
|
+
"$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; }
|
|
1637
|
+
# 内核锁绑定同一文件描述符;绝不删除/替换锁文件,避免新 inode 绕过活跃部署。
|
|
1638
|
+
# 子命令继承 FD 9:即使父 shell 被强杀,仍执行的命令继续持锁;全部退出或重启后自动释放。
|
|
1639
|
+
exec 9>"$root/.deploy.lock"
|
|
1640
|
+
if flock -n 9; then
|
|
1641
|
+
:
|
|
1642
|
+
else
|
|
1643
|
+
lock_code=$?
|
|
1644
|
+
if [ "$lock_code" -eq 1 ]; then
|
|
1645
|
+
printf 'Another deployment is running. Wait for its commands to finish; do not remove %s/.deploy.lock.\n' "$root" >&2
|
|
1646
|
+
else
|
|
1647
|
+
printf 'Cannot acquire deployment lock (exit %s). Check filesystem flock support and permissions.\n' "$lock_code" >&2
|
|
1648
|
+
fi
|
|
1649
|
+
exit "$lock_code"
|
|
1650
|
+
fi
|
|
1651
|
+
locked=true
|
|
1652
|
+
cd "$source_dir"
|
|
870
1653
|
sha256sum -c SHA256SUMS
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1654
|
+
if [ -f "$root/current" ]; then
|
|
1655
|
+
previous=$(cat "$root/current")
|
|
1656
|
+
case "$previous" in ''|*[!a-zA-Z0-9._-]*) printf 'Invalid deployment state\n' >&2; exit 1;; esac
|
|
1657
|
+
test -f "$root/releases/$previous/compose-files"
|
|
1658
|
+
fi
|
|
1659
|
+
if [ "$partial" = true ]; then
|
|
1660
|
+
test -n "$previous" || { printf 'Partial deployment requires a successful full installation. Deploy a complete bundle first.\n' >&2; exit 1; }
|
|
1661
|
+
cmp -s layout "$root/releases/$previous/layout" || { printf 'Ports, API routes or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
|
|
1662
|
+
fi
|
|
1663
|
+
test ! -e "$release" || { printf 'This deployment ID was already used. Create a new deployment bundle.\n' >&2; exit 1; }
|
|
1664
|
+
mkdir -p "$release"
|
|
1665
|
+
cp -R "$source_dir/." "$release/"
|
|
1666
|
+
# Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
|
|
1667
|
+
if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
|
|
1668
|
+
: > "$release/compose-files"
|
|
1669
|
+
if [ "$partial" = true ]; then
|
|
1670
|
+
cp "$root/releases/$previous/compose-files" "$release/compose-files"
|
|
1671
|
+
cp "$root/releases/$previous/middleware.env" "$release/middleware.env"
|
|
1672
|
+
for service in $selected; do
|
|
1673
|
+
grep -qx "$service" "$root/releases/$previous/services" || { printf 'Service %s has no installed baseline. Use a full deployment.\n' "$service" >&2; exit 1; }
|
|
1674
|
+
done
|
|
1675
|
+
fi
|
|
1676
|
+
printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
|
|
1677
|
+
compose config >/dev/null
|
|
1678
|
+
compose config --services > "$release/services"
|
|
1679
|
+
: > "$release/rollback-images"
|
|
1680
|
+
if [ -n "$previous" ]; then
|
|
1681
|
+
for service in $selected; do
|
|
1682
|
+
containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service")
|
|
1683
|
+
for container in $containers; do
|
|
1684
|
+
"$runtime" inspect --format '{{.Image}} {{.Config.Image}}' "$container" >> "$release/rollback-images"
|
|
1685
|
+
done
|
|
1686
|
+
done
|
|
1687
|
+
fi
|
|
1688
|
+
step images 'Loading packaged images (no registry access)'
|
|
1689
|
+
while IFS=' ' read -r archive reference; do
|
|
1690
|
+
test -n "$archive" || continue
|
|
1691
|
+
"$runtime" load -i "$release/images/$archive"
|
|
1692
|
+
"$runtime" image inspect "$reference" >/dev/null
|
|
1693
|
+
architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
|
|
1694
|
+
host_arch=$(uname -m)
|
|
1695
|
+
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
|
|
1696
|
+
done < "$release/image-list"
|
|
1697
|
+
step dependencies 'Checking dependencies of selected services'
|
|
1698
|
+
if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
|
|
1699
|
+
while IFS= read -r dependency; do
|
|
1700
|
+
test -n "$dependency" || continue
|
|
1701
|
+
case "$dependency" in *-init) continue;; esac
|
|
1702
|
+
case " $selected " in *" $dependency "*) continue;; esac
|
|
1703
|
+
running=$("$runtime" ps -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$dependency")
|
|
1704
|
+
test -n "$running" || { printf 'Dependency %s is not running. Include it in a full deployment.\n' "$dependency" >&2; exit 1; }
|
|
1705
|
+
for container in $running; do
|
|
1706
|
+
test "$("$runtime" inspect --format '{{.State.Running}}' "$container")" = true || { printf 'Dependency %s is stopped. Start it before retrying.\n' "$dependency" >&2; exit 1; }
|
|
1707
|
+
done
|
|
1708
|
+
done < "$release/dependencies"
|
|
1709
|
+
step start 'Updating selected services'
|
|
1710
|
+
changed=true
|
|
1711
|
+
remove_selected $selected
|
|
1712
|
+
if [ -f "$release/initialize-storage" ]; then
|
|
1713
|
+
remove_selected storage-init
|
|
1714
|
+
compose up -d --no-deps --no-build --pull never storage
|
|
1715
|
+
compose up --no-deps --no-build --pull never "$recreate_option" --exit-code-from storage-init storage-init
|
|
1716
|
+
fi
|
|
1717
|
+
compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $selected
|
|
1718
|
+
case " $selected " in
|
|
1719
|
+
*' app '*)
|
|
1720
|
+
case " $selected " in *' web '*) ;; *)
|
|
1721
|
+
if grep -qx web "$release/services"; then compose exec -T web nginx -s reload; fi
|
|
1722
|
+
esac
|
|
1723
|
+
;;
|
|
1724
|
+
esac
|
|
1725
|
+
step health 'Checking HTTP access'
|
|
1726
|
+
for path in @@HEALTH@@; do
|
|
1727
|
+
ready=false
|
|
1728
|
+
attempt=0
|
|
1729
|
+
while [ "$attempt" -lt 40 ]; do
|
|
1730
|
+
if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:@@PORT@@$path" >/dev/null 2>&1; then ready=true; break; fi
|
|
1731
|
+
attempt=$((attempt + 1))
|
|
1732
|
+
sleep 3
|
|
1733
|
+
done
|
|
1734
|
+
test "$ready" = true || { printf 'HTTP check failed: %s. Inspect compose logs for selected services.\n' "$path" >&2; exit 1; }
|
|
876
1735
|
done
|
|
877
|
-
|
|
1736
|
+
step complete 'Deployment passed all checks'
|
|
1737
|
+
printf '%s\n' "$id" > "$root/current.next"
|
|
1738
|
+
mv "$root/current.next" "$root/current"
|
|
1739
|
+
printf '[PASS] %s\n' "$id"
|
|
1740
|
+
`.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
|
|
1741
|
+
}
|
|
1742
|
+
function createUploadScripts() {
|
|
1743
|
+
const sh = String.raw`#!/bin/sh
|
|
1744
|
+
set -eu
|
|
1745
|
+
source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
1746
|
+
cd "$source_dir/.."
|
|
1747
|
+
command -v awk >/dev/null || { printf 'Install POSIX awk to read deploy-target.json.\n' >&2; exit 1; }
|
|
1748
|
+
read_target() {
|
|
1749
|
+
LC_ALL=C awk -v field="$1" -v expected="$2" -v fallback="$3" -f "$source_dir/read-target.awk" "$source_dir/deploy-target.json"
|
|
1750
|
+
}
|
|
1751
|
+
host=$(read_target host string SERVER_HOST)
|
|
1752
|
+
user=$(read_target username string deploy)
|
|
1753
|
+
port=$(read_target port number 22)
|
|
1754
|
+
remote=$(read_target directory string '')
|
|
1755
|
+
bundle=$(read_target bundle string '')
|
|
1756
|
+
identity=$(read_target identityFile string '')
|
|
1757
|
+
known_hosts=$(read_target knownHostsFile string '')
|
|
1758
|
+
strict=$(read_target strictHostKeyChecking boolean true)
|
|
1759
|
+
timeout=$(read_target connectTimeoutSeconds number 10)
|
|
1760
|
+
if [ "$#" -ge 1 ]; then host=$1; fi
|
|
1761
|
+
if [ "$#" -ge 2 ]; then port=$2; fi
|
|
1762
|
+
if [ "$#" -ge 3 ]; then user=$3; fi
|
|
1763
|
+
if value=$(printenv HAI_DEPLOY_IDENTITY_FILE) && [ -n "$value" ]; then identity=$value; fi
|
|
1764
|
+
if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE) && [ -n "$value" ]; then known_hosts=$value; fi
|
|
1765
|
+
invalid_target() { printf 'Invalid deployment target. Edit deploy-target.json before running this script.\n' >&2; exit 1; }
|
|
1766
|
+
test "$host" != SERVER_HOST || { printf 'Specify the server: sh deploy.sh SERVER [PORT] [USER].\n' >&2; exit 1; }
|
|
1767
|
+
case "$host" in ''|*[!a-zA-Z0-9.:-]*|[!a-zA-Z0-9]*) invalid_target;; esac
|
|
1768
|
+
case "$user" in ''|*[!a-zA-Z0-9_-]*|[!a-zA-Z_]*) invalid_target;; esac
|
|
1769
|
+
case "$port" in ''|*[!0-9]*|???????????*) invalid_target;; esac
|
|
1770
|
+
test "$port" -ge 1 && test "$port" -le 65535 || invalid_target
|
|
1771
|
+
case "$timeout" in ''|*[!0-9]*|???????????*) invalid_target;; esac
|
|
1772
|
+
test "$timeout" -ge 1 && test "$timeout" -le 2147483647 || invalid_target
|
|
1773
|
+
case "$remote" in /*) ;; *) invalid_target;; esac
|
|
1774
|
+
case "$remote" in *[!a-zA-Z0-9/_.\ -]*|*/../*|*/..|/..) invalid_target;; esac
|
|
1775
|
+
case "$bundle" in *.tar) ;; *) invalid_target;; esac
|
|
1776
|
+
case "$bundle" in ''|*[!a-zA-Z0-9_.+-]*) invalid_target;; esac
|
|
1777
|
+
case "$strict" in true) strict=accept-new;; false) strict=no;; *) invalid_target;; esac
|
|
1778
|
+
test -f "./$bundle" || { printf 'Bundle missing: %s\n' "$bundle" >&2; exit 1; }
|
|
1779
|
+
for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
|
|
1780
|
+
set -- -o BatchMode=yes -o "StrictHostKeyChecking=$strict" -o "ConnectTimeout=$timeout"
|
|
1781
|
+
if [ -n "$identity" ]; then set -- "$@" -i "$identity" -o IdentitiesOnly=yes; fi
|
|
1782
|
+
if [ -n "$known_hosts" ]; then
|
|
1783
|
+
known_hosts=$(printf '%s' "$known_hosts" | sed 's/[\\"]/\\&/g')
|
|
1784
|
+
set -- "$@" -o "UserKnownHostsFile=\"$known_hosts\""
|
|
1785
|
+
fi
|
|
1786
|
+
printf '[1/3] Connecting to %s\n' "$host"
|
|
1787
|
+
ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
|
|
1788
|
+
printf '[2/3] Uploading %s\n' "$bundle"
|
|
1789
|
+
scp_host=$host
|
|
1790
|
+
case "$host" in *:*) scp_host="[$host]";; esac
|
|
1791
|
+
scp "$@" -P "$port" "./$bundle" "$user@$scp_host:$remote/incoming/$bundle"
|
|
1792
|
+
printf '[3/3] Applying package and checking HTTP access\n'
|
|
1793
|
+
if ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"; then
|
|
1794
|
+
:
|
|
1795
|
+
else
|
|
1796
|
+
code=$?
|
|
1797
|
+
printf '[UNKNOWN] Remote execution may continue. Inspect %s/result-<deploymentId>.json (ID in manifest.json), current and containers before retrying.\n' "$remote" >&2
|
|
1798
|
+
exit "$code"
|
|
1799
|
+
fi
|
|
1800
|
+
`;
|
|
1801
|
+
const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
|
|
1802
|
+
$ErrorActionPreference = 'Stop'
|
|
1803
|
+
Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)
|
|
1804
|
+
$target = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'deploy-target.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
1805
|
+
if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
|
|
1806
|
+
if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
|
|
1807
|
+
$port = if ($null -eq $target.port) { 22 } else { $target.port }
|
|
1808
|
+
$timeout = if ($null -eq $target.connectTimeoutSeconds) { 10 } else { $target.connectTimeoutSeconds }
|
|
1809
|
+
if ($port -is [string] -or [string]$port -notmatch '^[0-9]+$' -or $port -lt 1 -or $port -gt 65535 -or $timeout -is [string] -or [string]$timeout -notmatch '^[0-9]+$' -or $timeout -lt 1 -or $timeout -gt 2147483647) { throw 'Invalid deployment port or timeout.' }
|
|
1810
|
+
if ($null -ne $target.strictHostKeyChecking -and $target.strictHostKeyChecking -isnot [bool]) { throw 'Invalid host key policy.' }
|
|
1811
|
+
if (($null -ne $target.identityFile -and $target.identityFile -isnot [string]) -or ($null -ne $target.knownHostsFile -and $target.knownHostsFile -isnot [string])) { throw 'Invalid SSH file path.' }
|
|
1812
|
+
$strict = if ($target.strictHostKeyChecking -eq $false) { 'no' } else { 'accept-new' }
|
|
1813
|
+
$options = @('-o', 'BatchMode=yes', '-o', "StrictHostKeyChecking=$strict", '-o', "ConnectTimeout=$timeout")
|
|
1814
|
+
if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
|
|
1815
|
+
if ($IdentityFile) { $options += @('-i', $IdentityFile, '-o', 'IdentitiesOnly=yes') }
|
|
1816
|
+
$knownHosts = if ($env:HAI_DEPLOY_KNOWN_HOSTS_FILE) { $env:HAI_DEPLOY_KNOWN_HOSTS_FILE } else { $target.knownHostsFile }
|
|
1817
|
+
if ($knownHosts) {
|
|
1818
|
+
$knownHosts = $knownHosts.Replace('\', '\\').Replace('"', '\"')
|
|
1819
|
+
$options += @('-o', ('UserKnownHostsFile="' + $knownHosts + '"'))
|
|
1820
|
+
}
|
|
1821
|
+
$destination = "$($target.username)@$($target.host)"
|
|
1822
|
+
$scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
|
|
1823
|
+
$remote = $target.directory
|
|
1824
|
+
$bundle = $target.bundle
|
|
1825
|
+
if (-not (Test-Path -LiteralPath "./$bundle" -PathType Leaf)) { throw "Bundle missing: $bundle" }
|
|
1826
|
+
function ConvertTo-NativeArgument([string]$Value) {
|
|
1827
|
+
if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value }
|
|
1828
|
+
$escaped = [regex]::Replace($Value, '(\\*)"', '$1$1\"')
|
|
1829
|
+
$escaped = [regex]::Replace($escaped, '(\\+)$', '$1$1')
|
|
1830
|
+
return '"' + $escaped + '"'
|
|
1831
|
+
}
|
|
1832
|
+
function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
|
|
1833
|
+
Write-Host "[$Name]"
|
|
1834
|
+
$nativeCommand = Get-Command $Command -ErrorAction Stop
|
|
1835
|
+
if ($PSVersionTable.PSVersion.Major -lt 6 -and $nativeCommand.CommandType -eq 'Application') {
|
|
1836
|
+
$start = New-Object System.Diagnostics.ProcessStartInfo
|
|
1837
|
+
$start.FileName = $nativeCommand.Source
|
|
1838
|
+
$start.WorkingDirectory = (Get-Location).ProviderPath
|
|
1839
|
+
$start.UseShellExecute = $false
|
|
1840
|
+
$start.CreateNoWindow = $true
|
|
1841
|
+
$start.Arguments = ($Arguments | ForEach-Object { ConvertTo-NativeArgument $_ }) -join ' '
|
|
1842
|
+
$child = [System.Diagnostics.Process]::Start($start)
|
|
1843
|
+
try { $child.WaitForExit(); $code = $child.ExitCode } finally { $child.Dispose() }
|
|
1844
|
+
} else {
|
|
1845
|
+
& $Command @Arguments
|
|
1846
|
+
$code = $LASTEXITCODE
|
|
1847
|
+
}
|
|
1848
|
+
$global:LASTEXITCODE = $code
|
|
1849
|
+
if ($code -ne 0) { throw "$Name failed (exit $code)." }
|
|
1850
|
+
}
|
|
1851
|
+
$applying = $false
|
|
1852
|
+
try {
|
|
1853
|
+
foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
|
|
1854
|
+
Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
|
|
1855
|
+
Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$port, "./$bundle", "$($scpDestination):$remote/incoming/$bundle"))
|
|
1856
|
+
$applying = $true
|
|
1857
|
+
Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
|
|
1858
|
+
} catch {
|
|
1859
|
+
if ($applying) { [Console]::Error.WriteLine("[UNKNOWN] Remote execution may continue. Inspect $remote/result-<deploymentId>.json (ID in manifest.json), current and containers before retrying.") }
|
|
1860
|
+
Write-Error $_
|
|
1861
|
+
exit 1
|
|
1862
|
+
}
|
|
878
1863
|
`;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1864
|
+
return { sh, ps1, reader: createTargetReader() };
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/container/deploy-bundle.ts
|
|
1868
|
+
async function createDeploymentBundle(options) {
|
|
1869
|
+
const { directory, artifactDir, artifact, projectName, environment, config, selected } = options;
|
|
1870
|
+
const stem = basename(options.output).replace(/\.tar$/i, "");
|
|
1871
|
+
const suffix = `-${artifact.appVersion}`;
|
|
1872
|
+
const output = resolve(dirname(options.output), `${stem.endsWith(suffix) ? stem : `${stem}${suffix}`}.tar`);
|
|
1873
|
+
if (!/^[\w.+-]+\.tar$/.test(basename(output)))
|
|
1874
|
+
throw new Error(deployM("deploy_bundleNameInvalid"));
|
|
1875
|
+
const staging = mkdtempSync(join(directory, ".bundle-"));
|
|
1876
|
+
const temporaryTar = `${output}.partial`;
|
|
1877
|
+
mkdirSync(dirname(output), { recursive: true, mode: 448 });
|
|
1878
|
+
chmodSync(staging, 448);
|
|
1879
|
+
const files = [];
|
|
1880
|
+
const put = (file, content) => {
|
|
1881
|
+
mkdirSync(dirname(join(staging, file)), { recursive: true });
|
|
1882
|
+
writeFileSync(join(staging, file), content, { mode: 384 });
|
|
1883
|
+
files.push(file);
|
|
1884
|
+
};
|
|
1885
|
+
try {
|
|
1886
|
+
mkdirSync(join(staging, "images"));
|
|
1887
|
+
const compose = parse(readFileSync(join(directory, `docker-compose-${artifact.appVersion}.yml`), "utf8"));
|
|
1888
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
1889
|
+
const references = /* @__PURE__ */ new Set();
|
|
1890
|
+
for (const name of selected) {
|
|
1891
|
+
const service = compose.services[name];
|
|
1892
|
+
if (!service)
|
|
1893
|
+
throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
|
|
1894
|
+
if (typeof service.image === "string")
|
|
1895
|
+
references.add(service.image);
|
|
1896
|
+
if (service.depends_on) {
|
|
1897
|
+
for (const dependency of Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on))
|
|
1898
|
+
dependencies.add(dependency);
|
|
1899
|
+
}
|
|
1900
|
+
if (service.env_file)
|
|
1901
|
+
service.env_file = [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`];
|
|
1902
|
+
if (Array.isArray(service.volumes))
|
|
1903
|
+
service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`) : mount);
|
|
1904
|
+
}
|
|
1905
|
+
put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
|
|
1906
|
+
put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
|
|
1907
|
+
let runtime;
|
|
1908
|
+
const imageList = [];
|
|
1909
|
+
for (const reference of references) {
|
|
1910
|
+
if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
|
|
1911
|
+
throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
|
|
1912
|
+
const built = artifact.images.find((image) => image.imageRef === reference);
|
|
1913
|
+
const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
|
|
1914
|
+
if (built) {
|
|
1915
|
+
copyFileSync(join(artifactDir, file), join(staging, "images", file));
|
|
1916
|
+
} else {
|
|
1917
|
+
if (!runtime) {
|
|
1918
|
+
const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
|
|
1919
|
+
for (const candidate of candidates) {
|
|
1920
|
+
if ((await runCommand(candidate, ["version"])).success) {
|
|
1921
|
+
runtime = candidate;
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (!runtime)
|
|
1926
|
+
throw new Error(deployM("deploy_bundleRuntimeRequired"));
|
|
1927
|
+
}
|
|
1928
|
+
if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
|
|
1929
|
+
const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
|
|
1930
|
+
if (!pulled.success)
|
|
1931
|
+
throw new Error(pulled.error.message);
|
|
1932
|
+
}
|
|
1933
|
+
const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file), reference], { timeoutMs: 6e5 });
|
|
1934
|
+
if (!saved.success)
|
|
1935
|
+
throw new Error(saved.error.message);
|
|
1936
|
+
}
|
|
1937
|
+
files.push(`images/${file}`);
|
|
1938
|
+
imageList.push(`${file} ${reference}`);
|
|
1939
|
+
}
|
|
1940
|
+
put(".env.runtime", readFileSync(join(directory, ".env.runtime"), "utf8"));
|
|
1941
|
+
put("middleware.env", options.middlewareEnv);
|
|
1942
|
+
if (selected.includes("web"))
|
|
1943
|
+
put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
|
|
1944
|
+
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)}
|
|
1945
|
+
`);
|
|
1946
|
+
put("layout", `${config.expose.hostPort}:${options.containerPort}:${options.hasWeb}:${options.hasWeb ? await artifactChecksum(join(directory, "web.conf")) : ""}
|
|
1947
|
+
`);
|
|
1948
|
+
put("image-list", `${imageList.join("\n")}
|
|
1949
|
+
`);
|
|
1950
|
+
put("dependencies", `${[...dependencies].join("\n")}
|
|
1951
|
+
`);
|
|
1952
|
+
if (selected.includes("storage-init"))
|
|
1953
|
+
put("initialize-storage", "");
|
|
1954
|
+
put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: `${config.remote.baseDir}/${projectName}`, runtime: config.remote.runtime, partial: options.partial, selected: selected.filter((name) => name !== "storage-init"), hostPort: config.expose.hostPort, healthPaths: options.healthPaths }));
|
|
1955
|
+
const instructionParams = { file: basename(output), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" };
|
|
1956
|
+
const instructions = deployM("deploy_bundleInstructions", { params: instructionParams });
|
|
1957
|
+
put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
|
|
884
1958
|
${instructions}
|
|
885
1959
|
`);
|
|
886
|
-
copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
|
|
887
1960
|
const checksums = [];
|
|
888
|
-
for (const file of
|
|
1961
|
+
for (const file of files)
|
|
889
1962
|
checksums.push(`${await artifactChecksum(join(staging, file))} ${file}`);
|
|
890
1963
|
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
891
1964
|
`);
|
|
@@ -893,25 +1966,56 @@ ${instructions}
|
|
|
893
1966
|
if (!tar.success)
|
|
894
1967
|
throw new Error(tar.error.message);
|
|
895
1968
|
chmodSync(temporaryTar, 384);
|
|
896
|
-
const { renameSync } = await import('fs');
|
|
897
1969
|
renameSync(temporaryTar, output);
|
|
898
|
-
|
|
1970
|
+
const upload = createUploadScripts();
|
|
1971
|
+
const scripts = `${output.slice(0, -4)}-deploy`;
|
|
1972
|
+
mkdirSync(scripts, { recursive: true, mode: 448 });
|
|
1973
|
+
writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
|
|
1974
|
+
writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
|
|
1975
|
+
writeFileSync(join(scripts, "read-target.awk"), upload.reader);
|
|
1976
|
+
const target = {
|
|
1977
|
+
host: config.ssh?.host ?? "SERVER_HOST",
|
|
1978
|
+
port: config.ssh?.port ?? 22,
|
|
1979
|
+
username: config.ssh?.username ?? "deploy",
|
|
1980
|
+
identityFile: config.ssh?.identityFile ?? "",
|
|
1981
|
+
knownHostsFile: config.ssh?.knownHostsFile ?? "",
|
|
1982
|
+
strictHostKeyChecking: config.ssh?.strictHostKeyChecking ?? true,
|
|
1983
|
+
connectTimeoutSeconds: config.ssh?.connectTimeoutSeconds ?? 10,
|
|
1984
|
+
directory: `${config.remote.baseDir}/${projectName}`,
|
|
1985
|
+
bundle: basename(output)
|
|
1986
|
+
};
|
|
1987
|
+
const comments = {
|
|
1988
|
+
host: deployM("deploy_targetHost"),
|
|
1989
|
+
port: deployM("deploy_targetPort"),
|
|
1990
|
+
username: deployM("deploy_targetUsername"),
|
|
1991
|
+
identityFile: deployM("deploy_targetIdentityFile"),
|
|
1992
|
+
knownHostsFile: deployM("deploy_targetKnownHostsFile"),
|
|
1993
|
+
strictHostKeyChecking: deployM("deploy_targetStrictHostKeyChecking"),
|
|
1994
|
+
connectTimeoutSeconds: deployM("deploy_targetConnectTimeoutSeconds"),
|
|
1995
|
+
directory: deployM("deploy_targetDirectory"),
|
|
1996
|
+
bundle: deployM("deploy_targetBundle")
|
|
1997
|
+
};
|
|
1998
|
+
writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
|
|
1999
|
+
`);
|
|
2000
|
+
writeFileSync(join(scripts, "README.txt"), instructions);
|
|
2001
|
+
return { bundlePath: output, instructions: `${deployM("deploy_bundleInstructions", { params: { ...instructionParams, file: output } })}
|
|
2002
|
+
${scripts}`, startName: "apply.sh" };
|
|
899
2003
|
} finally {
|
|
900
2004
|
rmSync(staging, { recursive: true, force: true });
|
|
901
2005
|
rmSync(temporaryTar, { force: true });
|
|
902
2006
|
}
|
|
903
2007
|
}
|
|
904
2008
|
var IMAGES = {
|
|
905
|
-
postgres: "postgres:16-alpine",
|
|
906
|
-
mysql: "mysql:8.4",
|
|
907
|
-
redis: "redis:7-alpine",
|
|
2009
|
+
postgres: "docker.io/library/postgres:16.15-alpine",
|
|
2010
|
+
mysql: "docker.io/library/mysql:8.4.11",
|
|
2011
|
+
redis: "docker.io/library/redis:7.4.11-alpine",
|
|
908
2012
|
rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
|
|
909
2013
|
minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
|
|
910
|
-
qdrant: "qdrant/qdrant:v1.
|
|
911
|
-
pgvector: "pgvector/pgvector:pg17",
|
|
912
|
-
chroma: "chromadb/chroma:1.5.5",
|
|
2014
|
+
qdrant: "docker.io/qdrant/qdrant:v1.19.2",
|
|
2015
|
+
pgvector: "docker.io/pgvector/pgvector:0.8.0-pg17",
|
|
2016
|
+
chroma: "docker.io/chromadb/chroma:1.5.5",
|
|
913
2017
|
ldap: "docker.io/osixia/openldap:1.5.0",
|
|
914
|
-
smtp: "axllent/mailpit:v1.27.4"
|
|
2018
|
+
smtp: "docker.io/axllent/mailpit:v1.27.4"
|
|
915
2019
|
};
|
|
916
2020
|
function generatePassword() {
|
|
917
2021
|
return randomBytes(24).toString("base64url");
|
|
@@ -920,8 +2024,12 @@ function resolveSidecarSecrets(appDir, projectName) {
|
|
|
920
2024
|
const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
|
|
921
2025
|
if (existsSync(secretFile)) {
|
|
922
2026
|
try {
|
|
923
|
-
|
|
2027
|
+
const saved = JSON.parse(readFileSync(secretFile, "utf-8"));
|
|
2028
|
+
if (!saved || !["dbPassword", "storagePassword", "ldapPassword"].every((key) => typeof saved[key] === "string" && saved[key].length > 0))
|
|
2029
|
+
throw new Error("Invalid credentials");
|
|
2030
|
+
return saved;
|
|
924
2031
|
} catch {
|
|
2032
|
+
throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
|
|
925
2033
|
}
|
|
926
2034
|
}
|
|
927
2035
|
const secrets = {
|
|
@@ -1089,6 +2197,28 @@ function planChromaVecdb(plan, projectName, net) {
|
|
|
1089
2197
|
plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
|
|
1090
2198
|
}
|
|
1091
2199
|
function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
|
|
2200
|
+
const serviceModes = { ...options?.serviceModes };
|
|
2201
|
+
for (const requirement of scan.middlewares ?? []) {
|
|
2202
|
+
if (options?.selectedServices && !options.selectedServices.includes(requirement.service))
|
|
2203
|
+
continue;
|
|
2204
|
+
const mode = serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service];
|
|
2205
|
+
if (mode !== "keep")
|
|
2206
|
+
continue;
|
|
2207
|
+
const endpoint = requirement.config.url ?? requirement.config.endpoint;
|
|
2208
|
+
let host = requirement.config.host;
|
|
2209
|
+
if (typeof endpoint === "string") {
|
|
2210
|
+
try {
|
|
2211
|
+
host = new URL(endpoint).hostname;
|
|
2212
|
+
} catch {
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (host === requirement.service || ["sqlite", "local", "memory", "lancedb"].includes(requirement.backend)) {
|
|
2217
|
+
serviceModes[requirement.envPrefix] = "default";
|
|
2218
|
+
serviceModes[requirement.service] = "default";
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
options = { ...options, serviceModes };
|
|
1092
2222
|
const plan = {
|
|
1093
2223
|
services: [],
|
|
1094
2224
|
composeServices: {},
|
|
@@ -1215,23 +2345,6 @@ function generateComposeFile(options) {
|
|
|
1215
2345
|
};
|
|
1216
2346
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
1217
2347
|
compose.volumes = sidecar.volumes;
|
|
1218
|
-
if (options.previousCompose && options.serviceModes) {
|
|
1219
|
-
const previous = parse(options.previousCompose);
|
|
1220
|
-
for (const [service, mode] of Object.entries(options.serviceModes)) {
|
|
1221
|
-
if (mode !== "keep")
|
|
1222
|
-
continue;
|
|
1223
|
-
for (const name of [service, `${service}-init`]) {
|
|
1224
|
-
if (previous.services?.[name]) {
|
|
1225
|
-
services[name] = previous.services[name];
|
|
1226
|
-
app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
const previousMounts = previous.services?.app?.volumes;
|
|
1231
|
-
if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
|
|
1232
|
-
app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
|
|
1233
|
-
compose.volumes = { ...previous.volumes, ...sidecar.volumes };
|
|
1234
|
-
}
|
|
1235
2348
|
if (services["storage-init"]) {
|
|
1236
2349
|
app.depends_on = {
|
|
1237
2350
|
...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
|
|
@@ -1249,6 +2362,7 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
|
|
|
1249
2362
|
listen ${webPort};
|
|
1250
2363
|
server_name _;
|
|
1251
2364
|
root /usr/share/nginx/html;
|
|
2365
|
+
large_client_header_buffers 4 32k;
|
|
1252
2366
|
location / {
|
|
1253
2367
|
try_files $uri $uri/ /index.html;
|
|
1254
2368
|
}
|
|
@@ -1266,11 +2380,15 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
|
|
|
1266
2380
|
proxy_set_header Host $http_host;
|
|
1267
2381
|
}
|
|
1268
2382
|
location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
2383
|
+
client_max_body_size 2129920;
|
|
1269
2384
|
proxy_pass http://app:${port};
|
|
1270
2385
|
proxy_http_version 1.1;
|
|
1271
2386
|
proxy_set_header Host $http_host;
|
|
1272
2387
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
2388
|
+
proxy_request_buffering off;
|
|
1273
2389
|
proxy_buffering off;
|
|
2390
|
+
proxy_buffer_size 32k;
|
|
2391
|
+
proxy_buffers 8 32k;
|
|
1274
2392
|
}
|
|
1275
2393
|
}
|
|
1276
2394
|
`;
|
|
@@ -1295,8 +2413,10 @@ function buildConnectOptions(config) {
|
|
|
1295
2413
|
const args = ["-o", "BatchMode=yes"];
|
|
1296
2414
|
args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
|
|
1297
2415
|
args.push("-o", `StrictHostKeyChecking=${config.strictHostKeyChecking ? "accept-new" : "no"}`);
|
|
1298
|
-
if (config.knownHostsFile)
|
|
1299
|
-
|
|
2416
|
+
if (config.knownHostsFile) {
|
|
2417
|
+
const path = config.knownHostsFile.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
2418
|
+
args.push("-o", `UserKnownHostsFile="${path}"`);
|
|
2419
|
+
}
|
|
1300
2420
|
if (config.identityFile) {
|
|
1301
2421
|
args.push("-i", config.identityFile);
|
|
1302
2422
|
args.push("-o", "IdentitiesOnly=yes");
|
|
@@ -1318,25 +2438,28 @@ function buildScpUploadArgs(config, localPath, remotePath) {
|
|
|
1318
2438
|
"-P",
|
|
1319
2439
|
String(config.port),
|
|
1320
2440
|
localPath,
|
|
1321
|
-
`${config.username}@${config.host}:${remotePath}`
|
|
2441
|
+
`${config.username}@${config.host.includes(":") ? `[${config.host}]` : config.host}:${remotePath}`
|
|
1322
2442
|
];
|
|
1323
2443
|
}
|
|
1324
2444
|
function createSshExecutor(config) {
|
|
1325
2445
|
const exec = async (remoteCommand, options) => {
|
|
1326
|
-
|
|
2446
|
+
const result = await runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
|
|
1327
2447
|
timeoutMs: options?.timeoutMs,
|
|
1328
|
-
|
|
2448
|
+
terminationTimeoutMs: options?.terminationTimeoutMs,
|
|
2449
|
+
maxCaptureChars: options?.maxCaptureChars,
|
|
2450
|
+
secrets: options?.secrets,
|
|
2451
|
+
onOutput: options?.onOutput
|
|
1329
2452
|
});
|
|
2453
|
+
if (result.success)
|
|
2454
|
+
return result;
|
|
2455
|
+
const state = getDeployCommandState(result.error);
|
|
2456
|
+
return err({ ...result.error, suggestion: deployM("deploy_remoteUnknown"), ext: { ...result.error.ext, commandExecution: { ...state, reason: state?.reason ?? "exit", localTermination: state?.localTermination ?? "unknown", remoteExecution: "unknown" } } });
|
|
1330
2457
|
};
|
|
1331
2458
|
const upload = async (localPath, remotePath) => {
|
|
1332
2459
|
const args = buildScpUploadArgs(config, localPath, remotePath);
|
|
1333
|
-
const result = await runCommand("scp", args, {});
|
|
2460
|
+
const result = await runCommand("scp", args, { timeoutMs: 6e5 });
|
|
1334
2461
|
if (!result.success) {
|
|
1335
|
-
return err(
|
|
1336
|
-
HaiDeployError.IMAGE_TRANSFER_FAILED,
|
|
1337
|
-
deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
|
|
1338
|
-
result.error
|
|
1339
|
-
);
|
|
2462
|
+
return err({ ...HaiDeployError.IMAGE_TRANSFER_FAILED, message: deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }), ext: result.error.ext });
|
|
1340
2463
|
}
|
|
1341
2464
|
return ok(void 0);
|
|
1342
2465
|
};
|
|
@@ -1344,11 +2467,7 @@ function createSshExecutor(config) {
|
|
|
1344
2467
|
logger4.debug("SSH preflight", { host: config.host, port: config.port });
|
|
1345
2468
|
const result = await exec("true", { timeoutMs: 2e4 });
|
|
1346
2469
|
if (!result.success) {
|
|
1347
|
-
return err(
|
|
1348
|
-
HaiDeployError.REMOTE_CONNECT_FAILED,
|
|
1349
|
-
deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }),
|
|
1350
|
-
result.error
|
|
1351
|
-
);
|
|
2470
|
+
return err({ ...HaiDeployError.REMOTE_CONNECT_FAILED, message: deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }), ext: result.error.ext });
|
|
1352
2471
|
}
|
|
1353
2472
|
return ok(void 0);
|
|
1354
2473
|
};
|
|
@@ -1359,6 +2478,12 @@ function createSshExecutor(config) {
|
|
|
1359
2478
|
var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
|
|
1360
2479
|
var HEALTH_POLL_INTERVAL = 3e3;
|
|
1361
2480
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
2481
|
+
var ServerResultSchema = z.object({
|
|
2482
|
+
deploymentId: z.string(),
|
|
2483
|
+
stage: z.enum(["preflight", "images", "dependencies", "start", "health", "complete"]),
|
|
2484
|
+
exitCode: z.number().int().min(0).max(255),
|
|
2485
|
+
durationSeconds: z.number().int().nonnegative()
|
|
2486
|
+
});
|
|
1362
2487
|
function makeDeploymentId() {
|
|
1363
2488
|
const now = /* @__PURE__ */ new Date();
|
|
1364
2489
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -1406,7 +2531,7 @@ function createDockerSshProvider(config, container) {
|
|
|
1406
2531
|
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
1407
2532
|
const deploymentId = makeDeploymentId();
|
|
1408
2533
|
const hostPort = config.expose.hostPort;
|
|
1409
|
-
const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
|
|
2534
|
+
const artifactDir = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
|
|
1410
2535
|
mkdirSync(artifactDir, { recursive: true, mode: 448 });
|
|
1411
2536
|
chmodSync(artifactDir, 448);
|
|
1412
2537
|
const artifact = context.artifact;
|
|
@@ -1414,11 +2539,23 @@ function createDockerSshProvider(config, container) {
|
|
|
1414
2539
|
const images = artifact.images;
|
|
1415
2540
|
const web = images.find((image) => image.name === "web");
|
|
1416
2541
|
const app = images.find((image) => image.name === "app");
|
|
1417
|
-
|
|
1418
|
-
|
|
2542
|
+
if (options?.selectedServices) {
|
|
2543
|
+
const available = [...images.map((image) => image.name), ...scan.requiredServices];
|
|
2544
|
+
const invalid = options.selectedServices.find((name) => !available.includes(name));
|
|
2545
|
+
if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
|
|
2546
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
|
|
2547
|
+
}
|
|
2548
|
+
const hasWeb = artifact.hasWeb || !!web;
|
|
2549
|
+
const partial = artifact.partial || options?.selectedServices !== void 0;
|
|
2550
|
+
const containerPort = app?.containerPort ?? artifact.appContainerPort ?? config.expose.containerPort;
|
|
2551
|
+
const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
|
|
2552
|
+
const host = config.ssh?.host;
|
|
2553
|
+
const url = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
1419
2554
|
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
2555
|
+
if (sidecar.composeServices["storage-init"] && !app && options?.selectedServices?.includes("storage"))
|
|
2556
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageArtifactRequired"));
|
|
1420
2557
|
let webProxy;
|
|
1421
|
-
if (
|
|
2558
|
+
if (hasWeb) {
|
|
1422
2559
|
const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
|
|
1423
2560
|
if (!server.success)
|
|
1424
2561
|
return server;
|
|
@@ -1427,12 +2564,10 @@ function createDockerSshProvider(config, container) {
|
|
|
1427
2564
|
const apiPrefix = http?.apiPrefix ?? "/api/v1";
|
|
1428
2565
|
if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
|
|
1429
2566
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
1430
|
-
webProxy = generateWebProxyConfig(containerPort, apiPrefix,
|
|
2567
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
|
|
1431
2568
|
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config.ssh ? url : cors?.origin ?? "*" } });
|
|
1432
2569
|
}
|
|
1433
2570
|
const envPath = join(artifactDir, ".env.runtime");
|
|
1434
|
-
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
1435
|
-
Object.assign(env, { ...previousEnv, ...env });
|
|
1436
2571
|
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
1437
2572
|
if (options?.environment) {
|
|
1438
2573
|
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
@@ -1443,36 +2578,46 @@ function createDockerSshProvider(config, container) {
|
|
|
1443
2578
|
return saved;
|
|
1444
2579
|
}
|
|
1445
2580
|
const composeName = `docker-compose-${artifact.appVersion}.yml`;
|
|
1446
|
-
const latestPath = join(artifactDir, "latest");
|
|
1447
|
-
const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
|
|
1448
|
-
const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
|
|
1449
2581
|
const composeContent = generateComposeFile({
|
|
1450
2582
|
projectName,
|
|
1451
2583
|
appVersion: artifact.appVersion,
|
|
1452
|
-
imageRef: app
|
|
2584
|
+
imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
|
|
1453
2585
|
hostPort,
|
|
1454
2586
|
containerPort,
|
|
1455
2587
|
sidecar,
|
|
1456
|
-
web:
|
|
1457
|
-
serviceModes: options?.serviceModes,
|
|
1458
|
-
previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
|
|
2588
|
+
web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
|
|
1459
2589
|
});
|
|
1460
2590
|
const composePath = join(artifactDir, composeName);
|
|
1461
2591
|
writeFileSync(composePath, composeContent);
|
|
1462
2592
|
if (webProxy)
|
|
1463
2593
|
writeFileSync(join(artifactDir, "web.conf"), webProxy);
|
|
2594
|
+
const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
|
|
2595
|
+
if (!selected.length || selected.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) || new Set(selected).size !== selected.length)
|
|
2596
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
|
|
2597
|
+
for (const name of selected) {
|
|
2598
|
+
if (["app", "web"].includes(name) && !images.some((image) => image.name === name))
|
|
2599
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name } }));
|
|
2600
|
+
}
|
|
2601
|
+
if (selected.includes("storage") && sidecar.composeServices["storage-init"] && !selected.includes("storage-init"))
|
|
2602
|
+
selected.push("storage-init");
|
|
2603
|
+
options?.onProgress?.("packaging");
|
|
1464
2604
|
const bundle = await createDeploymentBundle({
|
|
1465
2605
|
directory: artifactDir,
|
|
1466
2606
|
artifactDir: resolvedBuildDir,
|
|
1467
2607
|
artifact,
|
|
1468
2608
|
projectName,
|
|
1469
2609
|
environment: options?.environment ?? "prod",
|
|
1470
|
-
|
|
1471
|
-
|
|
2610
|
+
deploymentId,
|
|
2611
|
+
config,
|
|
2612
|
+
runtime: options?.runtime ?? "auto",
|
|
2613
|
+
selected,
|
|
2614
|
+
partial,
|
|
2615
|
+
hasWeb,
|
|
2616
|
+
containerPort,
|
|
2617
|
+
middlewareEnv: serializeEnvFile(sidecar.env),
|
|
2618
|
+
healthPaths: hasWeb ? ["/health", "/_hai/web-health", "/"] : [scan.isSvelteKit ? "/" : "/health"],
|
|
1472
2619
|
output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
|
|
1473
2620
|
});
|
|
1474
|
-
writeFileSync(latestPath, `${composeName}
|
|
1475
|
-
`);
|
|
1476
2621
|
if (options?.bundleOnly)
|
|
1477
2622
|
return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
|
|
1478
2623
|
if (!ssh)
|
|
@@ -1484,22 +2629,56 @@ function createDockerSshProvider(config, container) {
|
|
|
1484
2629
|
if (!remoteRuntime.success)
|
|
1485
2630
|
return remoteRuntime;
|
|
1486
2631
|
const remoteDir = `${baseDir}/${projectName}`;
|
|
1487
|
-
const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
|
|
1488
|
-
const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
|
|
2632
|
+
const remoteTar = `${remoteDir}/incoming/${basename(bundle.bundlePath)}`;
|
|
2633
|
+
const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(`${remoteDir}/incoming`)} && chmod 700 -- ${shellArgument(remoteDir)} ${shellArgument(`${remoteDir}/incoming`)}`);
|
|
1489
2634
|
if (!prepared.success)
|
|
1490
2635
|
return prepared;
|
|
1491
2636
|
const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
|
|
1492
2637
|
if (!uploaded.success)
|
|
1493
2638
|
return uploaded;
|
|
2639
|
+
let pendingOutput = "";
|
|
1494
2640
|
const started = await ssh.exec(
|
|
1495
|
-
`
|
|
1496
|
-
{ timeoutMs:
|
|
2641
|
+
`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)}`,
|
|
2642
|
+
{ timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
|
|
2643
|
+
const parts = output.split("\n");
|
|
2644
|
+
for (let index = 0; index < parts.length; index++) {
|
|
2645
|
+
pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
|
|
2646
|
+
if (index === parts.length - 1)
|
|
2647
|
+
break;
|
|
2648
|
+
const line = pendingOutput;
|
|
2649
|
+
pendingOutput = "";
|
|
2650
|
+
const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
|
|
2651
|
+
if (stage)
|
|
2652
|
+
options?.onProgress?.(stage);
|
|
2653
|
+
}
|
|
2654
|
+
} }
|
|
1497
2655
|
);
|
|
1498
|
-
if (!started.success)
|
|
1499
|
-
|
|
1500
|
-
|
|
2656
|
+
if (!started.success) {
|
|
2657
|
+
const resultFile = `${remoteDir}/result-${deploymentId}.json`;
|
|
2658
|
+
const state = getDeployCommandState(started.error);
|
|
2659
|
+
const checked = await ssh.exec(`if [ -f ${shellArgument(resultFile)} ]; then cat -- ${shellArgument(resultFile)}; fi`, { timeoutMs: 2e4, maxCaptureChars: 8192 });
|
|
2660
|
+
let terminal;
|
|
2661
|
+
if (checked.success && !checked.data.stdoutTruncated) {
|
|
2662
|
+
try {
|
|
2663
|
+
const parsed = ServerResultSchema.safeParse(JSON.parse(checked.data.stdout));
|
|
2664
|
+
if (parsed.success && parsed.data.deploymentId === deploymentId)
|
|
2665
|
+
terminal = parsed.data;
|
|
2666
|
+
} catch {
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return err({ ...started.error, message: `${started.error.message}
|
|
2670
|
+
${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown")}`, suggestion: terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown"), ext: { ...started.error.ext, commandExecution: {
|
|
2671
|
+
...state,
|
|
2672
|
+
reason: state?.reason ?? "exit",
|
|
2673
|
+
localTermination: state?.localTermination ?? "unknown",
|
|
2674
|
+
remoteExecution: terminal ? "finished" : "unknown",
|
|
2675
|
+
deploymentId,
|
|
2676
|
+
resultFile,
|
|
2677
|
+
...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage } : {}
|
|
2678
|
+
} } });
|
|
2679
|
+
}
|
|
1501
2680
|
logger5.info("Running health check", { url });
|
|
1502
|
-
const healthy = await healthCheck(`${url}${!
|
|
2681
|
+
const healthy = await healthCheck(`${url}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
|
|
1503
2682
|
if (!healthy) {
|
|
1504
2683
|
return err(
|
|
1505
2684
|
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
@@ -1522,8 +2701,14 @@ function createDockerSshProvider(config, container) {
|
|
|
1522
2701
|
try {
|
|
1523
2702
|
return await deploy2(context);
|
|
1524
2703
|
} catch (error) {
|
|
1525
|
-
|
|
1526
|
-
|
|
2704
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2705
|
+
let safeReason = "Deployment bundle failed";
|
|
2706
|
+
try {
|
|
2707
|
+
safeReason = maskSecrets(reason, Object.values(context.appEnv ?? {}).filter(Boolean));
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
logger5.error("Deployment bundle failed", { reason: safeReason });
|
|
2711
|
+
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: safeReason } }));
|
|
1527
2712
|
}
|
|
1528
2713
|
};
|
|
1529
2714
|
return { name: "docker-ssh", deploy: deploySafely };
|
|
@@ -2502,11 +3687,19 @@ var deploy = {
|
|
|
2502
3687
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
2503
3688
|
...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
|
|
2504
3689
|
};
|
|
3690
|
+
if (options.selectedServices) {
|
|
3691
|
+
options.serviceModes = {
|
|
3692
|
+
...options.serviceModes,
|
|
3693
|
+
...Object.fromEntries(scan.requiredServices.filter((service) => !options.selectedServices.includes(service)).map((service) => [service, "keep"]))
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
2505
3696
|
if (currentContainerProvider) {
|
|
2506
3697
|
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
2507
3698
|
}
|
|
2508
3699
|
if (options?.bundleOnly)
|
|
2509
3700
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
|
|
3701
|
+
if (options.selectedServices)
|
|
3702
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
|
|
2510
3703
|
if (!currentProvider) {
|
|
2511
3704
|
return notInitialized.result();
|
|
2512
3705
|
}
|
|
@@ -2579,6 +3772,17 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
|
|
|
2579
3772
|
} catch {
|
|
2580
3773
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
2581
3774
|
}
|
|
3775
|
+
if (artifact.partial && !options?.selectedServices) {
|
|
3776
|
+
const selectedServices = artifact.images.map((image) => image.name);
|
|
3777
|
+
options = {
|
|
3778
|
+
...options,
|
|
3779
|
+
selectedServices,
|
|
3780
|
+
serviceModes: {
|
|
3781
|
+
...options?.serviceModes,
|
|
3782
|
+
...Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
2582
3786
|
if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
|
|
2583
3787
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
2584
3788
|
let provisionEnv = {};
|
|
@@ -2630,13 +3834,15 @@ var DEPLOY_MIDDLEWARE = {
|
|
|
2630
3834
|
{ backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
|
|
2631
3835
|
],
|
|
2632
3836
|
sms: [
|
|
2633
|
-
{ backend: "
|
|
3837
|
+
{ backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
|
|
3838
|
+
{ backend: "aliyun-sms", label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", fields: [required("accessKeyId"), { ...required("accessKeySecret"), secret: true }, required("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] },
|
|
3839
|
+
{ backend: "api", label: "HTTP notification API", purpose: "deploy_purposeSms", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
|
|
2634
3840
|
]
|
|
2635
3841
|
};
|
|
2636
3842
|
function describeMiddlewareBackend(backend) {
|
|
2637
3843
|
return deployM(backend.purpose);
|
|
2638
3844
|
}
|
|
2639
3845
|
|
|
2640
|
-
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
|
|
3846
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
|
|
2641
3847
|
//# sourceMappingURL=index.js.map
|
|
2642
3848
|
//# sourceMappingURL=index.js.map
|