project-auto-wizard 0.15.0 → 0.16.0

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.
Files changed (65) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +1 -1
  5. package/package.json +1 -1
  6. package/payload/config/breaking-changes.json +31 -5
  7. package/payload/config/wizard-prompts.yml +0 -4
  8. package/payload/scripts/messages.py +162 -26
  9. package/payload/scripts/version_manager.py +69 -40
  10. package/payload/version.yml.template +6 -2
  11. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +3 -0
  12. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +64 -1
  13. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +61 -12
  14. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +46 -10
  15. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +40 -11
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
  24. package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
  25. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
  26. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
  27. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
  28. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
  29. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
  30. package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
  31. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
  32. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
  33. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
  37. package/src/cli/args.js +32 -22
  38. package/src/commands/doctor.js +20 -0
  39. package/src/commands/dry-run.js +9 -1
  40. package/src/commands/full.js +9 -4
  41. package/src/commands/interactive.js +29 -30
  42. package/src/commands/status.js +19 -5
  43. package/src/context.js +4 -2
  44. package/src/core/copy/simple.js +31 -0
  45. package/src/core/copy/workflows.js +2 -2
  46. package/src/core/deploy-style.js +1 -1
  47. package/src/core/detect.js +3 -2
  48. package/src/core/options.js +88 -0
  49. package/src/core/release-options.js +6 -23
  50. package/src/core/types.js +15 -8
  51. package/src/core/version-yml.js +68 -26
  52. package/src/i18n/catalog/en/cli.js +3 -1
  53. package/src/i18n/catalog/en/commands.js +15 -0
  54. package/src/i18n/catalog/en/core2.js +3 -0
  55. package/src/i18n/catalog/en/ui.js +1 -0
  56. package/src/i18n/catalog/ko/cli.js +3 -1
  57. package/src/i18n/catalog/ko/commands.js +15 -0
  58. package/src/i18n/catalog/ko/core2.js +3 -0
  59. package/src/i18n/catalog/ko/ui.js +1 -0
  60. package/src/index.js +7 -3
  61. package/src/ui/env-plan.js +1 -1
  62. package/src/ui/prompts.js +9 -5
  63. package/src/ui/status-cards.js +2 -2
  64. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
  65. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
@@ -86,6 +86,7 @@ jobs:
86
86
  SSH_KEY: ${{ secrets.SSH_KEY }}
87
87
  DOCKERFILE: Dockerfile
88
88
  run: |
89
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
89
90
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
90
91
  MISSING=""
91
92
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
@@ -113,6 +114,7 @@ jobs:
113
114
 
114
115
  - name: Free up disk space
115
116
  run: |
117
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
116
118
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
117
119
  m cicd.disk_before
118
120
  df -h
@@ -132,6 +134,7 @@ jobs:
132
134
  - name: Create .env file
133
135
  working-directory: ${{ env.PROJECT_PATH }}
134
136
  run: |
137
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
135
138
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
136
139
  cat > .env << 'EOF'
137
140
  ${{ secrets.ENV_FILE }}
@@ -139,16 +142,16 @@ jobs:
139
142
  m cicd.dotenv_created
140
143
 
141
144
  - name: Set up Docker Buildx
142
- uses: docker/setup-buildx-action@v3
145
+ uses: docker/setup-buildx-action@v4
143
146
 
144
147
  - name: Log in to DockerHub
145
- uses: docker/login-action@v3
148
+ uses: docker/login-action@v4
146
149
  with:
147
150
  username: ${{ secrets.DOCKERHUB_USERNAME }}
148
151
  password: ${{ secrets.DOCKERHUB_TOKEN }}
149
152
 
150
153
  - name: Build and push Docker image
151
- uses: docker/build-push-action@v5
154
+ uses: docker/build-push-action@v7
152
155
  with:
153
156
  context: ${{ env.PROJECT_PATH }}
154
157
  file: ${{ env.PROJECT_PATH }}/Dockerfile
@@ -161,6 +164,7 @@ jobs:
161
164
  - name: Clean up after build
162
165
  if: always()
163
166
  run: |
167
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
164
168
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
165
169
  m cicd.post_build_cleanup
166
170
  docker system prune -f || true
@@ -186,6 +190,7 @@ jobs:
186
190
 
187
191
  - name: Load messages
188
192
  run: |
193
+ [ -f .github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
189
194
  python3 .github/scripts/messages.py dump cicd. | PYTHONIOENCODING=utf-8 python3 -c 'import json,sys; [print("PAW_M_" + k.replace(".", "_") + "=" + v) for k, v in json.load(sys.stdin).items()]' >> "$GITHUB_ENV"
190
195
 
191
196
  - name: Deploy
@@ -364,6 +369,6 @@ jobs:
364
369
  m "${{ env.PAW_M_cicd_summary_branch }}" branch="$BRANCH"
365
370
  m "${{ env.PAW_M_cicd_summary_container }}" container="$CONTAINER_NAME"
366
371
  m "${{ env.PAW_M_cicd_summary_port }}" port="$PORT"
367
- m "${{ env.PAW_M_cicd_summary_time }}" time="$(date '+%Y-%m-%d %H:%M:%S')"
372
+ m "${{ env.PAW_M_cicd_summary_time }}" time="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
368
373
  echo ""
369
374
  m "${{ env.PAW_M_cicd_summary_url }}" url="http://${{ secrets.SERVER_HOST }}:${PORT}/docs"
@@ -1,5 +1,5 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # React CI (build verification)
2
+ # React / Next.js CI (build verification)
3
3
  # On push/PR to the {{DEVELOP_BRANCH}} branch, verifies with npm ci, npm test (when a test script exists) and npm run build. Caches node_modules and the build output.
4
4
  #
5
5
  # GitHub Secrets
@@ -73,6 +73,7 @@ jobs:
73
73
 
74
74
  - name: Create environment file (${{ env.ENV_FILE }})
75
75
  run: |
76
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
76
77
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
77
78
  echo -e "${{ secrets.ENV_FILE || secrets.ENV }}" > ${{ env.ENV_FILE }}
78
79
  m cicd.env_file_created file="${{ env.ENV_FILE }}"
@@ -106,6 +107,7 @@ jobs:
106
107
  - name: Install dependencies
107
108
  if: steps.node-modules-cache.outputs.cache-hit != 'true'
108
109
  run: |
110
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
109
111
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
110
112
  m cibuild.node_deps_installing
111
113
  npm ci
@@ -125,6 +127,7 @@ jobs:
125
127
  # Runs only when package.json has a test script (the npm init default "no test specified" counts as none)
126
128
  - name: Run tests
127
129
  run: |
130
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
128
131
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
129
132
  TEST_SCRIPT=$(node -p "(require('./package.json').scripts || {}).test || ''")
130
133
  if [ -z "$TEST_SCRIPT" ] || echo "$TEST_SCRIPT" | grep -q "no test specified"; then
@@ -139,6 +142,7 @@ jobs:
139
142
  - name: Build project
140
143
  id: build
141
144
  run: |
145
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
142
146
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
143
147
  m cibuild.node_build_start
144
148
  m cibuild.node_build_project project="${{ env.PROJECT_NAME }}"
@@ -159,10 +163,19 @@ jobs:
159
163
  echo "status=success" >> $GITHUB_OUTPUT
160
164
  m cibuild.node_build_ok
161
165
 
166
+ # Only Next.js builds create .next; other React builds skip this block
167
+ if [ -d ".next" ]; then
168
+ echo ""
169
+ m cibuild.node_build_output
170
+ m cibuild.node_build_next_dir
171
+ du -sh .next/ || true
172
+ fi
173
+
162
174
  # Runs only when the build failed
163
175
  - name: Print build error log
164
176
  if: steps.build.outputs.build_failed == 'true'
165
177
  run: |
178
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
166
179
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
167
180
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
168
181
  m cibuild.node_errlog_title
@@ -182,9 +195,9 @@ jobs:
182
195
  # End the workflow because the build failed
183
196
  exit 1
184
197
 
185
- # Save the log regardless of build success or failure
198
+ # Save the log regardless of build success or failure (nothing to upload when the build step did not run, e.g. tests failed)
186
199
  - name: Upload build log
187
- if: always()
200
+ if: ${{ always() && steps.build.outcome != 'skipped' }}
188
201
  uses: actions/upload-artifact@v7
189
202
  with:
190
203
  name: build-log
@@ -194,11 +207,17 @@ jobs:
194
207
  # Summarize the workflow result for readability
195
208
  - name: Build result summary
196
209
  if: always()
210
+ # Values go through env (not inlined into the script). On a PR, github.head_ref is the PR branch (ref_name would be '<n>/merge')
211
+ env:
212
+ BUILD_STATUS: ${{ steps.build.outputs.status }}
213
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
214
+ COMMIT_SHA: ${{ github.sha }}
197
215
  run: |
216
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
198
217
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
199
218
  echo ""
200
219
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
201
- if [ "${{ steps.build.outputs.status }}" = "success" ]; then
220
+ if [ "$BUILD_STATUS" = "success" ]; then
202
221
  m cibuild.node_result_ok
203
222
  else
204
223
  m cibuild.node_result_fail
@@ -208,16 +227,25 @@ jobs:
208
227
  m cibuild.node_result_title
209
228
  m cibuild.node_result_project project="${{ env.PROJECT_NAME }}"
210
229
  m cibuild.node_result_node node="${{ env.NODE_VERSION }}"
211
- m cibuild.node_result_status status="${{ steps.build.outputs.status }}"
212
- m cibuild.node_result_branch branch="${{ github.ref_name }}"
213
- m cibuild.node_result_commit sha="${{ github.sha }}"
214
- m cibuild.node_result_time time="$(date '+%Y-%m-%d %H:%M:%S UTC')"
230
+ # An empty status means the build step never ran (an earlier step such as the tests failed)
231
+ if [ -z "$BUILD_STATUS" ]; then
232
+ m cibuild.node_result_status_not_run
233
+ else
234
+ m cibuild.node_result_status status="$BUILD_STATUS"
235
+ fi
236
+ m cibuild.node_result_branch branch="$BRANCH_NAME"
237
+ m cibuild.node_result_commit sha="$COMMIT_SHA"
238
+ m cibuild.node_result_time time="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
215
239
  echo ""
216
240
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
217
241
  echo ""
218
242
 
219
- if [ "${{ steps.build.outputs.status }}" = "success" ]; then
243
+ if [ "$BUILD_STATUS" = "success" ]; then
220
244
  m cibuild.node_all_ok
245
+ elif [ -z "$BUILD_STATUS" ]; then
246
+ # The build log artifact does not exist in this case, so do not point to it
247
+ m cibuild.node_build_not_run
248
+ m cibuild.node_build_not_run_hint
221
249
  else
222
250
  m cibuild.node_error_occurred
223
251
  m cibuild.node_log_hint
@@ -243,6 +271,7 @@ jobs:
243
271
  env:
244
272
  RESULTS: ${{ toJSON(needs.*.result) }}
245
273
  run: |
274
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
246
275
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
247
276
  m cibuild.gate_results results="$RESULTS"
248
277
  if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
@@ -1,5 +1,5 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # React CI/CD deployment
2
+ # React / Next.js CI/CD deployment
3
3
  # Builds a Docker image and deploys it when the {{MAIN_BRANCH}} branch is pushed.
4
4
  #
5
5
  # GitHub Secrets
@@ -68,6 +68,7 @@ jobs:
68
68
  SSH_KEY: ${{ secrets.SSH_KEY }}
69
69
  DOCKERFILE: Dockerfile
70
70
  run: |
71
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
71
72
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
72
73
  MISSING=""
73
74
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
@@ -102,6 +103,7 @@ jobs:
102
103
 
103
104
  - name: Create environment file (${{ env.ENV_FILE_NAME }})
104
105
  run: |
106
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
105
107
  m() { (cd "$GITHUB_WORKSPACE" && python3 .github/scripts/messages.py get "$@"); }
106
108
  cat << 'EOF' > ./${{ env.ENV_FILE_NAME }}
107
109
  ${{ secrets.ENV_FILE || secrets.ENV }}
@@ -142,10 +144,10 @@ jobs:
142
144
  run: npm run build
143
145
 
144
146
  - name: Set up Docker Buildx
145
- uses: docker/setup-buildx-action@v3
147
+ uses: docker/setup-buildx-action@v4
146
148
 
147
149
  - name: Log in to DockerHub
148
- uses: docker/login-action@v3
150
+ uses: docker/login-action@v4
149
151
  with:
150
152
  username: ${{ secrets.DOCKERHUB_USERNAME }}
151
153
  password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -159,7 +161,7 @@ jobs:
159
161
  ${{ runner.os }}-buildx-
160
162
 
161
163
  - name: Build and push Docker image
162
- uses: docker/build-push-action@v5
164
+ uses: docker/build-push-action@v7
163
165
  with:
164
166
  context: ${{ env.PROJECT_PATH }}
165
167
  file: ${{ env.PROJECT_PATH }}/Dockerfile
@@ -191,6 +193,7 @@ jobs:
191
193
 
192
194
  - name: Load messages
193
195
  run: |
196
+ [ -f .github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
194
197
  python3 .github/scripts/messages.py dump cicd. | PYTHONIOENCODING=utf-8 python3 -c 'import json,sys; [print("PAW_M_" + k.replace(".", "_") + "=" + v) for k, v in json.load(sys.stdin).items()]' >> "$GITHUB_ENV"
195
198
 
196
199
  - name: Deploy
@@ -280,14 +283,25 @@ jobs:
280
283
  m "${{ env.PAW_M_cicd_container_none }}" container="$CONTAINER_NAME"
281
284
  fi
282
285
 
286
+ # Next.js projects used to deploy as <project>-nextjs[-deploy]. Those containers keep the same host port
287
+ # (--restart unless-stopped), so remove them before starting the new one. No-op when they do not exist.
288
+ for LEGACY_NAME in "${PROJECT_NAME}-nextjs-deploy" "${PROJECT_NAME}-nextjs"; do
289
+ if [ "$LEGACY_NAME" != "$CONTAINER_NAME" ]; then
290
+ SUDO docker rm -f "$LEGACY_NAME" >/dev/null 2>&1 || true
291
+ fi
292
+ done
293
+
283
294
  # Start the new container
284
295
  m "${{ env.PAW_M_cicd_container_run }}" container="$CONTAINER_NAME"
285
296
 
297
+ # NODE_ENV=production: Next.js (SSR) and most React servers expect it at runtime
286
298
  SUDO docker run -d \
287
299
  -p ${PORT}:3000 \
288
300
  --name $CONTAINER_NAME \
289
301
  -e TZ=Asia/Seoul \
302
+ -e NODE_ENV=production \
290
303
  -v /etc/localtime:/etc/localtime:ro \
304
+ --restart unless-stopped \
291
305
  ${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}-${{ env.DOCKER_IMAGE_PREFIX }}:${BRANCH}
292
306
 
293
307
  # Confirm the deployment
@@ -298,7 +312,7 @@ jobs:
298
312
  m "${{ env.PAW_M_cicd_summary_branch }}" branch="$BRANCH"
299
313
  m "${{ env.PAW_M_cicd_summary_container }}" container="$CONTAINER_NAME"
300
314
  m "${{ env.PAW_M_cicd_summary_port }}" port="$PORT"
301
- m "${{ env.PAW_M_cicd_summary_time }}" time="$(date '+%Y-%m-%d %H:%M:%S')"
315
+ m "${{ env.PAW_M_cicd_summary_time }}" time="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
302
316
  echo ""
303
317
  m "${{ env.PAW_M_cicd_summary_url }}" url="http://${{ secrets.SERVER_HOST }}:${PORT}"
304
318
 
@@ -65,7 +65,10 @@ jobs:
65
65
  fetch-depth: 0
66
66
 
67
67
  - name: Load messages
68
- run: echo "PAW_MSG=$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_ci.)" >> "$GITHUB_ENV"
68
+ id: load_messages
69
+ run: |
70
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
71
+ echo "PAW_MSG=$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_ci.)" >> "$GITHUB_ENV"
69
72
 
70
73
  - name: Setup JDK
71
74
  uses: actions/setup-java@v5
@@ -74,11 +77,11 @@ jobs:
74
77
  distribution: 'temurin'
75
78
 
76
79
  - name: Validate Gradle Wrapper
77
- uses: gradle/wrapper-validation-action@v2
80
+ uses: gradle/actions/wrapper-validation@v5
78
81
  continue-on-error: true
79
82
 
80
83
  - name: Setup Gradle
81
- uses: gradle/actions/setup-gradle@v4
84
+ uses: gradle/actions/setup-gradle@v5
82
85
  with:
83
86
  gradle-version: wrapper
84
87
 
@@ -117,27 +120,38 @@ jobs:
117
120
 
118
121
  - name: Generate Build Report
119
122
  id: report
120
- if: always()
123
+ if: always() && steps.load_messages.outcome == 'success'
121
124
  run: |
125
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
122
126
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
123
127
  STATUS_OK="$(m wf_spring_ci.status_ok)"
124
128
  STATUS_FAIL="$(m wf_spring_ci.status_fail)"
129
+ STATUS_NOT_RUN="$(m wf_spring_ci.status_not_run)"
125
130
  COMPILE_STATUS="$STATUS_OK"
126
131
  TEST_STATUS="$STATUS_OK"
127
132
  BUILD_STATUS="$STATUS_OK"
128
133
 
129
134
  COMPILE_EXIT_CODE="${{ steps.compile.outputs.compile_exit_code }}"
130
- if [ "$COMPILE_EXIT_CODE" != "0" ] && [ "$COMPILE_EXIT_CODE" != "" ]; then
135
+ TEST_EXIT_CODE="${{ steps.test.outputs.test_exit_code }}"
136
+ BUILD_EXIT_CODE="${{ steps.build.outputs.build_exit_code }}"
137
+
138
+ # An empty exit code means the step never ran; a failed compile means gradle never got to run the tests
139
+ # or package the jar, so their non-zero exit codes are not failures of those steps themselves
140
+ if [ -z "$COMPILE_EXIT_CODE" ]; then
141
+ COMPILE_STATUS="$STATUS_NOT_RUN"
142
+ elif [ "$COMPILE_EXIT_CODE" != "0" ]; then
131
143
  COMPILE_STATUS="$STATUS_FAIL"
132
144
  fi
133
-
134
- TEST_EXIT_CODE="${{ steps.test.outputs.test_exit_code }}"
135
- if [ "$TEST_EXIT_CODE" != "0" ] && [ "$TEST_EXIT_CODE" != "" ]; then
145
+
146
+ if [ -z "$TEST_EXIT_CODE" ] || { [ -n "$COMPILE_EXIT_CODE" ] && [ "$COMPILE_EXIT_CODE" != "0" ]; }; then
147
+ TEST_STATUS="$STATUS_NOT_RUN"
148
+ elif [ "$TEST_EXIT_CODE" != "0" ]; then
136
149
  TEST_STATUS="$STATUS_FAIL"
137
150
  fi
138
-
139
- BUILD_EXIT_CODE="${{ steps.build.outputs.build_exit_code }}"
140
- if [ "$BUILD_EXIT_CODE" != "0" ] && [ "$BUILD_EXIT_CODE" != "" ]; then
151
+
152
+ if [ -z "$BUILD_EXIT_CODE" ] || { [ -n "$COMPILE_EXIT_CODE" ] && [ "$COMPILE_EXIT_CODE" != "0" ]; }; then
153
+ BUILD_STATUS="$STATUS_NOT_RUN"
154
+ elif [ "$BUILD_EXIT_CODE" != "0" ]; then
141
155
  BUILD_STATUS="$STATUS_FAIL"
142
156
  fi
143
157
 
@@ -157,8 +171,9 @@ jobs:
157
171
 
158
172
  - name: Extract Error Details
159
173
  id: errors
160
- if: always()
174
+ if: always() && steps.load_messages.outcome == 'success'
161
175
  run: |
176
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
162
177
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
163
178
  if [ -f compile_output.txt ] && [ "${{ steps.compile.outputs.compile_exit_code }}" != "0" ]; then
164
179
  echo "compile_errors<<EOF" >> $GITHUB_OUTPUT
@@ -170,7 +185,7 @@ jobs:
170
185
  echo "EOF" >> $GITHUB_OUTPUT
171
186
  fi
172
187
 
173
- if [ -f test_output.txt ] && [ "${{ steps.test.outputs.test_exit_code }}" != "0" ]; then
188
+ if [ -f test_output.txt ] && [ "${{ steps.test.outputs.test_exit_code }}" != "0" ] && [ "${{ steps.compile.outputs.compile_exit_code }}" = "0" ]; then
174
189
  echo "test_errors<<EOF" >> $GITHUB_OUTPUT
175
190
  m wf_spring_ci.err_test_header >> $GITHUB_OUTPUT
176
191
  echo "" >> $GITHUB_OUTPUT
@@ -180,7 +195,7 @@ jobs:
180
195
  echo "EOF" >> $GITHUB_OUTPUT
181
196
  fi
182
197
 
183
- if [ -f build_output.txt ] && [ "${{ steps.build.outputs.build_exit_code }}" != "0" ]; then
198
+ if [ -f build_output.txt ] && [ "${{ steps.build.outputs.build_exit_code }}" != "0" ] && [ "${{ steps.compile.outputs.compile_exit_code }}" = "0" ]; then
184
199
  echo "build_errors<<EOF" >> $GITHUB_OUTPUT
185
200
  m wf_spring_ci.err_build_header >> $GITHUB_OUTPUT
186
201
  echo "" >> $GITHUB_OUTPUT
@@ -193,6 +208,11 @@ jobs:
193
208
  - name: Comment on PR (Success)
194
209
  if: github.event_name == 'pull_request' && steps.report.outputs.overall_ok == 'true'
195
210
  uses: actions/github-script@v9
211
+ env:
212
+ PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS: ${{ steps.report.outputs.build_status }}
213
+ PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS: ${{ steps.report.outputs.compile_status }}
214
+ PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_STATUS: ${{ steps.report.outputs.overall_status }}
215
+ PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS: ${{ steps.report.outputs.test_status }}
196
216
  with:
197
217
  github-token: ${{ secrets.GITHUB_TOKEN }}
198
218
  script: |
@@ -217,14 +237,14 @@ jobs:
217
237
  '',
218
238
  m('table_header'),
219
239
  '|------|------|',
220
- m('row_compile', { status: '${{ steps.report.outputs.compile_status }}' }),
221
- m('row_test', { status: '${{ steps.report.outputs.test_status }}' }),
222
- m('row_build', { status: '${{ steps.report.outputs.build_status }}' }),
240
+ m('row_compile', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS }),
241
+ m('row_test', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS }),
242
+ m('row_build', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS }),
223
243
  '',
224
- m('overall', { status: '${{ steps.report.outputs.overall_status }}' }),
244
+ m('overall', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_STATUS }),
225
245
  '',
226
246
  '---',
227
- m('footer', { time: new Date().toLocaleString(m('date_locale'), { timeZone: 'Asia/Seoul' }) }),
247
+ m('footer', { time: new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC' }),
228
248
  ].join('\n');
229
249
 
230
250
  if (botComment) {
@@ -246,6 +266,15 @@ jobs:
246
266
  - name: Comment on PR (Failure)
247
267
  if: github.event_name == 'pull_request' && steps.report.outputs.overall_ok == 'false'
248
268
  uses: actions/github-script@v9
269
+ # Pass multi-line error logs via env; inlining them into a JS string literal breaks the script
270
+ env:
271
+ PAW_IN_STEPS_ERRORS_OUTPUTS_BUILD_ERRORS: ${{ steps.errors.outputs.build_errors }}
272
+ PAW_IN_STEPS_ERRORS_OUTPUTS_COMPILE_ERRORS: ${{ steps.errors.outputs.compile_errors }}
273
+ PAW_IN_STEPS_ERRORS_OUTPUTS_TEST_ERRORS: ${{ steps.errors.outputs.test_errors }}
274
+ PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS: ${{ steps.report.outputs.build_status }}
275
+ PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS: ${{ steps.report.outputs.compile_status }}
276
+ PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_STATUS: ${{ steps.report.outputs.overall_status }}
277
+ PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS: ${{ steps.report.outputs.test_status }}
249
278
  with:
250
279
  github-token: ${{ secrets.GITHUB_TOKEN }}
251
280
  script: |
@@ -266,18 +295,18 @@ jobs:
266
295
  let errorDetails = '';
267
296
 
268
297
  // Append compile errors
269
- if ('${{ steps.errors.outputs.compile_errors }}') {
270
- errorDetails += '${{ steps.errors.outputs.compile_errors }}\n\n';
298
+ if (process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_COMPILE_ERRORS) {
299
+ errorDetails += process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_COMPILE_ERRORS + '\n\n';
271
300
  }
272
301
 
273
302
  // Append test errors
274
- if ('${{ steps.errors.outputs.test_errors }}') {
275
- errorDetails += '${{ steps.errors.outputs.test_errors }}\n\n';
303
+ if (process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_TEST_ERRORS) {
304
+ errorDetails += process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_TEST_ERRORS + '\n\n';
276
305
  }
277
306
 
278
307
  // Append build errors
279
- if ('${{ steps.errors.outputs.build_errors }}') {
280
- errorDetails += '${{ steps.errors.outputs.build_errors }}\n\n';
308
+ if (process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_BUILD_ERRORS) {
309
+ errorDetails += process.env.PAW_IN_STEPS_ERRORS_OUTPUTS_BUILD_ERRORS + '\n\n';
281
310
  }
282
311
 
283
312
  const failureMessage = [
@@ -287,11 +316,11 @@ jobs:
287
316
  '',
288
317
  m('table_header'),
289
318
  '|------|------|',
290
- m('row_compile', { status: '${{ steps.report.outputs.compile_status }}' }),
291
- m('row_test', { status: '${{ steps.report.outputs.test_status }}' }),
292
- m('row_build', { status: '${{ steps.report.outputs.build_status }}' }),
319
+ m('row_compile', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS }),
320
+ m('row_test', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS }),
321
+ m('row_build', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS }),
293
322
  '',
294
- m('overall', { status: '${{ steps.report.outputs.overall_status }}' }),
323
+ m('overall', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_STATUS }),
295
324
  '',
296
325
  m('details_header'),
297
326
  '',
@@ -303,7 +332,7 @@ jobs:
303
332
  m('fix_test'),
304
333
  '',
305
334
  '---',
306
- m('footer', { time: new Date().toLocaleString(m('date_locale'), { timeZone: 'Asia/Seoul' }) }),
335
+ m('footer', { time: new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC' }),
307
336
  ].join('\n');
308
337
 
309
338
  if (botComment) {
@@ -323,14 +352,19 @@ jobs:
323
352
  }
324
353
 
325
354
  - name: Create Check Run
326
- if: always()
355
+ if: always() && steps.load_messages.outcome == 'success'
327
356
  uses: actions/github-script@v9
357
+ env:
358
+ PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS: ${{ steps.report.outputs.build_status }}
359
+ PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS: ${{ steps.report.outputs.compile_status }}
360
+ PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_OK: ${{ steps.report.outputs.overall_ok }}
361
+ PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS: ${{ steps.report.outputs.test_status }}
328
362
  with:
329
363
  github-token: ${{ secrets.GITHUB_TOKEN }}
330
364
  script: |
331
365
  const M = JSON.parse(process.env.PAW_MSG);
332
366
  const m = (k, p = {}) => M['wf_spring_ci.' + k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
333
- const conclusion = '${{ steps.report.outputs.overall_ok }}' === 'true' ? 'success' : 'failure';
367
+ const conclusion = process.env.PAW_IN_STEPS_REPORT_OUTPUTS_OVERALL_OK === 'true' ? 'success' : 'failure';
334
368
  const summary = m(conclusion === 'success' ? 'check_summary_success' : 'check_summary_failure');
335
369
 
336
370
  await github.rest.checks.create({
@@ -343,9 +377,9 @@ jobs:
343
377
  output: {
344
378
  title: '🔧 CI Build Check',
345
379
  summary: summary,
346
- text: '\n ' + m('check_compile', { status: '${{ steps.report.outputs.compile_status }}' }) +
347
- '\n ' + m('check_test', { status: '${{ steps.report.outputs.test_status }}' }) + ' ' +
348
- '\n ' + m('check_build', { status: '${{ steps.report.outputs.build_status }}' }) +
380
+ text: '\n ' + m('check_compile', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_COMPILE_STATUS }) +
381
+ '\n ' + m('check_test', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_TEST_STATUS }) + ' ' +
382
+ '\n ' + m('check_build', { status: process.env.PAW_IN_STEPS_REPORT_OUTPUTS_BUILD_STATUS }) +
349
383
  '\n '
350
384
  }
351
385
  });
@@ -353,6 +387,7 @@ jobs:
353
387
  - name: Fail Job on Build Failure
354
388
  if: steps.report.outputs.overall_ok == 'false'
355
389
  run: |
390
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
356
391
  python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get wf_spring_ci.build_failed
357
392
  exit 1
358
393
 
@@ -376,6 +411,7 @@ jobs:
376
411
  env:
377
412
  RESULTS: ${{ toJSON(needs.*.result) }}
378
413
  run: |
414
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
379
415
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
380
416
  m wf_spring_ci.gate_results results="$RESULTS"
381
417
  if echo "$RESULTS" | grep -Eq '"(failure|cancelled)"'; then
@@ -142,6 +142,7 @@ jobs:
142
142
  SSH_KEY: ${{ secrets.SSH_KEY }}
143
143
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
144
144
  run: |
145
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
145
146
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
146
147
  MISSING=""
147
148
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
@@ -180,6 +181,7 @@ jobs:
180
181
 
181
182
  - name: Generate application-prod.yml
182
183
  run: |
184
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
183
185
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
184
186
  mkdir -p ${{ env.APPLICATION_YML_DIR }}
185
187
  cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
@@ -192,10 +194,10 @@ jobs:
192
194
  run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
193
195
 
194
196
  - name: Set up Docker Buildx
195
- uses: docker/setup-buildx-action@v3
197
+ uses: docker/setup-buildx-action@v4
196
198
 
197
199
  - name: Log in to DockerHub
198
- uses: docker/login-action@v3
200
+ uses: docker/login-action@v4
199
201
  with:
200
202
  username: ${{ secrets.DOCKERHUB_USERNAME }}
201
203
  password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -209,7 +211,7 @@ jobs:
209
211
  ${{ runner.os }}-buildx-
210
212
 
211
213
  - name: Build and push Docker image
212
- uses: docker/build-push-action@v5
214
+ uses: docker/build-push-action@v7
213
215
  with:
214
216
  context: ${{ env.PROJECT_PATH }}
215
217
  file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
@@ -242,6 +244,7 @@ jobs:
242
244
 
243
245
  - name: Load messages
244
246
  run: |
247
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
245
248
  MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
246
249
  | python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
247
250
  | base64 | tr -d '\n')"
@@ -316,8 +319,8 @@ jobs:
316
319
  m banner_project name="${PROJECT_NAME}"
317
320
  m banner_domain domain="${SERVICE_DOMAIN}"
318
321
  m banner_image image="${IMAGE}"
319
- echo "🎨 Blue=${BLUE_PORT} | Green=${GREEN_PORT}"
320
- echo "🔧 Nginx config: ${NGINX_RP_CONF}"
322
+ m nginx_colors blue="${BLUE_PORT}" green="${GREEN_PORT}"
323
+ m nginx_conf_path path="${NGINX_RP_CONF}"
321
324
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
322
325
 
323
326
  # 1. Pull the latest image
@@ -485,7 +488,7 @@ jobs:
485
488
  m nginx_test_ok
486
489
  rm -f "${TMP_FILE}"
487
490
 
488
- echo "🔄 nginx reload"
491
+ m nginx_reload
489
492
  if ! (SUDO systemctl reload nginx); then
490
493
  m nginx_reload_fail
491
494
  if ! (SUDO systemctl restart nginx); then
@@ -134,6 +134,7 @@ jobs:
134
134
  SSH_KEY: ${{ secrets.SSH_KEY }}
135
135
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
136
136
  run: |
137
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
137
138
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
138
139
  MISSING=""
139
140
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
@@ -172,6 +173,7 @@ jobs:
172
173
 
173
174
  - name: Generate application-prod.yml
174
175
  run: |
176
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
175
177
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
176
178
  mkdir -p ${{ env.APPLICATION_YML_DIR }}
177
179
  cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
@@ -184,10 +186,10 @@ jobs:
184
186
  run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
185
187
 
186
188
  - name: Set up Docker Buildx
187
- uses: docker/setup-buildx-action@v3
189
+ uses: docker/setup-buildx-action@v4
188
190
 
189
191
  - name: Log in to DockerHub
190
- uses: docker/login-action@v3
192
+ uses: docker/login-action@v4
191
193
  with:
192
194
  username: ${{ secrets.DOCKERHUB_USERNAME }}
193
195
  password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -201,7 +203,7 @@ jobs:
201
203
  ${{ runner.os }}-buildx-
202
204
 
203
205
  - name: Build and push Docker image
204
- uses: docker/build-push-action@v5
206
+ uses: docker/build-push-action@v7
205
207
  with:
206
208
  context: ${{ env.PROJECT_PATH }}
207
209
  file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
@@ -234,6 +236,7 @@ jobs:
234
236
 
235
237
  - name: Load messages
236
238
  run: |
239
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
237
240
  MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
238
241
  | python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
239
242
  | base64 | tr -d '\n')"
@@ -333,7 +336,7 @@ jobs:
333
336
  NEW_NAME="${PROJECT_NAME}-${NEW_COLOR}"
334
337
  OLD_NAME="${PROJECT_NAME}-${OLD_COLOR}"
335
338
 
336
- echo "🎨 active=${ACTIVE:-none} → new=${NEW_NAME}, old=${OLD_NAME}"
339
+ m traefik_active active="${ACTIVE:-none}" new="${NEW_NAME}" old="${OLD_NAME}"
337
340
 
338
341
  # 3. Clean up a leftover container in the same (idle) slot
339
342
  m idle_slot_cleanup name="${NEW_NAME}"