@h-ai/deploy 0.1.0-alpha.65 → 0.1.0-alpha.66
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 +46 -169
- package/README.ja.md +46 -169
- package/README.md +88 -144
- package/dist/index.d.ts +51 -26
- package/dist/index.js +486 -470
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'crypto';
|
|
2
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { resolve, join, basename, dirname, relative, sep } from 'path';
|
|
1
4
|
import { z } from 'zod';
|
|
2
5
|
import { core, ok, err } from '@h-ai/core';
|
|
3
|
-
import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
4
|
-
import { resolve, join, basename, dirname, relative, sep } from 'path';
|
|
5
6
|
import process2 from 'process';
|
|
6
7
|
import { parseEnv } from 'util';
|
|
7
8
|
import { parse, stringify, parseAllDocuments } from 'yaml';
|
|
8
9
|
import { homedir } from 'os';
|
|
9
|
-
import {
|
|
10
|
-
import { randomBytes, createHash } from 'crypto';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
11
|
import { Buffer } from 'buffer';
|
|
12
12
|
|
|
13
|
-
// src/deploy-
|
|
13
|
+
// src/deploy-artifact.ts
|
|
14
|
+
var DeploymentArtifactSchema = z.object({
|
|
15
|
+
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
16
|
+
version: z.literal(2),
|
|
17
|
+
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
18
|
+
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
|
+
/** 一次完整构建的标识,app/web 必须来自同一份清单。 */
|
|
20
|
+
buildId: z.string().regex(/^[a-z0-9][\w.+-]*$/i),
|
|
21
|
+
/** API/独立应用和可选 Web 的镜像归档,至少包含一个 app。 */
|
|
22
|
+
images: z.array(z.object({
|
|
23
|
+
/** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
|
|
24
|
+
name: z.enum(["app", "web"]),
|
|
25
|
+
/** 相对于清单目录的 tar 文件名。 */
|
|
26
|
+
file: z.string().regex(/^[\w.+-]+\.tar$/),
|
|
27
|
+
/** 归档导入后可直接使用的不可变镜像引用。 */
|
|
28
|
+
imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]+$/),
|
|
29
|
+
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
30
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
31
|
+
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
32
|
+
containerPort: z.number().int().min(1).max(65535).optional()
|
|
33
|
+
})).min(1).max(2).refine((images) => images.filter((image) => image.name === "app").length === 1 && new Set(images.map((image) => image.name)).size === images.length)
|
|
34
|
+
}).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
|
|
35
|
+
async function artifactChecksum(file) {
|
|
36
|
+
const hash = createHash("sha256");
|
|
37
|
+
for await (const chunk of createReadStream(file))
|
|
38
|
+
hash.update(chunk);
|
|
39
|
+
return hash.digest("hex");
|
|
40
|
+
}
|
|
41
|
+
async function readDeploymentArtifact(directory) {
|
|
42
|
+
const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
|
|
43
|
+
for (const image of artifact.images) {
|
|
44
|
+
const file = resolve(directory, image.file);
|
|
45
|
+
if (!existsSync(file) || !lstatSync(file).isFile() || lstatSync(file).isSymbolicLink() || await artifactChecksum(file) !== image.sha256)
|
|
46
|
+
throw new Error(`Invalid artifact: ${image.file}`);
|
|
47
|
+
}
|
|
48
|
+
return artifact;
|
|
49
|
+
}
|
|
14
50
|
|
|
15
51
|
// messages/en-US.json
|
|
16
52
|
var en_US_default = {
|
|
@@ -22,7 +58,6 @@ var en_US_default = {
|
|
|
22
58
|
deploy_authRequired: "Authentication required",
|
|
23
59
|
deploy_authFailed: "Authentication failed: {error}",
|
|
24
60
|
deploy_projectCreateFailed: "Project creation failed: {error}",
|
|
25
|
-
deploy_buildFailed: "Application build failed: {error}",
|
|
26
61
|
deploy_uploadFailed: "Build output upload failed: {error}",
|
|
27
62
|
deploy_deployFailed: "Deployment failed: {error}",
|
|
28
63
|
deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
|
|
@@ -42,8 +77,6 @@ var en_US_default = {
|
|
|
42
77
|
deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
|
|
43
78
|
deploy_commandTimeout: "Command timed out: {command}",
|
|
44
79
|
deploy_commandFailed: "Command failed ({command}): {error}",
|
|
45
|
-
deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
|
|
46
|
-
deploy_containerBuildFailed: "Container image build failed: {error}",
|
|
47
80
|
deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
|
|
48
81
|
deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
|
|
49
82
|
deploy_imageTransferFailed: "Image transfer failed: {error}",
|
|
@@ -64,7 +97,13 @@ var en_US_default = {
|
|
|
64
97
|
deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
|
|
65
98
|
deploy_purposeConsole: "Messages go to application logs without delivery",
|
|
66
99
|
deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
|
|
67
|
-
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota"
|
|
100
|
+
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
|
|
101
|
+
deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
|
|
102
|
+
deploy_artifactInvalid: "Build artifacts are missing, corrupt, or could not be packaged. Run pnpm build --target docker at the project root and point --artifact-dir to a complete build directory.",
|
|
103
|
+
deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
|
|
104
|
+
deploy_bundleInstructions: "This bundle contains private configuration. Transfer it securely and restrict access.\n1. Copy {file} to Linux using removable media, a private network or scp.\n2. Install Docker/Podman with compose, tar, sha256sum and sh. Load middleware images using docker load/podman load with the exact tags below.\n3. Create a private release directory: mkdir -p release && chmod 700 release\n4. From the bundle directory run tar -xf {file} -C release\n5. Run cd release && sh {start}. Set HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker or podman to select the runtime explicitly.\n6. Check docker compose -f {compose} --env-file .env.runtime ps (or logs; use podman instead of docker for Podman), Web and /health after startup. Bundle generation does not perform server health checks.\nRequired images (app/web are included; prepare middleware separately):\n{images}\nBuilt images can be reused across environments; this bundle contains configuration for the selected environment only.",
|
|
105
|
+
deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
|
|
106
|
+
deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
|
|
68
107
|
};
|
|
69
108
|
|
|
70
109
|
// messages/zh-CN.json
|
|
@@ -77,7 +116,6 @@ var zh_CN_default = {
|
|
|
77
116
|
deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
|
|
78
117
|
deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
|
|
79
118
|
deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
80
|
-
deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
81
119
|
deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
|
|
82
120
|
deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
|
|
83
121
|
deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
|
|
@@ -97,8 +135,6 @@ var zh_CN_default = {
|
|
|
97
135
|
deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
|
|
98
136
|
deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
|
|
99
137
|
deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
|
|
100
|
-
deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
|
|
101
|
-
deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
102
138
|
deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
|
|
103
139
|
deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
|
|
104
140
|
deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
|
|
@@ -119,7 +155,13 @@ var zh_CN_default = {
|
|
|
119
155
|
deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
|
|
120
156
|
deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
|
|
121
157
|
deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
|
|
122
|
-
deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6"
|
|
158
|
+
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
|
+
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: "\u6784\u5EFA\u4EA7\u7269\u7F3A\u5931\u3001\u635F\u574F\u6216\u90E8\u7F72\u6253\u5305\u5931\u8D25\u3002\u8BF7\u5148\u5728\u5DE5\u7A0B\u6839\u8FD0\u884C pnpm build --target docker\uFF0C\u5E76\u68C0\u67E5 --artifact-dir \u6307\u5411\u5B8C\u6574\u7684\u6784\u5EFA\u76EE\u5F55\u3002",
|
|
161
|
+
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: "\u6B64\u5305\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u642C\u8FD0\u5E76\u9650\u5236\u8BBF\u95EE\u6743\u9650\u3002\n1. \u5C06 {file} \u590D\u5236\u5230 Linux \u670D\u52A1\u5668\uFF08U \u76D8\u3001\u5185\u7F51\u4F20\u8F93\u6216 scp \u5747\u53EF\uFF09\u3002\n2. \u51C6\u5907 Docker/Podman\u3001\u5BF9\u5E94 compose \u547D\u4EE4\u3001tar\u3001sha256sum \u548C sh\uFF1B\u5C06\u6240\u9700\u4E2D\u95F4\u4EF6\u955C\u50CF\u7528 docker load/podman load \u5BFC\u5165\uFF0C\u955C\u50CF\u6807\u7B7E\u987B\u4E0E\u4E0B\u5217\u6E05\u5355\u5B8C\u5168\u4E00\u81F4\u3002\n3. \u65B0\u5EFA\u4EC5\u90E8\u7F72\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u76EE\u5F55\uFF1Amkdir -p release && chmod 700 release\n4. \u5728\u5305\u6240\u5728\u76EE\u5F55\u6267\u884C tar -xf {file} -C release\n5. \u6267\u884C cd release && sh {start}\uFF1B\u53EF\u7528 HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker \u6216 podman \u663E\u5F0F\u9009\u62E9\u8FD0\u884C\u65F6\u3002\n6. \u542F\u52A8\u540E\u7528 docker compose -f {compose} --env-file .env.runtime ps\uFF08\u6216 logs\uFF1BPodman \u5C06 docker \u6362\u4E3A podman\uFF09\u68C0\u67E5\u72B6\u6001\u5E76\u9A8C\u8BC1 Web\u3001/health\uFF1B\u4EC5\u6253\u5305\u4E0D\u4F1A\u6267\u884C\u670D\u52A1\u5668\u5065\u5EB7\u68C0\u67E5\u3002\n\u6240\u9700\u955C\u50CF\uFF08app/web \u5DF2\u542B\u5728\u5305\u4E2D\uFF0C\u4E2D\u95F4\u4EF6\u9700\u53E6\u884C\u51C6\u5907\uFF09\uFF1A\n{images}\n\u6784\u5EFA\u955C\u50CF\u53EF\u7528\u4E8E\u591A\u73AF\u5883\uFF1B\u6B64\u5305\u7684\u73AF\u5883\u914D\u7F6E\u4EC5\u7528\u4E8E\u672C\u6B21\u6240\u9009\u73AF\u5883\u3002",
|
|
163
|
+
deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
|
|
164
|
+
deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
|
|
123
165
|
};
|
|
124
166
|
|
|
125
167
|
// src/deploy-i18n.ts
|
|
@@ -160,7 +202,7 @@ var ExposeSchema = PortExposeSchema;
|
|
|
160
202
|
var DockerSshProviderConfigSchema = z.object({
|
|
161
203
|
type: z.literal("docker-ssh"),
|
|
162
204
|
/** SSH 连接参数 */
|
|
163
|
-
ssh: SshConfigSchema,
|
|
205
|
+
ssh: SshConfigSchema.optional(),
|
|
164
206
|
/** 远程主机运行参数 */
|
|
165
207
|
remote: z.object({
|
|
166
208
|
/** 远程应用根目录 */
|
|
@@ -214,24 +256,15 @@ var DeployConfigSchema = z.object({
|
|
|
214
256
|
environment: z.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
|
|
215
257
|
provider: ProviderConfigSchema,
|
|
216
258
|
services: ServicesConfigSchema,
|
|
217
|
-
/**
|
|
259
|
+
/** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
|
|
218
260
|
container: z.object({
|
|
219
|
-
runtime: z.enum(["auto", "docker", "podman"]).default("auto"),
|
|
220
|
-
/** Podman 构建是否继承代理;容器不能访问宿主机 localhost 代理时设为 false。 */
|
|
221
|
-
httpProxy: z.boolean().default(true),
|
|
222
261
|
/** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
|
|
223
|
-
s3Engine: z.enum(["rustfs", "minio"]).default("rustfs")
|
|
224
|
-
/** SQLite 预编译包镜像根地址;省略时从官方 GitHub Release 下载,不允许嵌入凭据。 */
|
|
225
|
-
sqlitePrebuildMirror: z.url().refine((value) => {
|
|
226
|
-
const url = new URL(value);
|
|
227
|
-
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
|
|
228
|
-
}).optional()
|
|
262
|
+
s3Engine: z.enum(["rustfs", "minio"]).default("rustfs")
|
|
229
263
|
}).optional()
|
|
230
264
|
});
|
|
231
265
|
var DeployErrorInfo = {
|
|
232
266
|
DEPLOY_FAILED: "001:500",
|
|
233
267
|
PROJECT_CREATE_FAILED: "002:500",
|
|
234
|
-
BUILD_FAILED: "003:500",
|
|
235
268
|
UPLOAD_FAILED: "004:500",
|
|
236
269
|
AUTH_REQUIRED: "005:401",
|
|
237
270
|
AUTH_FAILED: "006:401",
|
|
@@ -243,8 +276,6 @@ var DeployErrorInfo = {
|
|
|
243
276
|
UNSUPPORTED_TYPE: "012:400",
|
|
244
277
|
CONFIG_ERROR: "013:500",
|
|
245
278
|
CREDENTIAL_ERROR: "014:500",
|
|
246
|
-
CONTAINER_RUNTIME_NOT_FOUND: "015:500",
|
|
247
|
-
CONTAINER_BUILD_FAILED: "016:500",
|
|
248
279
|
REMOTE_CONNECT_FAILED: "017:500",
|
|
249
280
|
REMOTE_COMMAND_FAILED: "018:500",
|
|
250
281
|
IMAGE_TRANSFER_FAILED: "019:500",
|
|
@@ -253,9 +284,16 @@ var DeployErrorInfo = {
|
|
|
253
284
|
var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
|
|
254
285
|
|
|
255
286
|
// src/deploy-environment.ts
|
|
287
|
+
function environmentValue(value) {
|
|
288
|
+
try {
|
|
289
|
+
return parse(value) ?? value;
|
|
290
|
+
} catch {
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
256
294
|
function resolveConfig(value, prefix, env) {
|
|
257
295
|
if (env[prefix] !== void 0)
|
|
258
|
-
return
|
|
296
|
+
return environmentValue(env[prefix]);
|
|
259
297
|
if (Array.isArray(value))
|
|
260
298
|
return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
|
|
261
299
|
if (value && typeof value === "object")
|
|
@@ -263,7 +301,7 @@ function resolveConfig(value, prefix, env) {
|
|
|
263
301
|
if (typeof value !== "string")
|
|
264
302
|
return value;
|
|
265
303
|
const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
|
|
266
|
-
return /^\$\{[^}]+\}$/.test(value) ?
|
|
304
|
+
return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
|
|
267
305
|
}
|
|
268
306
|
function loadDeploymentConfig(name, path, values) {
|
|
269
307
|
try {
|
|
@@ -531,39 +569,25 @@ function saveCredentials(entries) {
|
|
|
531
569
|
);
|
|
532
570
|
}
|
|
533
571
|
}
|
|
534
|
-
|
|
572
|
+
function resolveArtifactDirectory(appDir) {
|
|
573
|
+
let directory = resolve(appDir);
|
|
574
|
+
while (true) {
|
|
575
|
+
if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
|
|
576
|
+
return join(directory, ".hai/build/docker");
|
|
577
|
+
const parent = dirname(directory);
|
|
578
|
+
if (parent === directory)
|
|
579
|
+
return join(appDir, ".hai/build/docker");
|
|
580
|
+
directory = parent;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
535
583
|
function usesDefaultMiddleware(scan, service, options) {
|
|
536
584
|
const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
|
|
537
585
|
return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
|
|
538
586
|
}
|
|
539
|
-
function buildApp(appDir, buildCommand, envVars = {}) {
|
|
540
|
-
logger2.info("Building application", { appDir, buildCommand });
|
|
541
|
-
try {
|
|
542
|
-
execSync(buildCommand, {
|
|
543
|
-
cwd: appDir,
|
|
544
|
-
stdio: "inherit",
|
|
545
|
-
env: { ...process2.env, ...envVars }
|
|
546
|
-
});
|
|
547
|
-
return ok(void 0);
|
|
548
|
-
} catch (error) {
|
|
549
|
-
return err(
|
|
550
|
-
HaiDeployError.BUILD_FAILED,
|
|
551
|
-
deployM("deploy_buildFailed", {
|
|
552
|
-
params: { error: error instanceof Error ? error.message : String(error) }
|
|
553
|
-
}),
|
|
554
|
-
error
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
587
|
function resolveOutputDir(appDir, providerType) {
|
|
559
588
|
switch (providerType) {
|
|
560
|
-
case "vercel":
|
|
561
|
-
|
|
562
|
-
if (existsSync(vercelOutput)) {
|
|
563
|
-
return vercelOutput;
|
|
564
|
-
}
|
|
565
|
-
return join(appDir, "build");
|
|
566
|
-
}
|
|
589
|
+
case "vercel":
|
|
590
|
+
return join(appDir, ".vercel", "output");
|
|
567
591
|
default:
|
|
568
592
|
return join(appDir, "build");
|
|
569
593
|
}
|
|
@@ -594,7 +618,7 @@ function recoveryFailure(error, recovery) {
|
|
|
594
618
|
ext: { recovery }
|
|
595
619
|
});
|
|
596
620
|
}
|
|
597
|
-
var
|
|
621
|
+
var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
|
|
598
622
|
function record(value) {
|
|
599
623
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
600
624
|
}
|
|
@@ -713,15 +737,170 @@ async function scanApp(inputDir, options) {
|
|
|
713
737
|
adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
|
|
714
738
|
nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
|
|
715
739
|
requiredServices: [...new Set(middlewares.map((item) => item.service))],
|
|
716
|
-
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
717
|
-
buildCommand: "pnpm build"
|
|
740
|
+
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
718
741
|
};
|
|
719
|
-
|
|
742
|
+
logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
|
|
720
743
|
return ok(scan);
|
|
721
744
|
} catch {
|
|
722
745
|
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
|
|
723
746
|
}
|
|
724
747
|
}
|
|
748
|
+
var logger3 = core.logger.child({ module: "deploy", scope: "command" });
|
|
749
|
+
function maskSecrets(text, secrets) {
|
|
750
|
+
if (!secrets || secrets.length === 0)
|
|
751
|
+
return text;
|
|
752
|
+
let masked = text;
|
|
753
|
+
for (const secret of secrets) {
|
|
754
|
+
if (secret && secret.length > 0)
|
|
755
|
+
masked = masked.split(secret).join("***");
|
|
756
|
+
}
|
|
757
|
+
return masked;
|
|
758
|
+
}
|
|
759
|
+
function runCommand(command, args, options = {}) {
|
|
760
|
+
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 = "";
|
|
768
|
+
let settled = false;
|
|
769
|
+
let timer;
|
|
770
|
+
const finish = (result) => {
|
|
771
|
+
if (settled)
|
|
772
|
+
return;
|
|
773
|
+
settled = true;
|
|
774
|
+
if (timer)
|
|
775
|
+
clearTimeout(timer);
|
|
776
|
+
resolve6(result);
|
|
777
|
+
};
|
|
778
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
779
|
+
timer = setTimeout(() => {
|
|
780
|
+
child.kill("SIGKILL");
|
|
781
|
+
logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
|
|
782
|
+
finish(err(
|
|
783
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
784
|
+
deployM("deploy_commandTimeout", { params: { command } })
|
|
785
|
+
));
|
|
786
|
+
}, options.timeoutMs);
|
|
787
|
+
}
|
|
788
|
+
child.stdout.on("data", (chunk) => {
|
|
789
|
+
stdout += chunk.toString("utf-8");
|
|
790
|
+
});
|
|
791
|
+
child.stderr.on("data", (chunk) => {
|
|
792
|
+
stderr += chunk.toString("utf-8");
|
|
793
|
+
});
|
|
794
|
+
child.on("error", (error) => {
|
|
795
|
+
logger3.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
|
|
796
|
+
finish(err(
|
|
797
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
798
|
+
deployM("deploy_commandFailed", {
|
|
799
|
+
params: { command, error: maskSecrets(error.message, options.secrets) }
|
|
800
|
+
}),
|
|
801
|
+
error
|
|
802
|
+
));
|
|
803
|
+
});
|
|
804
|
+
child.on("close", (code) => {
|
|
805
|
+
const exitCode = code ?? -1;
|
|
806
|
+
if (exitCode === 0) {
|
|
807
|
+
finish(ok({ code: exitCode, stdout, stderr }));
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
logger3.error("Command exited with non-zero code", {
|
|
811
|
+
command,
|
|
812
|
+
code: exitCode,
|
|
813
|
+
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
814
|
+
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
815
|
+
});
|
|
816
|
+
finish(err(
|
|
817
|
+
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
818
|
+
deployM("deploy_commandFailed", {
|
|
819
|
+
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
820
|
+
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
821
|
+
})
|
|
822
|
+
));
|
|
823
|
+
});
|
|
824
|
+
if (options.input !== void 0) {
|
|
825
|
+
child.stdin.write(options.input);
|
|
826
|
+
child.stdin.end();
|
|
827
|
+
} else {
|
|
828
|
+
child.stdin.end();
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/container/deploy-bundle.ts
|
|
834
|
+
function shellArgument(value) {
|
|
835
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
836
|
+
}
|
|
837
|
+
async function createDeploymentBundle(options) {
|
|
838
|
+
const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
|
|
839
|
+
const stem = basename(options.output).replace(/\.tar$/i, "");
|
|
840
|
+
const versionSuffix = `-${artifact.appVersion}`;
|
|
841
|
+
const output = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
|
|
842
|
+
const composeName = `docker-compose-${artifact.appVersion}.yml`;
|
|
843
|
+
mkdirSync(dirname(output), { recursive: true, mode: 448 });
|
|
844
|
+
const staging = mkdtempSync(join(directory, ".bundle-"));
|
|
845
|
+
chmodSync(staging, 448);
|
|
846
|
+
const temporaryTar = `${output}.partial`;
|
|
847
|
+
const startName = `start-${environment}.sh`;
|
|
848
|
+
try {
|
|
849
|
+
mkdirSync(join(staging, "images"));
|
|
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
|
|
857
|
+
set -eu
|
|
858
|
+
cd "$(dirname "$0")"
|
|
859
|
+
umask 077
|
|
860
|
+
export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
|
|
861
|
+
export HAI_CORE_ENV=${shellArgument(coreEnv)}
|
|
862
|
+
runtime=${shellArgument(runtime)}
|
|
863
|
+
if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
|
|
864
|
+
if [ "$runtime" = auto ]; then
|
|
865
|
+
if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
|
|
866
|
+
fi
|
|
867
|
+
case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
|
|
868
|
+
"$runtime" version >/dev/null
|
|
869
|
+
"$runtime" compose version >/dev/null
|
|
870
|
+
sha256sum -c SHA256SUMS
|
|
871
|
+
chmod 600 .env.runtime
|
|
872
|
+
${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
|
|
873
|
+
# \u79BB\u7EBF\u670D\u52A1\u5668\u5E94\u5148\u5BFC\u5165\u5168\u90E8\u4E2D\u95F4\u4EF6\u955C\u50CF\uFF1B\u7981\u6B62\u9759\u9ED8\u5C1D\u8BD5\u8054\u7F51\u3002
|
|
874
|
+
for image in ${images.map(shellArgument).join(" ")}; do
|
|
875
|
+
"$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
|
|
876
|
+
done
|
|
877
|
+
exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
|
|
878
|
+
`;
|
|
879
|
+
writeFileSync(join(directory, startName), start, { mode: 448 });
|
|
880
|
+
writeFileSync(join(staging, startName), start, { mode: 448 });
|
|
881
|
+
const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output), start: startName, images: images.join("\n"), compose: composeName } });
|
|
882
|
+
const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
|
|
883
|
+
writeFileSync(join(staging, "README.txt"), `${metadata}
|
|
884
|
+
${instructions}
|
|
885
|
+
`);
|
|
886
|
+
copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
|
|
887
|
+
const checksums = [];
|
|
888
|
+
for (const file of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
|
|
889
|
+
checksums.push(`${await artifactChecksum(join(staging, file))} ${file}`);
|
|
890
|
+
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
891
|
+
`);
|
|
892
|
+
const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
|
|
893
|
+
if (!tar.success)
|
|
894
|
+
throw new Error(tar.error.message);
|
|
895
|
+
chmodSync(temporaryTar, 384);
|
|
896
|
+
const { renameSync } = await import('fs');
|
|
897
|
+
renameSync(temporaryTar, output);
|
|
898
|
+
return { bundlePath: output, instructions, startName };
|
|
899
|
+
} finally {
|
|
900
|
+
rmSync(staging, { recursive: true, force: true });
|
|
901
|
+
rmSync(temporaryTar, { force: true });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
725
904
|
var IMAGES = {
|
|
726
905
|
postgres: "postgres:16-alpine",
|
|
727
906
|
mysql: "mysql:8.4",
|
|
@@ -995,6 +1174,7 @@ function generateComposeFile(options) {
|
|
|
995
1174
|
const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
|
|
996
1175
|
const app = {
|
|
997
1176
|
image: imageRef,
|
|
1177
|
+
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
998
1178
|
container_name: `hai-${projectName}-app`,
|
|
999
1179
|
restart: "unless-stopped",
|
|
1000
1180
|
// 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
|
|
@@ -1008,11 +1188,28 @@ function generateComposeFile(options) {
|
|
|
1008
1188
|
if (sidecar.dependsOn.length > 0)
|
|
1009
1189
|
app.depends_on = sidecar.dependsOn;
|
|
1010
1190
|
const services = { app, ...sidecar.composeServices };
|
|
1191
|
+
if (options.web) {
|
|
1192
|
+
delete app.ports;
|
|
1193
|
+
services.web = {
|
|
1194
|
+
image: options.web.imageRef,
|
|
1195
|
+
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
1196
|
+
container_name: `hai-${projectName}-web`,
|
|
1197
|
+
restart: "unless-stopped",
|
|
1198
|
+
pull_policy: "never",
|
|
1199
|
+
ports: [`${hostPort}:${options.web.containerPort}`],
|
|
1200
|
+
environment: { PUBLIC_API_BASE: "" },
|
|
1201
|
+
healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
|
|
1202
|
+
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
1203
|
+
depends_on: ["app"],
|
|
1204
|
+
networks: ["hai-app"]
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1011
1207
|
if (services["storage-init"])
|
|
1012
1208
|
services["storage-init"] = { ...services["storage-init"], image: imageRef };
|
|
1013
1209
|
const compose = {
|
|
1210
|
+
"x-hai-version": options.appVersion,
|
|
1014
1211
|
services,
|
|
1015
|
-
networks: {
|
|
1212
|
+
"networks": {
|
|
1016
1213
|
"hai-app": { name: `hai-${projectName}` }
|
|
1017
1214
|
}
|
|
1018
1215
|
};
|
|
@@ -1041,8 +1238,43 @@ function generateComposeFile(options) {
|
|
|
1041
1238
|
"storage-init": { condition: "service_completed_successfully" }
|
|
1042
1239
|
};
|
|
1043
1240
|
}
|
|
1241
|
+
for (const service of Object.values(services))
|
|
1242
|
+
service.pull_policy = "never";
|
|
1044
1243
|
return stringify(compose);
|
|
1045
1244
|
}
|
|
1245
|
+
function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
|
|
1246
|
+
if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
|
|
1247
|
+
throw new Error("Invalid web proxy target");
|
|
1248
|
+
return `server {
|
|
1249
|
+
listen ${webPort};
|
|
1250
|
+
server_name _;
|
|
1251
|
+
root /usr/share/nginx/html;
|
|
1252
|
+
location / {
|
|
1253
|
+
try_files $uri $uri/ /index.html;
|
|
1254
|
+
}
|
|
1255
|
+
location = /runtime-config.js {
|
|
1256
|
+
add_header Cache-Control "no-store";
|
|
1257
|
+
default_type application/javascript;
|
|
1258
|
+
return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
|
|
1259
|
+
}
|
|
1260
|
+
location = /_hai/web-health {
|
|
1261
|
+
access_log off;
|
|
1262
|
+
return 200 'ok';
|
|
1263
|
+
}
|
|
1264
|
+
location = /health {
|
|
1265
|
+
proxy_pass http://app:${port};
|
|
1266
|
+
proxy_set_header Host $http_host;
|
|
1267
|
+
}
|
|
1268
|
+
location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
1269
|
+
proxy_pass http://app:${port};
|
|
1270
|
+
proxy_http_version 1.1;
|
|
1271
|
+
proxy_set_header Host $http_host;
|
|
1272
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1273
|
+
proxy_buffering off;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
`;
|
|
1277
|
+
}
|
|
1046
1278
|
function serializeEnvFile(env) {
|
|
1047
1279
|
return `${Object.entries(env).map(([key, value]) => {
|
|
1048
1280
|
let literal = value;
|
|
@@ -1058,194 +1290,7 @@ function serializeEnvFile(env) {
|
|
|
1058
1290
|
}).join("\n")}
|
|
1059
1291
|
`;
|
|
1060
1292
|
}
|
|
1061
|
-
|
|
1062
|
-
return `**/node_modules
|
|
1063
|
-
**/dist
|
|
1064
|
-
**/build
|
|
1065
|
-
**/.turbo
|
|
1066
|
-
**/.svelte-kit
|
|
1067
|
-
**/coverage
|
|
1068
|
-
**/test-results
|
|
1069
|
-
**/playwright-report
|
|
1070
|
-
**/.hai/deploy
|
|
1071
|
-
**/.env.local
|
|
1072
|
-
**/.env.*.local
|
|
1073
|
-
**/.env.runtime
|
|
1074
|
-
.git
|
|
1075
|
-
`;
|
|
1076
|
-
}
|
|
1077
|
-
function generateFallbackDockerfile(packageName, containerPort, options = {}) {
|
|
1078
|
-
if (!/^@?[a-z0-9][\w./-]*$/i.test(packageName) || !/^[a-z0-9][\w-]*$/i.test(options.environment ?? "prod"))
|
|
1079
|
-
throw new Error("Invalid container build target");
|
|
1080
|
-
const environment = options.environment ?? "prod";
|
|
1081
|
-
const coreEnv = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
1082
|
-
return `# syntax=docker/dockerfile:1.7
|
|
1083
|
-
FROM node:24-bookworm-slim AS build
|
|
1084
|
-
ARG SQLITE_PREBUILD_MIRROR
|
|
1085
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
1086
|
-
ENV PNPM_HOME=/pnpm
|
|
1087
|
-
ENV PATH=$PNPM_HOME:$PATH
|
|
1088
|
-
ENV CI=true
|
|
1089
|
-
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
1090
|
-
ENV HAI_CORE_ENV=${coreEnv}
|
|
1091
|
-
WORKDIR /workspace
|
|
1092
|
-
RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
|
|
1093
|
-
COPY . .
|
|
1094
|
-
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
1095
|
-
RUN pnpm --filter ${packageName}... build
|
|
1096
|
-
RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
|
|
1097
|
-
RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
|
|
1098
|
-
|
|
1099
|
-
FROM node:24-bookworm-slim AS runtime
|
|
1100
|
-
ENV NODE_ENV=production
|
|
1101
|
-
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
1102
|
-
ENV HAI_CORE_ENV=${coreEnv}
|
|
1103
|
-
ENV HOST=0.0.0.0
|
|
1104
|
-
ENV PORT=${containerPort}
|
|
1105
|
-
WORKDIR /app
|
|
1106
|
-
COPY --from=build --chown=node:node /prod/app ./
|
|
1107
|
-
RUN mkdir -p data && chown node:node data
|
|
1108
|
-
USER node
|
|
1109
|
-
EXPOSE ${containerPort}
|
|
1110
|
-
CMD ["node", ${JSON.stringify(options.entrypoint ?? "build")}]
|
|
1111
|
-
`;
|
|
1112
|
-
}
|
|
1113
|
-
var logger4 = core.logger.child({ module: "deploy", scope: "command" });
|
|
1114
|
-
function maskSecrets(text, secrets) {
|
|
1115
|
-
if (!secrets || secrets.length === 0)
|
|
1116
|
-
return text;
|
|
1117
|
-
let masked = text;
|
|
1118
|
-
for (const secret of secrets) {
|
|
1119
|
-
if (secret && secret.length > 0)
|
|
1120
|
-
masked = masked.split(secret).join("***");
|
|
1121
|
-
}
|
|
1122
|
-
return masked;
|
|
1123
|
-
}
|
|
1124
|
-
function runCommand(command, args, options = {}) {
|
|
1125
|
-
return new Promise((resolve3) => {
|
|
1126
|
-
const child = spawn(command, args, {
|
|
1127
|
-
cwd: options.cwd,
|
|
1128
|
-
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
1129
|
-
shell: false
|
|
1130
|
-
});
|
|
1131
|
-
let stdout = "";
|
|
1132
|
-
let stderr = "";
|
|
1133
|
-
let settled = false;
|
|
1134
|
-
let timer;
|
|
1135
|
-
const finish = (result) => {
|
|
1136
|
-
if (settled)
|
|
1137
|
-
return;
|
|
1138
|
-
settled = true;
|
|
1139
|
-
if (timer)
|
|
1140
|
-
clearTimeout(timer);
|
|
1141
|
-
resolve3(result);
|
|
1142
|
-
};
|
|
1143
|
-
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
1144
|
-
timer = setTimeout(() => {
|
|
1145
|
-
child.kill("SIGKILL");
|
|
1146
|
-
logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
|
|
1147
|
-
finish(err(
|
|
1148
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
1149
|
-
deployM("deploy_commandTimeout", { params: { command } })
|
|
1150
|
-
));
|
|
1151
|
-
}, options.timeoutMs);
|
|
1152
|
-
}
|
|
1153
|
-
child.stdout.on("data", (chunk) => {
|
|
1154
|
-
stdout += chunk.toString("utf-8");
|
|
1155
|
-
});
|
|
1156
|
-
child.stderr.on("data", (chunk) => {
|
|
1157
|
-
stderr += chunk.toString("utf-8");
|
|
1158
|
-
});
|
|
1159
|
-
child.on("error", (error) => {
|
|
1160
|
-
logger4.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
|
|
1161
|
-
finish(err(
|
|
1162
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
1163
|
-
deployM("deploy_commandFailed", {
|
|
1164
|
-
params: { command, error: maskSecrets(error.message, options.secrets) }
|
|
1165
|
-
}),
|
|
1166
|
-
error
|
|
1167
|
-
));
|
|
1168
|
-
});
|
|
1169
|
-
child.on("close", (code) => {
|
|
1170
|
-
const exitCode = code ?? -1;
|
|
1171
|
-
if (exitCode === 0) {
|
|
1172
|
-
finish(ok({ code: exitCode, stdout, stderr }));
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
logger4.error("Command exited with non-zero code", {
|
|
1176
|
-
command,
|
|
1177
|
-
code: exitCode,
|
|
1178
|
-
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
1179
|
-
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
1180
|
-
});
|
|
1181
|
-
finish(err(
|
|
1182
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
1183
|
-
deployM("deploy_commandFailed", {
|
|
1184
|
-
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
1185
|
-
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
1186
|
-
})
|
|
1187
|
-
));
|
|
1188
|
-
});
|
|
1189
|
-
if (options.input !== void 0) {
|
|
1190
|
-
child.stdin.write(options.input);
|
|
1191
|
-
child.stdin.end();
|
|
1192
|
-
} else {
|
|
1193
|
-
child.stdin.end();
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// src/container/deploy-container-runtime.ts
|
|
1199
|
-
var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
|
|
1200
|
-
async function detectLocalRuntime(preferred = "auto") {
|
|
1201
|
-
const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
|
|
1202
|
-
for (const runtime of candidates) {
|
|
1203
|
-
const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
|
|
1204
|
-
if (probe.success) {
|
|
1205
|
-
logger5.info("Local container runtime detected", { runtime });
|
|
1206
|
-
return ok(runtime);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return err(
|
|
1210
|
-
HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
|
|
1211
|
-
deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
async function buildImage(runtime, options) {
|
|
1215
|
-
const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
|
|
1216
|
-
if (runtime === "podman")
|
|
1217
|
-
args.push("--format", "docker", "--jobs", "1");
|
|
1218
|
-
if (runtime === "podman" && existsSync(`${options.dockerfilePath}.dockerignore`))
|
|
1219
|
-
args.push("--ignorefile", `${options.dockerfilePath}.dockerignore`);
|
|
1220
|
-
if (runtime === "podman" && options.httpProxy === false)
|
|
1221
|
-
args.push("--http-proxy=false");
|
|
1222
|
-
for (const [key, value] of Object.entries(options.buildArgs ?? {}))
|
|
1223
|
-
args.push("--build-arg", `${key}=${value}`);
|
|
1224
|
-
args.push(options.contextDir);
|
|
1225
|
-
logger5.info("Building container image", { runtime, imageRef: options.imageRef });
|
|
1226
|
-
const result = await runCommand(runtime, args, { cwd: options.contextDir });
|
|
1227
|
-
if (!result.success) {
|
|
1228
|
-
return err(
|
|
1229
|
-
HaiDeployError.CONTAINER_BUILD_FAILED,
|
|
1230
|
-
deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
|
|
1231
|
-
result.error
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
return ok(void 0);
|
|
1235
|
-
}
|
|
1236
|
-
async function saveImage(runtime, imageRef, tarPath) {
|
|
1237
|
-
logger5.info("Saving container image", { runtime, imageRef, tarPath });
|
|
1238
|
-
const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
|
|
1239
|
-
if (!result.success) {
|
|
1240
|
-
return err(
|
|
1241
|
-
HaiDeployError.CONTAINER_BUILD_FAILED,
|
|
1242
|
-
deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
|
|
1243
|
-
result.error
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
return ok(void 0);
|
|
1247
|
-
}
|
|
1248
|
-
var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
|
|
1293
|
+
var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
|
|
1249
1294
|
function buildConnectOptions(config) {
|
|
1250
1295
|
const args = ["-o", "BatchMode=yes"];
|
|
1251
1296
|
args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
|
|
@@ -1296,7 +1341,7 @@ function createSshExecutor(config) {
|
|
|
1296
1341
|
return ok(void 0);
|
|
1297
1342
|
};
|
|
1298
1343
|
const ping = async () => {
|
|
1299
|
-
|
|
1344
|
+
logger4.debug("SSH preflight", { host: config.host, port: config.port });
|
|
1300
1345
|
const result = await exec("true", { timeoutMs: 2e4 });
|
|
1301
1346
|
if (!result.success) {
|
|
1302
1347
|
return err(
|
|
@@ -1311,52 +1356,24 @@ function createSshExecutor(config) {
|
|
|
1311
1356
|
}
|
|
1312
1357
|
|
|
1313
1358
|
// src/providers/deploy-provider-docker-ssh.ts
|
|
1314
|
-
var
|
|
1359
|
+
var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
|
|
1315
1360
|
var HEALTH_POLL_INTERVAL = 3e3;
|
|
1316
1361
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
1317
|
-
function shellArgument(value) {
|
|
1318
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1319
|
-
}
|
|
1320
1362
|
function makeDeploymentId() {
|
|
1321
1363
|
const now = /* @__PURE__ */ new Date();
|
|
1322
1364
|
const pad = (n) => String(n).padStart(2, "0");
|
|
1323
1365
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
1324
1366
|
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
1325
1367
|
}
|
|
1326
|
-
function parseLoadedImageRef(stdout, fallback) {
|
|
1327
|
-
const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
|
|
1328
|
-
return match ? match[1] : fallback;
|
|
1329
|
-
}
|
|
1330
1368
|
function sleep(ms) {
|
|
1331
|
-
return new Promise((
|
|
1332
|
-
}
|
|
1333
|
-
function readPackageName(appDir) {
|
|
1334
|
-
try {
|
|
1335
|
-
const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
|
|
1336
|
-
return pkg.name ?? "app";
|
|
1337
|
-
} catch {
|
|
1338
|
-
return "app";
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
function resolveWorkspaceRoot(appDir) {
|
|
1342
|
-
if (existsSync(join(appDir, "pnpm-workspace.yaml")))
|
|
1343
|
-
return appDir;
|
|
1344
|
-
let current = appDir;
|
|
1345
|
-
let parent = dirname(current);
|
|
1346
|
-
while (parent !== current) {
|
|
1347
|
-
if (existsSync(join(parent, "pnpm-workspace.yaml")))
|
|
1348
|
-
return parent;
|
|
1349
|
-
current = parent;
|
|
1350
|
-
parent = dirname(parent);
|
|
1351
|
-
}
|
|
1352
|
-
return appDir;
|
|
1369
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1353
1370
|
}
|
|
1354
1371
|
function createDockerSshProvider(config, container) {
|
|
1355
|
-
const
|
|
1356
|
-
const httpProxy = container?.httpProxy ?? true;
|
|
1357
|
-
const ssh = createSshExecutor(config.ssh);
|
|
1372
|
+
const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
|
|
1358
1373
|
const baseDir = config.remote.baseDir;
|
|
1359
1374
|
async function detectRemoteRuntime() {
|
|
1375
|
+
if (!ssh)
|
|
1376
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
1360
1377
|
const candidates = config.remote.runtime === "auto" ? ["podman", "docker"] : [config.remote.runtime];
|
|
1361
1378
|
for (const runtime of candidates) {
|
|
1362
1379
|
const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
|
|
@@ -1364,7 +1381,7 @@ function createDockerSshProvider(config, container) {
|
|
|
1364
1381
|
continue;
|
|
1365
1382
|
const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
|
|
1366
1383
|
if (composeResult.success) {
|
|
1367
|
-
|
|
1384
|
+
logger5.info("Remote container runtime detected", { runtime });
|
|
1368
1385
|
return ok(runtime);
|
|
1369
1386
|
}
|
|
1370
1387
|
}
|
|
@@ -1388,29 +1405,31 @@ function createDockerSshProvider(config, container) {
|
|
|
1388
1405
|
const deploy2 = async (context) => {
|
|
1389
1406
|
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
1390
1407
|
const deploymentId = makeDeploymentId();
|
|
1391
|
-
const containerPort = config.expose.containerPort;
|
|
1392
1408
|
const hostPort = config.expose.hostPort;
|
|
1393
|
-
const localImageTag = `hai-${projectName}:${deploymentId}`;
|
|
1394
|
-
const runtimeResult = await detectLocalRuntime(localRuntimePref);
|
|
1395
|
-
if (!runtimeResult.success)
|
|
1396
|
-
return err(runtimeResult.error);
|
|
1397
|
-
const localRuntime = runtimeResult.data;
|
|
1398
1409
|
const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
|
|
1399
|
-
mkdirSync(artifactDir, { recursive: true });
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort, { environment: options?.environment, entrypoint: scan.isSvelteKit ? "build" : "dist/index.js" }));
|
|
1409
|
-
writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
|
|
1410
|
-
contextDir = resolveWorkspaceRoot(appDir);
|
|
1411
|
-
logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
|
|
1412
|
-
}
|
|
1410
|
+
mkdirSync(artifactDir, { recursive: true, mode: 448 });
|
|
1411
|
+
chmodSync(artifactDir, 448);
|
|
1412
|
+
const artifact = context.artifact;
|
|
1413
|
+
const resolvedBuildDir = context.artifactDir;
|
|
1414
|
+
const images = artifact.images;
|
|
1415
|
+
const web = images.find((image) => image.name === "web");
|
|
1416
|
+
const app = images.find((image) => image.name === "app");
|
|
1417
|
+
const containerPort = app.containerPort ?? config.expose.containerPort;
|
|
1418
|
+
const url = config.ssh ? `http://${config.ssh.host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
1413
1419
|
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
1420
|
+
let webProxy;
|
|
1421
|
+
if (web) {
|
|
1422
|
+
const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
|
|
1423
|
+
if (!server.success)
|
|
1424
|
+
return server;
|
|
1425
|
+
const http = server.data.http;
|
|
1426
|
+
const cors = server.data.cors;
|
|
1427
|
+
const apiPrefix = http?.apiPrefix ?? "/api/v1";
|
|
1428
|
+
if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
|
|
1429
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
1430
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, web.containerPort ?? 8080);
|
|
1431
|
+
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config.ssh ? url : cors?.origin ?? "*" } });
|
|
1432
|
+
}
|
|
1414
1433
|
const envPath = join(artifactDir, ".env.runtime");
|
|
1415
1434
|
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
1416
1435
|
Object.assign(env, { ...previousEnv, ...env });
|
|
@@ -1423,88 +1442,64 @@ function createDockerSshProvider(config, container) {
|
|
|
1423
1442
|
if (!saved.success)
|
|
1424
1443
|
return saved;
|
|
1425
1444
|
}
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
buildArgs: { HAI_DEPLOY_ENVIRONMENT: options?.environment ?? "prod", HAI_CORE_ENV: env.HAI_CORE_ENV ?? "production", SQLITE_PREBUILD_MIRROR: container?.sqlitePrebuildMirror ?? "" },
|
|
1431
|
-
httpProxy
|
|
1432
|
-
});
|
|
1433
|
-
if (!buildResult.success)
|
|
1434
|
-
return err(buildResult.error);
|
|
1435
|
-
const tarPath = join(artifactDir, `${projectName}.tar`);
|
|
1436
|
-
const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
|
|
1437
|
-
if (!saveResult.success)
|
|
1438
|
-
return err(saveResult.error);
|
|
1439
|
-
const pingResult = await ssh.ping();
|
|
1440
|
-
if (!pingResult.success)
|
|
1441
|
-
return err(pingResult.error);
|
|
1442
|
-
const remoteRuntimeResult = await detectRemoteRuntime();
|
|
1443
|
-
if (!remoteRuntimeResult.success)
|
|
1444
|
-
return err(remoteRuntimeResult.error);
|
|
1445
|
-
const remoteRuntime = remoteRuntimeResult.data;
|
|
1446
|
-
const remoteDir = `${baseDir}/${projectName}`;
|
|
1447
|
-
const prepareResult = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
|
|
1448
|
-
if (!prepareResult.success)
|
|
1449
|
-
return err(prepareResult.error);
|
|
1450
|
-
const remoteTar = `${remoteDir}/image.tar`;
|
|
1451
|
-
const uploadTarResult = await ssh.upload(tarPath, remoteTar);
|
|
1452
|
-
if (!uploadTarResult.success)
|
|
1453
|
-
return err(uploadTarResult.error);
|
|
1454
|
-
const loadResult = await ssh.exec(`${remoteRuntime} load -i ${shellArgument(remoteTar)}`, { timeoutMs: 3e5 });
|
|
1455
|
-
if (!loadResult.success)
|
|
1456
|
-
return err(loadResult.error);
|
|
1457
|
-
const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
|
|
1458
|
-
const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
|
|
1459
|
-
if (loadedRef !== remoteImageRef) {
|
|
1460
|
-
const tagResult = await ssh.exec(`${remoteRuntime} tag ${shellArgument(loadedRef)} ${shellArgument(remoteImageRef)}`);
|
|
1461
|
-
if (!tagResult.success)
|
|
1462
|
-
return err(tagResult.error);
|
|
1463
|
-
}
|
|
1445
|
+
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;
|
|
1464
1449
|
const composeContent = generateComposeFile({
|
|
1465
1450
|
projectName,
|
|
1466
|
-
|
|
1451
|
+
appVersion: artifact.appVersion,
|
|
1452
|
+
imageRef: app.imageRef,
|
|
1467
1453
|
hostPort,
|
|
1468
1454
|
containerPort,
|
|
1469
1455
|
sidecar,
|
|
1456
|
+
web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
|
|
1470
1457
|
serviceModes: options?.serviceModes,
|
|
1471
|
-
previousCompose: existsSync(
|
|
1458
|
+
previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
|
|
1472
1459
|
});
|
|
1473
|
-
const composePath = join(artifactDir,
|
|
1460
|
+
const composePath = join(artifactDir, composeName);
|
|
1474
1461
|
writeFileSync(composePath, composeContent);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1462
|
+
if (webProxy)
|
|
1463
|
+
writeFileSync(join(artifactDir, "web.conf"), webProxy);
|
|
1464
|
+
const bundle = await createDeploymentBundle({
|
|
1465
|
+
directory: artifactDir,
|
|
1466
|
+
artifactDir: resolvedBuildDir,
|
|
1467
|
+
artifact,
|
|
1468
|
+
projectName,
|
|
1469
|
+
environment: options?.environment ?? "prod",
|
|
1470
|
+
runtime: config.remote.runtime,
|
|
1471
|
+
coreEnv: env.HAI_CORE_ENV ?? "production",
|
|
1472
|
+
output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
|
|
1473
|
+
});
|
|
1474
|
+
writeFileSync(latestPath, `${composeName}
|
|
1475
|
+
`);
|
|
1476
|
+
if (options?.bundleOnly)
|
|
1477
|
+
return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
|
|
1478
|
+
if (!ssh)
|
|
1479
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
1480
|
+
const ping = await ssh.ping();
|
|
1481
|
+
if (!ping.success)
|
|
1482
|
+
return ping;
|
|
1483
|
+
const remoteRuntime = await detectRemoteRuntime();
|
|
1484
|
+
if (!remoteRuntime.success)
|
|
1485
|
+
return remoteRuntime;
|
|
1486
|
+
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)}`);
|
|
1489
|
+
if (!prepared.success)
|
|
1490
|
+
return prepared;
|
|
1491
|
+
const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
|
|
1492
|
+
if (!uploaded.success)
|
|
1493
|
+
return uploaded;
|
|
1494
|
+
const started = await ssh.exec(
|
|
1495
|
+
`cd ${shellArgument(remoteDir)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} && HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=${shellArgument(remoteRuntime.data)} sh ${shellArgument(bundle.startName)}`,
|
|
1496
|
+
{ timeoutMs: 3e5, secrets: sidecar.secrets }
|
|
1501
1497
|
);
|
|
1502
|
-
if (!
|
|
1503
|
-
return
|
|
1504
|
-
await ssh.exec(`rm -f -- ${shellArgument(remoteTar)}`);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
const healthy = await healthCheck(`${url}${scan.isSvelteKit ? "/" : "/health"}`);
|
|
1498
|
+
if (!started.success)
|
|
1499
|
+
return started;
|
|
1500
|
+
await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
|
|
1501
|
+
logger5.info("Running health check", { url });
|
|
1502
|
+
const healthy = await healthCheck(`${url}${!web && scan.isSvelteKit ? "/" : "/health"}`) && (!web || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
|
|
1508
1503
|
if (!healthy) {
|
|
1509
1504
|
return err(
|
|
1510
1505
|
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
@@ -1517,14 +1512,23 @@ exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -
|
|
|
1517
1512
|
status: "ready",
|
|
1518
1513
|
provider: "docker-ssh",
|
|
1519
1514
|
healthCheck: "passed",
|
|
1520
|
-
envVarsSet: Object.keys(env)
|
|
1515
|
+
envVarsSet: Object.keys(env),
|
|
1516
|
+
bundlePath: bundle.bundlePath
|
|
1521
1517
|
};
|
|
1522
|
-
|
|
1518
|
+
logger5.info("docker-ssh deployment complete", { url, deploymentId });
|
|
1523
1519
|
return ok(result);
|
|
1524
1520
|
};
|
|
1525
|
-
|
|
1521
|
+
const deploySafely = async (context) => {
|
|
1522
|
+
try {
|
|
1523
|
+
return await deploy2(context);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
logger5.error("Deployment bundle failed", { error });
|
|
1526
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"), error);
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
return { name: "docker-ssh", deploy: deploySafely };
|
|
1526
1530
|
}
|
|
1527
|
-
var
|
|
1531
|
+
var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
|
|
1528
1532
|
var VERCEL_API = "https://api.vercel.com";
|
|
1529
1533
|
var POLL_INTERVAL = 3e3;
|
|
1530
1534
|
var MAX_POLL_ATTEMPTS = 60;
|
|
@@ -1577,7 +1581,7 @@ function createVercelProvider() {
|
|
|
1577
1581
|
return {
|
|
1578
1582
|
name: "vercel",
|
|
1579
1583
|
async authenticate(apiToken) {
|
|
1580
|
-
|
|
1584
|
+
logger6.debug("Authenticating with Vercel");
|
|
1581
1585
|
try {
|
|
1582
1586
|
const user = await vercelFetch(
|
|
1583
1587
|
apiToken,
|
|
@@ -1585,10 +1589,10 @@ function createVercelProvider() {
|
|
|
1585
1589
|
);
|
|
1586
1590
|
token = apiToken;
|
|
1587
1591
|
const username = user.user.username || user.user.email;
|
|
1588
|
-
|
|
1592
|
+
logger6.info("Vercel authenticated", { user: username });
|
|
1589
1593
|
return ok(username);
|
|
1590
1594
|
} catch (error) {
|
|
1591
|
-
|
|
1595
|
+
logger6.error("Vercel authentication failed", { error });
|
|
1592
1596
|
return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error));
|
|
1593
1597
|
}
|
|
1594
1598
|
},
|
|
@@ -1599,11 +1603,11 @@ function createVercelProvider() {
|
|
|
1599
1603
|
deployM("deploy_authRequired")
|
|
1600
1604
|
);
|
|
1601
1605
|
}
|
|
1602
|
-
|
|
1606
|
+
logger6.debug("Creating Vercel project", { projectName });
|
|
1603
1607
|
try {
|
|
1604
1608
|
const existingId = await findExistingProject(token, projectName);
|
|
1605
1609
|
if (existingId !== null) {
|
|
1606
|
-
|
|
1610
|
+
logger6.info("Vercel project found", { projectName, projectId: existingId });
|
|
1607
1611
|
return ok(existingId);
|
|
1608
1612
|
}
|
|
1609
1613
|
try {
|
|
@@ -1618,13 +1622,13 @@ function createVercelProvider() {
|
|
|
1618
1622
|
})
|
|
1619
1623
|
}
|
|
1620
1624
|
);
|
|
1621
|
-
|
|
1625
|
+
logger6.info("Vercel project created", { projectName, projectId: project.id });
|
|
1622
1626
|
return ok(project.id);
|
|
1623
1627
|
} catch (error) {
|
|
1624
1628
|
if (error instanceof VercelApiError && error.status === 409) {
|
|
1625
1629
|
const concurrentProjectId = await findExistingProject(token, projectName);
|
|
1626
1630
|
if (concurrentProjectId !== null) {
|
|
1627
|
-
|
|
1631
|
+
logger6.info("Vercel project reused after create conflict", {
|
|
1628
1632
|
projectName,
|
|
1629
1633
|
projectId: concurrentProjectId
|
|
1630
1634
|
});
|
|
@@ -1634,7 +1638,7 @@ function createVercelProvider() {
|
|
|
1634
1638
|
throw error;
|
|
1635
1639
|
}
|
|
1636
1640
|
} catch (error) {
|
|
1637
|
-
|
|
1641
|
+
logger6.error("Vercel project creation failed", { projectName, error });
|
|
1638
1642
|
return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error));
|
|
1639
1643
|
}
|
|
1640
1644
|
},
|
|
@@ -1645,7 +1649,7 @@ function createVercelProvider() {
|
|
|
1645
1649
|
deployM("deploy_authRequired")
|
|
1646
1650
|
);
|
|
1647
1651
|
}
|
|
1648
|
-
|
|
1652
|
+
logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
|
|
1649
1653
|
try {
|
|
1650
1654
|
const envEntries = Object.entries(envVars).map(([key, value]) => ({
|
|
1651
1655
|
key,
|
|
@@ -1661,10 +1665,10 @@ function createVercelProvider() {
|
|
|
1661
1665
|
body: JSON.stringify(envEntries)
|
|
1662
1666
|
}
|
|
1663
1667
|
);
|
|
1664
|
-
|
|
1668
|
+
logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
|
|
1665
1669
|
return ok(void 0);
|
|
1666
1670
|
} catch (error) {
|
|
1667
|
-
|
|
1671
|
+
logger6.error("Vercel env vars setup failed", { projectId, error });
|
|
1668
1672
|
return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error));
|
|
1669
1673
|
}
|
|
1670
1674
|
},
|
|
@@ -1675,8 +1679,11 @@ function createVercelProvider() {
|
|
|
1675
1679
|
deployM("deploy_authRequired")
|
|
1676
1680
|
);
|
|
1677
1681
|
}
|
|
1678
|
-
|
|
1682
|
+
logger6.debug("Deploying to Vercel", { projectId, outputDir });
|
|
1679
1683
|
try {
|
|
1684
|
+
const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
|
|
1685
|
+
if (outputConfig.version !== 3)
|
|
1686
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
1680
1687
|
const filePaths = collectFiles(outputDir, outputDir);
|
|
1681
1688
|
const files = [];
|
|
1682
1689
|
for (const filePath of filePaths) {
|
|
@@ -1685,18 +1692,20 @@ function createVercelProvider() {
|
|
|
1685
1692
|
const sha = createHash("sha1").update(content).digest("hex");
|
|
1686
1693
|
const size = statSync(fullPath).size;
|
|
1687
1694
|
await uploadFile(token, content, sha);
|
|
1688
|
-
files.push({ file: filePath
|
|
1695
|
+
files.push({ file: `.vercel/output/${filePath}`, sha, size });
|
|
1689
1696
|
}
|
|
1690
|
-
|
|
1697
|
+
logger6.debug("Files uploaded", { count: files.length });
|
|
1691
1698
|
const deployment = await vercelFetch(
|
|
1692
1699
|
token,
|
|
1693
|
-
"/v13/deployments",
|
|
1700
|
+
"/v13/deployments?prebuilt=1",
|
|
1694
1701
|
{
|
|
1695
1702
|
method: "POST",
|
|
1696
1703
|
body: JSON.stringify({
|
|
1697
1704
|
name: projectId,
|
|
1698
1705
|
files,
|
|
1699
|
-
|
|
1706
|
+
version: 2,
|
|
1707
|
+
project: projectId,
|
|
1708
|
+
projectSettings: { framework: null, buildCommand: "", installCommand: "" }
|
|
1700
1709
|
})
|
|
1701
1710
|
}
|
|
1702
1711
|
);
|
|
@@ -1707,14 +1716,14 @@ function createVercelProvider() {
|
|
|
1707
1716
|
status: finalStatus,
|
|
1708
1717
|
envVarsSet: []
|
|
1709
1718
|
};
|
|
1710
|
-
|
|
1719
|
+
logger6.info("Vercel deployment complete", {
|
|
1711
1720
|
deploymentId: deployment.id,
|
|
1712
1721
|
url: result.url,
|
|
1713
1722
|
status: finalStatus
|
|
1714
1723
|
});
|
|
1715
1724
|
return ok(result);
|
|
1716
1725
|
} catch (error) {
|
|
1717
|
-
|
|
1726
|
+
logger6.error("Vercel deployment failed", { projectId, error });
|
|
1718
1727
|
return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error));
|
|
1719
1728
|
}
|
|
1720
1729
|
}
|
|
@@ -1759,11 +1768,11 @@ async function pollDeploymentStatus(token, deploymentId) {
|
|
|
1759
1768
|
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
1760
1769
|
return "error";
|
|
1761
1770
|
}
|
|
1762
|
-
await new Promise((
|
|
1771
|
+
await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
|
|
1763
1772
|
}
|
|
1764
1773
|
return "building";
|
|
1765
1774
|
}
|
|
1766
|
-
var
|
|
1775
|
+
var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
|
|
1767
1776
|
function createAliyunProvisioner() {
|
|
1768
1777
|
let accessKey = null;
|
|
1769
1778
|
let secretKey = null;
|
|
@@ -1772,7 +1781,7 @@ function createAliyunProvisioner() {
|
|
|
1772
1781
|
name: "aliyun",
|
|
1773
1782
|
serviceType: "sms",
|
|
1774
1783
|
async authenticate(credentials) {
|
|
1775
|
-
|
|
1784
|
+
logger7.debug("Authenticating with Aliyun");
|
|
1776
1785
|
try {
|
|
1777
1786
|
const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
|
|
1778
1787
|
const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
|
|
@@ -1782,10 +1791,10 @@ function createAliyunProvisioner() {
|
|
|
1782
1791
|
accessKey = akId;
|
|
1783
1792
|
secretKey = akSecret;
|
|
1784
1793
|
signName = credentials.signName;
|
|
1785
|
-
|
|
1794
|
+
logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
|
|
1786
1795
|
return ok(akId);
|
|
1787
1796
|
} catch (error) {
|
|
1788
|
-
|
|
1797
|
+
logger7.error("Aliyun authentication failed", { error });
|
|
1789
1798
|
return err(
|
|
1790
1799
|
HaiDeployError.AUTH_FAILED,
|
|
1791
1800
|
deployM("deploy_authFailed", {
|
|
@@ -1802,7 +1811,7 @@ function createAliyunProvisioner() {
|
|
|
1802
1811
|
deployM("deploy_authRequired")
|
|
1803
1812
|
);
|
|
1804
1813
|
}
|
|
1805
|
-
|
|
1814
|
+
logger7.debug("Provisioning Aliyun SMS service");
|
|
1806
1815
|
return ok({
|
|
1807
1816
|
serviceType: "sms",
|
|
1808
1817
|
provisionerName: "aliyun",
|
|
@@ -1815,7 +1824,7 @@ function createAliyunProvisioner() {
|
|
|
1815
1824
|
}
|
|
1816
1825
|
};
|
|
1817
1826
|
}
|
|
1818
|
-
var
|
|
1827
|
+
var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
|
|
1819
1828
|
var NEON_API = "https://console.neon.tech/api/v2";
|
|
1820
1829
|
var DEFAULT_NEON_DATABASE_NAME = "neondb";
|
|
1821
1830
|
var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
|
|
@@ -1874,7 +1883,7 @@ function createNeonProvisioner() {
|
|
|
1874
1883
|
name: "neon",
|
|
1875
1884
|
serviceType: "db",
|
|
1876
1885
|
async authenticate(credentials) {
|
|
1877
|
-
|
|
1886
|
+
logger8.debug("Authenticating with Neon");
|
|
1878
1887
|
try {
|
|
1879
1888
|
const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
|
|
1880
1889
|
if (!apiToken) {
|
|
@@ -1882,10 +1891,10 @@ function createNeonProvisioner() {
|
|
|
1882
1891
|
}
|
|
1883
1892
|
await listNeonProjects(apiToken);
|
|
1884
1893
|
token = apiToken;
|
|
1885
|
-
|
|
1894
|
+
logger8.info("Neon authenticated");
|
|
1886
1895
|
return ok("neon-user");
|
|
1887
1896
|
} catch (error) {
|
|
1888
|
-
|
|
1897
|
+
logger8.error("Neon authentication failed", { error });
|
|
1889
1898
|
return err(
|
|
1890
1899
|
HaiDeployError.AUTH_FAILED,
|
|
1891
1900
|
deployM("deploy_authFailed", {
|
|
@@ -1902,7 +1911,7 @@ function createNeonProvisioner() {
|
|
|
1902
1911
|
deployM("deploy_authRequired")
|
|
1903
1912
|
);
|
|
1904
1913
|
}
|
|
1905
|
-
|
|
1914
|
+
logger8.debug("Provisioning Neon database", { appName });
|
|
1906
1915
|
let resource;
|
|
1907
1916
|
try {
|
|
1908
1917
|
const projectName = `${appName}-db`;
|
|
@@ -1910,7 +1919,7 @@ function createNeonProvisioner() {
|
|
|
1910
1919
|
if (existingProject !== null) {
|
|
1911
1920
|
resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
|
|
1912
1921
|
const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
|
|
1913
|
-
|
|
1922
|
+
logger8.info("Neon project reused", {
|
|
1914
1923
|
projectId: existingProject.id,
|
|
1915
1924
|
projectName
|
|
1916
1925
|
});
|
|
@@ -1932,10 +1941,10 @@ function createNeonProvisioner() {
|
|
|
1932
1941
|
if (!projectId || !connectionUri) {
|
|
1933
1942
|
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
|
|
1934
1943
|
}
|
|
1935
|
-
|
|
1944
|
+
logger8.info("Neon database provisioned", { projectId });
|
|
1936
1945
|
return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
|
|
1937
1946
|
} catch (error) {
|
|
1938
|
-
|
|
1947
|
+
logger8.error("Neon provisioning failed", { appName });
|
|
1939
1948
|
if (resource) {
|
|
1940
1949
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
1941
1950
|
projectName: appName,
|
|
@@ -1957,7 +1966,7 @@ function createNeonProvisioner() {
|
|
|
1957
1966
|
}
|
|
1958
1967
|
};
|
|
1959
1968
|
}
|
|
1960
|
-
var
|
|
1969
|
+
var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
|
|
1961
1970
|
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
1962
1971
|
function createR2Provisioner() {
|
|
1963
1972
|
let token = null;
|
|
@@ -1968,7 +1977,7 @@ function createR2Provisioner() {
|
|
|
1968
1977
|
name: "r2",
|
|
1969
1978
|
serviceType: "storage",
|
|
1970
1979
|
async authenticate(credentials) {
|
|
1971
|
-
|
|
1980
|
+
logger9.debug("Authenticating with Cloudflare");
|
|
1972
1981
|
try {
|
|
1973
1982
|
const acctId = credentials.accountId ?? credentials.account_id ?? "";
|
|
1974
1983
|
const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
|
|
@@ -1985,10 +1994,10 @@ function createR2Provisioner() {
|
|
|
1985
1994
|
token = apiTok;
|
|
1986
1995
|
accessKey = credentials.accessKeyId;
|
|
1987
1996
|
secretKey = credentials.secretAccessKey;
|
|
1988
|
-
|
|
1997
|
+
logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
|
|
1989
1998
|
return ok(acctId);
|
|
1990
1999
|
} catch (error) {
|
|
1991
|
-
|
|
2000
|
+
logger9.error("Cloudflare R2 authentication failed", { error });
|
|
1992
2001
|
return err(
|
|
1993
2002
|
HaiDeployError.AUTH_FAILED,
|
|
1994
2003
|
deployM("deploy_authFailed", {
|
|
@@ -2006,7 +2015,7 @@ function createR2Provisioner() {
|
|
|
2006
2015
|
);
|
|
2007
2016
|
}
|
|
2008
2017
|
const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
2009
|
-
|
|
2018
|
+
logger9.debug("Provisioning R2 bucket", { appName, bucketName });
|
|
2010
2019
|
try {
|
|
2011
2020
|
const createRes = await fetch(
|
|
2012
2021
|
`${CF_API}/accounts/${accountId}/r2/buckets`,
|
|
@@ -2022,7 +2031,7 @@ function createR2Provisioner() {
|
|
|
2022
2031
|
if (!createRes.ok && createRes.status !== 409) {
|
|
2023
2032
|
throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
|
|
2024
2033
|
}
|
|
2025
|
-
|
|
2034
|
+
logger9.info("R2 bucket provisioned", { bucketName });
|
|
2026
2035
|
return ok({
|
|
2027
2036
|
serviceType: "storage",
|
|
2028
2037
|
provisionerName: "r2",
|
|
@@ -2040,7 +2049,7 @@ function createR2Provisioner() {
|
|
|
2040
2049
|
resourceInfo: `r2-bucket:${bucketName}`
|
|
2041
2050
|
});
|
|
2042
2051
|
} catch (error) {
|
|
2043
|
-
|
|
2052
|
+
logger9.error("R2 provisioning failed", { appName, error });
|
|
2044
2053
|
return err(
|
|
2045
2054
|
HaiDeployError.PROVISION_FAILED,
|
|
2046
2055
|
deployM("deploy_provisionFailed", {
|
|
@@ -2055,7 +2064,7 @@ function createR2Provisioner() {
|
|
|
2055
2064
|
}
|
|
2056
2065
|
};
|
|
2057
2066
|
}
|
|
2058
|
-
var
|
|
2067
|
+
var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
|
|
2059
2068
|
var RESEND_API = "https://api.resend.com";
|
|
2060
2069
|
function createResendProvisioner() {
|
|
2061
2070
|
let token = null;
|
|
@@ -2064,7 +2073,7 @@ function createResendProvisioner() {
|
|
|
2064
2073
|
name: "resend",
|
|
2065
2074
|
serviceType: "email",
|
|
2066
2075
|
async authenticate(credentials) {
|
|
2067
|
-
|
|
2076
|
+
logger10.debug("Authenticating with Resend");
|
|
2068
2077
|
try {
|
|
2069
2078
|
const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
2070
2079
|
if (!apiToken || !credentials.from) {
|
|
@@ -2078,10 +2087,10 @@ function createResendProvisioner() {
|
|
|
2078
2087
|
}
|
|
2079
2088
|
token = apiToken;
|
|
2080
2089
|
from = credentials.from;
|
|
2081
|
-
|
|
2090
|
+
logger10.info("Resend authenticated");
|
|
2082
2091
|
return ok("resend-user");
|
|
2083
2092
|
} catch (error) {
|
|
2084
|
-
|
|
2093
|
+
logger10.error("Resend authentication failed", { error });
|
|
2085
2094
|
return err(
|
|
2086
2095
|
HaiDeployError.AUTH_FAILED,
|
|
2087
2096
|
deployM("deploy_authFailed", {
|
|
@@ -2098,7 +2107,7 @@ function createResendProvisioner() {
|
|
|
2098
2107
|
deployM("deploy_authRequired")
|
|
2099
2108
|
);
|
|
2100
2109
|
}
|
|
2101
|
-
|
|
2110
|
+
logger10.debug("Provisioning Resend email service");
|
|
2102
2111
|
return ok({
|
|
2103
2112
|
serviceType: "email",
|
|
2104
2113
|
provisionerName: "resend",
|
|
@@ -2111,7 +2120,7 @@ function createResendProvisioner() {
|
|
|
2111
2120
|
}
|
|
2112
2121
|
};
|
|
2113
2122
|
}
|
|
2114
|
-
var
|
|
2123
|
+
var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
|
|
2115
2124
|
var UPSTASH_API = "https://api.upstash.com";
|
|
2116
2125
|
function createUpstashAuthHeader(email, apiKey) {
|
|
2117
2126
|
return `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
|
|
@@ -2162,7 +2171,7 @@ function createUpstashProvisioner() {
|
|
|
2162
2171
|
name: "upstash",
|
|
2163
2172
|
serviceType: "cache",
|
|
2164
2173
|
async authenticate(credentials) {
|
|
2165
|
-
|
|
2174
|
+
logger11.debug("Authenticating with Upstash");
|
|
2166
2175
|
try {
|
|
2167
2176
|
const userEmail = credentials.email ?? "";
|
|
2168
2177
|
const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
@@ -2172,10 +2181,10 @@ function createUpstashProvisioner() {
|
|
|
2172
2181
|
await listUpstashDatabases(userEmail, userKey);
|
|
2173
2182
|
email = userEmail;
|
|
2174
2183
|
apiKey = userKey;
|
|
2175
|
-
|
|
2184
|
+
logger11.info("Upstash authenticated", { email: userEmail });
|
|
2176
2185
|
return ok(userEmail);
|
|
2177
2186
|
} catch (error) {
|
|
2178
|
-
|
|
2187
|
+
logger11.error("Upstash authentication failed", { error });
|
|
2179
2188
|
return err(
|
|
2180
2189
|
HaiDeployError.AUTH_FAILED,
|
|
2181
2190
|
deployM("deploy_authFailed", {
|
|
@@ -2192,7 +2201,7 @@ function createUpstashProvisioner() {
|
|
|
2192
2201
|
deployM("deploy_authRequired")
|
|
2193
2202
|
);
|
|
2194
2203
|
}
|
|
2195
|
-
|
|
2204
|
+
logger11.debug("Provisioning Upstash Redis", { appName });
|
|
2196
2205
|
let resource;
|
|
2197
2206
|
try {
|
|
2198
2207
|
const databaseName = `${appName}-cache`;
|
|
@@ -2204,7 +2213,7 @@ function createUpstashProvisioner() {
|
|
|
2204
2213
|
}
|
|
2205
2214
|
resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
|
|
2206
2215
|
const detail = await getUpstashDatabase(email, apiKey, databaseId);
|
|
2207
|
-
|
|
2216
|
+
logger11.info("Upstash Redis reused", { databaseId, databaseName });
|
|
2208
2217
|
return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
|
|
2209
2218
|
}
|
|
2210
2219
|
const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
|
|
@@ -2217,10 +2226,10 @@ function createUpstashProvisioner() {
|
|
|
2217
2226
|
});
|
|
2218
2227
|
if (data.database_id)
|
|
2219
2228
|
resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
|
|
2220
|
-
|
|
2229
|
+
logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
|
|
2221
2230
|
return ok(buildUpstashProvisionResult(data, "created"));
|
|
2222
2231
|
} catch (error) {
|
|
2223
|
-
|
|
2232
|
+
logger11.error("Upstash provisioning failed", { appName });
|
|
2224
2233
|
if (resource) {
|
|
2225
2234
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
2226
2235
|
projectName: appName,
|
|
@@ -2244,7 +2253,7 @@ function createUpstashProvisioner() {
|
|
|
2244
2253
|
}
|
|
2245
2254
|
|
|
2246
2255
|
// src/deploy-main.ts
|
|
2247
|
-
var
|
|
2256
|
+
var logger12 = core.logger.child({ module: "deploy", scope: "main" });
|
|
2248
2257
|
var currentConfig = null;
|
|
2249
2258
|
var currentProvider = null;
|
|
2250
2259
|
var currentContainerProvider = null;
|
|
@@ -2329,7 +2338,7 @@ var deploy = {
|
|
|
2329
2338
|
credentials: deployCredentials,
|
|
2330
2339
|
async init(config) {
|
|
2331
2340
|
if (initInProgress) {
|
|
2332
|
-
|
|
2341
|
+
logger12.warn("Deploy init already in progress, skipping concurrent call");
|
|
2333
2342
|
return err(
|
|
2334
2343
|
HaiDeployError.CONFIG_ERROR,
|
|
2335
2344
|
deployM("deploy_configError", { params: { error: "init already in progress" } })
|
|
@@ -2338,13 +2347,13 @@ var deploy = {
|
|
|
2338
2347
|
initInProgress = true;
|
|
2339
2348
|
try {
|
|
2340
2349
|
if (currentProvider || currentContainerProvider) {
|
|
2341
|
-
|
|
2350
|
+
logger12.warn("Deploy module is already initialized, reinitializing");
|
|
2342
2351
|
await deploy.close();
|
|
2343
2352
|
}
|
|
2344
|
-
|
|
2353
|
+
logger12.info("Initializing deploy module");
|
|
2345
2354
|
const parseResult = DeployConfigSchema.safeParse(config);
|
|
2346
2355
|
if (!parseResult.success) {
|
|
2347
|
-
|
|
2356
|
+
logger12.error("Deploy config validation failed", { error: parseResult.error.message });
|
|
2348
2357
|
return err(
|
|
2349
2358
|
HaiDeployError.CONFIG_ERROR,
|
|
2350
2359
|
deployM("deploy_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -2357,14 +2366,14 @@ var deploy = {
|
|
|
2357
2366
|
let containerProvider = null;
|
|
2358
2367
|
if (parsed.provider.type === "docker-ssh") {
|
|
2359
2368
|
containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
|
|
2360
|
-
|
|
2369
|
+
logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
|
|
2361
2370
|
} else {
|
|
2362
2371
|
provider = createProviderByType(parsed.provider.type);
|
|
2363
2372
|
const authResult = await provider.authenticate(parsed.provider.token);
|
|
2364
2373
|
if (!authResult.success) {
|
|
2365
2374
|
return err(authResult.error);
|
|
2366
2375
|
}
|
|
2367
|
-
|
|
2376
|
+
logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
|
|
2368
2377
|
}
|
|
2369
2378
|
currentProvisioners.clear();
|
|
2370
2379
|
if (parsed.services) {
|
|
@@ -2376,14 +2385,14 @@ var deploy = {
|
|
|
2376
2385
|
const credentials = extractCredentials(serviceConfig);
|
|
2377
2386
|
const provAuthResult = await provisioner.authenticate(credentials);
|
|
2378
2387
|
if (!provAuthResult.success) {
|
|
2379
|
-
|
|
2388
|
+
logger12.error("Provisioner authentication failed", {
|
|
2380
2389
|
service: serviceType,
|
|
2381
2390
|
provisioner: serviceConfig.provisioner
|
|
2382
2391
|
});
|
|
2383
2392
|
currentProvisioners.clear();
|
|
2384
2393
|
return err(provAuthResult.error);
|
|
2385
2394
|
}
|
|
2386
|
-
|
|
2395
|
+
logger12.info("Provisioner authenticated", {
|
|
2387
2396
|
service: serviceType,
|
|
2388
2397
|
user: provAuthResult.data
|
|
2389
2398
|
});
|
|
@@ -2393,7 +2402,7 @@ var deploy = {
|
|
|
2393
2402
|
currentProvider = provider;
|
|
2394
2403
|
currentContainerProvider = containerProvider;
|
|
2395
2404
|
currentConfig = parsed;
|
|
2396
|
-
|
|
2405
|
+
logger12.info("Deploy module initialized", {
|
|
2397
2406
|
provider: parsed.provider.type,
|
|
2398
2407
|
provisioners: [...currentProvisioners.keys()]
|
|
2399
2408
|
});
|
|
@@ -2403,7 +2412,7 @@ var deploy = {
|
|
|
2403
2412
|
currentContainerProvider = null;
|
|
2404
2413
|
currentConfig = null;
|
|
2405
2414
|
currentProvisioners.clear();
|
|
2406
|
-
|
|
2415
|
+
logger12.error("Deploy module initialization failed", { error });
|
|
2407
2416
|
return err(
|
|
2408
2417
|
HaiDeployError.CONFIG_ERROR,
|
|
2409
2418
|
deployM("deploy_configError", {
|
|
@@ -2418,15 +2427,15 @@ var deploy = {
|
|
|
2418
2427
|
},
|
|
2419
2428
|
async close() {
|
|
2420
2429
|
if (!currentProvider && !currentContainerProvider) {
|
|
2421
|
-
|
|
2430
|
+
logger12.info("Deploy module already closed, skipping");
|
|
2422
2431
|
return;
|
|
2423
2432
|
}
|
|
2424
|
-
|
|
2433
|
+
logger12.info("Closing deploy module");
|
|
2425
2434
|
currentProvider = null;
|
|
2426
2435
|
currentContainerProvider = null;
|
|
2427
2436
|
currentConfig = null;
|
|
2428
2437
|
currentProvisioners.clear();
|
|
2429
|
-
|
|
2438
|
+
logger12.info("Deploy module closed");
|
|
2430
2439
|
},
|
|
2431
2440
|
get config() {
|
|
2432
2441
|
return currentConfig;
|
|
@@ -2435,22 +2444,22 @@ var deploy = {
|
|
|
2435
2444
|
return currentProvider !== null || currentContainerProvider !== null;
|
|
2436
2445
|
},
|
|
2437
2446
|
async scan(appDir, options) {
|
|
2438
|
-
|
|
2447
|
+
logger12.debug("Scanning application", { appDir });
|
|
2439
2448
|
return scanApp(appDir, options);
|
|
2440
2449
|
},
|
|
2441
2450
|
async provisionAll(projectName, services) {
|
|
2442
2451
|
if (!currentProvider && !currentContainerProvider) {
|
|
2443
2452
|
return notInitialized.result();
|
|
2444
2453
|
}
|
|
2445
|
-
|
|
2454
|
+
logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
|
|
2446
2455
|
const results = [];
|
|
2447
2456
|
for (const [serviceType, provisioner] of currentProvisioners) {
|
|
2448
2457
|
if (services && !services.includes(serviceType))
|
|
2449
2458
|
continue;
|
|
2450
|
-
|
|
2459
|
+
logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
|
|
2451
2460
|
const provResult = await provisioner.provision(projectName);
|
|
2452
2461
|
if (!provResult.success) {
|
|
2453
|
-
|
|
2462
|
+
logger12.error("Service provisioning failed", {
|
|
2454
2463
|
serviceType,
|
|
2455
2464
|
provisioner: provisioner.name,
|
|
2456
2465
|
code: provResult.error.code
|
|
@@ -2467,7 +2476,7 @@ var deploy = {
|
|
|
2467
2476
|
});
|
|
2468
2477
|
}
|
|
2469
2478
|
results.push(provResult.data);
|
|
2470
|
-
|
|
2479
|
+
logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
|
|
2471
2480
|
}
|
|
2472
2481
|
return ok(results);
|
|
2473
2482
|
},
|
|
@@ -2475,7 +2484,7 @@ var deploy = {
|
|
|
2475
2484
|
if (!currentProvider && !currentContainerProvider) {
|
|
2476
2485
|
return notInitialized.result();
|
|
2477
2486
|
}
|
|
2478
|
-
|
|
2487
|
+
logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
|
|
2479
2488
|
const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
|
|
2480
2489
|
const scanResult = await scanApp(appDir, scanOptions);
|
|
2481
2490
|
if (!scanResult.success) {
|
|
@@ -2491,11 +2500,13 @@ var deploy = {
|
|
|
2491
2500
|
options = {
|
|
2492
2501
|
...options,
|
|
2493
2502
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
2494
|
-
...options?.
|
|
2503
|
+
...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
|
|
2495
2504
|
};
|
|
2496
2505
|
if (currentContainerProvider) {
|
|
2497
2506
|
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
2498
2507
|
}
|
|
2508
|
+
if (options?.bundleOnly)
|
|
2509
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
|
|
2499
2510
|
if (!currentProvider) {
|
|
2500
2511
|
return notInitialized.result();
|
|
2501
2512
|
}
|
|
@@ -2507,9 +2518,11 @@ var deploy = {
|
|
|
2507
2518
|
})
|
|
2508
2519
|
);
|
|
2509
2520
|
}
|
|
2521
|
+
if (!existsSync(resolveOutputDir(appDir, "vercel")))
|
|
2522
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
2510
2523
|
let allEnvVars = appEnv;
|
|
2511
2524
|
const recovery = { projectName, stage: "provision", resources: [] };
|
|
2512
|
-
if (!options?.
|
|
2525
|
+
if (!options?.skipMiddleware) {
|
|
2513
2526
|
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
2514
2527
|
if (!provResults.success) {
|
|
2515
2528
|
return err(provResults.error);
|
|
@@ -2534,12 +2547,6 @@ var deploy = {
|
|
|
2534
2547
|
return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
|
|
2535
2548
|
}
|
|
2536
2549
|
}
|
|
2537
|
-
if (!options?.skipBuild) {
|
|
2538
|
-
const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
|
|
2539
|
-
if (!buildResult.success) {
|
|
2540
|
-
return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
2550
|
const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
|
|
2544
2551
|
const deployResult = await currentProvider.deploy(projectId, outputDir);
|
|
2545
2552
|
if (!deployResult.success) {
|
|
@@ -2549,7 +2556,7 @@ var deploy = {
|
|
|
2549
2556
|
...deployResult.data,
|
|
2550
2557
|
envVarsSet: Object.keys(allEnvVars)
|
|
2551
2558
|
};
|
|
2552
|
-
|
|
2559
|
+
logger12.info("Deployment completed", {
|
|
2553
2560
|
url: result.url,
|
|
2554
2561
|
deploymentId: result.deploymentId,
|
|
2555
2562
|
status: result.status
|
|
@@ -2559,21 +2566,30 @@ var deploy = {
|
|
|
2559
2566
|
};
|
|
2560
2567
|
async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
|
|
2561
2568
|
const projectName = normalizeProjectName(rawProjectName);
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2569
|
+
let artifact;
|
|
2570
|
+
let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
|
|
2571
|
+
try {
|
|
2572
|
+
if (existsSync(join(artifactDir, "latest"))) {
|
|
2573
|
+
const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
|
|
2574
|
+
if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
|
|
2575
|
+
throw new Error("Invalid artifact pointer");
|
|
2576
|
+
artifactDir = join(artifactDir, latest);
|
|
2577
|
+
}
|
|
2578
|
+
artifact = await readDeploymentArtifact(artifactDir);
|
|
2579
|
+
} catch {
|
|
2580
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
2567
2581
|
}
|
|
2582
|
+
if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
|
|
2583
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
2568
2584
|
let provisionEnv = {};
|
|
2569
|
-
if (!options?.
|
|
2585
|
+
if (!options?.skipMiddleware) {
|
|
2570
2586
|
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
2571
2587
|
if (!provResults.success) {
|
|
2572
2588
|
return err(provResults.error);
|
|
2573
2589
|
}
|
|
2574
2590
|
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
2575
2591
|
}
|
|
2576
|
-
return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
|
|
2592
|
+
return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
|
|
2577
2593
|
}
|
|
2578
2594
|
|
|
2579
2595
|
// src/deploy-middleware.ts
|
|
@@ -2621,6 +2637,6 @@ function describeMiddlewareBackend(backend) {
|
|
|
2621
2637
|
return deployM(backend.purpose);
|
|
2622
2638
|
}
|
|
2623
2639
|
|
|
2624
|
-
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, saveDeploymentEnvironment };
|
|
2640
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
|
|
2625
2641
|
//# sourceMappingURL=index.js.map
|
|
2626
2642
|
//# sourceMappingURL=index.js.map
|