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
@@ -108,6 +108,8 @@ jobs:
108
108
  uses: actions/checkout@v7
109
109
  with:
110
110
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
111
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
112
+ path: .paw-msg
111
113
  sparse-checkout: |
112
114
  .github/scripts/messages.py
113
115
  version.yml
@@ -115,11 +117,16 @@ jobs:
115
117
 
116
118
  - name: Load messages
117
119
  run: |
118
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
120
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
119
121
  mkdir -p "$RUNNER_TEMP/paw"
120
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
122
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
123
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
124
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
125
+ rm -rf .paw-msg
126
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
127
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
121
128
  {
122
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
129
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
123
130
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
124
131
  } >> "$GITHUB_ENV"
125
132
 
@@ -210,18 +217,26 @@ jobs:
210
217
  uses: actions/checkout@v7
211
218
  with:
212
219
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
220
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
221
+ path: .paw-msg
213
222
  sparse-checkout: |
214
223
  .github/scripts/messages.py
215
224
  version.yml
216
225
  sparse-checkout-cone-mode: false
217
226
 
218
227
  - name: Load messages
228
+ id: load_messages
219
229
  run: |
220
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
230
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
221
231
  mkdir -p "$RUNNER_TEMP/paw"
222
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
232
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
233
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
234
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
235
+ rm -rf .paw-msg
236
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
237
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
223
238
  {
224
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
239
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
225
240
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
226
241
  } >> "$GITHUB_ENV"
227
242
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -399,13 +414,16 @@ jobs:
399
414
  - name: Progress - build done
400
415
  id: build_progress
401
416
  uses: actions/github-script@v9
417
+ env:
418
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
419
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME: ${{ steps.progress.outputs.start_time }}
402
420
  with:
403
421
  script: |
404
422
  const M = JSON.parse(process.env.PAW_MSG);
405
423
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
406
424
 
407
- const commentId = ${{ steps.progress.outputs.comment_id }};
408
- const startTime = ${{ steps.progress.outputs.start_time }};
425
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
426
+ const startTime = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME, 10) || Date.now();
409
427
  const now = Date.now();
410
428
  const elapsed = now - startTime;
411
429
  const minutes = Math.floor(elapsed / 60000);
@@ -439,16 +457,16 @@ jobs:
439
457
  core.setOutput('docker_start', now);
440
458
 
441
459
  - name: Log in to Docker Hub
442
- uses: docker/login-action@v3
460
+ uses: docker/login-action@v4
443
461
  with:
444
462
  username: ${{ secrets.DOCKERHUB_USERNAME }}
445
463
  password: ${{ secrets.DOCKERHUB_TOKEN }}
446
464
 
447
465
  - name: Set up Docker Buildx
448
- uses: docker/setup-buildx-action@v3
466
+ uses: docker/setup-buildx-action@v4
449
467
 
450
468
  - name: Build & push Docker image
451
- uses: docker/build-push-action@v5
469
+ uses: docker/build-push-action@v7
452
470
  with:
453
471
  context: ${{ env.PROJECT_PATH }}
454
472
  file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
@@ -460,14 +478,18 @@ jobs:
460
478
  - name: Progress - Docker done
461
479
  id: docker_progress
462
480
  uses: actions/github-script@v9
481
+ env:
482
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
483
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
484
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START: ${{ steps.build_progress.outputs.docker_start }}
463
485
  with:
464
486
  script: |
465
487
  const M = JSON.parse(process.env.PAW_MSG);
466
488
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
467
489
 
468
- const commentId = ${{ steps.progress.outputs.comment_id }};
469
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
470
- const dockerStart = ${{ steps.build_progress.outputs.docker_start }};
490
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
491
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
492
+ const dockerStart = parseInt(process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START, 10) || Date.now();
471
493
  const now = Date.now();
472
494
  const dockerElapsed = now - dockerStart;
473
495
  const minutes = Math.floor(dockerElapsed / 60000);
@@ -645,29 +667,36 @@ jobs:
645
667
 
646
668
  - name: Deploy complete comment
647
669
  uses: actions/github-script@v9
670
+ env:
671
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.docker_progress.outputs.build_duration }}
672
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
673
+ PAW_IN_STEPS_PR_OUTPUTS_REF: ${{ steps.pr.outputs.ref }}
674
+ PAW_IN_STEPS_PR_OUTPUTS_SHA: ${{ steps.pr.outputs.sha }}
675
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
676
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DEPLOY_START: ${{ steps.docker_progress.outputs.deploy_start }}
648
677
  with:
649
678
  script: |
650
679
  const M = JSON.parse(process.env.PAW_MSG);
651
680
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
652
681
 
653
- const commentId = ${{ steps.progress.outputs.comment_id }};
654
- const buildDuration = '${{ steps.docker_progress.outputs.build_duration }}';
655
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
656
- const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
682
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
683
+ const buildDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_BUILD_DURATION;
684
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
685
+ const deployStart = parseInt(process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DEPLOY_START, 10) || Date.now();
657
686
  const now = Date.now();
658
687
  const deployElapsed = now - deployStart;
659
688
  const minutes = Math.floor(deployElapsed / 60000);
660
689
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
661
690
  const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
662
691
 
663
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
664
- const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
665
- const previewPort = '${{ env.PREVIEW_PORT }}';
666
- const apiDocsPath = '${{ env.API_DOCS_PATH }}';
692
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
693
+ const domainSuffix = process.env.PREVIEW_DOMAIN_SUFFIX;
694
+ const previewPort = process.env.PREVIEW_PORT;
695
+ const apiDocsPath = process.env.API_DOCS_PATH;
667
696
  const prNumber = context.issue.number;
668
697
  const domain = `${projectName}-pr-${prNumber}.${domainSuffix}`;
669
698
  const previewUrl = `http://${domain}:${previewPort}`;
670
- const sha = '${{ steps.pr.outputs.sha }}';
699
+ const sha = process.env.PAW_IN_STEPS_PR_OUTPUTS_SHA;
671
700
 
672
701
  // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
673
702
  const envRows = [
@@ -679,7 +708,7 @@ jobs:
679
708
  envRows.push(m('wf_preview.env_container_pr', {projectName, prNumber}));
680
709
  envRows.push(m('wf_preview.env_commit', {sha}));
681
710
 
682
- const branchName = '${{ steps.pr.outputs.ref }}';
711
+ const branchName = process.env.PAW_IN_STEPS_PR_OUTPUTS_REF;
683
712
 
684
713
  const body = [
685
714
  m('wf_preview.title_pr_deployed'),
@@ -724,21 +753,25 @@ jobs:
724
753
  });
725
754
 
726
755
  - name: Build/deploy failure comment
727
- if: failure()
756
+ if: failure() && steps.load_messages.outcome == 'success'
728
757
  uses: actions/github-script@v9
758
+ env:
759
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
760
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
761
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
729
762
  with:
730
763
  script: |
731
764
  const M = JSON.parse(process.env.PAW_MSG);
732
765
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
733
766
 
734
- const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
735
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
767
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
768
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
736
769
  const prNumber = context.issue.number;
737
770
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
738
771
 
739
772
  // Show where it failed based on progress
740
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
741
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
773
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
774
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
742
775
 
743
776
  // Work out where it failed - early-stage failures are distinguished too
744
777
  let buildStatus, dockerStatus, deployStatus;
@@ -832,6 +865,8 @@ jobs:
832
865
  uses: actions/checkout@v7
833
866
  with:
834
867
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
868
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
869
+ path: .paw-msg
835
870
  sparse-checkout: |
836
871
  .github/scripts/messages.py
837
872
  version.yml
@@ -839,11 +874,16 @@ jobs:
839
874
 
840
875
  - name: Load messages
841
876
  run: |
842
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
877
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
843
878
  mkdir -p "$RUNNER_TEMP/paw"
844
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
879
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
880
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
881
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
882
+ rm -rf .paw-msg
883
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
884
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
845
885
  {
846
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
886
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
847
887
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
848
888
  } >> "$GITHUB_ENV"
849
889
 
@@ -856,7 +896,7 @@ jobs:
856
896
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
857
897
 
858
898
  const issueNumber = context.issue.number;
859
- const marker = '${{ env.ISSUE_HELPER_MARKER }}';
899
+ const marker = process.env.ISSUE_HELPER_MARKER;
860
900
 
861
901
  console.log(m('wf_preview.log_search_branch', {issueNumber}));
862
902
  console.log(m('wf_preview.log_marker', {marker}));
@@ -901,7 +941,7 @@ jobs:
901
941
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
902
942
 
903
943
  const issueNumber = context.issue.number;
904
- const marker = '${{ env.ISSUE_HELPER_MARKER }}';
944
+ const marker = process.env.ISSUE_HELPER_MARKER;
905
945
 
906
946
  const body = [
907
947
  m('wf_preview.title_branch_missing_warn'),
@@ -945,18 +985,26 @@ jobs:
945
985
  uses: actions/checkout@v7
946
986
  with:
947
987
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
988
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
989
+ path: .paw-msg
948
990
  sparse-checkout: |
949
991
  .github/scripts/messages.py
950
992
  version.yml
951
993
  sparse-checkout-cone-mode: false
952
994
 
953
995
  - name: Load messages
996
+ id: load_messages
954
997
  run: |
955
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
998
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
956
999
  mkdir -p "$RUNNER_TEMP/paw"
957
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1000
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
1001
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1002
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
1003
+ rm -rf .paw-msg
1004
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1005
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
958
1006
  {
959
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1007
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
960
1008
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
961
1009
  } >> "$GITHUB_ENV"
962
1010
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -986,13 +1034,16 @@ jobs:
986
1034
  - name: Check branch exists
987
1035
  id: check_branch
988
1036
  uses: actions/github-script@v9
1037
+ env:
1038
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1039
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
989
1040
  with:
990
1041
  script: |
991
1042
  const M = JSON.parse(process.env.PAW_MSG);
992
1043
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
993
1044
 
994
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
995
- const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
1045
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1046
+ const issueNumber = parseInt(process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER, 10) || 0;
996
1047
 
997
1048
  console.log(m('wf_preview.log_check_exists', {branchName}));
998
1049
 
@@ -1049,23 +1100,28 @@ jobs:
1049
1100
  - name: Set issue info
1050
1101
  if: steps.check_branch.outputs.exists == 'true'
1051
1102
  id: issue
1103
+ env:
1104
+ PAW_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1052
1105
  run: |
1053
- echo "ref=${{ needs.get-branch-from-issue.outputs.branch_name }}" >> $GITHUB_OUTPUT
1106
+ echo "ref=$PAW_BRANCH_NAME" >> $GITHUB_OUTPUT
1054
1107
  echo "issue_number=${{ needs.get-branch-from-issue.outputs.issue_number }}" >> $GITHUB_OUTPUT
1055
1108
  # Get the latest commit SHA of the branch
1056
- echo "sha=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.get-branch-from-issue.outputs.branch_name }} | cut -c1-7)" >> $GITHUB_OUTPUT
1109
+ echo "sha=$(git ls-remote https://github.com/${{ github.repository }}.git "refs/heads/$PAW_BRANCH_NAME" | cut -c1-7)" >> $GITHUB_OUTPUT
1057
1110
 
1058
1111
  - name: Create progress comment
1059
1112
  if: steps.check_branch.outputs.exists == 'true'
1060
1113
  id: progress
1061
1114
  uses: actions/github-script@v9
1115
+ env:
1116
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1117
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1062
1118
  with:
1063
1119
  script: |
1064
1120
  const M = JSON.parse(process.env.PAW_MSG);
1065
1121
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1066
1122
 
1067
- const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
1068
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1123
+ const issueNumber = parseInt(process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER, 10) || 0;
1124
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1069
1125
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1070
1126
  const startTime = Date.now();
1071
1127
 
@@ -1173,14 +1229,18 @@ jobs:
1173
1229
  if: steps.check_branch.outputs.exists == 'true'
1174
1230
  id: build_progress
1175
1231
  uses: actions/github-script@v9
1232
+ env:
1233
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1234
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
1235
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME: ${{ steps.progress.outputs.start_time }}
1176
1236
  with:
1177
1237
  script: |
1178
1238
  const M = JSON.parse(process.env.PAW_MSG);
1179
1239
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1180
1240
 
1181
- const commentId = ${{ steps.progress.outputs.comment_id }};
1182
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1183
- const startTime = ${{ steps.progress.outputs.start_time }};
1241
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
1242
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1243
+ const startTime = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME, 10) || Date.now();
1184
1244
  const now = Date.now();
1185
1245
  const elapsed = now - startTime;
1186
1246
  const minutes = Math.floor(elapsed / 60000);
@@ -1217,18 +1277,18 @@ jobs:
1217
1277
 
1218
1278
  - name: Log in to Docker Hub
1219
1279
  if: steps.check_branch.outputs.exists == 'true'
1220
- uses: docker/login-action@v3
1280
+ uses: docker/login-action@v4
1221
1281
  with:
1222
1282
  username: ${{ secrets.DOCKERHUB_USERNAME }}
1223
1283
  password: ${{ secrets.DOCKERHUB_TOKEN }}
1224
1284
 
1225
1285
  - name: Set up Docker Buildx
1226
1286
  if: steps.check_branch.outputs.exists == 'true'
1227
- uses: docker/setup-buildx-action@v3
1287
+ uses: docker/setup-buildx-action@v4
1228
1288
 
1229
1289
  - name: Build & push Docker image
1230
1290
  if: steps.check_branch.outputs.exists == 'true'
1231
- uses: docker/build-push-action@v5
1291
+ uses: docker/build-push-action@v7
1232
1292
  with:
1233
1293
  context: ${{ env.PROJECT_PATH }}
1234
1294
  file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
@@ -1241,15 +1301,20 @@ jobs:
1241
1301
  if: steps.check_branch.outputs.exists == 'true'
1242
1302
  id: docker_progress
1243
1303
  uses: actions/github-script@v9
1304
+ env:
1305
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1306
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
1307
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
1308
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START: ${{ steps.build_progress.outputs.docker_start }}
1244
1309
  with:
1245
1310
  script: |
1246
1311
  const M = JSON.parse(process.env.PAW_MSG);
1247
1312
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1248
1313
 
1249
- const commentId = ${{ steps.progress.outputs.comment_id }};
1250
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1251
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
1252
- const dockerStart = ${{ steps.build_progress.outputs.docker_start }};
1314
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
1315
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1316
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
1317
+ const dockerStart = parseInt(process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START, 10) || Date.now();
1253
1318
  const now = Date.now();
1254
1319
  const dockerElapsed = now - dockerStart;
1255
1320
  const minutes = Math.floor(dockerElapsed / 60000);
@@ -1431,30 +1496,38 @@ jobs:
1431
1496
  - name: Deploy complete comment
1432
1497
  if: steps.check_branch.outputs.exists == 'true'
1433
1498
  uses: actions/github-script@v9
1499
+ env:
1500
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1501
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.docker_progress.outputs.build_duration }}
1502
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
1503
+ PAW_IN_STEPS_ISSUE_OUTPUTS_SHA: ${{ steps.issue.outputs.sha }}
1504
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
1505
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DEPLOY_START: ${{ steps.docker_progress.outputs.deploy_start }}
1506
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1434
1507
  with:
1435
1508
  script: |
1436
1509
  const M = JSON.parse(process.env.PAW_MSG);
1437
1510
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1438
1511
 
1439
- const commentId = ${{ steps.progress.outputs.comment_id }};
1440
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1441
- const buildDuration = '${{ steps.docker_progress.outputs.build_duration }}';
1442
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
1443
- const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
1512
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
1513
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1514
+ const buildDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_BUILD_DURATION;
1515
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
1516
+ const deployStart = parseInt(process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DEPLOY_START, 10) || Date.now();
1444
1517
  const now = Date.now();
1445
1518
  const deployElapsed = now - deployStart;
1446
1519
  const minutes = Math.floor(deployElapsed / 60000);
1447
1520
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
1448
1521
  const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1449
1522
 
1450
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1451
- const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
1452
- const previewPort = '${{ env.PREVIEW_PORT }}';
1453
- const apiDocsPath = '${{ env.API_DOCS_PATH }}';
1454
- const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
1523
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
1524
+ const domainSuffix = process.env.PREVIEW_DOMAIN_SUFFIX;
1525
+ const previewPort = process.env.PREVIEW_PORT;
1526
+ const apiDocsPath = process.env.API_DOCS_PATH;
1527
+ const issueNumber = parseInt(process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER, 10) || 0;
1455
1528
  const domain = `${projectName}-pr-${issueNumber}.${domainSuffix}`;
1456
1529
  const previewUrl = `http://${domain}:${previewPort}`;
1457
- const sha = '${{ steps.issue.outputs.sha }}';
1530
+ const sha = process.env.PAW_IN_STEPS_ISSUE_OUTPUTS_SHA;
1458
1531
 
1459
1532
  // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
1460
1533
  const envRows = [
@@ -1512,21 +1585,27 @@ jobs:
1512
1585
  });
1513
1586
 
1514
1587
  - name: Build/deploy failure comment
1515
- if: failure() && steps.check_branch.outputs.exists == 'true'
1588
+ if: failure() && steps.load_messages.outcome == 'success' && steps.check_branch.outputs.exists == 'true'
1516
1589
  uses: actions/github-script@v9
1590
+ env:
1591
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME: ${{ needs.get-branch-from-issue.outputs.branch_name }}
1592
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
1593
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
1594
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
1595
+ PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1517
1596
  with:
1518
1597
  script: |
1519
1598
  const M = JSON.parse(process.env.PAW_MSG);
1520
1599
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1521
1600
 
1522
- const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1523
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1524
- const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
1525
- const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1601
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
1602
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
1603
+ const issueNumber = parseInt(process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER, 10) || 0;
1604
+ const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
1526
1605
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1527
1606
 
1528
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
1529
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
1607
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
1608
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
1530
1609
 
1531
1610
  let buildStatus, dockerStatus, deployStatus;
1532
1611
  let buildTime = '-', dockerTime = '-';
@@ -1612,6 +1691,8 @@ jobs:
1612
1691
  uses: actions/checkout@v7
1613
1692
  with:
1614
1693
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1694
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
1695
+ path: .paw-msg
1615
1696
  sparse-checkout: |
1616
1697
  .github/scripts/messages.py
1617
1698
  version.yml
@@ -1619,11 +1700,16 @@ jobs:
1619
1700
 
1620
1701
  - name: Load messages
1621
1702
  run: |
1622
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1703
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
1623
1704
  mkdir -p "$RUNNER_TEMP/paw"
1624
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1705
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
1706
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1707
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
1708
+ rm -rf .paw-msg
1709
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1710
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1625
1711
  {
1626
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1712
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1627
1713
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1628
1714
  } >> "$GITHUB_ENV"
1629
1715
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -1650,6 +1736,41 @@ jobs:
1650
1736
  print("PAW_EOF")
1651
1737
  PY
1652
1738
 
1739
+ # Without the deploy secrets no preview could have been deployed: on close events skip the cleanup quietly,
1740
+ # and on an explicit command report what is missing instead of surfacing a raw SSH action error
1741
+ - name: Deploy pre-check (Secrets)
1742
+ id: precheck
1743
+ env:
1744
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
1745
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
1746
+ SERVER_USER: ${{ secrets.SERVER_USER }}
1747
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
1748
+ SSH_KEY: ${{ secrets.SSH_KEY }}
1749
+ EVENT_NAME: ${{ github.event_name }}
1750
+ run: |
1751
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1752
+
1753
+ MISSING=""
1754
+ for NAME in DOCKERHUB_USERNAME SERVER_HOST SERVER_USER; do
1755
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
1756
+ done
1757
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
1758
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
1759
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
1760
+ else
1761
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
1762
+ fi
1763
+ if [ -n "$MISSING" ]; then
1764
+ if [ "$EVENT_NAME" = "issue_comment" ]; then
1765
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
1766
+ exit 1
1767
+ fi
1768
+ echo "::warning title=$(m wf_preview.precheck_skip_title)::$(m wf_preview.precheck_skip_body missing="$MISSING")"
1769
+ echo "ready=false" >> "$GITHUB_OUTPUT"
1770
+ exit 0
1771
+ fi
1772
+ echo "ready=true" >> "$GITHUB_OUTPUT"
1773
+
1653
1774
  - name: Get PR/issue number
1654
1775
  id: pr_number
1655
1776
  run: |
@@ -1665,6 +1786,7 @@ jobs:
1665
1786
  fi
1666
1787
 
1667
1788
  - name: Remove container & image
1789
+ if: steps.precheck.outputs.ready == 'true'
1668
1790
  uses: appleboy/ssh-action@v1.0.3
1669
1791
  with:
1670
1792
  host: ${{ secrets.SERVER_HOST }}
@@ -1710,17 +1832,20 @@ jobs:
1710
1832
 
1711
1833
  - name: Destroy complete comment
1712
1834
  if: |
1713
- github.event_name == 'issue_comment' ||
1714
- (github.event_name == 'pull_request' && github.event.action == 'closed') ||
1715
- (github.event_name == 'issues' && github.event.action == 'closed')
1835
+ steps.precheck.outputs.ready == 'true' && (
1836
+ github.event_name == 'issue_comment' ||
1837
+ (github.event_name == 'pull_request' && github.event.action == 'closed') ||
1838
+ (github.event_name == 'issues' && github.event.action == 'closed'))
1716
1839
  uses: actions/github-script@v9
1840
+ env:
1841
+ PAW_IN_STEPS_PR_NUMBER_OUTPUTS_NUMBER: ${{ steps.pr_number.outputs.number }}
1717
1842
  with:
1718
1843
  script: |
1719
1844
  const M = JSON.parse(process.env.PAW_MSG);
1720
1845
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1721
1846
 
1722
- const number = ${{ steps.pr_number.outputs.number }};
1723
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1847
+ const number = parseInt(process.env.PAW_IN_STEPS_PR_NUMBER_OUTPUTS_NUMBER, 10) || 0;
1848
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
1724
1849
 
1725
1850
  const body = [
1726
1851
  m('wf_preview.title_destroyed'),
@@ -1756,6 +1881,8 @@ jobs:
1756
1881
  uses: actions/checkout@v7
1757
1882
  with:
1758
1883
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1884
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
1885
+ path: .paw-msg
1759
1886
  sparse-checkout: |
1760
1887
  .github/scripts/messages.py
1761
1888
  version.yml
@@ -1763,11 +1890,16 @@ jobs:
1763
1890
 
1764
1891
  - name: Load messages
1765
1892
  run: |
1766
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1893
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
1767
1894
  mkdir -p "$RUNNER_TEMP/paw"
1768
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1895
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
1896
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1897
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
1898
+ rm -rf .paw-msg
1899
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1900
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1769
1901
  {
1770
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1902
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1771
1903
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1772
1904
  } >> "$GITHUB_ENV"
1773
1905
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -1794,6 +1926,32 @@ jobs:
1794
1926
  print("PAW_EOF")
1795
1927
  PY
1796
1928
 
1929
+ # Report the missing secrets by name instead of surfacing a raw SSH action error
1930
+ - name: Deploy pre-check (Secrets)
1931
+ id: precheck
1932
+ env:
1933
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
1934
+ SERVER_USER: ${{ secrets.SERVER_USER }}
1935
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
1936
+ SSH_KEY: ${{ secrets.SSH_KEY }}
1937
+ run: |
1938
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1939
+
1940
+ MISSING=""
1941
+ for NAME in SERVER_HOST SERVER_USER; do
1942
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
1943
+ done
1944
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
1945
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
1946
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
1947
+ else
1948
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
1949
+ fi
1950
+ if [ -n "$MISSING" ]; then
1951
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
1952
+ exit 1
1953
+ fi
1954
+
1797
1955
  - name: Check container status
1798
1956
  id: status
1799
1957
  uses: appleboy/ssh-action@v1.0.3
@@ -1845,19 +2003,22 @@ jobs:
1845
2003
 
1846
2004
  - name: Post status comment
1847
2005
  uses: actions/github-script@v9
2006
+ env:
2007
+ PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_IS_PR: ${{ needs.check-command.outputs.is_pr }}
2008
+ PAW_IN_STEPS_CHECK_RUNNING_OUTCOME: ${{ steps.check_running.outcome }}
1848
2009
  with:
1849
2010
  script: |
1850
2011
  const M = JSON.parse(process.env.PAW_MSG);
1851
2012
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1852
2013
 
1853
2014
  const number = context.issue.number;
1854
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1855
- const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
1856
- const previewPort = '${{ env.PREVIEW_PORT }}';
2015
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2016
+ const domainSuffix = process.env.PREVIEW_DOMAIN_SUFFIX;
2017
+ const previewPort = process.env.PREVIEW_PORT;
1857
2018
  const domain = `${projectName}-pr-${number}.${domainSuffix}`;
1858
2019
  const previewUrl = `http://${domain}:${previewPort}`;
1859
- const isRunning = '${{ steps.check_running.outcome }}' === 'success';
1860
- const isPr = '${{ needs.check-command.outputs.is_pr }}' === 'true';
2020
+ const isRunning = process.env.PAW_IN_STEPS_CHECK_RUNNING_OUTCOME === 'success';
2021
+ const isPr = process.env.PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_IS_PR === 'true';
1861
2022
  const contextType = isPr ? 'PR' : 'Issue';
1862
2023
 
1863
2024
  let body;
@@ -1922,18 +2083,26 @@ jobs:
1922
2083
  uses: actions/checkout@v7
1923
2084
  with:
1924
2085
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2086
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
2087
+ path: .paw-msg
1925
2088
  sparse-checkout: |
1926
2089
  .github/scripts/messages.py
1927
2090
  version.yml
1928
2091
  sparse-checkout-cone-mode: false
1929
2092
 
1930
2093
  - name: Load messages
2094
+ id: load_messages
1931
2095
  run: |
1932
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2096
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
1933
2097
  mkdir -p "$RUNNER_TEMP/paw"
1934
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2098
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
2099
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2100
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
2101
+ rm -rf .paw-msg
2102
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2103
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1935
2104
  {
1936
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2105
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1937
2106
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1938
2107
  } >> "$GITHUB_ENV"
1939
2108
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -1963,9 +2132,11 @@ jobs:
1963
2132
  - name: Extract issue number from branch name
1964
2133
  id: extract
1965
2134
  uses: actions/github-script@v9
2135
+ env:
2136
+ PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH: ${{ needs.check-command.outputs.custom_branch }}
1966
2137
  with:
1967
2138
  script: |
1968
- const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2139
+ const branchName = process.env.PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH;
1969
2140
  const issueMatch = branchName.match(/#(\d+)/);
1970
2141
 
1971
2142
  if (issueMatch) {
@@ -1990,9 +2161,11 @@ jobs:
1990
2161
  - name: Check branch exists
1991
2162
  id: check_branch
1992
2163
  uses: actions/github-script@v9
2164
+ env:
2165
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
1993
2166
  with:
1994
2167
  script: |
1995
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2168
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
1996
2169
  try {
1997
2170
  await github.rest.repos.getBranch({
1998
2171
  owner: context.repo.owner,
@@ -2011,12 +2184,14 @@ jobs:
2011
2184
  - name: Branch not found error comment
2012
2185
  if: steps.check_branch.outputs.exists == 'false'
2013
2186
  uses: actions/github-script@v9
2187
+ env:
2188
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2014
2189
  with:
2015
2190
  script: |
2016
2191
  const M = JSON.parse(process.env.PAW_MSG);
2017
2192
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2018
2193
 
2019
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2194
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2020
2195
 
2021
2196
  const body = [
2022
2197
  m('wf_preview.title_branch_missing'),
@@ -2097,15 +2272,19 @@ jobs:
2097
2272
  if: steps.check_branch.outputs.exists == 'true'
2098
2273
  id: progress
2099
2274
  uses: actions/github-script@v9
2275
+ env:
2276
+ PAW_IN_STEPS_COMMIT_OUTPUTS_SHA: ${{ steps.commit.outputs.sha }}
2277
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2278
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2100
2279
  with:
2101
2280
  script: |
2102
2281
  const M = JSON.parse(process.env.PAW_MSG);
2103
2282
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2104
2283
 
2105
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2106
- const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2107
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2108
- const sha = '${{ steps.commit.outputs.sha }}';
2284
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2285
+ const containerSuffix = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX;
2286
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2287
+ const sha = process.env.PAW_IN_STEPS_COMMIT_OUTPUTS_SHA;
2109
2288
 
2110
2289
  const body = [
2111
2290
  m('wf_preview.title_custom_started'),
@@ -2166,16 +2345,21 @@ jobs:
2166
2345
  if: steps.check_branch.outputs.exists == 'true'
2167
2346
  id: build_progress
2168
2347
  uses: actions/github-script@v9
2348
+ env:
2349
+ PAW_IN_STEPS_BUILD_OUTPUTS_DURATION: ${{ steps.build.outputs.duration }}
2350
+ PAW_IN_STEPS_COMMIT_OUTPUTS_SHA: ${{ steps.commit.outputs.sha }}
2351
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2352
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2169
2353
  with:
2170
2354
  script: |
2171
2355
  const M = JSON.parse(process.env.PAW_MSG);
2172
2356
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2173
2357
 
2174
- const commentId = ${{ steps.progress.outputs.comment_id }};
2175
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2176
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2177
- const sha = '${{ steps.commit.outputs.sha }}';
2178
- const buildDuration = '${{ steps.build.outputs.duration }}';
2358
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
2359
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2360
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2361
+ const sha = process.env.PAW_IN_STEPS_COMMIT_OUTPUTS_SHA;
2362
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_OUTPUTS_DURATION;
2179
2363
 
2180
2364
  const body = [
2181
2365
  m('wf_preview.title_custom_started'),
@@ -2204,7 +2388,7 @@ jobs:
2204
2388
 
2205
2389
  - name: Log in to Docker Hub
2206
2390
  if: steps.check_branch.outputs.exists == 'true'
2207
- uses: docker/login-action@v3
2391
+ uses: docker/login-action@v4
2208
2392
  with:
2209
2393
  username: ${{ secrets.DOCKERHUB_USERNAME }}
2210
2394
  password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -2228,17 +2412,23 @@ jobs:
2228
2412
  if: steps.check_branch.outputs.exists == 'true'
2229
2413
  id: docker_progress
2230
2414
  uses: actions/github-script@v9
2415
+ env:
2416
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
2417
+ PAW_IN_STEPS_COMMIT_OUTPUTS_SHA: ${{ steps.commit.outputs.sha }}
2418
+ PAW_IN_STEPS_DOCKER_OUTPUTS_DURATION: ${{ steps.docker.outputs.duration }}
2419
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2420
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2231
2421
  with:
2232
2422
  script: |
2233
2423
  const M = JSON.parse(process.env.PAW_MSG);
2234
2424
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2235
2425
 
2236
- const commentId = ${{ steps.progress.outputs.comment_id }};
2237
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2238
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2239
- const sha = '${{ steps.commit.outputs.sha }}';
2240
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
2241
- const dockerDuration = '${{ steps.docker.outputs.duration }}';
2426
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
2427
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2428
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2429
+ const sha = process.env.PAW_IN_STEPS_COMMIT_OUTPUTS_SHA;
2430
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
2431
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_OUTPUTS_DURATION;
2242
2432
 
2243
2433
  const body = [
2244
2434
  m('wf_preview.title_custom_started'),
@@ -2325,7 +2515,7 @@ jobs:
2325
2515
  fi
2326
2516
 
2327
2517
  m wf_preview.r_deploy_done
2328
- echo "🌐 Preview URL: http://${CONTAINER_NAME}.${DOMAIN_SUFFIX}:${PREVIEW_PORT}"
2518
+ m wf_preview.r_preview_url url="http://${CONTAINER_NAME}.${DOMAIN_SUFFIX}:${PREVIEW_PORT}"
2329
2519
 
2330
2520
  - name: Record deploy completion
2331
2521
  if: steps.check_branch.outputs.exists == 'true'
@@ -2338,26 +2528,34 @@ jobs:
2338
2528
  - name: Deploy complete comment
2339
2529
  if: steps.check_branch.outputs.exists == 'true'
2340
2530
  uses: actions/github-script@v9
2531
+ env:
2532
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
2533
+ PAW_IN_STEPS_COMMIT_OUTPUTS_SHA: ${{ steps.commit.outputs.sha }}
2534
+ PAW_IN_STEPS_DEPLOY_TIME_OUTPUTS_DURATION: ${{ steps.deploy_time.outputs.duration }}
2535
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
2536
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2537
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2538
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2341
2539
  with:
2342
2540
  script: |
2343
2541
  const M = JSON.parse(process.env.PAW_MSG);
2344
2542
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2345
2543
 
2346
- const commentId = ${{ steps.progress.outputs.comment_id }};
2347
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2348
- const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2349
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2350
- const sha = '${{ steps.commit.outputs.sha }}';
2351
- const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
2352
- const previewPort = '${{ env.PREVIEW_PORT }}';
2353
- const apiDocsPath = '${{ env.API_DOCS_PATH }}';
2544
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
2545
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2546
+ const containerSuffix = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX;
2547
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2548
+ const sha = process.env.PAW_IN_STEPS_COMMIT_OUTPUTS_SHA;
2549
+ const domainSuffix = process.env.PREVIEW_DOMAIN_SUFFIX;
2550
+ const previewPort = process.env.PREVIEW_PORT;
2551
+ const apiDocsPath = process.env.API_DOCS_PATH;
2354
2552
 
2355
2553
  const containerName = `${projectName}-${containerSuffix}`;
2356
2554
  const previewUrl = `http://${containerName}.${domainSuffix}:${previewPort}`;
2357
2555
 
2358
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
2359
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
2360
- const deployDuration = '${{ steps.deploy_time.outputs.duration }}';
2556
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
2557
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
2558
+ const deployDuration = process.env.PAW_IN_STEPS_DEPLOY_TIME_OUTPUTS_DURATION;
2361
2559
 
2362
2560
  const envRows = [
2363
2561
  `| **Preview URL** | ${previewUrl} |`,
@@ -2404,20 +2602,25 @@ jobs:
2404
2602
  });
2405
2603
 
2406
2604
  - name: Build/deploy failure comment
2407
- if: failure() && steps.check_branch.outputs.exists == 'true'
2605
+ if: failure() && steps.load_messages.outcome == 'success' && steps.check_branch.outputs.exists == 'true'
2408
2606
  uses: actions/github-script@v9
2607
+ env:
2608
+ PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
2609
+ PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
2610
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2611
+ PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2409
2612
  with:
2410
2613
  script: |
2411
2614
  const M = JSON.parse(process.env.PAW_MSG);
2412
2615
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2413
2616
 
2414
- const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
2415
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2416
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2617
+ const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
2618
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2619
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2417
2620
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
2418
2621
 
2419
- const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
2420
- const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
2622
+ const buildDuration = process.env.PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION;
2623
+ const dockerDuration = process.env.PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION;
2421
2624
 
2422
2625
  let buildStatus, dockerStatus, deployStatus;
2423
2626
  let buildTime = '-', dockerTime = '-';
@@ -2498,6 +2701,8 @@ jobs:
2498
2701
  uses: actions/checkout@v7
2499
2702
  with:
2500
2703
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2704
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
2705
+ path: .paw-msg
2501
2706
  sparse-checkout: |
2502
2707
  .github/scripts/messages.py
2503
2708
  version.yml
@@ -2505,11 +2710,16 @@ jobs:
2505
2710
 
2506
2711
  - name: Load messages
2507
2712
  run: |
2508
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2713
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
2509
2714
  mkdir -p "$RUNNER_TEMP/paw"
2510
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2715
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
2716
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2717
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
2718
+ rm -rf .paw-msg
2719
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2720
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
2511
2721
  {
2512
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2722
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
2513
2723
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2514
2724
  } >> "$GITHUB_ENV"
2515
2725
  # The deploy script runs on the remote server, where messages.py does not exist:
@@ -2536,12 +2746,41 @@ jobs:
2536
2746
  print("PAW_EOF")
2537
2747
  PY
2538
2748
 
2749
+ # Report the missing secrets by name instead of surfacing a raw SSH action error
2750
+ - name: Deploy pre-check (Secrets)
2751
+ id: precheck
2752
+ env:
2753
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
2754
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
2755
+ SERVER_USER: ${{ secrets.SERVER_USER }}
2756
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
2757
+ SSH_KEY: ${{ secrets.SSH_KEY }}
2758
+ run: |
2759
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
2760
+
2761
+ MISSING=""
2762
+ for NAME in DOCKERHUB_USERNAME SERVER_HOST SERVER_USER; do
2763
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
2764
+ done
2765
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
2766
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
2767
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
2768
+ else
2769
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
2770
+ fi
2771
+ if [ -n "$MISSING" ]; then
2772
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
2773
+ exit 1
2774
+ fi
2775
+
2539
2776
  - name: Extract container suffix from branch name
2540
2777
  id: extract
2541
2778
  uses: actions/github-script@v9
2779
+ env:
2780
+ PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH: ${{ needs.check-command.outputs.custom_branch }}
2542
2781
  with:
2543
2782
  script: |
2544
- const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2783
+ const branchName = process.env.PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH;
2545
2784
  const issueMatch = branchName.match(/#(\d+)/);
2546
2785
 
2547
2786
  if (issueMatch) {
@@ -2593,14 +2832,17 @@ jobs:
2593
2832
 
2594
2833
  - name: Destroy confirmation comment
2595
2834
  uses: actions/github-script@v9
2835
+ env:
2836
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2837
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2596
2838
  with:
2597
2839
  script: |
2598
2840
  const M = JSON.parse(process.env.PAW_MSG);
2599
2841
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2600
2842
 
2601
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2602
- const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2603
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2843
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2844
+ const containerSuffix = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX;
2845
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2604
2846
  const containerName = `${projectName}-${containerSuffix}`;
2605
2847
 
2606
2848
  const body = [
@@ -2639,6 +2881,8 @@ jobs:
2639
2881
  uses: actions/checkout@v7
2640
2882
  with:
2641
2883
  ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2884
+ # Separate path: a sparse checkout at the workspace root would leave the later full checkout sparse too
2885
+ path: .paw-msg
2642
2886
  sparse-checkout: |
2643
2887
  .github/scripts/messages.py
2644
2888
  version.yml
@@ -2646,20 +2890,53 @@ jobs:
2646
2890
 
2647
2891
  - name: Load messages
2648
2892
  run: |
2649
- # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2893
+ # Keep a copy outside the workspace and drop the sparse clone so the later checkout sees a clean tree
2650
2894
  mkdir -p "$RUNNER_TEMP/paw"
2651
- cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2895
+ [ -f .paw-msg/.github/scripts/messages.py ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
2896
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2897
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw/messages.py" lang)
2898
+ rm -rf .paw-msg
2899
+ # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2900
+ export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
2652
2901
  {
2653
- echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2902
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
2654
2903
  echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2655
2904
  } >> "$GITHUB_ENV"
2656
2905
 
2906
+ # Report the missing secrets by name instead of surfacing a raw SSH action error
2907
+ - name: Deploy pre-check (Secrets)
2908
+ id: precheck
2909
+ env:
2910
+ SERVER_HOST: ${{ secrets.SERVER_HOST }}
2911
+ SERVER_USER: ${{ secrets.SERVER_USER }}
2912
+ SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
2913
+ SSH_KEY: ${{ secrets.SSH_KEY }}
2914
+ run: |
2915
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
2916
+
2917
+ MISSING=""
2918
+ for NAME in SERVER_HOST SERVER_USER; do
2919
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
2920
+ done
2921
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
2922
+ if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
2923
+ [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
2924
+ else
2925
+ [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
2926
+ fi
2927
+ if [ -n "$MISSING" ]; then
2928
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
2929
+ exit 1
2930
+ fi
2931
+
2657
2932
  - name: Extract container suffix from branch name
2658
2933
  id: extract
2659
2934
  uses: actions/github-script@v9
2935
+ env:
2936
+ PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH: ${{ needs.check-command.outputs.custom_branch }}
2660
2937
  with:
2661
2938
  script: |
2662
- const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2939
+ const branchName = process.env.PAW_IN_NEEDS_CHECK_COMMAND_OUTPUTS_CUSTOM_BRANCH;
2663
2940
  const issueMatch = branchName.match(/#(\d+)/);
2664
2941
 
2665
2942
  if (issueMatch) {
@@ -2698,20 +2975,24 @@ jobs:
2698
2975
 
2699
2976
  - name: Post status comment
2700
2977
  uses: actions/github-script@v9
2978
+ env:
2979
+ PAW_IN_STEPS_CHECK_RUNNING_OUTCOME: ${{ steps.check_running.outcome }}
2980
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2981
+ PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2701
2982
  with:
2702
2983
  script: |
2703
2984
  const M = JSON.parse(process.env.PAW_MSG);
2704
2985
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2705
2986
 
2706
- const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2707
- const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2708
- const branchName = '${{ steps.extract.outputs.branch_name }}';
2709
- const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
2710
- const previewPort = '${{ env.PREVIEW_PORT }}';
2987
+ const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
2988
+ const containerSuffix = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX;
2989
+ const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
2990
+ const domainSuffix = process.env.PREVIEW_DOMAIN_SUFFIX;
2991
+ const previewPort = process.env.PREVIEW_PORT;
2711
2992
 
2712
2993
  const containerName = `${projectName}-${containerSuffix}`;
2713
2994
  const previewUrl = `http://${containerName}.${domainSuffix}:${previewPort}`;
2714
- const isRunning = '${{ steps.check_running.outcome }}' === 'success';
2995
+ const isRunning = process.env.PAW_IN_STEPS_CHECK_RUNNING_OUTCOME === 'success';
2715
2996
 
2716
2997
  let body;
2717
2998
  if (isRunning) {