@ranger1/dx 0.1.82 → 0.1.84

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.
@@ -8,6 +8,11 @@ import { basenameOrThrow, resolveWithinBase } from './path-utils.js'
8
8
  import { createRuntimePackage } from './runtime-package.js'
9
9
 
10
10
  const execFileAsync = promisify(execFile)
11
+ const tarEnv = {
12
+ ...process.env,
13
+ COPYFILE_DISABLE: '1',
14
+ COPY_EXTENDED_ATTRIBUTES_DISABLE: '1',
15
+ }
11
16
 
12
17
  function assertSafeNamePart(value, label) {
13
18
  const text = String(value || '').trim()
@@ -125,15 +130,21 @@ async function defaultCreateInnerArchive({ stageDir, innerArchivePath }) {
125
130
  await mkdir(dirname(innerArchivePath), { recursive: true })
126
131
  await execFileAsync('tar', ['-czf', innerArchivePath, '.'], {
127
132
  cwd: stageDir,
133
+ env: tarEnv,
128
134
  })
129
135
  }
130
136
 
131
137
  async function defaultWriteChecksum({ archivePath, checksumPath }) {
138
+ const archiveName = basename(archivePath)
132
139
  try {
133
- const { stdout } = await execFileAsync('sha256sum', [archivePath])
140
+ const { stdout } = await execFileAsync('sha256sum', [archiveName], {
141
+ cwd: dirname(archivePath),
142
+ })
134
143
  await writeFile(checksumPath, stdout)
135
144
  } catch {
136
- const { stdout } = await execFileAsync('shasum', ['-a', '256', archivePath])
145
+ const { stdout } = await execFileAsync('shasum', ['-a', '256', archiveName], {
146
+ cwd: dirname(archivePath),
147
+ })
137
148
  await writeFile(checksumPath, stdout)
138
149
  }
139
150
  }
@@ -142,7 +153,10 @@ async function defaultCreateBundle({ outputDir, bundlePath, innerArchivePath, ch
142
153
  await execFileAsync(
143
154
  'tar',
144
155
  ['-czf', bundlePath, basename(innerArchivePath), basename(checksumPath)],
145
- { cwd: outputDir },
156
+ {
157
+ cwd: outputDir,
158
+ env: tarEnv,
159
+ },
146
160
  )
147
161
  }
148
162
 
@@ -16,6 +16,32 @@ function requirePositiveInteger(value, fieldPath) {
16
16
  return parsed
17
17
  }
18
18
 
19
+ function resolveVerifyConfig(verifyConfig = {}) {
20
+ const healthCheckConfig = verifyConfig?.healthCheck
21
+ if (healthCheckConfig == null) {
22
+ return {
23
+ healthCheck: null,
24
+ }
25
+ }
26
+
27
+ const url = requireString(healthCheckConfig.url, 'verify.healthCheck.url')
28
+ try {
29
+ new URL(url)
30
+ } catch {
31
+ throw new Error(`缺少必填配置: verify.healthCheck.url`)
32
+ }
33
+
34
+ return {
35
+ healthCheck: {
36
+ url,
37
+ timeoutSeconds:
38
+ healthCheckConfig.timeoutSeconds == null
39
+ ? 10
40
+ : requirePositiveInteger(healthCheckConfig.timeoutSeconds, 'verify.healthCheck.timeoutSeconds'),
41
+ },
42
+ }
43
+ }
44
+
19
45
  function resolveBuildCommand(buildConfig, environment) {
20
46
  if (buildConfig?.commands && typeof buildConfig.commands === 'object') {
21
47
  const selected = buildConfig.commands[environment]
@@ -55,6 +81,7 @@ export function resolveBackendDeployConfig({ cli, targetConfig, environment, fla
55
81
  const remoteConfig = deployConfig.remote || null
56
82
  const startupConfig = deployConfig.startup || {}
57
83
  const runConfig = deployConfig.deploy || {}
84
+ const verifyConfig = deployConfig.verify || {}
58
85
  const buildOnly = Boolean(flags.buildOnly)
59
86
  const startupMode = String(startupConfig.mode || 'pm2').trim()
60
87
  const prismaGenerate = runConfig.prismaGenerate !== false
@@ -117,6 +144,7 @@ export function resolveBackendDeployConfig({ cli, targetConfig, environment, fla
117
144
  prismaMigrateDeploy,
118
145
  skipMigration: Boolean(flags.skipMigration),
119
146
  },
147
+ verify: resolveVerifyConfig(verifyConfig),
120
148
  }
121
149
 
122
150
  if (!['pm2', 'direct'].includes(normalized.startup.mode)) {
@@ -8,6 +8,7 @@ export function createRemotePhaseModel(payload) {
8
8
  { phase: 'prisma-migrate', payload },
9
9
  { phase: 'switch-current', payload },
10
10
  { phase: 'startup', payload },
11
+ { phase: 'verify', payload },
11
12
  { phase: 'cleanup', payload },
12
13
  ]
13
14
  }
@@ -8,7 +8,11 @@ export function buildRemoteDeployScript(phaseModel = []) {
8
8
  const runtime = payload.runtime || {}
9
9
  const startup = payload.startup || {}
10
10
  const deploy = payload.deploy || {}
11
+ const verify = payload.verify || {}
12
+ const healthCheck = verify.healthCheck || null
11
13
  const environment = String(payload.environment || 'production')
14
+ const expectedAppEnv = environment
15
+ const expectedNodeEnv = environment === 'development' ? 'development' : 'production'
12
16
  const baseDir = String(remote.baseDir || '.')
13
17
  const releaseDir = `${baseDir}/releases/${payload.versionName || 'unknown'}`
14
18
  const currentLink = `${baseDir}/current`
@@ -26,6 +30,8 @@ export function buildRemoteDeployScript(phaseModel = []) {
26
30
  const keepReleases = Number(deploy.keepReleases || 5)
27
31
  const shouldGenerate = deploy.prismaGenerate !== false
28
32
  const shouldMigrate = deploy.prismaMigrateDeploy !== false && deploy.skipMigration !== true
33
+ const healthCheckUrl = healthCheck?.url ? String(healthCheck.url) : ''
34
+ const healthCheckTimeoutSeconds = Number(healthCheck?.timeoutSeconds || 10)
29
35
 
30
36
  return `#!/usr/bin/env bash
31
37
  set -euo pipefail
@@ -36,6 +42,8 @@ ARCHIVE=${escapeShell(uploadedBundlePath)}
36
42
  RELEASE_DIR=${escapeShell(releaseDir)}
37
43
  CURRENT_LINK=${escapeShell(currentLink)}
38
44
  ENV_NAME=${escapeShell(environment)}
45
+ EXPECTED_APP_ENV=${escapeShell(expectedAppEnv)}
46
+ EXPECTED_NODE_ENV=${escapeShell(expectedNodeEnv)}
39
47
  ENV_FILE_NAME=${escapeShell(envFileName)}
40
48
  ENV_LOCAL_FILE_NAME=${escapeShell(envLocalFileName)}
41
49
  PRISMA_SCHEMA=${escapeShell(prismaSchema)}
@@ -45,6 +53,8 @@ INSTALL_COMMAND=${escapeShell(installCommand)}
45
53
  START_MODE=${escapeShell(startupMode)}
46
54
  SERVICE_NAME=${escapeShell(serviceName)}
47
55
  START_ENTRY=${escapeShell(startupEntry)}
56
+ HEALTHCHECK_URL=${escapeShell(healthCheckUrl)}
57
+ HEALTHCHECK_TIMEOUT_SECONDS=${healthCheckTimeoutSeconds}
48
58
  KEEP_RELEASES=${keepReleases}
49
59
  SHOULD_GENERATE=${shouldGenerate ? '1' : '0'}
50
60
  SHOULD_MIGRATE=${shouldMigrate ? '1' : '0'}
@@ -157,15 +167,16 @@ find_single_bundle_file() {
157
167
 
158
168
  sha256_check() {
159
169
  local checksum_file="$1"
160
- if command -v sha256sum >/dev/null 2>&1; then
161
- sha256sum -c "$checksum_file"
162
- return
163
- fi
164
- local checksum expected file
170
+ local checksum file actual
165
171
  checksum="$(awk '{print $1}' "$checksum_file")"
166
172
  file="$(awk '{print $2}' "$checksum_file")"
167
- expected="$(shasum -a 256 "$file" | awk '{print $1}')"
168
- [[ "$checksum" == "$expected" ]]
173
+ file="$(basename "$file")"
174
+ if command -v sha256sum >/dev/null 2>&1; then
175
+ actual="$(sha256sum "$file" | awk '{print $1}')"
176
+ else
177
+ actual="$(shasum -a 256 "$file" | awk '{print $1}')"
178
+ fi
179
+ [[ "$checksum" == "$actual" ]]
169
180
  }
170
181
 
171
182
  run_with_env() {
@@ -173,10 +184,22 @@ run_with_env() {
173
184
  shift
174
185
  (
175
186
  cd "$cwd"
176
- APP_ENV="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- "$@"
187
+ APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
188
+ "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- "$@"
177
189
  )
178
190
  }
179
191
 
192
+ read_pm2_env_var() {
193
+ local key="$1"
194
+ pm2 jlist | node -e '
195
+ const fs = require("node:fs")
196
+ const key = process.argv[1]
197
+ const list = JSON.parse(fs.readFileSync(0, "utf8"))
198
+ const app = list.find(item => item?.name === process.argv[2])
199
+ process.stdout.write(String(app?.pm2_env?.[key] || ""))
200
+ ' "$key" "$SERVICE_NAME"
201
+ }
202
+
180
203
  attempt_pm2_restore() {
181
204
  if [[ -z "$PREVIOUS_CURRENT_TARGET" || ! -e "$PREVIOUS_CURRENT_TARGET/$ECOSYSTEM_CONFIG" ]]; then
182
205
  ROLLBACK_SUCCEEDED=false
@@ -184,7 +207,8 @@ attempt_pm2_restore() {
184
207
  fi
185
208
  if (
186
209
  cd "$PREVIOUS_CURRENT_TARGET"
187
- APP_ENV="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
210
+ APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
211
+ "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
188
212
  pm2 start "$ECOSYSTEM_CONFIG" --only "$SERVICE_NAME" --update-env
189
213
  pm2 save
190
214
  ); then
@@ -213,7 +237,7 @@ CURRENT_PHASE="extract"
213
237
  echo "DX_REMOTE_PHASE=extract"
214
238
  validate_archive_entries "$ARCHIVE"
215
239
  BUNDLE_TEMP_DIR="$(mktemp -d "$APP_ROOT/.bundle-extract.XXXXXX")"
216
- tar -xzf "$ARCHIVE" -C "$BUNDLE_TEMP_DIR" --strip-components=1
240
+ tar -xzf "$ARCHIVE" -C "$BUNDLE_TEMP_DIR"
217
241
 
218
242
  INNER_ARCHIVE="$(find_single_bundle_file "$BUNDLE_TEMP_DIR" 'backend-v*.tgz')"
219
243
  INNER_ARCHIVE_SHA256_FILE="$(find_single_bundle_file "$BUNDLE_TEMP_DIR" 'backend-v*.tgz.sha256')"
@@ -290,7 +314,8 @@ if [[ "$START_MODE" == "pm2" ]]; then
290
314
  if ! (
291
315
  cd "$CURRENT_LINK"
292
316
  pm2 delete "$SERVICE_NAME" || true
293
- APP_ENV="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
317
+ APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
318
+ "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
294
319
  pm2 start "$ECOSYSTEM_CONFIG" --only "$SERVICE_NAME" --update-env
295
320
  pm2 save
296
321
  ); then
@@ -305,7 +330,8 @@ if [[ "$START_MODE" == "pm2" ]]; then
305
330
  else
306
331
  if ! (
307
332
  cd "$CURRENT_LINK"
308
- APP_ENV="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
333
+ APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
334
+ "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
309
335
  node "$START_ENTRY"
310
336
  ); then
311
337
  emit_result false "startup" "direct startup failed" false null
@@ -315,6 +341,50 @@ else
315
341
  exit 0
316
342
  fi
317
343
 
344
+ CURRENT_PHASE="verify"
345
+ echo "DX_REMOTE_PHASE=verify"
346
+ if [[ ! -L "$CURRENT_LINK" ]]; then
347
+ echo "current 软链接不存在: $CURRENT_LINK" >&2
348
+ exit 1
349
+ fi
350
+
351
+ current_release="$(readlink -f "$CURRENT_LINK")"
352
+ expected_release="$(readlink -f "$RELEASE_DIR")"
353
+ if [[ -z "$current_release" || ! -d "$current_release" ]]; then
354
+ echo "current 软链接未指向有效目录: \${current_release:-<empty>}" >&2
355
+ exit 1
356
+ fi
357
+ if [[ "$current_release" != "$expected_release" ]]; then
358
+ echo "current 软链接未指向本次 release: expected=$expected_release actual=$current_release" >&2
359
+ exit 1
360
+ fi
361
+
362
+ if [[ "$START_MODE" == "pm2" ]]; then
363
+ if ! pm2 describe "$SERVICE_NAME" >/dev/null 2>&1; then
364
+ echo "PM2 进程不存在: $SERVICE_NAME" >&2
365
+ pm2 list || true
366
+ exit 1
367
+ fi
368
+
369
+ pm2_app_env="$(read_pm2_env_var APP_ENV)"
370
+ if [[ "$pm2_app_env" != "$EXPECTED_APP_ENV" ]]; then
371
+ echo "APP_ENV 不匹配,期望=$EXPECTED_APP_ENV,实际=\${pm2_app_env:-<empty>}" >&2
372
+ pm2 describe "$SERVICE_NAME" || true
373
+ exit 1
374
+ fi
375
+
376
+ pm2_node_env="$(read_pm2_env_var NODE_ENV)"
377
+ if [[ "$pm2_node_env" != "$EXPECTED_NODE_ENV" ]]; then
378
+ echo "NODE_ENV 不匹配,期望=$EXPECTED_NODE_ENV,实际=\${pm2_node_env:-<empty>}" >&2
379
+ pm2 describe "$SERVICE_NAME" || true
380
+ exit 1
381
+ fi
382
+ fi
383
+
384
+ if [[ -n "$HEALTHCHECK_URL" ]]; then
385
+ curl -fsS --max-time "$HEALTHCHECK_TIMEOUT_SECONDS" "$HEALTHCHECK_URL" >/dev/null
386
+ fi
387
+
318
388
  CURRENT_PHASE="cleanup"
319
389
  echo "DX_REMOTE_PHASE=cleanup"
320
390
  release_count=0
@@ -100,6 +100,7 @@ function createRemotePayload(config, bundle) {
100
100
  },
101
101
  startup: config.startup,
102
102
  deploy: config.deploy,
103
+ verify: config.verify,
103
104
  }
104
105
  }
105
106
 
@@ -114,5 +115,9 @@ export async function deployBackendArtifactRemotely(config, bundle, deps = {}) {
114
115
  const phaseModel = createRemotePhaseModel(payload)
115
116
  const script = buildRemoteDeployScript(phaseModel)
116
117
  const commandResult = await runRemoteScript(config.remote, script)
117
- return parseRemoteResult(commandResult)
118
+ const result = parseRemoteResult(commandResult)
119
+ if (!result.ok) {
120
+ throw new Error(`远端部署失败(${result.phase}): ${result.message}`)
121
+ }
122
+ return result
118
123
  }
@@ -1,5 +1,5 @@
1
1
  const UNSUPPORTED_LOCAL_DEP_PATTERN = /^(workspace:|file:|link:)/
2
- const REQUIRED_DEPENDENCIES_FROM_DEV = ['prisma']
2
+ const REQUIRED_DEPENDENCIES = ['prisma', 'tslib', 'dotenv-cli', '@prisma/adapter-pg']
3
3
 
4
4
  function assertSupportedDependencies(dependencies = {}) {
5
5
  for (const [name, version] of Object.entries(dependencies)) {
@@ -13,10 +13,15 @@ function assertSupportedDependencies(dependencies = {}) {
13
13
  export function createRuntimePackage({ appPackage, rootPackage }) {
14
14
  const runtimeDependencies = { ...(appPackage?.dependencies || {}) }
15
15
  const appDevDependencies = appPackage?.devDependencies || {}
16
-
17
- for (const dependencyName of REQUIRED_DEPENDENCIES_FROM_DEV) {
18
- if (!runtimeDependencies[dependencyName] && appDevDependencies[dependencyName]) {
19
- runtimeDependencies[dependencyName] = appDevDependencies[dependencyName]
16
+ const rootDependencies = rootPackage?.dependencies || {}
17
+ const rootDevDependencies = rootPackage?.devDependencies || {}
18
+
19
+ for (const dependencyName of REQUIRED_DEPENDENCIES) {
20
+ if (!runtimeDependencies[dependencyName]) {
21
+ runtimeDependencies[dependencyName] =
22
+ appDevDependencies[dependencyName]
23
+ || rootDependencies[dependencyName]
24
+ || rootDevDependencies[dependencyName]
20
25
  }
21
26
  }
22
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,122 +0,0 @@
1
- ---
2
- name: backend-artifact-deploy
3
- description: 将后端部署从“目标机拉源码并编译”改造为“本地构建制品、目标机仅安装运行依赖并启动”的标准流程。用于 Node/NestJS/Nx/Prisma 等后端项目,尤其适合需要无源码部署、支持 dev/staging/prod 多环境、要求双层环境文件覆盖(如 .env.production 与 .env.production.local)、以及需要在 pm2 与 direct 启动方式之间切换的场景。
4
- ---
5
-
6
- # 后端制品部署
7
-
8
- ## 概览
9
-
10
- 使用该技能时,先识别项目当前的环境变量加载链路与启动链路,再落地“制品打包脚本 + 服务器发布脚本 + 回滚策略”。
11
- 目标是保证目标机器不需要源码编译,同时保持与项目既有环境覆盖规则一致。
12
-
13
- ## 执行流程
14
-
15
- ### 第一步:确认现状链路
16
-
17
- 依次核对:
18
-
19
- 1. 构建链路是否依赖源码目录(例如 dist 里软链回源码 `node_modules`)。
20
- 2. 运行链路如何加载环境变量(是否是两层覆盖,是否通过 `dotenv -e A -e B`)。
21
- 3. 数据库迁移链路是否依赖运行时环境(Prisma `generate` / `migrate deploy` 前是否已加载 env)。
22
- 4. 进程启动是否仅支持 pm2,是否需要 direct 前台测试模式。
23
-
24
- 如果项目已有统一入口(例如 `dx` 或内部脚手架),优先复用该入口,不要绕开既有环境策略。
25
-
26
- ### 第二步:定义制品边界
27
-
28
- 默认采用“轻制品”模式:
29
-
30
- 1. 本地只打包编译产物与必要运行文件,不打包 `node_modules`。
31
- 2. 目标机解压后再安装生产依赖。
32
- 3. 制品命名固定含版本与时间片,例如 `backend-v<version>-<月-日-时-分>.tgz`。
33
-
34
- 制品最小清单应包含:
35
-
36
- 1. 编译产物目录(如 `dist/backend/**`)。
37
- 2. 数据库 schema 与迁移目录(如 `prisma/schema/**`)。
38
- 3. 生产依赖清单(`package.production.json` 重命名为 `package.json`)。
39
- 4. 锁文件(`pnpm-lock.yaml`)。
40
- 5. 启动配置(如 `ecosystem.config.cjs`)。
41
- 6. 双层环境文件(`.env.<env>` 与 `.env.<env>.local`)。
42
-
43
- ### 第三步:实现打包脚本
44
-
45
- 打包脚本应支持参数:
46
-
47
- 1. `--env dev|staging|prod`。
48
- 2. `--version`(默认取后端 `package.json` 版本)。
49
- 3. `--time`(格式 `MM-DD-HH-mm`)。
50
-
51
- 脚本关键行为:
52
-
53
- 1. 按环境构建(`dev -> --dev`,`staging/prod -> --prod`)。
54
- 2. 复制双层环境文件到制品目录。
55
- 3. 不在本地安装运行依赖。
56
- 4. 生成 `tgz`。
57
-
58
- ### 第四步:实现发布脚本
59
-
60
- 发布脚本应支持参数:
61
-
62
- 1. `--archive`(必填)。
63
- 2. `--env dev|staging|prod`。
64
- 3. `--start-mode pm2|direct`(默认 `pm2`)。
65
- 4. `--env-file` 与 `--env-local-file`(可选覆盖路径)。
66
- 5. `--skip-install`、`--skip-migration`、`--skip-pm2`。
67
-
68
- 发布顺序建议:
69
-
70
- 1. 解压到 `releases/<version>`。
71
- 2. 准备双层 env 文件。
72
- 3. 安装生产依赖。
73
- 4. 执行 `prisma generate`。
74
- 5. 执行 `prisma migrate deploy`。
75
- 6. 切换 `current` 软链。
76
- 7. 启动服务(pm2 或 direct)。
77
- 8. 清理旧版本。
78
-
79
- ### 第五步:双层环境加载规则(必须一致)
80
-
81
- 所有关键步骤统一使用相同加载顺序:
82
-
83
- 1. 基础层 `.env.<env>`。
84
- 2. 覆盖层 `.env.<env>.local`。
85
-
86
- 推荐显式写法:
87
-
88
- ```bash
89
- APP_ENV="<env-name>" pnpm exec dotenv -e ".env.<env-name>" -e ".env.<env-name>.local" -- <command>
90
- ```
91
-
92
- 命令示例中的 `<command>` 包括:
93
-
94
- 1. `pnpm exec prisma generate --schema=...`
95
- 2. `pnpm exec prisma migrate deploy --schema=...`
96
- 3. `pm2 startOrReload ...` 或 `node apps/backend/src/main.js`
97
-
98
- ## 验证清单
99
-
100
- 交付前必须至少验证:
101
-
102
- 1. 打包脚本 `--help` 与语法检查通过。
103
- 2. 发布脚本 `--help` 与语法检查通过。
104
- 3. 制品内同时包含 `.env.<env>` 与 `.env.<env>.local`。
105
- 4. 发布脚本在默认路径下能正确识别并使用两层 env。
106
- 5. `start-mode=direct` 可前台启动。
107
- 6. `start-mode=pm2` 可重载或启动。
108
- 7. 版本目录与 `current` 切换正常,可回滚。
109
-
110
- ## 常见陷阱
111
-
112
- 1. 把 env 文件链接到自身,造成坏链路。
113
- 2. 只加载 `.env.<env>`,遗漏 `.local` 覆盖。
114
- 3. 迁移与启动阶段用不同 env 加载逻辑,导致行为不一致。
115
- 4. staging 构建误用 development 或 production 的 env 层。
116
- 5. 打包包含本机 `node_modules`,跨系统运行失败。
117
-
118
- ## 参考资料
119
-
120
- 需要细化实现时,读取:
121
-
122
- - `references/deployment-checklist.md`
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "后端制品部署"
3
- short_description: "跨项目复用的后端制品构建发布与双层环境变量合并流程"
4
- default_prompt: "Use $backend-artifact-deploy to design and implement source-free backend artifact deployment with layered env handling."
@@ -1,66 +0,0 @@
1
- # 后端制品部署检查清单
2
-
3
- ## 一、改造前采样
4
-
5
- 1. 查找构建后是否存在软链回源码依赖:
6
-
7
- ```bash
8
- rg -n "ln -sfn.*node_modules|node_modules.*ln -sfn" <backend-package-json-path>
9
- ```
10
-
11
- 2. 查找环境加载入口:
12
-
13
- ```bash
14
- rg -n "dotenv -e|dotenv --override|ConfigModule.forRoot|loadEnvironment|env-layers" -S <repo-root>
15
- ```
16
-
17
- 3. 查找启动命令:
18
-
19
- ```bash
20
- rg -n "start:prod|pm2|node .*main" -S <repo-root>
21
- ```
22
-
23
- ## 二、打包脚本最低要求
24
-
25
- 1. 接收 `--env`、`--version`、`--time`。
26
- 2. 制品名包含版本与时间片。
27
- 3. 打入 `.env.<env>` 与 `.env.<env>.local`。
28
- 4. 不打入 `node_modules`(轻制品模式)。
29
-
30
- ## 三、发布脚本最低要求
31
-
32
- 1. 解压到 `releases/<version>` 并切换 `current`。
33
- 2. 支持 `pm2` 与 `direct` 两种启动方式。
34
- 3. 在 install、migrate、start 三阶段都用同一套双层 env 加载顺序。
35
- 4. 支持 `--env-file` 与 `--env-local-file` 覆盖路径。
36
-
37
- ## 四、上线前验证命令
38
-
39
- ```bash
40
- bash -n scripts/release/backend-build-release.sh
41
- bash -n scripts/release/backend-deploy-release.sh
42
- scripts/release/backend-build-release.sh --env staging
43
- tar -tzf release/backend/*.tgz | rg "\.env\.staging(\.local)?$"
44
- ```
45
-
46
- ## 五、发布后验证
47
-
48
- 1. 进程检查:
49
-
50
- ```bash
51
- pm2 status
52
- pm2 logs backend --lines 120
53
- ```
54
-
55
- 2. 健康检查:
56
-
57
- ```bash
58
- curl -f http://127.0.0.1:3000/health
59
- ```
60
-
61
- 3. 回滚检查:
62
-
63
- ```bash
64
- ln -sfn /opt/ai-backend/releases/<old-version> /opt/ai-backend/current
65
- pm2 reload backend --update-env
66
- ```