@ranger1/dx 0.1.83 → 0.1.85
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/config.js +39 -0
- package/lib/backend-artifact-deploy/remote-phases.js +1 -0
- package/lib/backend-artifact-deploy/remote-result.js +2 -0
- package/lib/backend-artifact-deploy/remote-script.js +129 -7
- package/lib/backend-artifact-deploy/remote-transport.js +1 -0
- package/lib/backend-artifact-deploy.js +30 -1
- package/package.json +1 -1
|
@@ -16,6 +16,43 @@ 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
|
+
maxWaitSeconds:
|
|
42
|
+
healthCheckConfig.maxWaitSeconds == null
|
|
43
|
+
? 24
|
|
44
|
+
: requirePositiveInteger(healthCheckConfig.maxWaitSeconds, 'verify.healthCheck.maxWaitSeconds'),
|
|
45
|
+
retryIntervalSeconds:
|
|
46
|
+
healthCheckConfig.retryIntervalSeconds == null
|
|
47
|
+
? 2
|
|
48
|
+
: requirePositiveInteger(
|
|
49
|
+
healthCheckConfig.retryIntervalSeconds,
|
|
50
|
+
'verify.healthCheck.retryIntervalSeconds',
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
function resolveBuildCommand(buildConfig, environment) {
|
|
20
57
|
if (buildConfig?.commands && typeof buildConfig.commands === 'object') {
|
|
21
58
|
const selected = buildConfig.commands[environment]
|
|
@@ -55,6 +92,7 @@ export function resolveBackendDeployConfig({ cli, targetConfig, environment, fla
|
|
|
55
92
|
const remoteConfig = deployConfig.remote || null
|
|
56
93
|
const startupConfig = deployConfig.startup || {}
|
|
57
94
|
const runConfig = deployConfig.deploy || {}
|
|
95
|
+
const verifyConfig = deployConfig.verify || {}
|
|
58
96
|
const buildOnly = Boolean(flags.buildOnly)
|
|
59
97
|
const startupMode = String(startupConfig.mode || 'pm2').trim()
|
|
60
98
|
const prismaGenerate = runConfig.prismaGenerate !== false
|
|
@@ -117,6 +155,7 @@ export function resolveBackendDeployConfig({ cli, targetConfig, environment, fla
|
|
|
117
155
|
prismaMigrateDeploy,
|
|
118
156
|
skipMigration: Boolean(flags.skipMigration),
|
|
119
157
|
},
|
|
158
|
+
verify: resolveVerifyConfig(verifyConfig),
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
if (!['pm2', 'direct'].includes(normalized.startup.mode)) {
|
|
@@ -28,6 +28,7 @@ export function parseRemoteResult({ stdout = '', stderr = '', exitCode = 0 }) {
|
|
|
28
28
|
message: 'ok',
|
|
29
29
|
rollbackAttempted: false,
|
|
30
30
|
rollbackSucceeded: null,
|
|
31
|
+
summary: null,
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -38,5 +39,6 @@ export function parseRemoteResult({ stdout = '', stderr = '', exitCode = 0 }) {
|
|
|
38
39
|
message,
|
|
39
40
|
rollbackAttempted: false,
|
|
40
41
|
rollbackSucceeded: null,
|
|
42
|
+
summary: null,
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -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,10 @@ 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)
|
|
35
|
+
const healthCheckMaxWaitSeconds = Number(healthCheck?.maxWaitSeconds || 24)
|
|
36
|
+
const healthCheckRetryIntervalSeconds = Number(healthCheck?.retryIntervalSeconds || 2)
|
|
29
37
|
|
|
30
38
|
return `#!/usr/bin/env bash
|
|
31
39
|
set -euo pipefail
|
|
@@ -36,6 +44,8 @@ ARCHIVE=${escapeShell(uploadedBundlePath)}
|
|
|
36
44
|
RELEASE_DIR=${escapeShell(releaseDir)}
|
|
37
45
|
CURRENT_LINK=${escapeShell(currentLink)}
|
|
38
46
|
ENV_NAME=${escapeShell(environment)}
|
|
47
|
+
EXPECTED_APP_ENV=${escapeShell(expectedAppEnv)}
|
|
48
|
+
EXPECTED_NODE_ENV=${escapeShell(expectedNodeEnv)}
|
|
39
49
|
ENV_FILE_NAME=${escapeShell(envFileName)}
|
|
40
50
|
ENV_LOCAL_FILE_NAME=${escapeShell(envLocalFileName)}
|
|
41
51
|
PRISMA_SCHEMA=${escapeShell(prismaSchema)}
|
|
@@ -45,6 +55,10 @@ INSTALL_COMMAND=${escapeShell(installCommand)}
|
|
|
45
55
|
START_MODE=${escapeShell(startupMode)}
|
|
46
56
|
SERVICE_NAME=${escapeShell(serviceName)}
|
|
47
57
|
START_ENTRY=${escapeShell(startupEntry)}
|
|
58
|
+
HEALTHCHECK_URL=${escapeShell(healthCheckUrl)}
|
|
59
|
+
HEALTHCHECK_TIMEOUT_SECONDS=${healthCheckTimeoutSeconds}
|
|
60
|
+
HEALTHCHECK_MAX_WAIT_SECONDS=${healthCheckMaxWaitSeconds}
|
|
61
|
+
HEALTHCHECK_RETRY_DELAY_SECONDS=${healthCheckRetryIntervalSeconds}
|
|
48
62
|
KEEP_RELEASES=${keepReleases}
|
|
49
63
|
SHOULD_GENERATE=${shouldGenerate ? '1' : '0'}
|
|
50
64
|
SHOULD_MIGRATE=${shouldMigrate ? '1' : '0'}
|
|
@@ -72,6 +86,7 @@ emit_result() {
|
|
|
72
86
|
local message="$3"
|
|
73
87
|
local rollback_attempted="$4"
|
|
74
88
|
local rollback_succeeded="$5"
|
|
89
|
+
local summary_json="\${6:-null}"
|
|
75
90
|
if [[ "$RESULT_EMITTED" -eq 1 ]]; then
|
|
76
91
|
return
|
|
77
92
|
fi
|
|
@@ -79,8 +94,8 @@ emit_result() {
|
|
|
79
94
|
message="\${message//\\\\/\\\\\\\\}"
|
|
80
95
|
message="\${message//\"/\\\\\"}"
|
|
81
96
|
message="\${message//$'\\n'/\\\\n}"
|
|
82
|
-
printf 'DX_REMOTE_RESULT={"ok":%s,"phase":"%s","message":"%s","rollbackAttempted":%s,"rollbackSucceeded":%s}\\n' \\
|
|
83
|
-
"$ok" "$phase" "$message" "$rollback_attempted" "$rollback_succeeded"
|
|
97
|
+
printf 'DX_REMOTE_RESULT={"ok":%s,"phase":"%s","message":"%s","rollbackAttempted":%s,"rollbackSucceeded":%s,"summary":%s}\\n' \\
|
|
98
|
+
"$ok" "$phase" "$message" "$rollback_attempted" "$rollback_succeeded" "$summary_json"
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
cleanup() {
|
|
@@ -174,10 +189,58 @@ run_with_env() {
|
|
|
174
189
|
shift
|
|
175
190
|
(
|
|
176
191
|
cd "$cwd"
|
|
177
|
-
APP_ENV="$
|
|
192
|
+
APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
|
|
193
|
+
"$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- "$@"
|
|
178
194
|
)
|
|
179
195
|
}
|
|
180
196
|
|
|
197
|
+
json_escape() {
|
|
198
|
+
local value="\${1-}"
|
|
199
|
+
value="\${value//\\\\/\\\\\\\\}"
|
|
200
|
+
value="\${value//\"/\\\\\"}"
|
|
201
|
+
value="\${value//$'\\n'/\\\\n}"
|
|
202
|
+
value="\${value//$'\\r'/\\\\r}"
|
|
203
|
+
value="\${value//$'\\t'/\\\\t}"
|
|
204
|
+
printf '%s' "$value"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
read_pm2_env_var() {
|
|
208
|
+
local key="$1"
|
|
209
|
+
pm2 jlist | node -e '
|
|
210
|
+
const fs = require("node:fs")
|
|
211
|
+
const key = process.argv[1]
|
|
212
|
+
const list = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
213
|
+
const app = list.find(item => item?.name === process.argv[2])
|
|
214
|
+
process.stdout.write(String(app?.pm2_env?.[key] || ""))
|
|
215
|
+
' "$key" "$SERVICE_NAME"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
read_pm2_status() {
|
|
219
|
+
pm2 jlist | node -e '
|
|
220
|
+
const fs = require("node:fs")
|
|
221
|
+
const list = JSON.parse(fs.readFileSync(0, "utf8"))
|
|
222
|
+
const app = list.find(item => item?.name === process.argv[1])
|
|
223
|
+
process.stdout.write(String(app?.pm2_env?.status || ""))
|
|
224
|
+
' "$SERVICE_NAME"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
build_summary_json() {
|
|
228
|
+
local release_name="$1"
|
|
229
|
+
local current_release_path="$2"
|
|
230
|
+
local service_status="$3"
|
|
231
|
+
local app_env="$4"
|
|
232
|
+
local node_env="$5"
|
|
233
|
+
local health_url="$6"
|
|
234
|
+
printf '{"releaseName":"%s","currentRelease":"%s","serviceName":"%s","serviceStatus":"%s","appEnv":"%s","nodeEnv":"%s","healthUrl":"%s"}' \
|
|
235
|
+
"$(json_escape "$release_name")" \
|
|
236
|
+
"$(json_escape "$current_release_path")" \
|
|
237
|
+
"$(json_escape "$SERVICE_NAME")" \
|
|
238
|
+
"$(json_escape "$service_status")" \
|
|
239
|
+
"$(json_escape "$app_env")" \
|
|
240
|
+
"$(json_escape "$node_env")" \
|
|
241
|
+
"$(json_escape "$health_url")"
|
|
242
|
+
}
|
|
243
|
+
|
|
181
244
|
attempt_pm2_restore() {
|
|
182
245
|
if [[ -z "$PREVIOUS_CURRENT_TARGET" || ! -e "$PREVIOUS_CURRENT_TARGET/$ECOSYSTEM_CONFIG" ]]; then
|
|
183
246
|
ROLLBACK_SUCCEEDED=false
|
|
@@ -185,7 +248,8 @@ attempt_pm2_restore() {
|
|
|
185
248
|
fi
|
|
186
249
|
if (
|
|
187
250
|
cd "$PREVIOUS_CURRENT_TARGET"
|
|
188
|
-
APP_ENV="$
|
|
251
|
+
APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
|
|
252
|
+
"$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
|
|
189
253
|
pm2 start "$ECOSYSTEM_CONFIG" --only "$SERVICE_NAME" --update-env
|
|
190
254
|
pm2 save
|
|
191
255
|
); then
|
|
@@ -291,7 +355,8 @@ if [[ "$START_MODE" == "pm2" ]]; then
|
|
|
291
355
|
if ! (
|
|
292
356
|
cd "$CURRENT_LINK"
|
|
293
357
|
pm2 delete "$SERVICE_NAME" || true
|
|
294
|
-
APP_ENV="$
|
|
358
|
+
APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
|
|
359
|
+
"$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
|
|
295
360
|
pm2 start "$ECOSYSTEM_CONFIG" --only "$SERVICE_NAME" --update-env
|
|
296
361
|
pm2 save
|
|
297
362
|
); then
|
|
@@ -306,7 +371,8 @@ if [[ "$START_MODE" == "pm2" ]]; then
|
|
|
306
371
|
else
|
|
307
372
|
if ! (
|
|
308
373
|
cd "$CURRENT_LINK"
|
|
309
|
-
APP_ENV="$
|
|
374
|
+
APP_ENV="$EXPECTED_APP_ENV" NODE_ENV="$EXPECTED_NODE_ENV" \\
|
|
375
|
+
"$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
|
|
310
376
|
node "$START_ENTRY"
|
|
311
377
|
); then
|
|
312
378
|
emit_result false "startup" "direct startup failed" false null
|
|
@@ -316,6 +382,62 @@ else
|
|
|
316
382
|
exit 0
|
|
317
383
|
fi
|
|
318
384
|
|
|
385
|
+
CURRENT_PHASE="verify"
|
|
386
|
+
echo "DX_REMOTE_PHASE=verify"
|
|
387
|
+
if [[ ! -L "$CURRENT_LINK" ]]; then
|
|
388
|
+
echo "current 软链接不存在: $CURRENT_LINK" >&2
|
|
389
|
+
exit 1
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
current_release="$(readlink -f "$CURRENT_LINK")"
|
|
393
|
+
expected_release="$(readlink -f "$RELEASE_DIR")"
|
|
394
|
+
if [[ -z "$current_release" || ! -d "$current_release" ]]; then
|
|
395
|
+
echo "current 软链接未指向有效目录: \${current_release:-<empty>}" >&2
|
|
396
|
+
exit 1
|
|
397
|
+
fi
|
|
398
|
+
if [[ "$current_release" != "$expected_release" ]]; then
|
|
399
|
+
echo "current 软链接未指向本次 release: expected=$expected_release actual=$current_release" >&2
|
|
400
|
+
exit 1
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
if [[ "$START_MODE" == "pm2" ]]; then
|
|
404
|
+
if ! pm2 describe "$SERVICE_NAME" >/dev/null 2>&1; then
|
|
405
|
+
echo "PM2 进程不存在: $SERVICE_NAME" >&2
|
|
406
|
+
pm2 list || true
|
|
407
|
+
exit 1
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
pm2_service_status="$(read_pm2_status)"
|
|
411
|
+
pm2_app_env="$(read_pm2_env_var APP_ENV)"
|
|
412
|
+
if [[ "$pm2_app_env" != "$EXPECTED_APP_ENV" ]]; then
|
|
413
|
+
echo "APP_ENV 不匹配,期望=$EXPECTED_APP_ENV,实际=\${pm2_app_env:-<empty>}" >&2
|
|
414
|
+
pm2 describe "$SERVICE_NAME" || true
|
|
415
|
+
exit 1
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
pm2_node_env="$(read_pm2_env_var NODE_ENV)"
|
|
419
|
+
if [[ "$pm2_node_env" != "$EXPECTED_NODE_ENV" ]]; then
|
|
420
|
+
echo "NODE_ENV 不匹配,期望=$EXPECTED_NODE_ENV,实际=\${pm2_node_env:-<empty>}" >&2
|
|
421
|
+
pm2 describe "$SERVICE_NAME" || true
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
if [[ -n "$HEALTHCHECK_URL" ]]; then
|
|
427
|
+
healthcheck_started_at="$(date +%s)"
|
|
428
|
+
until curl -fsS --max-time "$HEALTHCHECK_TIMEOUT_SECONDS" "$HEALTHCHECK_URL" >/dev/null; do
|
|
429
|
+
healthcheck_elapsed_seconds=$(( $(date +%s) - healthcheck_started_at ))
|
|
430
|
+
if [[ "$healthcheck_elapsed_seconds" -ge "$HEALTHCHECK_MAX_WAIT_SECONDS" ]]; then
|
|
431
|
+
echo "health check failed within $HEALTHCHECK_MAX_WAIT_SECONDS seconds: $HEALTHCHECK_URL" >&2
|
|
432
|
+
exit 1
|
|
433
|
+
fi
|
|
434
|
+
sleep "$HEALTHCHECK_RETRY_DELAY_SECONDS"
|
|
435
|
+
done
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
summary_service_status="\${pm2_service_status:-direct-attached}"
|
|
439
|
+
summary_json="$(build_summary_json "$VERSION_NAME" "$current_release" "$summary_service_status" "$EXPECTED_APP_ENV" "$EXPECTED_NODE_ENV" "$HEALTHCHECK_URL")"
|
|
440
|
+
|
|
319
441
|
CURRENT_PHASE="cleanup"
|
|
320
442
|
echo "DX_REMOTE_PHASE=cleanup"
|
|
321
443
|
release_count=0
|
|
@@ -333,6 +455,6 @@ done < <(
|
|
|
333
455
|
fi
|
|
334
456
|
)
|
|
335
457
|
|
|
336
|
-
emit_result true "cleanup" "ok" false null
|
|
458
|
+
emit_result true "cleanup" "ok" false null "$summary_json"
|
|
337
459
|
`
|
|
338
460
|
}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { buildBackendArtifact } from './backend-artifact-deploy/artifact-builder.js'
|
|
2
2
|
import { resolveBackendDeployConfig } from './backend-artifact-deploy/config.js'
|
|
3
3
|
import { deployBackendArtifactRemotely } from './backend-artifact-deploy/remote-transport.js'
|
|
4
|
+
import { logger as defaultLogger } from './logger.js'
|
|
5
|
+
|
|
6
|
+
function printSuccessfulDeploySummary(result, logger) {
|
|
7
|
+
const summary = result?.summary
|
|
8
|
+
if (!summary) return
|
|
9
|
+
|
|
10
|
+
logger.success(`后端部署成功: ${summary.releaseName || 'unknown-release'}`)
|
|
11
|
+
if (summary.currentRelease) {
|
|
12
|
+
logger.info(`[deploy-summary] current=${summary.currentRelease}`)
|
|
13
|
+
}
|
|
14
|
+
if (summary.serviceName || summary.serviceStatus) {
|
|
15
|
+
logger.info(
|
|
16
|
+
`[deploy-summary] service=${summary.serviceName || 'unknown'} status=${summary.serviceStatus || 'unknown'}`,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
if (summary.appEnv || summary.nodeEnv) {
|
|
20
|
+
logger.info(
|
|
21
|
+
`[deploy-summary] APP_ENV=${summary.appEnv || '<empty>'} NODE_ENV=${summary.nodeEnv || '<empty>'}`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
if (summary.healthUrl) {
|
|
25
|
+
logger.info(`[deploy-summary] health=${summary.healthUrl}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
export async function runBackendArtifactDeploy({
|
|
6
30
|
cli,
|
|
@@ -9,6 +33,7 @@ export async function runBackendArtifactDeploy({
|
|
|
9
33
|
environment,
|
|
10
34
|
deps = {},
|
|
11
35
|
}) {
|
|
36
|
+
const logger = deps.logger || defaultLogger
|
|
12
37
|
const resolveConfig = deps.resolveConfig || resolveBackendDeployConfig
|
|
13
38
|
const buildArtifact = deps.buildArtifact || buildBackendArtifact
|
|
14
39
|
const deployRemotely =
|
|
@@ -28,5 +53,9 @@ export async function runBackendArtifactDeploy({
|
|
|
28
53
|
return bundle
|
|
29
54
|
}
|
|
30
55
|
|
|
31
|
-
|
|
56
|
+
const result = await deployRemotely(config, bundle, deps)
|
|
57
|
+
if (result?.ok) {
|
|
58
|
+
printSuccessfulDeploySummary(result, logger)
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
32
61
|
}
|