@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.
@@ -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)) {
@@ -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
  }
@@ -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="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- "$@"
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="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
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="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
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="$ENV_NAME" "$DOTENV_BIN" -o -e "$ENV_FILE_NAME" -e "$ENV_LOCAL_FILE_NAME" -- \\
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
  }
@@ -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
 
@@ -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
- return deployRemotely(config, bundle, deps)
56
+ const result = await deployRemotely(config, bundle, deps)
57
+ if (result?.ok) {
58
+ printSuccessfulDeploySummary(result, logger)
59
+ }
60
+ return result
32
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {