@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.
- package/lib/backend-artifact-deploy/artifact-builder.js +17 -3
- package/lib/backend-artifact-deploy/config.js +28 -0
- package/lib/backend-artifact-deploy/remote-phases.js +1 -0
- package/lib/backend-artifact-deploy/remote-script.js +82 -12
- package/lib/backend-artifact-deploy/remote-transport.js +6 -1
- package/lib/backend-artifact-deploy/runtime-package.js +10 -5
- package/package.json +1 -1
- package/codex/skills/backend-artifact-deploy/SKILL.md +0 -122
- package/codex/skills/backend-artifact-deploy/agents/openai.yaml +0 -4
- package/codex/skills/backend-artifact-deploy/references/deployment-checklist.md +0 -66
|
@@ -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', [
|
|
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',
|
|
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
|
-
{
|
|
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,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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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="$
|
|
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="$
|
|
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"
|
|
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="$
|
|
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="$
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,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,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
|
-
```
|