project-auto-wizard 0.13.3 β†’ 0.14.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 (112) hide show
  1. package/README.md +1 -1
  2. package/bin/project-auto-wizard.js +18 -4
  3. package/package.json +2 -2
  4. package/payload/config/breaking-changes.json +28 -14
  5. package/payload/config/wizard-prompts.yml +102 -46
  6. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  7. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  8. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  9. package/payload/scripts/changelog_manager.py +296 -257
  10. package/payload/scripts/issue_helper.py +54 -50
  11. package/payload/scripts/messages.py +2279 -0
  12. package/payload/scripts/truncate_release_notes.py +7 -6
  13. package/payload/scripts/version_manager.py +64 -61
  14. package/payload/version.yml.template +2 -1
  15. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
  16. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
  17. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  18. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
  19. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
  20. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  29. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  30. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  31. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  32. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  33. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  34. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  35. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  36. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  37. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  38. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  39. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  40. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  41. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  42. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  44. package/src/cli/args.js +98 -52
  45. package/src/cli/help.js +8 -41
  46. package/src/commands/doctor.js +85 -89
  47. package/src/commands/dry-run.js +63 -43
  48. package/src/commands/full.js +79 -77
  49. package/src/commands/install-settings.js +16 -15
  50. package/src/commands/interactive-flutter.js +20 -19
  51. package/src/commands/interactive.js +135 -132
  52. package/src/commands/purge.js +47 -42
  53. package/src/commands/status.js +36 -36
  54. package/src/commands/uninstall.js +43 -41
  55. package/src/context.js +12 -11
  56. package/src/core/assets.js +12 -11
  57. package/src/core/baseline.js +27 -27
  58. package/src/core/branches.js +29 -27
  59. package/src/core/branding.js +7 -6
  60. package/src/core/breaking-check.js +22 -21
  61. package/src/core/breaking.js +15 -8
  62. package/src/core/copy/app-files.js +5 -4
  63. package/src/core/copy/flutter-app.js +8 -8
  64. package/src/core/copy/gitignore.js +58 -50
  65. package/src/core/copy/readme.js +45 -35
  66. package/src/core/copy/simple.js +16 -15
  67. package/src/core/copy/workflows.js +131 -124
  68. package/src/core/deploy-style.js +51 -48
  69. package/src/core/detect-fs.js +57 -57
  70. package/src/core/detect.js +67 -65
  71. package/src/core/errors.js +2 -2
  72. package/src/core/flutter-doctor.js +17 -16
  73. package/src/core/flutter-hooks.js +33 -32
  74. package/src/core/flutter-options.js +34 -33
  75. package/src/core/fsutil.js +12 -12
  76. package/src/core/installed-stores.js +4 -4
  77. package/src/core/logger.js +42 -40
  78. package/src/core/paths-resolve.js +88 -86
  79. package/src/core/paths.js +10 -10
  80. package/src/core/release-options.js +8 -8
  81. package/src/core/removal-exec.js +14 -13
  82. package/src/core/removal-plan.js +37 -37
  83. package/src/core/types.js +47 -47
  84. package/src/core/verify.js +44 -38
  85. package/src/core/version-yml.js +76 -68
  86. package/src/core/wizard-env.js +46 -46
  87. package/src/core/wizard-labels.js +63 -41
  88. package/src/i18n/catalog/en/cli.js +50 -0
  89. package/src/i18n/catalog/en/commands.js +187 -0
  90. package/src/i18n/catalog/en/copy.js +19 -0
  91. package/src/i18n/catalog/en/core.js +74 -0
  92. package/src/i18n/catalog/en/core2.js +15 -0
  93. package/src/i18n/catalog/en/ui.js +221 -0
  94. package/src/i18n/catalog/en.js +12 -0
  95. package/src/i18n/catalog/index.js +5 -0
  96. package/src/i18n/catalog/ko/cli.js +50 -0
  97. package/src/i18n/catalog/ko/commands.js +187 -0
  98. package/src/i18n/catalog/ko/copy.js +19 -0
  99. package/src/i18n/catalog/ko/core.js +74 -0
  100. package/src/i18n/catalog/ko/core2.js +15 -0
  101. package/src/i18n/catalog/ko/ui.js +221 -0
  102. package/src/i18n/catalog/ko.js +10 -0
  103. package/src/i18n/index.js +44 -0
  104. package/src/i18n/languages.js +9 -0
  105. package/src/index.js +128 -94
  106. package/src/ui/ansi.js +7 -7
  107. package/src/ui/banner.js +7 -6
  108. package/src/ui/env-plan.js +82 -81
  109. package/src/ui/prompts.js +97 -122
  110. package/src/ui/readline-engine.js +59 -58
  111. package/src/ui/status-cards.js +37 -31
  112. package/src/ui/summary.js +89 -88
@@ -1,88 +1,88 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # Spring Boot PR/Issue Preview μ‹œμŠ€ν…œ (SSH + Docker)
2
+ # Spring Boot PR/Issue Preview system (SSH + Docker)
3
3
  #
4
- # ν•„μˆ˜ GitHub Secrets
5
- # APPLICATION_PROD_YML (선택): application-prod.yml λ‚΄μš©
6
- # DOCKERHUB_USERNAME: DockerHub μ‚¬μš©μžλͺ…
7
- # DOCKERHUB_TOKEN: DockerHub μ•‘μ„ΈμŠ€ 토큰
8
- # SERVER_HOST: 배포 μ„œλ²„ μ£Όμ†Œ
9
- # SERVER_USER: SSH μ‚¬μš©μžλͺ…
10
- # SERVER_PASSWORD: SSH λΉ„λ°€λ²ˆν˜Έ (SSH_AUTH_METHOD=password일 λ•Œ)
11
- # SSH_KEY: SSH κ°œμΈν‚€ .pem λ‚΄μš© (SSH_AUTH_METHOD=key일 λ•Œ, AWS EC2 λ“±)
4
+ # Required GitHub Secrets
5
+ # APPLICATION_PROD_YML (optional): contents of application-prod.yml
6
+ # DOCKERHUB_USERNAME: DockerHub username
7
+ # DOCKERHUB_TOKEN: DockerHub access token
8
+ # SERVER_HOST: deploy server address
9
+ # SERVER_USER: SSH username
10
+ # SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
11
+ # SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
12
12
  #
13
- # μ‚¬μš©λ²•
14
- # /wizard server build - PR/Issue λΉŒλ“œ 및 배포
15
- # /wizard server destroy - Preview ν™˜κ²½ μ‚­μ œ
16
- # /wizard server status - ν˜„μž¬ μƒνƒœ 확인
13
+ # Usage
14
+ # /wizard server build - Build and deploy the PR/issue
15
+ # /wizard server destroy - Delete the preview environment
16
+ # /wizard server status - Check the current status
17
17
  #
18
- # λ¦¬μ†ŒμŠ€ 넀이밍 κ·œμΉ™
19
- # - μ»¨ν…Œμ΄λ„ˆ: {PROJECT_NAME}-pr-{PR번호}
20
- # - 이미지: {DOCKERHUB_USERNAME}/{PROJECT_NAME}:pr-{PR번호}
21
- # - 도메인: {PROJECT_NAME}-pr-{PR번호}.pr.example.com
22
- # - Preview URL: http://{도메인}:8079
18
+ # Resource naming rules
19
+ # - Container: {PROJECT_NAME}-pr-{PR number}
20
+ # - Image: {DOCKERHUB_USERNAME}/{PROJECT_NAME}:pr-{PR number}
21
+ # - Domain: {PROJECT_NAME}-pr-{PR number}.pr.example.com
22
+ # - Preview URL: http://{domain}:8079
23
23
 
24
24
  name: PROJECT-SPRING-PR-PREVIEW
25
25
 
26
- # [μ˜μ—­ 1] ν”„λ‘œμ νŠΈλ³„ μ„€μ • - λ‹€λ₯Έ ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš© μ‹œ 이 μ„Ήμ…˜λ§Œ μˆ˜μ •ν•˜μ„Έμš”
26
+ # [Section 1] Project settings - when reusing this in another project, edit only this section
27
27
  env:
28
- # ν”„λ‘œμ νŠΈ 고유 μ‹λ³„μž (μ»¨ν…Œμ΄λ„ˆλͺ…, 이미지λͺ…, 도메인에 μ‚¬μš©) (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”)
28
+ # Unique project identifier (used in container, image and domain names) (edit per project)
29
29
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
30
- # λͺ¨λ…Έλ ˆν¬(--paths)μ—μ„œ 이 ν”„λ‘œμ νŠΈκ°€ μžˆλŠ” ν•˜μœ„ 경둜. λΉŒλ“œΒ·Docker context의 κΈ°μ€€ ('.'이면 레포 루트)
30
+ # Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' means the repo root)
31
31
  PROJECT_PATH: "." # @wizard auto:project-path
32
- # λͺ¨λ…Έλ ˆν¬(PROJECT_PATHκ°€ '.'이 μ•„λ‹˜)μ—μ„œλŠ” μ΄λ―Έμ§€Β·μ»¨ν…Œμ΄λ„ˆΒ·λ„λ©”μΈ 이름에 '-spring'λ₯Ό λΆ™μ—¬
33
- # 같은 레포의 λ‹€λ₯Έ νƒ€μž… μ„œλΉ„μŠ€μ™€ 이름이 κ²ΉμΉ˜μ§€ μ•Šκ²Œ ν•œλ‹€ (예: my-app-spring)
32
+ # In a monorepo (PROJECT_PATH is not '.') '-spring' is appended to image, container and domain names
33
+ # so they do not collide with services of other types in the same repo (e.g. my-app-spring)
34
34
 
35
- # Spring Boot λΉŒλ“œ μ„€μ • (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”)
35
+ # Spring Boot build settings (edit per project)
36
36
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
37
37
  GRADLE_BUILD_CMD: './gradlew clean build -x test -Dspring.profiles.active=prod'
38
38
  JAR_PATH: "__JAR_PATH__" # @wizard ask:build/libs/*.jar
39
39
  APPLICATION_YML_PATH: "__APPLICATION_YML_PATH__" # @wizard auto:spring-app-yml-path
40
40
 
41
- # Docker μ„€μ • (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”)
42
- DOCKERFILE_PATH: './Dockerfile' # PROJECT_PATH κΈ°μ€€ μƒλŒ€ 경둜
41
+ # Docker settings (edit per project)
42
+ DOCKERFILE_PATH: './Dockerfile' # path relative to PROJECT_PATH
43
43
  INTERNAL_PORT: '8080'
44
44
 
45
- # λ³Όλ₯¨ 마운트 μ„€μ • (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”, λΉˆκ°’μ΄λ©΄ 마운트 μ•ˆν•¨)
46
- PROJECT_TARGET_DIR: "" # μ„œλ²„μ˜ 데이터 디렉토리 (예: /volume1/projects/myapp/data)
47
- PROJECT_MNT_DIR: "" # μ»¨ν…Œμ΄λ„ˆ λ‚΄λΆ€ 마운트 경둜 (예: /mnt/myapp)
45
+ # Volume mount settings (edit per project; leave empty to skip mounting)
46
+ PROJECT_TARGET_DIR: "" # data directory on the server (e.g. /volume1/projects/myapp/data)
47
+ PROJECT_MNT_DIR: "" # mount path inside the container (e.g. /mnt/myapp)
48
48
 
49
- # Traefik & Preview 도메인 μ„€μ • (ν™˜κ²½ ꡬ좕 ν›„ μˆ˜μ • κΈˆμ§€)
49
+ # Traefik & preview domain settings (do not change after the environment is set up)
50
50
  TRAEFIK_NETWORK: traefik-network
51
51
  PREVIEW_DOMAIN_SUFFIX: "__PREVIEW_DOMAIN_SUFFIX__" # @wizard ask:pr.example.com
52
52
  PREVIEW_PORT: '8079'
53
53
 
54
- # SSH 인증 방식: "password"(Synology·일반 μ„œλ²„) | "key"(AWS EC2Β·.pem)
55
- # password β†’ SERVER_PASSWORD secret μ‚¬μš©
56
- # key β†’ SSH_KEY secret μ‚¬μš© (.pem 파일 전체 λ‚΄μš©μ„ secret κ°’μœΌλ‘œ)
54
+ # SSH auth method: "password" (Synology / regular servers) | "key" (AWS EC2 / .pem)
55
+ # password β†’ uses the SERVER_PASSWORD secret
56
+ # key β†’ uses the SSH_KEY secret (store the whole .pem contents as the secret value)
57
57
  SSH_AUTH_METHOD: "password" # @wizard ask:password
58
58
 
59
- # SSH 포트 (μ„œλ²„ 포트 ν™˜κ²½ ꡬ좕 ν›„ μˆ˜μ • κΈˆμ§€)
59
+ # SSH port (do not change after the server port is set up)
60
60
  SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
61
61
 
62
- # Issue Helper λŒ“κΈ€ 마컀 (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”)
63
- # Issue λŒ“κΈ€μ—μ„œ 브랜치λͺ…을 μΆ”μΆœν•  λ•Œ μ‚¬μš©
62
+ # Issue Helper comment marker (edit per project)
63
+ # Used to extract the branch name from an issue comment
64
64
  ISSUE_HELPER_MARKER: '<!-- project-auto-wizard issue helper -->'
65
65
 
66
- # Health Check μ„€μ • (ν”„λ‘œμ νŠΈλ³„ μˆ˜μ • ν•„μš”)
67
- # - HEALTH_CHECK_PATH: HTTP Health Check 경둜 (λΉˆκ°’μ΄λ©΄ HTTP 체크 κ±΄λ„ˆλœ€)
68
- # - HEALTH_CHECK_LOG_PATTERN: 둜그 νŒ¨ν„΄ λ§€μΉ­ (HTTP μ‹€νŒ¨ μ‹œ 폴백)
69
- # - API_DOCS_PATH: API λ¬Έμ„œ URL (λΉˆκ°’μ΄λ©΄ 배포 μ½”λ©˜νŠΈμ— λ―Έν‘œμ‹œ)
66
+ # Health check settings (edit per project)
67
+ # - HEALTH_CHECK_PATH: HTTP health check path (empty skips the HTTP check)
68
+ # - HEALTH_CHECK_LOG_PATTERN: log pattern match (fallback when HTTP fails)
69
+ # - API_DOCS_PATH: API docs URL (empty hides it from the deploy comment)
70
70
  #
71
- # Spring Boot κΈ°λ³Έκ°’:
71
+ # Spring Boot defaults:
72
72
  # HEALTH_CHECK_PATH: '/actuator/health'
73
73
  # HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
74
- # API_DOCS_PATH: '/swagger-ui.html' λ˜λŠ” '/swagger-ui/index.html'
74
+ # API_DOCS_PATH: '/swagger-ui.html' or '/swagger-ui/index.html'
75
75
  HEALTH_CHECK_PATH: '/actuator/health'
76
76
  HEALTH_CHECK_LOG_PATTERN: 'Started .* in [0-9.]+ seconds'
77
77
  API_DOCS_PATH: '/docs/swagger'
78
78
 
79
79
  on:
80
80
  issue_comment:
81
- types: [created] # PR λŒ“κΈ€ + Issue λŒ“κΈ€
81
+ types: [created] # PR comments + issue comments
82
82
  issues:
83
- types: [closed] # Issue λ‹«νž˜ μ‹œ destroy
83
+ types: [closed] # destroy when the issue is closed
84
84
  pull_request:
85
- types: [closed] # PR λ‹«νž˜ μ‹œ destroy
85
+ types: [closed] # destroy when the PR is closed
86
86
 
87
87
  permissions:
88
88
  contents: read
@@ -90,9 +90,9 @@ permissions:
90
90
  issues: write
91
91
 
92
92
  jobs:
93
- # Job 1: λͺ…λ Ήμ–΄ νŒŒμ‹± (PR λŒ“κΈ€ + Issue λŒ“κΈ€ λͺ¨λ‘ 지원)
93
+ # Job 1: parse the command (supports both PR and issue comments)
94
94
  check-command:
95
- name: λͺ…λ Ήμ–΄ 확인
95
+ name: Check command
96
96
  if: github.event_name == 'issue_comment'
97
97
  runs-on: ubuntu-latest
98
98
  outputs:
@@ -102,7 +102,28 @@ jobs:
102
102
  custom_branch: ${{ steps.parse.outputs.custom_branch }}
103
103
  is_custom_branch: ${{ steps.parse.outputs.is_custom_branch }}
104
104
  steps:
105
- - name: λŒ“κΈ€μ— πŸ‘€ λ¦¬μ•‘μ…˜ μΆ”κ°€
105
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
106
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
107
+ - name: Checkout messages
108
+ uses: actions/checkout@v7
109
+ with:
110
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
111
+ sparse-checkout: |
112
+ .github/scripts/messages.py
113
+ version.yml
114
+ sparse-checkout-cone-mode: false
115
+
116
+ - name: Load messages
117
+ run: |
118
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
119
+ mkdir -p "$RUNNER_TEMP/paw"
120
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
121
+ {
122
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
123
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
124
+ } >> "$GITHUB_ENV"
125
+
126
+ - name: Add πŸ‘€ reaction to comment
106
127
  if: contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server')
107
128
  uses: actions/github-script@v9
108
129
  with:
@@ -115,33 +136,35 @@ jobs:
115
136
  content: 'eyes'
116
137
  });
117
138
 
118
- - name: μ»€λ§¨λ“œ νŒŒμ‹±
139
+ - name: Parse command
119
140
  id: parse
120
141
  env:
121
142
  COMMENT: ${{ github.event.comment.body }}
122
143
  IS_PR: ${{ github.event.issue.pull_request != null }}
123
144
  run: |
124
- # PR인지 Issue인지 확인
145
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
146
+
147
+ # Check whether this is a PR or an issue
125
148
  if [[ "$IS_PR" == "true" ]]; then
126
149
  echo "is_pr=true" >> $GITHUB_OUTPUT
127
- echo "ℹ️ PR λŒ“κΈ€μ—μ„œ λͺ…λ Ήμ–΄ 감지"
150
+ m wf_preview.cmd_from_pr
128
151
  else
129
152
  echo "is_pr=false" >> $GITHUB_OUTPUT
130
- echo "ℹ️ Issue λŒ“κΈ€μ—μ„œ λͺ…λ Ήμ–΄ 감지"
153
+ m wf_preview.cmd_from_issue
131
154
  fi
132
155
 
133
- # λͺ…λ Ήμ–΄ νŒŒμ‹± (브랜치 νŒŒλΌλ―Έν„° 지원)
134
- # ν˜•μ‹: /wizard server <command> [브랜치λͺ…]
156
+ # Parse the command (branch argument supported)
157
+ # Format: /wizard server <command> [branch]
135
158
  if [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
136
159
  echo "command=build" >> $GITHUB_OUTPUT
137
160
  echo "is_valid=true" >> $GITHUB_OUTPUT
138
161
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
139
162
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
140
163
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
141
- echo "βœ… λͺ…λ Ήμ–΄ 감지: build (브랜치: ${BASH_REMATCH[2]})"
164
+ m wf_preview.cmd_detected_build_branch branch="${BASH_REMATCH[2]}"
142
165
  else
143
166
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
144
- echo "βœ… λͺ…λ Ήμ–΄ 감지: build"
167
+ m wf_preview.cmd_detected_build
145
168
  fi
146
169
  elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
147
170
  echo "command=destroy" >> $GITHUB_OUTPUT
@@ -149,10 +172,10 @@ jobs:
149
172
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
150
173
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
151
174
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
152
- echo "βœ… λͺ…λ Ήμ–΄ 감지: destroy (브랜치: ${BASH_REMATCH[2]})"
175
+ m wf_preview.cmd_detected_destroy_branch branch="${BASH_REMATCH[2]}"
153
176
  else
154
177
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
155
- echo "βœ… λͺ…λ Ήμ–΄ 감지: destroy"
178
+ m wf_preview.cmd_detected_destroy
156
179
  fi
157
180
  elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
158
181
  echo "command=status" >> $GITHUB_OUTPUT
@@ -160,19 +183,19 @@ jobs:
160
183
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
161
184
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
162
185
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
163
- echo "βœ… λͺ…λ Ήμ–΄ 감지: status (브랜치: ${BASH_REMATCH[2]})"
186
+ m wf_preview.cmd_detected_status_branch branch="${BASH_REMATCH[2]}"
164
187
  else
165
188
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
166
- echo "βœ… λͺ…λ Ήμ–΄ 감지: status"
189
+ m wf_preview.cmd_detected_status
167
190
  fi
168
191
  else
169
192
  echo "is_valid=false" >> $GITHUB_OUTPUT
170
- echo "ℹ️ /wizard server λͺ…λ Ήμ–΄κ°€ μ•„λ‹˜"
193
+ m wf_preview.cmd_not_wizard
171
194
  fi
172
195
 
173
- # Job 2-1: λΉŒλ“œ & 배포 (PR λŒ“κΈ€μ—μ„œ μ‹€ν–‰)
196
+ # Job 2-1: build & deploy (triggered from a PR comment)
174
197
  build-preview-pr:
175
- name: Preview λΉŒλ“œ & 배포 (PR)
198
+ name: Preview build & deploy (PR)
176
199
  needs: check-command
177
200
  if: |
178
201
  needs.check-command.outputs.is_valid == 'true' &&
@@ -181,7 +204,51 @@ jobs:
181
204
  needs.check-command.outputs.is_custom_branch != 'true'
182
205
  runs-on: ubuntu-latest
183
206
  steps:
184
- - name: PR 정보 κ°€μ Έμ˜€κΈ°
207
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
208
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
209
+ - name: Checkout messages
210
+ uses: actions/checkout@v7
211
+ with:
212
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
213
+ sparse-checkout: |
214
+ .github/scripts/messages.py
215
+ version.yml
216
+ sparse-checkout-cone-mode: false
217
+
218
+ - name: Load messages
219
+ run: |
220
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
221
+ mkdir -p "$RUNNER_TEMP/paw"
222
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
223
+ {
224
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
225
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
226
+ } >> "$GITHUB_ENV"
227
+ # The deploy script runs on the remote server, where messages.py does not exist:
228
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
229
+ python3 - >> "$GITHUB_ENV" <<'PY'
230
+ import json, os, shlex, subprocess
231
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
232
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
233
+ head = ["m() {", ' case "$1" in']
234
+ for key, text in msgs.items():
235
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
236
+ tail = r''' *) _t="$1" ;;
237
+ esac
238
+ shift
239
+ while [ "$#" -gt 0 ]; do
240
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
241
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
242
+ shift
243
+ done
244
+ printf '%s\n' "$_t"
245
+ }'''
246
+ print("PAW_REMOTE<<PAW_EOF")
247
+ print("\n".join(head) + "\n" + tail)
248
+ print("PAW_EOF")
249
+ PY
250
+
251
+ - name: Get PR info
185
252
  id: pr
186
253
  uses: actions/github-script@v9
187
254
  with:
@@ -195,29 +262,32 @@ jobs:
195
262
  core.setOutput('sha', pr.data.head.sha.substring(0, 7));
196
263
  return pr.data.number;
197
264
 
198
- # μ§„ν–‰ 상황 λŒ“κΈ€ μ‹œμŠ€ν…œ
199
- - name: μ§„ν–‰ 상황 λŒ“κΈ€ 생성
265
+ # Progress comment system
266
+ - name: Create progress comment
200
267
  id: progress
201
268
  uses: actions/github-script@v9
202
269
  with:
203
270
  script: |
271
+ const M = JSON.parse(process.env.PAW_MSG);
272
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
273
+
204
274
  const prNumber = context.issue.number;
205
275
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
206
276
  const startTime = Date.now();
207
277
 
208
278
  const body = [
209
- '## πŸš€ PR Preview λΉŒλ“œ 쀑...',
279
+ m('wf_preview.title_pr_building'),
210
280
  '',
211
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
281
+ m('wf_preview.tbl_steps'),
212
282
  '|------|------|----------|',
213
- '| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ⏳ μ§„ν–‰ 쀑... | - |',
214
- '| 🐳 Docker 이미지 생성 | ⏸️ λŒ€κΈ° | - |',
215
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
283
+ m('wf_preview.row_build_progress'),
284
+ m('wf_preview.row_image_wait'),
285
+ m('wf_preview.row_deploy_wait'),
216
286
  '',
217
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
287
+ m('wf_preview.link_logs', {runUrl}),
218
288
  '',
219
289
  '---',
220
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
290
+ m('wf_preview.footer_progress')
221
291
  ].join('\n');
222
292
 
223
293
  const { data: comment } = await github.rest.issues.createComment({
@@ -230,13 +300,13 @@ jobs:
230
300
  core.setOutput('comment_id', comment.id);
231
301
  core.setOutput('start_time', startTime);
232
302
 
233
- - name: μ½”λ“œ 체크아웃
303
+ - name: Checkout code
234
304
  uses: actions/checkout@v7
235
305
  with:
236
306
  ref: ${{ steps.pr.outputs.ref }}
237
307
 
238
- # ν•„μˆ˜ SecretΒ·Dockerfile이 μ—†μœΌλ©΄ docker 둜그인 같은 μ„œλ“œνŒŒν‹° 였λ₯˜ λŒ€μ‹  λΉ μ§„ ν•­λͺ©μ„ μ•Œλ € μ£Όκ³  λ©ˆμΆ˜λ‹€
239
- - name: 배포 사전 점검 (SecretΒ·Dockerfile)
308
+ # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
309
+ - name: Deploy pre-check (Secrets and Dockerfile)
240
310
  working-directory: ${{ env.PROJECT_PATH }}
241
311
  env:
242
312
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -247,11 +317,13 @@ jobs:
247
317
  SSH_KEY: ${{ secrets.SSH_KEY }}
248
318
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
249
319
  run: |
320
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
321
+
250
322
  MISSING=""
251
323
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
252
324
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
253
325
  done
254
- # SSH_AUTH_METHOD=keyλ©΄ SSH_KEY, μ•„λ‹ˆλ©΄ SERVER_PASSWORDκ°€ ν•„μš”ν•˜λ‹€
326
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
255
327
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
256
328
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
257
329
  else
@@ -259,98 +331,101 @@ jobs:
259
331
  fi
260
332
  FAILED=false
261
333
  if [ -n "$MISSING" ]; then
262
- echo "::error title=ν•„μˆ˜ Secret λˆ„λ½::λ“±λ‘λ˜μ§€ μ•Šμ€ GitHub Secret:$MISSING β€” Settings β†’ Secrets and variables β†’ Actionsμ—μ„œ λ“±λ‘ν•˜μ„Έμš”"
334
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
263
335
  FAILED=true
264
336
  fi
265
337
  if [ ! -f "$DOCKERFILE" ]; then
266
- echo "::error title=Dockerfile μ—†μŒ::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 μ—†μŠ΅λ‹ˆλ‹€. 이 μ›Œν¬ν”Œλ‘œμš°λŠ” Docker μ΄λ―Έμ§€λ‘œ λ°°ν¬ν•˜λ―€λ‘œ Dockerfile이 ν•„μš”ν•©λ‹ˆλ‹€ (μ„œλ²„ 배포λ₯Ό ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ 이 μ›Œν¬ν”Œλ‘œμš° νŒŒμΌμ„ μ‚­μ œν•˜μ„Έμš”)"
338
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
267
339
  FAILED=true
268
340
  fi
269
341
  if [ "$FAILED" = "true" ]; then
270
342
  exit 1
271
343
  fi
272
- echo "βœ… 사전 점검 톡과: ν•„μˆ˜ Secretκ³Ό Dockerfile이 λͺ¨λ‘ μžˆμŠ΅λ‹ˆλ‹€"
344
+ m wf_preview.precheck_ok
273
345
 
274
- - name: JDK μ„€μ •
346
+ - name: Set up JDK
275
347
  uses: actions/setup-java@v5
276
348
  with:
277
349
  distribution: 'temurin'
278
350
  java-version: ${{ env.JAVA_VERSION }}
279
351
  cache: 'gradle'
280
352
 
281
- - name: Gradle κΆŒν•œ μ„€μ •
353
+ - name: Set Gradle permissions
282
354
  working-directory: ${{ env.PROJECT_PATH }}
283
355
  run: chmod +x gradlew
284
356
 
285
- # [μ˜μ—­ 2] ν”„λ‘œμ νŠΈλ³„ Secret 파일 생성
357
+ # [Section 2] Project-specific secret files
286
358
  #
287
- # ν•„μˆ˜ (λͺ¨λ“  Spring Boot ν”„λ‘œμ νŠΈ):
359
+ # Required (all Spring Boot projects):
288
360
  # - application-prod.yml
289
361
  #
290
- # 선택 (ν”„λ‘œμ νŠΈμ— 따라 μΆ”κ°€/제거):
362
+ # Optional (add or remove per project):
291
363
  # - Firebase: firebase-admin-sdk.json, firebase-messaging-sw.js
292
364
  # - Vertex AI: gen-lang-client-xxx.json
293
- # - 기타 μ„€μ • 파일: admin.yml, prompt.yml λ“±
365
+ # - Other config files: admin.yml, prompt.yml, etc.
294
366
  #
295
- # λ‹€λ₯Έ ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš© μ‹œ:
296
- # 1. ν•„μš” μ—†λŠ” step은 μ‚­μ œν•˜μ„Έμš”
297
- # 2. ν•„μš”ν•œ step을 μΆ”κ°€ν•˜μ„Έμš”
298
- # 3. 파일 경둜λ₯Ό ν”„λ‘œμ νŠΈμ— 맞게 μˆ˜μ •ν•˜μ„Έμš”
367
+ # When reusing this in another project:
368
+ # 1. Remove the steps you do not need
369
+ # 2. Add the steps you need
370
+ # 3. Adjust the file paths for your project
299
371
 
300
- # [ν•„μˆ˜] Spring Boot μ„€μ • 파일
301
- - name: "[ν•„μˆ˜] application-prod.yml 생성"
372
+ # [Required] Spring Boot config file
373
+ - name: "[Required] Create application-prod.yml"
302
374
  run: |
303
375
  mkdir -p $(dirname ${{ env.APPLICATION_YML_PATH }})
304
376
  cat << 'EOF' > ${{ env.APPLICATION_YML_PATH }}
305
377
  ${{ secrets.APPLICATION_PROD_YML }}
306
378
  EOF
307
379
 
308
- # [선택] μ•„λž˜λŠ” μ˜ˆμ‹œμž…λ‹ˆλ‹€. ν•„μš” μ—†μœΌλ©΄ μ‚­μ œν•˜μ„Έμš”.
309
- # - name: "[선택] Vertex AI Service Account Key 생성"
380
+ # [Optional] The steps below are examples. Delete them if unused.
381
+ # - name: "[Optional] Create Vertex AI service account key"
310
382
  # env:
311
383
  # VERTEX_SA_KEY: ${{ secrets.VERTEX_SA_KEY }}
312
384
  # run: |
313
- # echo "$VERTEX_SA_KEY" | sed 's/\\n/\n/g' > ./<λͺ¨λ“ˆλͺ…>/src/main/resources/vertex-ai-key.json
385
+ # echo "$VERTEX_SA_KEY" | sed 's/\\n/\n/g' > ./<module>/src/main/resources/vertex-ai-key.json
314
386
 
315
- # - name: "[선택] Firebase Admin SDK 생성"
387
+ # - name: "[Optional] Create Firebase Admin SDK key"
316
388
  # env:
317
389
  # FIREBASE_KEY_JSON: ${{ secrets.FIREBASE_KEY_JSON }}
318
390
  # run: |
319
- # echo "$FIREBASE_KEY_JSON" | sed 's/\\n/\n/g' > ./<λͺ¨λ“ˆλͺ…>/src/main/resources/firebase-admin-sdk.json
391
+ # echo "$FIREBASE_KEY_JSON" | sed 's/\\n/\n/g' > ./<module>/src/main/resources/firebase-admin-sdk.json
320
392
 
321
- # [μ˜μ—­ 2 끝] ν”„λ‘œμ νŠΈλ³„ Secret 파일 생성 끝
393
+ # [End of section 2] Project-specific secret files
322
394
 
323
- - name: Gradle λΉŒλ“œ
395
+ - name: Gradle build
324
396
  working-directory: ${{ env.PROJECT_PATH }}
325
397
  run: ${{ env.GRADLE_BUILD_CMD }}
326
398
 
327
- - name: μ§„ν–‰ 상황 - λΉŒλ“œ μ™„λ£Œ
399
+ - name: Progress - build done
328
400
  id: build_progress
329
401
  uses: actions/github-script@v9
330
402
  with:
331
403
  script: |
404
+ const M = JSON.parse(process.env.PAW_MSG);
405
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
406
+
332
407
  const commentId = ${{ steps.progress.outputs.comment_id }};
333
408
  const startTime = ${{ steps.progress.outputs.start_time }};
334
409
  const now = Date.now();
335
410
  const elapsed = now - startTime;
336
411
  const minutes = Math.floor(elapsed / 60000);
337
412
  const seconds = Math.floor((elapsed % 60000) / 1000);
338
- const buildDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
413
+ const buildDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
339
414
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
340
415
 
341
416
  const body = [
342
- '## πŸš€ PR Preview λΉŒλ“œ 쀑...',
417
+ m('wf_preview.title_pr_building'),
343
418
  '',
344
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
419
+ m('wf_preview.tbl_steps'),
345
420
  '|------|------|----------|',
346
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
347
- '| 🐳 Docker 이미지 생성 | ⏳ μ§„ν–‰ 쀑... | - |',
348
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
421
+ m('wf_preview.row_build_done', {buildDuration}),
422
+ m('wf_preview.row_image_progress'),
423
+ m('wf_preview.row_deploy_wait'),
349
424
  '',
350
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
425
+ m('wf_preview.link_logs', {runUrl}),
351
426
  '',
352
427
  '---',
353
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
428
+ m('wf_preview.footer_progress')
354
429
  ].join('\n');
355
430
 
356
431
  await github.rest.issues.updateComment({
@@ -363,16 +438,16 @@ jobs:
363
438
  core.setOutput('build_duration', buildDuration);
364
439
  core.setOutput('docker_start', now);
365
440
 
366
- - name: Docker 둜그인
441
+ - name: Log in to Docker Hub
367
442
  uses: docker/login-action@v3
368
443
  with:
369
444
  username: ${{ secrets.DOCKERHUB_USERNAME }}
370
445
  password: ${{ secrets.DOCKERHUB_TOKEN }}
371
446
 
372
- - name: Docker Buildx μ„€μ •
447
+ - name: Set up Docker Buildx
373
448
  uses: docker/setup-buildx-action@v3
374
449
 
375
- - name: Docker 이미지 λΉŒλ“œ & Push
450
+ - name: Build & push Docker image
376
451
  uses: docker/build-push-action@v5
377
452
  with:
378
453
  context: ${{ env.PROJECT_PATH }}
@@ -382,11 +457,14 @@ jobs:
382
457
  cache-from: type=gha
383
458
  cache-to: type=gha,mode=max
384
459
 
385
- - name: μ§„ν–‰ 상황 - Docker μ™„λ£Œ
460
+ - name: Progress - Docker done
386
461
  id: docker_progress
387
462
  uses: actions/github-script@v9
388
463
  with:
389
464
  script: |
465
+ const M = JSON.parse(process.env.PAW_MSG);
466
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
467
+
390
468
  const commentId = ${{ steps.progress.outputs.comment_id }};
391
469
  const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
392
470
  const dockerStart = ${{ steps.build_progress.outputs.docker_start }};
@@ -394,22 +472,22 @@ jobs:
394
472
  const dockerElapsed = now - dockerStart;
395
473
  const minutes = Math.floor(dockerElapsed / 60000);
396
474
  const seconds = Math.floor((dockerElapsed % 60000) / 1000);
397
- const dockerDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
475
+ const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
398
476
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
399
477
 
400
478
  const body = [
401
- '## πŸš€ PR Preview λΉŒλ“œ 쀑...',
479
+ m('wf_preview.title_pr_building'),
402
480
  '',
403
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
481
+ m('wf_preview.tbl_steps'),
404
482
  '|------|------|----------|',
405
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
406
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
407
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏳ μ§„ν–‰ 쀑... | - |',
483
+ m('wf_preview.row_build_done', {buildDuration}),
484
+ m('wf_preview.row_image_done', {dockerDuration}),
485
+ m('wf_preview.row_deploy_progress'),
408
486
  '',
409
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
487
+ m('wf_preview.link_logs', {runUrl}),
410
488
  '',
411
489
  '---',
412
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
490
+ m('wf_preview.footer_progress')
413
491
  ].join('\n');
414
492
 
415
493
  await github.rest.issues.updateComment({
@@ -423,7 +501,7 @@ jobs:
423
501
  core.setOutput('docker_duration', dockerDuration);
424
502
  core.setOutput('deploy_start', now);
425
503
 
426
- - name: μ„œλ²„μ— 배포
504
+ - name: Deploy to server
427
505
  uses: appleboy/ssh-action@v1.0.3
428
506
  with:
429
507
  host: ${{ secrets.SERVER_HOST }}
@@ -432,12 +510,14 @@ jobs:
432
510
  key: ${{ secrets.SSH_KEY }}
433
511
  port: ${{ env.SSH_PORT }}
434
512
  script: |
513
+ ${{ env.PAW_REMOTE }}
514
+
435
515
  set -e
436
516
 
437
517
  export PATH=$PATH:/usr/local/bin
438
518
  export PW="${{ secrets.SERVER_PASSWORD }}"
439
519
 
440
- # SSH 인증 방식에 λ”°λ₯Έ sudo 좔상화
520
+ # sudo wrapper that depends on the SSH auth method
441
521
  SSH_AUTH_METHOD="${{ env.SSH_AUTH_METHOD }}"
442
522
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
443
523
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
@@ -445,7 +525,7 @@ jobs:
445
525
  else
446
526
  SUDO() { echo "$PW" | sudo -S "$@"; }
447
527
  fi
448
- echo "πŸ” SSH 인증 방식: ${SSH_AUTH_METHOD}"
528
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
449
529
 
450
530
  PR_NUMBER=${{ github.event.issue.number }}
451
531
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -456,18 +536,18 @@ jobs:
456
536
  TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
457
537
 
458
538
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
459
- echo "πŸš€ PR Preview 배포 μ‹œμž‘"
539
+ m wf_preview.r_deploy_head_pr
460
540
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
461
- echo "πŸ“¦ ν”„λ‘œμ νŠΈ: ${PROJECT_NAME}"
462
- echo "πŸ”’ PR 번호: #${PR_NUMBER}"
463
- echo "πŸ“› μ»¨ν…Œμ΄λ„ˆ: ${CONTAINER_NAME}"
464
- echo "🌐 도메인: ${DOMAIN}"
541
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
542
+ m wf_preview.r_num_pr pr_number="${PR_NUMBER}"
543
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
544
+ m wf_preview.r_domain domain="${DOMAIN}"
465
545
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
466
546
 
467
- echo "πŸ“₯ Docker 이미지 Pull 쀑..."
547
+ m wf_preview.r_image_pulling
468
548
  SUDO docker pull "${IMAGE}"
469
549
 
470
- echo "πŸ—‘οΈ κΈ°μ‘΄ μ»¨ν…Œμ΄λ„ˆ 정리 쀑..."
550
+ m wf_preview.r_existing_cleanup
471
551
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
472
552
 
473
553
  VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
@@ -475,7 +555,7 @@ jobs:
475
555
  VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
476
556
  fi
477
557
 
478
- echo "🐳 μƒˆ μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ 쀑..."
558
+ m wf_preview.r_container_starting
479
559
  SUDO docker run -d \
480
560
  --name "${CONTAINER_NAME}" \
481
561
  --network "${TRAEFIK_NETWORK}" \
@@ -488,11 +568,11 @@ jobs:
488
568
  $VOLUME_OPTS \
489
569
  "${IMAGE}"
490
570
 
491
- # Health Check (μ΅œλŒ€ 120초 λŒ€κΈ°) - ν•˜μ΄λΈŒλ¦¬λ“œ 방식
492
- # 1. HEALTH_CHECK_PATHκ°€ μ„€μ •λ˜μ–΄ 있으면 HTTP 체크 μ‹œλ„
493
- # 2. 폴백: 둜그 νŒ¨ν„΄ λ§€μΉ­ (HEALTH_CHECK_LOG_PATTERN)
571
+ # Health check (waits up to 120s) - hybrid approach
572
+ # 1. Try the HTTP check when HEALTH_CHECK_PATH is set
573
+ # 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
494
574
  echo ""
495
- echo "⏳ Health Check μ‹œμž‘ (μ΅œλŒ€ 120초 λŒ€κΈ°)..."
575
+ m wf_preview.r_hc_start
496
576
  HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
497
577
  LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
498
578
  MAX_RETRIES=24
@@ -504,13 +584,13 @@ jobs:
504
584
  sleep 5
505
585
  RETRY_COUNT=$((RETRY_COUNT + 1))
506
586
 
507
- # 1. μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인
587
+ # 1. Check the container state
508
588
  STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
509
589
 
510
590
  if [ "$STATUS" = "exited" ]; then
511
- echo "❌ μ»¨ν…Œμ΄λ„ˆ 비정상 μ’…λ£Œ!"
591
+ m wf_preview.r_container_exited
512
592
  echo ""
513
- echo "πŸ“‹ μ»¨ν…Œμ΄λ„ˆ 둜그 (졜근 100쀄):"
593
+ m wf_preview.r_container_logs
514
594
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
515
595
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
516
596
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -518,25 +598,25 @@ jobs:
518
598
  fi
519
599
 
520
600
  if [ "$STATUS" = "running" ]; then
521
- # 2. HTTP Health Check μ‹œλ„ (HEALTH_CHECK_PATHκ°€ μ„€μ •λœ κ²½μš°μ—λ§Œ)
601
+ # 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
522
602
  if [ -n "$HEALTH_PATH" ]; then
523
603
  HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" wget -qO- "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
524
604
 
525
- # Spring Actuator 응닡 확인 ({"status":"UP"}) λ˜λŠ” 일반 HTTP 응닡
605
+ # Accept a Spring Actuator response ({"status":"UP"}) or any HTTP response
526
606
  if echo "$HEALTH" | grep -q '"status":"UP"' || [ -n "$HEALTH" ]; then
527
- echo "βœ… 정상 기동 확인! (HTTP 응닡: ${HEALTH_PATH})"
607
+ m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
528
608
  HEALTH_CHECK_PASSED=true
529
609
  HEALTH_CHECK_METHOD="HTTP"
530
610
  break
531
611
  fi
532
612
  fi
533
613
 
534
- # 3. HTTP 응닡 μ—†μœΌλ©΄ 둜그 νŒ¨ν„΄ 맀칭으둜 폴백
614
+ # 3. Without an HTTP response, fall back to log pattern matching
535
615
  if [ -n "$LOG_PATTERN" ]; then
536
616
  STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
537
617
 
538
618
  if [ -n "$STARTED" ]; then
539
- echo "βœ… 정상 기동 확인! (둜그 νŒ¨ν„΄)"
619
+ m wf_preview.r_hc_ok_log
540
620
  echo " $STARTED"
541
621
  HEALTH_CHECK_PASSED=true
542
622
  HEALTH_CHECK_METHOD="Log"
@@ -545,14 +625,14 @@ jobs:
545
625
  fi
546
626
  fi
547
627
 
548
- echo "⏳ λŒ€κΈ° 쀑... ($RETRY_COUNT/$MAX_RETRIES) - μƒνƒœ: $STATUS"
628
+ m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
549
629
  done
550
630
 
551
631
  if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
552
632
  echo ""
553
- echo "❌ Health Check νƒ€μž„μ•„μ›ƒ (120초)"
633
+ m wf_preview.r_hc_timeout
554
634
  echo ""
555
- echo "πŸ“‹ μ»¨ν…Œμ΄λ„ˆ 둜그 (졜근 100쀄):"
635
+ m wf_preview.r_container_logs
556
636
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
557
637
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
558
638
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -560,13 +640,16 @@ jobs:
560
640
  fi
561
641
 
562
642
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
563
- echo "βœ… 배포 및 Health Check μ™„λ£Œ! (방식: ${HEALTH_CHECK_METHOD})"
643
+ m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
564
644
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
565
645
 
566
- - name: 배포 μ™„λ£Œ μ½”λ©˜νŠΈ
646
+ - name: Deploy complete comment
567
647
  uses: actions/github-script@v9
568
648
  with:
569
649
  script: |
650
+ const M = JSON.parse(process.env.PAW_MSG);
651
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
652
+
570
653
  const commentId = ${{ steps.progress.outputs.comment_id }};
571
654
  const buildDuration = '${{ steps.docker_progress.outputs.build_duration }}';
572
655
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
@@ -575,7 +658,7 @@ jobs:
575
658
  const deployElapsed = now - deployStart;
576
659
  const minutes = Math.floor(deployElapsed / 60000);
577
660
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
578
- const deployDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
661
+ const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
579
662
 
580
663
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
581
664
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -586,41 +669,41 @@ jobs:
586
669
  const previewUrl = `http://${domain}:${previewPort}`;
587
670
  const sha = '${{ steps.pr.outputs.sha }}';
588
671
 
589
- // Preview ν™˜κ²½ ν…Œμ΄λΈ” ꡬ성 (API_DOCS_PATHκ°€ μžˆμ„ λ•Œλ§Œ API Docs ν–‰ μΆ”κ°€)
672
+ // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
590
673
  const envRows = [
591
674
  `| **Preview URL** | ${previewUrl} |`,
592
675
  ];
593
676
  if (apiDocsPath) {
594
677
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
595
678
  }
596
- envRows.push(`| **μ»¨ν…Œμ΄λ„ˆ** | \`${projectName}-pr-${prNumber}\` |`);
597
- envRows.push(`| **컀밋** | \`${sha}\` |`);
679
+ envRows.push(m('wf_preview.env_container_pr', {projectName, prNumber}));
680
+ envRows.push(m('wf_preview.env_commit', {sha}));
598
681
 
599
682
  const branchName = '${{ steps.pr.outputs.ref }}';
600
683
 
601
684
  const body = [
602
- '## βœ… PR Preview 배포 μ™„λ£Œ!',
685
+ m('wf_preview.title_pr_deployed'),
603
686
  '',
604
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
687
+ m('wf_preview.tbl_steps'),
605
688
  '|------|------|----------|',
606
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
607
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
608
- `| πŸš€ μ„œλ²„ 배포 & Health Check | βœ… μ™„λ£Œ | ${deployDuration} |`,
689
+ m('wf_preview.row_build_done', {buildDuration}),
690
+ m('wf_preview.row_image_done', {dockerDuration}),
691
+ m('wf_preview.row_deploy_done', {deployDuration}),
609
692
  '',
610
- '### 🌐 Preview ν™˜κ²½',
611
- '| ν•­λͺ© | κ°’ |',
693
+ m('wf_preview.sec_env'),
694
+ m('wf_preview.tbl_items'),
612
695
  '|------|-----|',
613
696
  ...envRows,
614
697
  '',
615
- '### πŸ“‹ λͺ…λ Ήμ–΄',
616
- '| λͺ…λ Ήμ–΄ | μ„€λͺ… |',
698
+ m('wf_preview.sec_commands'),
699
+ m('wf_preview.tbl_commands'),
617
700
  '|--------|------|',
618
- '| `/wizard server build` | μ΅œμ‹  μ»€λ°‹μœΌλ‘œ 재배포 |',
619
- '| `/wizard server destroy` | Preview ν™˜κ²½ μ‚­μ œ |',
620
- '| `/wizard server status` | ν˜„μž¬ μƒνƒœ 확인 |',
701
+ m('wf_preview.cmd_build'),
702
+ m('wf_preview.cmd_destroy'),
703
+ m('wf_preview.cmd_status'),
621
704
  '',
622
705
  '<details>',
623
- '<summary>πŸ”§ κ³ κΈ‰ λͺ…λ Ήμ–΄ (λ‹€λ₯Έ Issue/PRμ—μ„œ μ œμ–΄)</summary>',
706
+ m('wf_preview.cmd_advanced'),
624
707
  '',
625
708
  '```',
626
709
  `/wizard server build ${branchName}`,
@@ -630,7 +713,7 @@ jobs:
630
713
  '</details>',
631
714
  '',
632
715
  '---',
633
- '*πŸ€– 이 λŒ“κΈ€μ€ PR Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
716
+ m('wf_preview.footer_pr')
634
717
  ].join('\n');
635
718
 
636
719
  await github.rest.issues.updateComment({
@@ -640,76 +723,79 @@ jobs:
640
723
  body: body
641
724
  });
642
725
 
643
- - name: λΉŒλ“œ/배포 μ‹€νŒ¨ μ‹œ μ—λŸ¬ μ½”λ©˜νŠΈ
726
+ - name: Build/deploy failure comment
644
727
  if: failure()
645
728
  uses: actions/github-script@v9
646
729
  with:
647
730
  script: |
731
+ const M = JSON.parse(process.env.PAW_MSG);
732
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
733
+
648
734
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
649
735
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
650
736
  const prNumber = context.issue.number;
651
737
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
652
738
 
653
- // μ§„ν–‰ 상황에 따라 μ–΄λ””μ„œ μ‹€νŒ¨ν–ˆλŠ”μ§€ ν‘œμ‹œ
739
+ // Show where it failed based on progress
654
740
  const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
655
741
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
656
742
 
657
- // μ‹€νŒ¨ 지점 νŒλ‹¨ - 초기 단계 μ‹€νŒ¨λ„ ꡬ뢄
743
+ // Work out where it failed - early-stage failures are distinguished too
658
744
  let buildStatus, dockerStatus, deployStatus;
659
745
  let buildTime = '-', dockerTime = '-';
660
746
 
661
747
  if (!commentId) {
662
- // μ§„ν–‰ 상황 λŒ“κΈ€ 생성 μ „ μ‹€νŒ¨ (μ΄ˆκΈ°ν™” 단계)
663
- buildStatus = '⚠️ μ΄ˆκΈ°ν™” μ‹€νŒ¨';
748
+ // Failed before the progress comment was created (initialization stage)
749
+ buildStatus = m('wf_preview.status_init_failed');
664
750
  dockerStatus = '-';
665
751
  deployStatus = '-';
666
752
  } else if (!buildDuration || buildDuration === '') {
667
- // λΉŒλ“œ λ‹¨κ³„μ—μ„œ μ‹€νŒ¨
668
- buildStatus = '❌ μ‹€νŒ¨';
669
- dockerStatus = '⏸️ λŒ€κΈ°';
670
- deployStatus = '⏸️ λŒ€κΈ°';
753
+ // Failed at the build stage
754
+ buildStatus = m('wf_preview.status_failed');
755
+ dockerStatus = m('wf_preview.status_waiting');
756
+ deployStatus = m('wf_preview.status_waiting');
671
757
  } else if (!dockerDuration || dockerDuration === '') {
672
- // Docker λ‹¨κ³„μ—μ„œ μ‹€νŒ¨
673
- buildStatus = 'βœ… μ™„λ£Œ';
674
- dockerStatus = '❌ μ‹€νŒ¨';
675
- deployStatus = '⏸️ λŒ€κΈ°';
758
+ // Failed at the Docker stage
759
+ buildStatus = m('wf_preview.status_done');
760
+ dockerStatus = m('wf_preview.status_failed');
761
+ deployStatus = m('wf_preview.status_waiting');
676
762
  buildTime = buildDuration;
677
763
  } else {
678
- // 배포 λ‹¨κ³„μ—μ„œ μ‹€νŒ¨
679
- buildStatus = 'βœ… μ™„λ£Œ';
680
- dockerStatus = 'βœ… μ™„λ£Œ';
681
- deployStatus = '❌ μ‹€νŒ¨';
764
+ // Failed at the deploy stage
765
+ buildStatus = m('wf_preview.status_done');
766
+ dockerStatus = m('wf_preview.status_done');
767
+ deployStatus = m('wf_preview.status_failed');
682
768
  buildTime = buildDuration;
683
769
  dockerTime = dockerDuration;
684
770
  }
685
771
 
686
772
  const body = [
687
- '## ❌ PR Preview 배포 μ‹€νŒ¨!',
773
+ m('wf_preview.title_pr_failed'),
688
774
  '',
689
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
775
+ m('wf_preview.tbl_steps'),
690
776
  '|------|------|----------|',
691
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ${buildStatus} | ${buildTime} |`,
692
- `| 🐳 Docker 이미지 생성 | ${dockerStatus} | ${dockerTime} |`,
693
- `| πŸš€ μ„œλ²„ 배포 & Health Check | ${deployStatus} | - |`,
777
+ m('wf_preview.row_build_status', {buildStatus, buildTime}),
778
+ m('wf_preview.row_image_status', {dockerStatus, dockerTime}),
779
+ m('wf_preview.row_deploy_status', {deployStatus}),
694
780
  '',
695
- `**[πŸ“‹ λΉŒλ“œ/배포 둜그 확인](${runUrl})**`,
781
+ m('wf_preview.link_build_logs', {runUrl}),
696
782
  '',
697
- '### πŸ” κ°€λŠ₯ν•œ 원인',
698
- '- ν•„μˆ˜ Secret(DOCKERHUB_*, SERVER_*) 미등둝 λ˜λŠ” Dockerfile μ—†μŒ β†’ 둜그의 "배포 사전 점검" 단계 확인',
699
- '- Gradle λΉŒλ“œ μ‹€νŒ¨',
700
- '- Docker 이미지 λΉŒλ“œ μ‹€νŒ¨',
701
- '- μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ μ‹€νŒ¨ (Spring Boot 기동 였λ₯˜)',
702
- '- Health Check νƒ€μž„μ•„μ›ƒ (120초 λ‚΄ 기동 μ™„λ£Œ μ•ˆλ¨)',
783
+ m('wf_preview.sec_causes'),
784
+ m('wf_preview.hint_secrets'),
785
+ m('wf_preview.hint_gradle'),
786
+ m('wf_preview.hint_docker'),
787
+ m('wf_preview.hint_start_spring'),
788
+ m('wf_preview.hint_health_timeout'),
703
789
  '',
704
- '### πŸ’‘ λ‹€μŒ 단계',
705
- '1. μœ„ λ§ν¬μ—μ„œ λΉŒλ“œ/배포 둜그λ₯Ό ν™•μΈν•˜μ„Έμš”',
706
- '2. 문제λ₯Ό μˆ˜μ •ν•œ ν›„ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”: `/wizard server build`',
790
+ m('wf_preview.sec_next'),
791
+ m('wf_preview.next_logs'),
792
+ m('wf_preview.next_retry'),
707
793
  '',
708
794
  '---',
709
- '*πŸ€– 이 λŒ“κΈ€μ€ PR Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
795
+ m('wf_preview.footer_pr')
710
796
  ].join('\n');
711
797
 
712
- // μ§„ν–‰ 상황 λŒ“κΈ€μ΄ 있으면 μ—…λ°μ΄νŠΈ, μ—†μœΌλ©΄ μƒˆλ‘œ 생성
798
+ // Update the progress comment if it exists, otherwise create a new one
713
799
  if (commentId) {
714
800
  await github.rest.issues.updateComment({
715
801
  owner: context.repo.owner,
@@ -726,9 +812,9 @@ jobs:
726
812
  });
727
813
  }
728
814
 
729
- # Job 2-2: Issueμ—μ„œ 브랜치λͺ… μΆ”μΆœ
815
+ # Job 2-2: extract the branch name from the issue
730
816
  get-branch-from-issue:
731
- name: Issueμ—μ„œ 브랜치 μΆ”μΆœ
817
+ name: Extract branch from issue
732
818
  needs: check-command
733
819
  if: |
734
820
  needs.check-command.outputs.is_valid == 'true' &&
@@ -740,30 +826,54 @@ jobs:
740
826
  found: ${{ steps.extract.outputs.found }}
741
827
  issue_number: ${{ steps.extract.outputs.issue_number }}
742
828
  steps:
743
- - name: Issue λŒ“κΈ€μ—μ„œ 브랜치λͺ… μΆ”μΆœ
829
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
830
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
831
+ - name: Checkout messages
832
+ uses: actions/checkout@v7
833
+ with:
834
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
835
+ sparse-checkout: |
836
+ .github/scripts/messages.py
837
+ version.yml
838
+ sparse-checkout-cone-mode: false
839
+
840
+ - name: Load messages
841
+ run: |
842
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
843
+ mkdir -p "$RUNNER_TEMP/paw"
844
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
845
+ {
846
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
847
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
848
+ } >> "$GITHUB_ENV"
849
+
850
+ - name: Extract branch name from issue comment
744
851
  id: extract
745
852
  uses: actions/github-script@v9
746
853
  with:
747
854
  script: |
855
+ const M = JSON.parse(process.env.PAW_MSG);
856
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
857
+
748
858
  const issueNumber = context.issue.number;
749
859
  const marker = '${{ env.ISSUE_HELPER_MARKER }}';
750
860
 
751
- console.log(`πŸ” Issue #${issueNumber}μ—μ„œ 브랜치 검색 쀑...`);
752
- console.log(`πŸ“ 마컀: ${marker}`);
861
+ console.log(m('wf_preview.log_search_branch', {issueNumber}));
862
+ console.log(m('wf_preview.log_marker', {marker}));
753
863
 
754
- // Issue의 λͺ¨λ“  λŒ“κΈ€ 쑰회
864
+ // Fetch all comments of the issue
755
865
  const comments = await github.rest.issues.listComments({
756
866
  owner: context.repo.owner,
757
867
  repo: context.repo.repo,
758
868
  issue_number: issueNumber
759
869
  });
760
870
 
761
- // 브랜치 μΆ”μΆœ νŒ¨ν„΄ (λ§ˆμ»€μ™€ λ¬΄κ΄€ν•˜κ²Œ λ™μΌν•œ ν˜•μ‹)
762
- // "### 브랜치" λ˜λŠ” "### 브랜치λͺ…" λ‘˜ λ‹€ λ§€μΉ­
763
- const branchRegex = /###\s*브랜치(?:λͺ…)?\s*\n```\s*\n([^\n]+)\s*\n```/;
871
+ // Branch extraction pattern (same format regardless of the marker)
872
+ // Match the issue helper heading in either language: "### Branch name" (en) or the Korean "### \uBE0C\uB79C\uCE58(\uBA85)" (ko)
873
+ const branchRegex = /###\s*(?:\uBE0C\uB79C\uCE58(?:\uBA85)?|Branch(?: name)?)\s*\n```\s*\n([^\n]+)\s*\n```/i;
764
874
 
765
875
  for (const comment of comments.data) {
766
- // λ§ˆμ»€κ°€ ν¬ν•¨λœ λŒ“κΈ€ μ°ΎκΈ°
876
+ // Find comments that contain the marker
767
877
  if (comment.body.includes(marker)) {
768
878
  const match = comment.body.match(branchRegex);
769
879
  if (match) {
@@ -771,40 +881,43 @@ jobs:
771
881
  core.setOutput('branch', branchName);
772
882
  core.setOutput('found', 'true');
773
883
  core.setOutput('issue_number', issueNumber);
774
- console.log(`βœ… 브랜치 발견: ${branchName}`);
884
+ console.log(m('wf_preview.log_branch_found', {branchName}));
775
885
  return;
776
886
  }
777
887
  }
778
888
  }
779
889
 
780
- // 브랜치λ₯Ό μ°Ύμ§€ λͺ»ν•œ 경우 - μ—λŸ¬κ°€ μ•„λ‹˜, gracefulν•˜κ²Œ 처리
890
+ // Branch not found - not an error, handle it gracefully
781
891
  core.setOutput('found', 'false');
782
892
  core.setOutput('issue_number', issueNumber);
783
- console.log('⚠️ Issue Helper λŒ“κΈ€μ—μ„œ 브랜치λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.');
893
+ console.log(m('wf_preview.log_helper_no_branch'));
784
894
 
785
- - name: 브랜치 μ—†μŒ μ•Œλ¦Ό
895
+ - name: Branch not found notice
786
896
  if: steps.extract.outputs.found == 'false'
787
897
  uses: actions/github-script@v9
788
898
  with:
789
899
  script: |
900
+ const M = JSON.parse(process.env.PAW_MSG);
901
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
902
+
790
903
  const issueNumber = context.issue.number;
791
904
  const marker = '${{ env.ISSUE_HELPER_MARKER }}';
792
905
 
793
906
  const body = [
794
- '## ⚠️ 브랜치λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€',
907
+ m('wf_preview.title_branch_missing_warn'),
795
908
  '',
796
- '| ν•­λͺ© | κ°’ |',
909
+ m('wf_preview.tbl_items'),
797
910
  '|------|-----|',
798
911
  `| **Issue** | #${issueNumber} |`,
799
- `| **마컀** | \`${marker}\` |`,
912
+ m('wf_preview.env_marker', {marker}),
800
913
  '',
801
- '### πŸ’‘ 확인 사항',
802
- '1. Issue Helper λŒ“κΈ€μ΄ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
803
- '2. λŒ“κΈ€μ— `### 브랜치` μ„Ήμ…˜μ΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
804
- '3. `ISSUE_HELPER_MARKER` ν™˜κ²½λ³€μˆ˜κ°€ μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•˜μ„Έμš”',
914
+ m('wf_preview.sec_check'),
915
+ m('wf_preview.chk_helper_exists'),
916
+ m('wf_preview.chk_helper_section'),
917
+ m('wf_preview.chk_helper_marker'),
805
918
  '',
806
919
  '---',
807
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
920
+ m('wf_preview.footer_generic')
808
921
  ].join('\n');
809
922
 
810
923
  await github.rest.issues.createComment({
@@ -814,9 +927,9 @@ jobs:
814
927
  body: body
815
928
  });
816
929
 
817
- # Job 2-3: λΉŒλ“œ & 배포 (Issue λŒ“κΈ€μ—μ„œ μ‹€ν–‰)
930
+ # Job 2-3: build & deploy (triggered from an issue comment)
818
931
  build-preview-issue:
819
- name: Preview λΉŒλ“œ & 배포 (Issue)
932
+ name: Preview build & deploy (Issue)
820
933
  needs: [check-command, get-branch-from-issue]
821
934
  if: |
822
935
  needs.check-command.outputs.is_valid == 'true' &&
@@ -826,15 +939,62 @@ jobs:
826
939
  needs.get-branch-from-issue.outputs.found == 'true'
827
940
  runs-on: ubuntu-latest
828
941
  steps:
829
- - name: 브랜치 쑴재 확인
942
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
943
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
944
+ - name: Checkout messages
945
+ uses: actions/checkout@v7
946
+ with:
947
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
948
+ sparse-checkout: |
949
+ .github/scripts/messages.py
950
+ version.yml
951
+ sparse-checkout-cone-mode: false
952
+
953
+ - name: Load messages
954
+ run: |
955
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
956
+ mkdir -p "$RUNNER_TEMP/paw"
957
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
958
+ {
959
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
960
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
961
+ } >> "$GITHUB_ENV"
962
+ # The deploy script runs on the remote server, where messages.py does not exist:
963
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
964
+ python3 - >> "$GITHUB_ENV" <<'PY'
965
+ import json, os, shlex, subprocess
966
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
967
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
968
+ head = ["m() {", ' case "$1" in']
969
+ for key, text in msgs.items():
970
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
971
+ tail = r''' *) _t="$1" ;;
972
+ esac
973
+ shift
974
+ while [ "$#" -gt 0 ]; do
975
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
976
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
977
+ shift
978
+ done
979
+ printf '%s\n' "$_t"
980
+ }'''
981
+ print("PAW_REMOTE<<PAW_EOF")
982
+ print("\n".join(head) + "\n" + tail)
983
+ print("PAW_EOF")
984
+ PY
985
+
986
+ - name: Check branch exists
830
987
  id: check_branch
831
988
  uses: actions/github-script@v9
832
989
  with:
833
990
  script: |
991
+ const M = JSON.parse(process.env.PAW_MSG);
992
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
993
+
834
994
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
835
995
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
836
996
 
837
- console.log(`πŸ” 브랜치 쑴재 확인: ${branchName}`);
997
+ console.log(m('wf_preview.log_check_exists', {branchName}));
838
998
 
839
999
  try {
840
1000
  await github.rest.repos.getBranch({
@@ -843,28 +1003,28 @@ jobs:
843
1003
  branch: branchName
844
1004
  });
845
1005
  core.setOutput('exists', 'true');
846
- console.log(`βœ… 브랜치 쑴재: ${branchName}`);
1006
+ console.log(m('wf_preview.log_branch_exists', {branchName}));
847
1007
  } catch (error) {
848
1008
  if (error.status === 404) {
849
1009
  core.setOutput('exists', 'false');
850
- console.log(`❌ 브랜치 μ—†μŒ: ${branchName}`);
1010
+ console.log(m('wf_preview.log_branch_missing', {branchName}));
851
1011
 
852
- // 브랜치 μ—†μŒ μ•Œλ¦Ό λŒ“κΈ€
1012
+ // Comment for a missing branch
853
1013
  const body = [
854
- '## ❌ 브랜치λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€',
1014
+ m('wf_preview.title_branch_missing'),
855
1015
  '',
856
- '| ν•­λͺ© | κ°’ |',
1016
+ m('wf_preview.tbl_items'),
857
1017
  '|------|-----|',
858
1018
  `| **Issue** | #${issueNumber} |`,
859
- `| **브랜치** | \`${branchName}\` |`,
1019
+ m('wf_preview.env_branch', {branchName}),
860
1020
  '',
861
- '### πŸ’‘ 확인 사항',
862
- '1. λΈŒλžœμΉ˜κ°€ pushλ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
863
- '2. 브랜치λͺ…이 μ •ν™•ν•œμ§€ ν™•μΈν•˜μ„Έμš”',
864
- '3. λΈŒλžœμΉ˜κ°€ μ‚­μ œλ˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
1021
+ m('wf_preview.sec_check'),
1022
+ m('wf_preview.chk_pushed_1'),
1023
+ m('wf_preview.chk_name_2'),
1024
+ m('wf_preview.chk_deleted_3'),
865
1025
  '',
866
1026
  '---',
867
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
1027
+ m('wf_preview.footer_generic')
868
1028
  ].join('\n');
869
1029
 
870
1030
  await github.rest.issues.createComment({
@@ -878,47 +1038,52 @@ jobs:
878
1038
  }
879
1039
  }
880
1040
 
881
- - name: 브랜치 μ—†μœΌλ©΄ 쀑단
1041
+ - name: Skip when branch is missing
882
1042
  if: steps.check_branch.outputs.exists == 'false'
883
1043
  run: |
884
- echo "⚠️ λΈŒλžœμΉ˜κ°€ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ λΉŒλ“œλ₯Ό κ±΄λ„ˆλœλ‹ˆλ‹€."
1044
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1045
+
1046
+ m wf_preview.skip_no_branch
885
1047
  exit 0
886
1048
 
887
- - name: Issue 정보 μ„€μ •
1049
+ - name: Set issue info
888
1050
  if: steps.check_branch.outputs.exists == 'true'
889
1051
  id: issue
890
1052
  run: |
891
1053
  echo "ref=${{ needs.get-branch-from-issue.outputs.branch_name }}" >> $GITHUB_OUTPUT
892
1054
  echo "issue_number=${{ needs.get-branch-from-issue.outputs.issue_number }}" >> $GITHUB_OUTPUT
893
- # 브랜치의 μ΅œμ‹  컀밋 SHA κ°€μ Έμ˜€κΈ°
1055
+ # Get the latest commit SHA of the branch
894
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
895
1057
 
896
- - name: μ§„ν–‰ 상황 λŒ“κΈ€ 생성
1058
+ - name: Create progress comment
897
1059
  if: steps.check_branch.outputs.exists == 'true'
898
1060
  id: progress
899
1061
  uses: actions/github-script@v9
900
1062
  with:
901
1063
  script: |
1064
+ const M = JSON.parse(process.env.PAW_MSG);
1065
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1066
+
902
1067
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
903
1068
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
904
1069
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
905
1070
  const startTime = Date.now();
906
1071
 
907
1072
  const body = [
908
- '## πŸš€ Issue Preview λΉŒλ“œ 쀑...',
1073
+ m('wf_preview.title_issue_building'),
909
1074
  '',
910
- `**브랜치**: \`${branchName}\``,
1075
+ m('wf_preview.line_branch', {branchName}),
911
1076
  '',
912
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
1077
+ m('wf_preview.tbl_steps'),
913
1078
  '|------|------|----------|',
914
- '| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ⏳ μ§„ν–‰ 쀑... | - |',
915
- '| 🐳 Docker 이미지 생성 | ⏸️ λŒ€κΈ° | - |',
916
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
1079
+ m('wf_preview.row_build_progress'),
1080
+ m('wf_preview.row_image_wait'),
1081
+ m('wf_preview.row_deploy_wait'),
917
1082
  '',
918
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
1083
+ m('wf_preview.link_logs', {runUrl}),
919
1084
  '',
920
1085
  '---',
921
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
1086
+ m('wf_preview.footer_progress')
922
1087
  ].join('\n');
923
1088
 
924
1089
  const { data: comment } = await github.rest.issues.createComment({
@@ -931,14 +1096,14 @@ jobs:
931
1096
  core.setOutput('comment_id', comment.id);
932
1097
  core.setOutput('start_time', startTime);
933
1098
 
934
- - name: μ½”λ“œ 체크아웃
1099
+ - name: Checkout code
935
1100
  if: steps.check_branch.outputs.exists == 'true'
936
1101
  uses: actions/checkout@v7
937
1102
  with:
938
1103
  ref: ${{ needs.get-branch-from-issue.outputs.branch_name }}
939
1104
 
940
- # ν•„μˆ˜ SecretΒ·Dockerfile이 μ—†μœΌλ©΄ docker 둜그인 같은 μ„œλ“œνŒŒν‹° 였λ₯˜ λŒ€μ‹  λΉ μ§„ ν•­λͺ©μ„ μ•Œλ € μ£Όκ³  λ©ˆμΆ˜λ‹€
941
- - name: 배포 사전 점검 (SecretΒ·Dockerfile)
1105
+ # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
1106
+ - name: Deploy pre-check (Secrets and Dockerfile)
942
1107
  if: steps.check_branch.outputs.exists == 'true'
943
1108
  working-directory: ${{ env.PROJECT_PATH }}
944
1109
  env:
@@ -950,11 +1115,13 @@ jobs:
950
1115
  SSH_KEY: ${{ secrets.SSH_KEY }}
951
1116
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
952
1117
  run: |
1118
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1119
+
953
1120
  MISSING=""
954
1121
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
955
1122
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
956
1123
  done
957
- # SSH_AUTH_METHOD=keyλ©΄ SSH_KEY, μ•„λ‹ˆλ©΄ SERVER_PASSWORDκ°€ ν•„μš”ν•˜λ‹€
1124
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
958
1125
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
959
1126
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
960
1127
  else
@@ -962,19 +1129,19 @@ jobs:
962
1129
  fi
963
1130
  FAILED=false
964
1131
  if [ -n "$MISSING" ]; then
965
- echo "::error title=ν•„μˆ˜ Secret λˆ„λ½::λ“±λ‘λ˜μ§€ μ•Šμ€ GitHub Secret:$MISSING β€” Settings β†’ Secrets and variables β†’ Actionsμ—μ„œ λ“±λ‘ν•˜μ„Έμš”"
1132
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
966
1133
  FAILED=true
967
1134
  fi
968
1135
  if [ ! -f "$DOCKERFILE" ]; then
969
- echo "::error title=Dockerfile μ—†μŒ::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 μ—†μŠ΅λ‹ˆλ‹€. 이 μ›Œν¬ν”Œλ‘œμš°λŠ” Docker μ΄λ―Έμ§€λ‘œ λ°°ν¬ν•˜λ―€λ‘œ Dockerfile이 ν•„μš”ν•©λ‹ˆλ‹€ (μ„œλ²„ 배포λ₯Ό ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ 이 μ›Œν¬ν”Œλ‘œμš° νŒŒμΌμ„ μ‚­μ œν•˜μ„Έμš”)"
1136
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
970
1137
  FAILED=true
971
1138
  fi
972
1139
  if [ "$FAILED" = "true" ]; then
973
1140
  exit 1
974
1141
  fi
975
- echo "βœ… 사전 점검 톡과: ν•„μˆ˜ Secretκ³Ό Dockerfile이 λͺ¨λ‘ μžˆμŠ΅λ‹ˆλ‹€"
1142
+ m wf_preview.precheck_ok
976
1143
 
977
- - name: JDK μ„€μ •
1144
+ - name: Set up JDK
978
1145
  if: steps.check_branch.outputs.exists == 'true'
979
1146
  uses: actions/setup-java@v5
980
1147
  with:
@@ -982,32 +1149,35 @@ jobs:
982
1149
  java-version: ${{ env.JAVA_VERSION }}
983
1150
  cache: 'gradle'
984
1151
 
985
- - name: Gradle κΆŒν•œ μ„€μ •
1152
+ - name: Set Gradle permissions
986
1153
  working-directory: ${{ env.PROJECT_PATH }}
987
1154
  if: steps.check_branch.outputs.exists == 'true'
988
1155
  run: chmod +x gradlew
989
1156
 
990
- # [μ˜μ—­ 2] ν”„λ‘œμ νŠΈλ³„ Secret 파일 생성
991
- - name: "[ν•„μˆ˜] application-prod.yml 생성"
1157
+ # [Section 2] Project-specific secret files
1158
+ - name: "[Required] Create application-prod.yml"
992
1159
  if: steps.check_branch.outputs.exists == 'true'
993
1160
  run: |
994
1161
  mkdir -p $(dirname ${{ env.APPLICATION_YML_PATH }})
995
1162
  cat << 'EOF' > ${{ env.APPLICATION_YML_PATH }}
996
1163
  ${{ secrets.APPLICATION_PROD_YML }}
997
1164
  EOF
998
- # [μ˜μ—­ 2 끝] ν”„λ‘œμ νŠΈλ³„ Secret 파일 생성 끝
1165
+ # [End of section 2] Project-specific secret files
999
1166
 
1000
- - name: Gradle λΉŒλ“œ
1167
+ - name: Gradle build
1001
1168
  working-directory: ${{ env.PROJECT_PATH }}
1002
1169
  if: steps.check_branch.outputs.exists == 'true'
1003
1170
  run: ${{ env.GRADLE_BUILD_CMD }}
1004
1171
 
1005
- - name: μ§„ν–‰ 상황 - λΉŒλ“œ μ™„λ£Œ
1172
+ - name: Progress - build done
1006
1173
  if: steps.check_branch.outputs.exists == 'true'
1007
1174
  id: build_progress
1008
1175
  uses: actions/github-script@v9
1009
1176
  with:
1010
1177
  script: |
1178
+ const M = JSON.parse(process.env.PAW_MSG);
1179
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1180
+
1011
1181
  const commentId = ${{ steps.progress.outputs.comment_id }};
1012
1182
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1013
1183
  const startTime = ${{ steps.progress.outputs.start_time }};
@@ -1015,24 +1185,24 @@ jobs:
1015
1185
  const elapsed = now - startTime;
1016
1186
  const minutes = Math.floor(elapsed / 60000);
1017
1187
  const seconds = Math.floor((elapsed % 60000) / 1000);
1018
- const buildDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
1188
+ const buildDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1019
1189
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1020
1190
 
1021
1191
  const body = [
1022
- '## πŸš€ Issue Preview λΉŒλ“œ 쀑...',
1192
+ m('wf_preview.title_issue_building'),
1023
1193
  '',
1024
- `**브랜치**: \`${branchName}\``,
1194
+ m('wf_preview.line_branch', {branchName}),
1025
1195
  '',
1026
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
1196
+ m('wf_preview.tbl_steps'),
1027
1197
  '|------|------|----------|',
1028
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
1029
- '| 🐳 Docker 이미지 생성 | ⏳ μ§„ν–‰ 쀑... | - |',
1030
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
1198
+ m('wf_preview.row_build_done', {buildDuration}),
1199
+ m('wf_preview.row_image_progress'),
1200
+ m('wf_preview.row_deploy_wait'),
1031
1201
  '',
1032
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
1202
+ m('wf_preview.link_logs', {runUrl}),
1033
1203
  '',
1034
1204
  '---',
1035
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
1205
+ m('wf_preview.footer_progress')
1036
1206
  ].join('\n');
1037
1207
 
1038
1208
  await github.rest.issues.updateComment({
@@ -1045,18 +1215,18 @@ jobs:
1045
1215
  core.setOutput('build_duration', buildDuration);
1046
1216
  core.setOutput('docker_start', now);
1047
1217
 
1048
- - name: Docker 둜그인
1218
+ - name: Log in to Docker Hub
1049
1219
  if: steps.check_branch.outputs.exists == 'true'
1050
1220
  uses: docker/login-action@v3
1051
1221
  with:
1052
1222
  username: ${{ secrets.DOCKERHUB_USERNAME }}
1053
1223
  password: ${{ secrets.DOCKERHUB_TOKEN }}
1054
1224
 
1055
- - name: Docker Buildx μ„€μ •
1225
+ - name: Set up Docker Buildx
1056
1226
  if: steps.check_branch.outputs.exists == 'true'
1057
1227
  uses: docker/setup-buildx-action@v3
1058
1228
 
1059
- - name: Docker 이미지 λΉŒλ“œ & Push
1229
+ - name: Build & push Docker image
1060
1230
  if: steps.check_branch.outputs.exists == 'true'
1061
1231
  uses: docker/build-push-action@v5
1062
1232
  with:
@@ -1067,12 +1237,15 @@ jobs:
1067
1237
  cache-from: type=gha
1068
1238
  cache-to: type=gha,mode=max
1069
1239
 
1070
- - name: μ§„ν–‰ 상황 - Docker μ™„λ£Œ
1240
+ - name: Progress - Docker done
1071
1241
  if: steps.check_branch.outputs.exists == 'true'
1072
1242
  id: docker_progress
1073
1243
  uses: actions/github-script@v9
1074
1244
  with:
1075
1245
  script: |
1246
+ const M = JSON.parse(process.env.PAW_MSG);
1247
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1248
+
1076
1249
  const commentId = ${{ steps.progress.outputs.comment_id }};
1077
1250
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1078
1251
  const buildDuration = '${{ steps.build_progress.outputs.build_duration }}';
@@ -1081,24 +1254,24 @@ jobs:
1081
1254
  const dockerElapsed = now - dockerStart;
1082
1255
  const minutes = Math.floor(dockerElapsed / 60000);
1083
1256
  const seconds = Math.floor((dockerElapsed % 60000) / 1000);
1084
- const dockerDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
1257
+ const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1085
1258
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1086
1259
 
1087
1260
  const body = [
1088
- '## πŸš€ Issue Preview λΉŒλ“œ 쀑...',
1261
+ m('wf_preview.title_issue_building'),
1089
1262
  '',
1090
- `**브랜치**: \`${branchName}\``,
1263
+ m('wf_preview.line_branch', {branchName}),
1091
1264
  '',
1092
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
1265
+ m('wf_preview.tbl_steps'),
1093
1266
  '|------|------|----------|',
1094
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
1095
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
1096
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏳ μ§„ν–‰ 쀑... | - |',
1267
+ m('wf_preview.row_build_done', {buildDuration}),
1268
+ m('wf_preview.row_image_done', {dockerDuration}),
1269
+ m('wf_preview.row_deploy_progress'),
1097
1270
  '',
1098
- `**[πŸ“‹ μ‹€μ‹œκ°„ 둜그 보기](${runUrl})**`,
1271
+ m('wf_preview.link_logs', {runUrl}),
1099
1272
  '',
1100
1273
  '---',
1101
- '*πŸ€– 이 λŒ“κΈ€μ€ μžλ™μœΌλ‘œ μ—…λ°μ΄νŠΈλ©λ‹ˆλ‹€.*'
1274
+ m('wf_preview.footer_progress')
1102
1275
  ].join('\n');
1103
1276
 
1104
1277
  await github.rest.issues.updateComment({
@@ -1112,7 +1285,7 @@ jobs:
1112
1285
  core.setOutput('docker_duration', dockerDuration);
1113
1286
  core.setOutput('deploy_start', now);
1114
1287
 
1115
- - name: μ„œλ²„μ— 배포
1288
+ - name: Deploy to server
1116
1289
  if: steps.check_branch.outputs.exists == 'true'
1117
1290
  uses: appleboy/ssh-action@v1.0.3
1118
1291
  with:
@@ -1122,12 +1295,14 @@ jobs:
1122
1295
  key: ${{ secrets.SSH_KEY }}
1123
1296
  port: ${{ env.SSH_PORT }}
1124
1297
  script: |
1298
+ ${{ env.PAW_REMOTE }}
1299
+
1125
1300
  set -e
1126
1301
 
1127
1302
  export PATH=$PATH:/usr/local/bin
1128
1303
  export PW="${{ secrets.SERVER_PASSWORD }}"
1129
1304
 
1130
- # SSH 인증 방식에 λ”°λ₯Έ sudo 좔상화
1305
+ # sudo wrapper that depends on the SSH auth method
1131
1306
  SSH_AUTH_METHOD="${{ env.SSH_AUTH_METHOD }}"
1132
1307
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
1133
1308
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
@@ -1135,7 +1310,7 @@ jobs:
1135
1310
  else
1136
1311
  SUDO() { echo "$PW" | sudo -S "$@"; }
1137
1312
  fi
1138
- echo "πŸ” SSH 인증 방식: ${SSH_AUTH_METHOD}"
1313
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
1139
1314
 
1140
1315
  ISSUE_NUMBER=${{ needs.get-branch-from-issue.outputs.issue_number }}
1141
1316
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -1146,18 +1321,18 @@ jobs:
1146
1321
  TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
1147
1322
 
1148
1323
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1149
- echo "πŸš€ Issue Preview 배포 μ‹œμž‘"
1324
+ m wf_preview.r_deploy_head_issue
1150
1325
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1151
- echo "πŸ“¦ ν”„λ‘œμ νŠΈ: ${PROJECT_NAME}"
1152
- echo "πŸ”’ Issue 번호: #${ISSUE_NUMBER}"
1153
- echo "πŸ“› μ»¨ν…Œμ΄λ„ˆ: ${CONTAINER_NAME}"
1154
- echo "🌐 도메인: ${DOMAIN}"
1326
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
1327
+ m wf_preview.r_num_issue issue_number="${ISSUE_NUMBER}"
1328
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
1329
+ m wf_preview.r_domain domain="${DOMAIN}"
1155
1330
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1156
1331
 
1157
- echo "πŸ“₯ Docker 이미지 Pull 쀑..."
1332
+ m wf_preview.r_image_pulling
1158
1333
  SUDO docker pull "${IMAGE}"
1159
1334
 
1160
- echo "πŸ—‘οΈ κΈ°μ‘΄ μ»¨ν…Œμ΄λ„ˆ 정리 쀑..."
1335
+ m wf_preview.r_existing_cleanup
1161
1336
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
1162
1337
 
1163
1338
  VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
@@ -1165,7 +1340,7 @@ jobs:
1165
1340
  VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
1166
1341
  fi
1167
1342
 
1168
- echo "🐳 μƒˆ μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ 쀑..."
1343
+ m wf_preview.r_container_starting
1169
1344
  SUDO docker run -d \
1170
1345
  --name "${CONTAINER_NAME}" \
1171
1346
  --network "${TRAEFIK_NETWORK}" \
@@ -1178,11 +1353,11 @@ jobs:
1178
1353
  $VOLUME_OPTS \
1179
1354
  "${IMAGE}"
1180
1355
 
1181
- # Health Check (μ΅œλŒ€ 120초 λŒ€κΈ°) - ν•˜μ΄λΈŒλ¦¬λ“œ 방식
1182
- # 1. HEALTH_CHECK_PATHκ°€ μ„€μ •λ˜μ–΄ 있으면 HTTP 체크 μ‹œλ„
1183
- # 2. 폴백: 둜그 νŒ¨ν„΄ λ§€μΉ­ (HEALTH_CHECK_LOG_PATTERN)
1356
+ # Health check (waits up to 120s) - hybrid approach
1357
+ # 1. Try the HTTP check when HEALTH_CHECK_PATH is set
1358
+ # 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
1184
1359
  echo ""
1185
- echo "⏳ Health Check μ‹œμž‘ (μ΅œλŒ€ 120초 λŒ€κΈ°)..."
1360
+ m wf_preview.r_hc_start
1186
1361
  HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
1187
1362
  LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
1188
1363
  MAX_RETRIES=24
@@ -1194,13 +1369,13 @@ jobs:
1194
1369
  sleep 5
1195
1370
  RETRY_COUNT=$((RETRY_COUNT + 1))
1196
1371
 
1197
- # 1. μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인
1372
+ # 1. Check the container state
1198
1373
  STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
1199
1374
 
1200
1375
  if [ "$STATUS" = "exited" ]; then
1201
- echo "❌ μ»¨ν…Œμ΄λ„ˆ 비정상 μ’…λ£Œ!"
1376
+ m wf_preview.r_container_exited
1202
1377
  echo ""
1203
- echo "πŸ“‹ μ»¨ν…Œμ΄λ„ˆ 둜그 (졜근 100쀄):"
1378
+ m wf_preview.r_container_logs
1204
1379
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1205
1380
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1206
1381
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1208,25 +1383,25 @@ jobs:
1208
1383
  fi
1209
1384
 
1210
1385
  if [ "$STATUS" = "running" ]; then
1211
- # 2. HTTP Health Check μ‹œλ„ (HEALTH_CHECK_PATHκ°€ μ„€μ •λœ κ²½μš°μ—λ§Œ)
1386
+ # 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
1212
1387
  if [ -n "$HEALTH_PATH" ]; then
1213
1388
  HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" wget -qO- "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
1214
1389
 
1215
- # Spring Actuator 응닡 확인 ({"status":"UP"}) λ˜λŠ” 일반 HTTP 응닡
1390
+ # Accept a Spring Actuator response ({"status":"UP"}) or any HTTP response
1216
1391
  if echo "$HEALTH" | grep -q '"status":"UP"' || [ -n "$HEALTH" ]; then
1217
- echo "βœ… 정상 기동 확인! (HTTP 응닡: ${HEALTH_PATH})"
1392
+ m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
1218
1393
  HEALTH_CHECK_PASSED=true
1219
1394
  HEALTH_CHECK_METHOD="HTTP"
1220
1395
  break
1221
1396
  fi
1222
1397
  fi
1223
1398
 
1224
- # 3. HTTP 응닡 μ—†μœΌλ©΄ 둜그 νŒ¨ν„΄ 맀칭으둜 폴백
1399
+ # 3. Without an HTTP response, fall back to log pattern matching
1225
1400
  if [ -n "$LOG_PATTERN" ]; then
1226
1401
  STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
1227
1402
 
1228
1403
  if [ -n "$STARTED" ]; then
1229
- echo "βœ… 정상 기동 확인! (둜그 νŒ¨ν„΄)"
1404
+ m wf_preview.r_hc_ok_log
1230
1405
  echo " $STARTED"
1231
1406
  HEALTH_CHECK_PASSED=true
1232
1407
  HEALTH_CHECK_METHOD="Log"
@@ -1235,14 +1410,14 @@ jobs:
1235
1410
  fi
1236
1411
  fi
1237
1412
 
1238
- echo "⏳ λŒ€κΈ° 쀑... ($RETRY_COUNT/$MAX_RETRIES) - μƒνƒœ: $STATUS"
1413
+ m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
1239
1414
  done
1240
1415
 
1241
1416
  if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
1242
1417
  echo ""
1243
- echo "❌ Health Check νƒ€μž„μ•„μ›ƒ (120초)"
1418
+ m wf_preview.r_hc_timeout
1244
1419
  echo ""
1245
- echo "πŸ“‹ μ»¨ν…Œμ΄λ„ˆ 둜그 (졜근 100쀄):"
1420
+ m wf_preview.r_container_logs
1246
1421
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1247
1422
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1248
1423
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1250,14 +1425,17 @@ jobs:
1250
1425
  fi
1251
1426
 
1252
1427
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1253
- echo "βœ… 배포 및 Health Check μ™„λ£Œ! (방식: ${HEALTH_CHECK_METHOD})"
1428
+ m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
1254
1429
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1255
1430
 
1256
- - name: 배포 μ™„λ£Œ μ½”λ©˜νŠΈ
1431
+ - name: Deploy complete comment
1257
1432
  if: steps.check_branch.outputs.exists == 'true'
1258
1433
  uses: actions/github-script@v9
1259
1434
  with:
1260
1435
  script: |
1436
+ const M = JSON.parse(process.env.PAW_MSG);
1437
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1438
+
1261
1439
  const commentId = ${{ steps.progress.outputs.comment_id }};
1262
1440
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1263
1441
  const buildDuration = '${{ steps.docker_progress.outputs.build_duration }}';
@@ -1267,7 +1445,7 @@ jobs:
1267
1445
  const deployElapsed = now - deployStart;
1268
1446
  const minutes = Math.floor(deployElapsed / 60000);
1269
1447
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
1270
- const deployDuration = minutes > 0 ? `${minutes}λΆ„ ${seconds}초` : `${seconds}초`;
1448
+ const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1271
1449
 
1272
1450
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1273
1451
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -1278,42 +1456,42 @@ jobs:
1278
1456
  const previewUrl = `http://${domain}:${previewPort}`;
1279
1457
  const sha = '${{ steps.issue.outputs.sha }}';
1280
1458
 
1281
- // Preview ν™˜κ²½ ν…Œμ΄λΈ” ꡬ성 (API_DOCS_PATHκ°€ μžˆμ„ λ•Œλ§Œ API Docs ν–‰ μΆ”κ°€)
1459
+ // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
1282
1460
  const envRows = [
1283
1461
  `| **Preview URL** | ${previewUrl} |`,
1284
1462
  ];
1285
1463
  if (apiDocsPath) {
1286
1464
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
1287
1465
  }
1288
- envRows.push(`| **μ»¨ν…Œμ΄λ„ˆ** | \`${projectName}-pr-${issueNumber}\` |`);
1289
- envRows.push(`| **브랜치** | \`${branchName}\` |`);
1290
- envRows.push(`| **컀밋** | \`${sha}\` |`);
1466
+ envRows.push(m('wf_preview.env_container_issue', {projectName, issueNumber}));
1467
+ envRows.push(m('wf_preview.env_branch', {branchName}));
1468
+ envRows.push(m('wf_preview.env_commit', {sha}));
1291
1469
 
1292
1470
  const body = [
1293
- '## βœ… Issue Preview 배포 μ™„λ£Œ!',
1471
+ m('wf_preview.title_issue_deployed'),
1294
1472
  '',
1295
- `**브랜치**: \`${branchName}\``,
1473
+ m('wf_preview.line_branch', {branchName}),
1296
1474
  '',
1297
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
1475
+ m('wf_preview.tbl_steps'),
1298
1476
  '|------|------|----------|',
1299
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
1300
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
1301
- `| πŸš€ μ„œλ²„ 배포 & Health Check | βœ… μ™„λ£Œ | ${deployDuration} |`,
1477
+ m('wf_preview.row_build_done', {buildDuration}),
1478
+ m('wf_preview.row_image_done', {dockerDuration}),
1479
+ m('wf_preview.row_deploy_done', {deployDuration}),
1302
1480
  '',
1303
- '### 🌐 Preview ν™˜κ²½',
1304
- '| ν•­λͺ© | κ°’ |',
1481
+ m('wf_preview.sec_env'),
1482
+ m('wf_preview.tbl_items'),
1305
1483
  '|------|-----|',
1306
1484
  ...envRows,
1307
1485
  '',
1308
- '### πŸ“‹ λͺ…λ Ήμ–΄',
1309
- '| λͺ…λ Ήμ–΄ | μ„€λͺ… |',
1486
+ m('wf_preview.sec_commands'),
1487
+ m('wf_preview.tbl_commands'),
1310
1488
  '|--------|------|',
1311
- '| `/wizard server build` | μ΅œμ‹  μ»€λ°‹μœΌλ‘œ 재배포 |',
1312
- '| `/wizard server destroy` | Preview ν™˜κ²½ μ‚­μ œ |',
1313
- '| `/wizard server status` | ν˜„μž¬ μƒνƒœ 확인 |',
1489
+ m('wf_preview.cmd_build'),
1490
+ m('wf_preview.cmd_destroy'),
1491
+ m('wf_preview.cmd_status'),
1314
1492
  '',
1315
1493
  '<details>',
1316
- '<summary>πŸ”§ κ³ κΈ‰ λͺ…λ Ήμ–΄ (λ‹€λ₯Έ Issue/PRμ—μ„œ μ œμ–΄)</summary>',
1494
+ m('wf_preview.cmd_advanced'),
1317
1495
  '',
1318
1496
  '```',
1319
1497
  `/wizard server build ${branchName}`,
@@ -1323,7 +1501,7 @@ jobs:
1323
1501
  '</details>',
1324
1502
  '',
1325
1503
  '---',
1326
- '*πŸ€– 이 λŒ“κΈ€μ€ Issue Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
1504
+ m('wf_preview.footer_issue')
1327
1505
  ].join('\n');
1328
1506
 
1329
1507
  await github.rest.issues.updateComment({
@@ -1333,11 +1511,14 @@ jobs:
1333
1511
  body: body
1334
1512
  });
1335
1513
 
1336
- - name: λΉŒλ“œ/배포 μ‹€νŒ¨ μ‹œ μ—λŸ¬ μ½”λ©˜νŠΈ
1514
+ - name: Build/deploy failure comment
1337
1515
  if: failure() && steps.check_branch.outputs.exists == 'true'
1338
1516
  uses: actions/github-script@v9
1339
1517
  with:
1340
1518
  script: |
1519
+ const M = JSON.parse(process.env.PAW_MSG);
1520
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1521
+
1341
1522
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1342
1523
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1343
1524
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
@@ -1351,52 +1532,52 @@ jobs:
1351
1532
  let buildTime = '-', dockerTime = '-';
1352
1533
 
1353
1534
  if (!commentId) {
1354
- buildStatus = '⚠️ μ΄ˆκΈ°ν™” μ‹€νŒ¨';
1535
+ buildStatus = m('wf_preview.status_init_failed');
1355
1536
  dockerStatus = '-';
1356
1537
  deployStatus = '-';
1357
1538
  } else if (!buildDuration || buildDuration === '') {
1358
- buildStatus = '❌ μ‹€νŒ¨';
1359
- dockerStatus = '⏸️ λŒ€κΈ°';
1360
- deployStatus = '⏸️ λŒ€κΈ°';
1539
+ buildStatus = m('wf_preview.status_failed');
1540
+ dockerStatus = m('wf_preview.status_waiting');
1541
+ deployStatus = m('wf_preview.status_waiting');
1361
1542
  } else if (!dockerDuration || dockerDuration === '') {
1362
- buildStatus = 'βœ… μ™„λ£Œ';
1363
- dockerStatus = '❌ μ‹€νŒ¨';
1364
- deployStatus = '⏸️ λŒ€κΈ°';
1543
+ buildStatus = m('wf_preview.status_done');
1544
+ dockerStatus = m('wf_preview.status_failed');
1545
+ deployStatus = m('wf_preview.status_waiting');
1365
1546
  buildTime = buildDuration;
1366
1547
  } else {
1367
- buildStatus = 'βœ… μ™„λ£Œ';
1368
- dockerStatus = 'βœ… μ™„λ£Œ';
1369
- deployStatus = '❌ μ‹€νŒ¨';
1548
+ buildStatus = m('wf_preview.status_done');
1549
+ dockerStatus = m('wf_preview.status_done');
1550
+ deployStatus = m('wf_preview.status_failed');
1370
1551
  buildTime = buildDuration;
1371
1552
  dockerTime = dockerDuration;
1372
1553
  }
1373
1554
 
1374
1555
  const body = [
1375
- '## ❌ Issue Preview 배포 μ‹€νŒ¨!',
1556
+ m('wf_preview.title_issue_failed'),
1376
1557
  '',
1377
- `**브랜치**: \`${branchName}\``,
1558
+ m('wf_preview.line_branch', {branchName}),
1378
1559
  '',
1379
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
1560
+ m('wf_preview.tbl_steps'),
1380
1561
  '|------|------|----------|',
1381
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ${buildStatus} | ${buildTime} |`,
1382
- `| 🐳 Docker 이미지 생성 | ${dockerStatus} | ${dockerTime} |`,
1383
- `| πŸš€ μ„œλ²„ 배포 & Health Check | ${deployStatus} | - |`,
1562
+ m('wf_preview.row_build_status', {buildStatus, buildTime}),
1563
+ m('wf_preview.row_image_status', {dockerStatus, dockerTime}),
1564
+ m('wf_preview.row_deploy_status', {deployStatus}),
1384
1565
  '',
1385
- `**[πŸ“‹ λΉŒλ“œ/배포 둜그 확인](${runUrl})**`,
1566
+ m('wf_preview.link_build_logs', {runUrl}),
1386
1567
  '',
1387
- '### πŸ” κ°€λŠ₯ν•œ 원인',
1388
- '- ν•„μˆ˜ Secret(DOCKERHUB_*, SERVER_*) 미등둝 λ˜λŠ” Dockerfile μ—†μŒ β†’ 둜그의 "배포 사전 점검" 단계 확인',
1389
- '- Gradle λΉŒλ“œ μ‹€νŒ¨',
1390
- '- Docker 이미지 λΉŒλ“œ μ‹€νŒ¨',
1391
- '- μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ μ‹€νŒ¨ (Spring Boot 기동 였λ₯˜)',
1392
- '- Health Check νƒ€μž„μ•„μ›ƒ (120초 λ‚΄ 기동 μ™„λ£Œ μ•ˆλ¨)',
1568
+ m('wf_preview.sec_causes'),
1569
+ m('wf_preview.hint_secrets'),
1570
+ m('wf_preview.hint_gradle'),
1571
+ m('wf_preview.hint_docker'),
1572
+ m('wf_preview.hint_start_spring'),
1573
+ m('wf_preview.hint_health_timeout'),
1393
1574
  '',
1394
- '### πŸ’‘ λ‹€μŒ 단계',
1395
- '1. μœ„ λ§ν¬μ—μ„œ λΉŒλ“œ/배포 둜그λ₯Ό ν™•μΈν•˜μ„Έμš”',
1396
- '2. 문제λ₯Ό μˆ˜μ •ν•œ ν›„ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”: `/wizard server build`',
1575
+ m('wf_preview.sec_next'),
1576
+ m('wf_preview.next_logs'),
1577
+ m('wf_preview.next_retry'),
1397
1578
  '',
1398
1579
  '---',
1399
- '*πŸ€– 이 λŒ“κΈ€μ€ Issue Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
1580
+ m('wf_preview.footer_issue')
1400
1581
  ].join('\n');
1401
1582
 
1402
1583
  if (commentId) {
@@ -1415,17 +1596,61 @@ jobs:
1415
1596
  });
1416
1597
  }
1417
1598
 
1418
- # Job 3: Preview μ‚­μ œ (PR/Issue λ‹«νž˜ λ˜λŠ” destroy λͺ…λ Ή)
1599
+ # Job 3: destroy the preview (PR/issue closed or destroy command)
1419
1600
  destroy-preview:
1420
- name: Preview μ‚­μ œ
1421
- # needs μ˜μ‘΄μ„± 제거: PR/Issue λ‹«νž˜ μ΄λ²€νŠΈμ—μ„œλ„ λ…λ¦½μ μœΌλ‘œ μ‹€ν–‰λ˜λ„λ‘ 함
1601
+ name: Destroy preview
1602
+ # No needs dependency: lets this run on its own for PR/issue closed events
1422
1603
  if: |
1423
1604
  (github.event_name == 'pull_request' && github.event.action == 'closed') ||
1424
1605
  (github.event_name == 'issues' && github.event.action == 'closed') ||
1425
1606
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server destroy'))
1426
1607
  runs-on: ubuntu-latest
1427
1608
  steps:
1428
- - name: PR/Issue 번호 κ°€μ Έμ˜€κΈ°
1609
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1610
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1611
+ - name: Checkout messages
1612
+ uses: actions/checkout@v7
1613
+ with:
1614
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1615
+ sparse-checkout: |
1616
+ .github/scripts/messages.py
1617
+ version.yml
1618
+ sparse-checkout-cone-mode: false
1619
+
1620
+ - name: Load messages
1621
+ run: |
1622
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1623
+ mkdir -p "$RUNNER_TEMP/paw"
1624
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1625
+ {
1626
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1627
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1628
+ } >> "$GITHUB_ENV"
1629
+ # The deploy script runs on the remote server, where messages.py does not exist:
1630
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1631
+ python3 - >> "$GITHUB_ENV" <<'PY'
1632
+ import json, os, shlex, subprocess
1633
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1634
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1635
+ head = ["m() {", ' case "$1" in']
1636
+ for key, text in msgs.items():
1637
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1638
+ tail = r''' *) _t="$1" ;;
1639
+ esac
1640
+ shift
1641
+ while [ "$#" -gt 0 ]; do
1642
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1643
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1644
+ shift
1645
+ done
1646
+ printf '%s\n' "$_t"
1647
+ }'''
1648
+ print("PAW_REMOTE<<PAW_EOF")
1649
+ print("\n".join(head) + "\n" + tail)
1650
+ print("PAW_EOF")
1651
+ PY
1652
+
1653
+ - name: Get PR/issue number
1429
1654
  id: pr_number
1430
1655
  run: |
1431
1656
  if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
@@ -1439,7 +1664,7 @@ jobs:
1439
1664
  echo "type=issue_closed" >> $GITHUB_OUTPUT
1440
1665
  fi
1441
1666
 
1442
- - name: μ»¨ν…Œμ΄λ„ˆ & 이미지 μ‚­μ œ
1667
+ - name: Remove container & image
1443
1668
  uses: appleboy/ssh-action@v1.0.3
1444
1669
  with:
1445
1670
  host: ${{ secrets.SERVER_HOST }}
@@ -1448,10 +1673,12 @@ jobs:
1448
1673
  key: ${{ secrets.SSH_KEY }}
1449
1674
  port: ${{ env.SSH_PORT }}
1450
1675
  script: |
1676
+ ${{ env.PAW_REMOTE }}
1677
+
1451
1678
  export PATH=$PATH:/usr/local/bin
1452
1679
  export PW="${{ secrets.SERVER_PASSWORD }}"
1453
1680
 
1454
- # SSH 인증 방식에 λ”°λ₯Έ sudo 좔상화
1681
+ # sudo wrapper that depends on the SSH auth method
1455
1682
  SSH_AUTH_METHOD="${{ env.SSH_AUTH_METHOD }}"
1456
1683
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
1457
1684
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
@@ -1459,7 +1686,7 @@ jobs:
1459
1686
  else
1460
1687
  SUDO() { echo "$PW" | sudo -S "$@"; }
1461
1688
  fi
1462
- echo "πŸ” SSH 인증 방식: ${SSH_AUTH_METHOD}"
1689
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
1463
1690
 
1464
1691
  PR_NUMBER=${{ steps.pr_number.outputs.number }}
1465
1692
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -1467,21 +1694,21 @@ jobs:
1467
1694
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
1468
1695
 
1469
1696
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1470
- echo "πŸ—‘οΈ Preview μ‚­μ œ"
1697
+ m wf_preview.r_destroy_head
1471
1698
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1472
- echo "πŸ“¦ ν”„λ‘œμ νŠΈ: ${PROJECT_NAME}"
1473
- echo "πŸ”’ 번호: #${PR_NUMBER}"
1699
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
1700
+ m wf_preview.r_num_generic pr_number="${PR_NUMBER}"
1474
1701
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1475
1702
 
1476
- echo "🐳 μ»¨ν…Œμ΄λ„ˆ μ‚­μ œ 쀑..."
1703
+ m wf_preview.r_container_removing
1477
1704
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
1478
1705
 
1479
- echo "πŸ–ΌοΈ 이미지 μ‚­μ œ 쀑..."
1706
+ m wf_preview.r_image_removing
1480
1707
  SUDO docker rmi "${IMAGE}" 2>/dev/null || true
1481
1708
 
1482
- echo "βœ… μ‚­μ œ μ™„λ£Œ!"
1709
+ m wf_preview.r_removed
1483
1710
 
1484
- - name: μ‚­μ œ μ™„λ£Œ μ½”λ©˜νŠΈ
1711
+ - name: Destroy complete comment
1485
1712
  if: |
1486
1713
  github.event_name == 'issue_comment' ||
1487
1714
  (github.event_name == 'pull_request' && github.event.action == 'closed') ||
@@ -1489,21 +1716,24 @@ jobs:
1489
1716
  uses: actions/github-script@v9
1490
1717
  with:
1491
1718
  script: |
1719
+ const M = JSON.parse(process.env.PAW_MSG);
1720
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1721
+
1492
1722
  const number = ${{ steps.pr_number.outputs.number }};
1493
1723
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1494
1724
 
1495
1725
  const body = [
1496
- '## πŸ—‘οΈ Preview ν™˜κ²½ μ‚­μ œ μ™„λ£Œ!',
1726
+ m('wf_preview.title_destroyed'),
1497
1727
  '',
1498
- '| ν•­λͺ© | κ°’ |',
1728
+ m('wf_preview.tbl_items'),
1499
1729
  '|------|-----|',
1500
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${projectName}-pr-${number}\` |`,
1501
- '| **μƒνƒœ** | μ‚­μ œλ¨ |',
1730
+ m('wf_preview.env_container_num', {projectName, number}),
1731
+ m('wf_preview.env_status_deleted'),
1502
1732
  '',
1503
- 'λ‹€μ‹œ λ°°ν¬ν•˜λ €λ©΄: `/wizard server build`',
1733
+ m('wf_preview.redeploy_hint'),
1504
1734
  '',
1505
1735
  '---',
1506
- '*πŸ€– 이 λŒ“κΈ€μ€ PR/Issue Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
1736
+ m('wf_preview.footer_pr_issue')
1507
1737
  ].join('\n');
1508
1738
 
1509
1739
  await github.rest.issues.createComment({
@@ -1513,14 +1743,58 @@ jobs:
1513
1743
  body: body
1514
1744
  });
1515
1745
 
1516
- # Job 4: μƒνƒœ 확인 (PR/Issue λͺ¨λ‘ 지원)
1746
+ # Job 4: status check (PR and issue)
1517
1747
  check-status:
1518
- name: Preview μƒνƒœ 확인
1748
+ name: Preview status
1519
1749
  needs: check-command
1520
1750
  if: needs.check-command.outputs.is_valid == 'true' && needs.check-command.outputs.command == 'status'
1521
1751
  runs-on: ubuntu-latest
1522
1752
  steps:
1523
- - name: μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인
1753
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1754
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1755
+ - name: Checkout messages
1756
+ uses: actions/checkout@v7
1757
+ with:
1758
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1759
+ sparse-checkout: |
1760
+ .github/scripts/messages.py
1761
+ version.yml
1762
+ sparse-checkout-cone-mode: false
1763
+
1764
+ - name: Load messages
1765
+ run: |
1766
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1767
+ mkdir -p "$RUNNER_TEMP/paw"
1768
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1769
+ {
1770
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1771
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1772
+ } >> "$GITHUB_ENV"
1773
+ # The deploy script runs on the remote server, where messages.py does not exist:
1774
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1775
+ python3 - >> "$GITHUB_ENV" <<'PY'
1776
+ import json, os, shlex, subprocess
1777
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1778
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1779
+ head = ["m() {", ' case "$1" in']
1780
+ for key, text in msgs.items():
1781
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1782
+ tail = r''' *) _t="$1" ;;
1783
+ esac
1784
+ shift
1785
+ while [ "$#" -gt 0 ]; do
1786
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1787
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1788
+ shift
1789
+ done
1790
+ printf '%s\n' "$_t"
1791
+ }'''
1792
+ print("PAW_REMOTE<<PAW_EOF")
1793
+ print("\n".join(head) + "\n" + tail)
1794
+ print("PAW_EOF")
1795
+ PY
1796
+
1797
+ - name: Check container status
1524
1798
  id: status
1525
1799
  uses: appleboy/ssh-action@v1.0.3
1526
1800
  with:
@@ -1530,6 +1804,8 @@ jobs:
1530
1804
  key: ${{ secrets.SSH_KEY }}
1531
1805
  port: ${{ env.SSH_PORT }}
1532
1806
  script: |
1807
+ ${{ env.PAW_REMOTE }}
1808
+
1533
1809
  export PATH=$PATH:/usr/local/bin
1534
1810
 
1535
1811
  NUMBER=${{ github.event.issue.number }}
@@ -1537,19 +1813,19 @@ jobs:
1537
1813
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1538
1814
 
1539
1815
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1540
- echo "πŸ” Preview μƒνƒœ 확인"
1816
+ m wf_preview.r_status_head
1541
1817
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1542
1818
 
1543
1819
  if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
1544
1820
  echo "STATUS=running"
1545
- echo "βœ… μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ 쀑"
1821
+ m wf_preview.r_container_running
1546
1822
  docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
1547
1823
  else
1548
1824
  echo "STATUS=not_found"
1549
- echo "❌ μ»¨ν…Œμ΄λ„ˆ μ—†μŒ"
1825
+ m wf_preview.r_container_absent
1550
1826
  fi
1551
1827
 
1552
- - name: μƒνƒœ μ½”λ©˜νŠΈ (μ‹€ν–‰ 쀑)
1828
+ - name: Status check (running)
1553
1829
  uses: appleboy/ssh-action@v1.0.3
1554
1830
  id: check_running
1555
1831
  continue-on-error: true
@@ -1567,10 +1843,13 @@ jobs:
1567
1843
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1568
1844
  docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
1569
1845
 
1570
- - name: μƒνƒœ μ½”λ©˜νŠΈ μž‘μ„±
1846
+ - name: Post status comment
1571
1847
  uses: actions/github-script@v9
1572
1848
  with:
1573
1849
  script: |
1850
+ const M = JSON.parse(process.env.PAW_MSG);
1851
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1852
+
1574
1853
  const number = context.issue.number;
1575
1854
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1576
1855
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -1584,36 +1863,36 @@ jobs:
1584
1863
  let body;
1585
1864
  if (isRunning) {
1586
1865
  body = [
1587
- '## βœ… Preview ν™˜κ²½ μ‹€ν–‰ 쀑',
1866
+ m('wf_preview.title_running'),
1588
1867
  '',
1589
- '| ν•­λͺ© | κ°’ |',
1868
+ m('wf_preview.tbl_items'),
1590
1869
  '|------|-----|',
1591
1870
  `| **Preview URL** | ${previewUrl} |`,
1592
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${projectName}-pr-${number}\` |`,
1593
- '| **μƒνƒœ** | 🟒 Running |',
1871
+ m('wf_preview.env_container_num', {projectName, number}),
1872
+ m('wf_preview.env_status_running'),
1594
1873
  '',
1595
- '### πŸ“‹ λͺ…λ Ήμ–΄',
1596
- '| λͺ…λ Ήμ–΄ | μ„€λͺ… |',
1874
+ m('wf_preview.sec_commands'),
1875
+ m('wf_preview.tbl_commands'),
1597
1876
  '|--------|------|',
1598
- '| `/wizard server build` | μ΅œμ‹  μ»€λ°‹μœΌλ‘œ 재배포 |',
1599
- '| `/wizard server destroy` | Preview ν™˜κ²½ μ‚­μ œ |',
1877
+ m('wf_preview.cmd_build'),
1878
+ m('wf_preview.cmd_destroy'),
1600
1879
  '',
1601
1880
  '---',
1602
- `*πŸ€– 이 λŒ“κΈ€μ€ ${contextType} Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*`
1881
+ m('wf_preview.footer_context', {contextType})
1603
1882
  ].join('\n');
1604
1883
  } else {
1605
1884
  body = [
1606
- '## ❌ Preview ν™˜κ²½ μ—†μŒ',
1885
+ m('wf_preview.title_absent'),
1607
1886
  '',
1608
- '| ν•­λͺ© | κ°’ |',
1887
+ m('wf_preview.tbl_items'),
1609
1888
  '|------|-----|',
1610
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${projectName}-pr-${number}\` |`,
1611
- '| **μƒνƒœ** | πŸ”΄ Not Found |',
1889
+ m('wf_preview.env_container_num', {projectName, number}),
1890
+ m('wf_preview.env_status_absent'),
1612
1891
  '',
1613
- 'λ°°ν¬ν•˜λ €λ©΄: `/wizard server build`',
1892
+ m('wf_preview.deploy_hint'),
1614
1893
  '',
1615
1894
  '---',
1616
- `*πŸ€– 이 λŒ“κΈ€μ€ ${contextType} Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*`
1895
+ m('wf_preview.footer_context', {contextType})
1617
1896
  ].join('\n');
1618
1897
  }
1619
1898
 
@@ -1625,11 +1904,11 @@ jobs:
1625
1904
  });
1626
1905
 
1627
1906
  # Custom Branch Preview Jobs
1628
- # νŠΉμ • 브랜치λ₯Ό μ§€μ •ν•˜μ—¬ λΉŒλ“œ/μ‚­μ œ/μƒνƒœ ν™•μΈν•˜λŠ” κΈ°λŠ₯
1629
- # λͺ…λ Ήμ–΄: /wizard server build <브랜치λͺ…>
1907
+ # Build, destroy or check status for a specific branch
1908
+ # Command: /wizard server build <branch>
1630
1909
 
1631
1910
  build-preview-custom-branch:
1632
- name: Preview λΉŒλ“œ & 배포 (Custom Branch)
1911
+ name: Preview build & deploy (Custom Branch)
1633
1912
  needs: check-command
1634
1913
  if: |
1635
1914
  needs.check-command.outputs.is_valid == 'true' &&
@@ -1637,7 +1916,51 @@ jobs:
1637
1916
  needs.check-command.outputs.is_custom_branch == 'true'
1638
1917
  runs-on: ubuntu-latest
1639
1918
  steps:
1640
- - name: 브랜치λͺ…μ—μ„œ Issue 번호 μΆ”μΆœ
1919
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1920
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1921
+ - name: Checkout messages
1922
+ uses: actions/checkout@v7
1923
+ with:
1924
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1925
+ sparse-checkout: |
1926
+ .github/scripts/messages.py
1927
+ version.yml
1928
+ sparse-checkout-cone-mode: false
1929
+
1930
+ - name: Load messages
1931
+ run: |
1932
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1933
+ mkdir -p "$RUNNER_TEMP/paw"
1934
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1935
+ {
1936
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1937
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1938
+ } >> "$GITHUB_ENV"
1939
+ # The deploy script runs on the remote server, where messages.py does not exist:
1940
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1941
+ python3 - >> "$GITHUB_ENV" <<'PY'
1942
+ import json, os, shlex, subprocess
1943
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1944
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1945
+ head = ["m() {", ' case "$1" in']
1946
+ for key, text in msgs.items():
1947
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1948
+ tail = r''' *) _t="$1" ;;
1949
+ esac
1950
+ shift
1951
+ while [ "$#" -gt 0 ]; do
1952
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1953
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1954
+ shift
1955
+ done
1956
+ printf '%s\n' "$_t"
1957
+ }'''
1958
+ print("PAW_REMOTE<<PAW_EOF")
1959
+ print("\n".join(head) + "\n" + tail)
1960
+ print("PAW_EOF")
1961
+ PY
1962
+
1963
+ - name: Extract issue number from branch name
1641
1964
  id: extract
1642
1965
  uses: actions/github-script@v9
1643
1966
  with:
@@ -1650,7 +1973,7 @@ jobs:
1650
1973
  core.setOutput('use_hash', 'false');
1651
1974
  core.setOutput('container_suffix', `pr-${issueMatch[1]}`);
1652
1975
  } else {
1653
- // ν•΄μ‹œ 생성
1976
+ // Generate a hash
1654
1977
  let hash = 0;
1655
1978
  for (let i = 0; i < branchName.length; i++) {
1656
1979
  const char = branchName.charCodeAt(i);
@@ -1664,7 +1987,7 @@ jobs:
1664
1987
 
1665
1988
  core.setOutput('branch_name', branchName);
1666
1989
 
1667
- - name: 브랜치 쑴재 μ—¬λΆ€ 확인
1990
+ - name: Check branch exists
1668
1991
  id: check_branch
1669
1992
  uses: actions/github-script@v9
1670
1993
  with:
@@ -1685,27 +2008,30 @@ jobs:
1685
2008
  }
1686
2009
  }
1687
2010
 
1688
- - name: 브랜치 μ—†μŒ μ—λŸ¬ μ½”λ©˜νŠΈ
2011
+ - name: Branch not found error comment
1689
2012
  if: steps.check_branch.outputs.exists == 'false'
1690
2013
  uses: actions/github-script@v9
1691
2014
  with:
1692
2015
  script: |
2016
+ const M = JSON.parse(process.env.PAW_MSG);
2017
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2018
+
1693
2019
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1694
2020
 
1695
2021
  const body = [
1696
- '## ❌ 브랜치λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€',
2022
+ m('wf_preview.title_branch_missing'),
1697
2023
  '',
1698
- '| ν•­λͺ© | κ°’ |',
2024
+ m('wf_preview.tbl_items'),
1699
2025
  '|------|-----|',
1700
- `| **브랜치** | \`${branchName}\` |`,
2026
+ m('wf_preview.env_branch', {branchName}),
1701
2027
  '',
1702
- '### πŸ’‘ 확인 사항',
1703
- '1. 브랜치λͺ…이 μ •ν™•ν•œμ§€ ν™•μΈν•˜μ„Έμš”',
1704
- '2. λΈŒλžœμΉ˜κ°€ pushλ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
1705
- '3. λΈŒλžœμΉ˜κ°€ μ‚­μ œλ˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•˜μ„Έμš”',
2028
+ m('wf_preview.sec_check'),
2029
+ m('wf_preview.chk_name_1'),
2030
+ m('wf_preview.chk_pushed_2'),
2031
+ m('wf_preview.chk_deleted_3'),
1706
2032
  '',
1707
2033
  '---',
1708
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2034
+ m('wf_preview.footer_generic')
1709
2035
  ].join('\n');
1710
2036
 
1711
2037
  await github.rest.issues.createComment({
@@ -1715,16 +2041,16 @@ jobs:
1715
2041
  body: body
1716
2042
  });
1717
2043
 
1718
- core.setFailed('브랜치λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: ' + branchName);
2044
+ core.setFailed(m('wf_preview.fail_branch_missing_named', {branchName}));
1719
2045
 
1720
- - name: 체크아웃
2046
+ - name: Checkout
1721
2047
  if: steps.check_branch.outputs.exists == 'true'
1722
2048
  uses: actions/checkout@v7
1723
2049
  with:
1724
2050
  ref: ${{ steps.extract.outputs.branch_name }}
1725
2051
 
1726
- # ν•„μˆ˜ SecretΒ·Dockerfile이 μ—†μœΌλ©΄ docker 둜그인 같은 μ„œλ“œνŒŒν‹° 였λ₯˜ λŒ€μ‹  λΉ μ§„ ν•­λͺ©μ„ μ•Œλ € μ£Όκ³  λ©ˆμΆ˜λ‹€
1727
- - name: 배포 사전 점검 (SecretΒ·Dockerfile)
2052
+ # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
2053
+ - name: Deploy pre-check (Secrets and Dockerfile)
1728
2054
  if: steps.check_branch.outputs.exists == 'true'
1729
2055
  working-directory: ${{ env.PROJECT_PATH }}
1730
2056
  env:
@@ -1736,11 +2062,13 @@ jobs:
1736
2062
  SSH_KEY: ${{ secrets.SSH_KEY }}
1737
2063
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
1738
2064
  run: |
2065
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
2066
+
1739
2067
  MISSING=""
1740
2068
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
1741
2069
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
1742
2070
  done
1743
- # SSH_AUTH_METHOD=keyλ©΄ SSH_KEY, μ•„λ‹ˆλ©΄ SERVER_PASSWORDκ°€ ν•„μš”ν•˜λ‹€
2071
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
1744
2072
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
1745
2073
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
1746
2074
  else
@@ -1748,48 +2076,51 @@ jobs:
1748
2076
  fi
1749
2077
  FAILED=false
1750
2078
  if [ -n "$MISSING" ]; then
1751
- echo "::error title=ν•„μˆ˜ Secret λˆ„λ½::λ“±λ‘λ˜μ§€ μ•Šμ€ GitHub Secret:$MISSING β€” Settings β†’ Secrets and variables β†’ Actionsμ—μ„œ λ“±λ‘ν•˜μ„Έμš”"
2079
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
1752
2080
  FAILED=true
1753
2081
  fi
1754
2082
  if [ ! -f "$DOCKERFILE" ]; then
1755
- echo "::error title=Dockerfile μ—†μŒ::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 μ—†μŠ΅λ‹ˆλ‹€. 이 μ›Œν¬ν”Œλ‘œμš°λŠ” Docker μ΄λ―Έμ§€λ‘œ λ°°ν¬ν•˜λ―€λ‘œ Dockerfile이 ν•„μš”ν•©λ‹ˆλ‹€ (μ„œλ²„ 배포λ₯Ό ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ 이 μ›Œν¬ν”Œλ‘œμš° νŒŒμΌμ„ μ‚­μ œν•˜μ„Έμš”)"
2083
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
1756
2084
  FAILED=true
1757
2085
  fi
1758
2086
  if [ "$FAILED" = "true" ]; then
1759
2087
  exit 1
1760
2088
  fi
1761
- echo "βœ… 사전 점검 톡과: ν•„μˆ˜ Secretκ³Ό Dockerfile이 λͺ¨λ‘ μžˆμŠ΅λ‹ˆλ‹€"
2089
+ m wf_preview.precheck_ok
1762
2090
 
1763
- - name: μ΅œμ‹  컀밋 정보 κ°€μ Έμ˜€κΈ°
2091
+ - name: Get latest commit info
1764
2092
  if: steps.check_branch.outputs.exists == 'true'
1765
2093
  id: commit
1766
2094
  run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
1767
2095
 
1768
- - name: μ§„ν–‰ 상황 μ½”λ©˜νŠΈ μž‘μ„±
2096
+ - name: Create progress comment
1769
2097
  if: steps.check_branch.outputs.exists == 'true'
1770
2098
  id: progress
1771
2099
  uses: actions/github-script@v9
1772
2100
  with:
1773
2101
  script: |
2102
+ const M = JSON.parse(process.env.PAW_MSG);
2103
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2104
+
1774
2105
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1775
2106
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
1776
2107
  const branchName = '${{ steps.extract.outputs.branch_name }}';
1777
2108
  const sha = '${{ steps.commit.outputs.sha }}';
1778
2109
 
1779
2110
  const body = [
1780
- '## πŸš€ Custom Branch Preview 배포 μ‹œμž‘',
2111
+ m('wf_preview.title_custom_started'),
1781
2112
  '',
1782
- `**브랜치**: \`${branchName}\``,
1783
- `**컀밋**: \`${sha}\``,
2113
+ m('wf_preview.line_branch', {branchName}),
2114
+ m('wf_preview.line_commit', {sha}),
1784
2115
  '',
1785
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
2116
+ m('wf_preview.tbl_steps'),
1786
2117
  '|------|------|----------|',
1787
- '| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ⏳ μ§„ν–‰ 쀑... | - |',
1788
- '| 🐳 Docker 이미지 생성 | ⏸️ λŒ€κΈ° | - |',
1789
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
2118
+ m('wf_preview.row_build_progress'),
2119
+ m('wf_preview.row_image_wait'),
2120
+ m('wf_preview.row_deploy_wait'),
1790
2121
  '',
1791
2122
  '---',
1792
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2123
+ m('wf_preview.footer_generic')
1793
2124
  ].join('\n');
1794
2125
 
1795
2126
  const comment = await github.rest.issues.createComment({
@@ -1801,14 +2132,14 @@ jobs:
1801
2132
 
1802
2133
  core.setOutput('comment_id', comment.data.id);
1803
2134
 
1804
- - name: JDK μ„€μ •
2135
+ - name: Set up JDK
1805
2136
  if: steps.check_branch.outputs.exists == 'true'
1806
2137
  uses: actions/setup-java@v5
1807
2138
  with:
1808
2139
  java-version: ${{ env.JAVA_VERSION }}
1809
2140
  distribution: 'temurin'
1810
2141
 
1811
- - name: Gradle μΊμ‹œ
2142
+ - name: Cache Gradle
1812
2143
  if: steps.check_branch.outputs.exists == 'true'
1813
2144
  uses: actions/cache@v6
1814
2145
  with:
@@ -1819,7 +2150,7 @@ jobs:
1819
2150
  restore-keys: |
1820
2151
  ${{ runner.os }}-gradle-
1821
2152
 
1822
- - name: Gradle λΉŒλ“œ
2153
+ - name: Gradle build
1823
2154
  working-directory: ${{ env.PROJECT_PATH }}
1824
2155
  if: steps.check_branch.outputs.exists == 'true'
1825
2156
  id: build
@@ -1831,12 +2162,15 @@ jobs:
1831
2162
  BUILD_DURATION=$((BUILD_END - BUILD_START))
1832
2163
  echo "duration=${BUILD_DURATION}s" >> $GITHUB_OUTPUT
1833
2164
 
1834
- - name: λΉŒλ“œ μ™„λ£Œ μ§„ν–‰ 상황 μ—…λ°μ΄νŠΈ
2165
+ - name: Update progress (build done)
1835
2166
  if: steps.check_branch.outputs.exists == 'true'
1836
2167
  id: build_progress
1837
2168
  uses: actions/github-script@v9
1838
2169
  with:
1839
2170
  script: |
2171
+ const M = JSON.parse(process.env.PAW_MSG);
2172
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2173
+
1840
2174
  const commentId = ${{ steps.progress.outputs.comment_id }};
1841
2175
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1842
2176
  const branchName = '${{ steps.extract.outputs.branch_name }}';
@@ -1844,19 +2178,19 @@ jobs:
1844
2178
  const buildDuration = '${{ steps.build.outputs.duration }}';
1845
2179
 
1846
2180
  const body = [
1847
- '## πŸš€ Custom Branch Preview 배포 μ‹œμž‘',
2181
+ m('wf_preview.title_custom_started'),
1848
2182
  '',
1849
- `**브랜치**: \`${branchName}\``,
1850
- `**컀밋**: \`${sha}\``,
2183
+ m('wf_preview.line_branch', {branchName}),
2184
+ m('wf_preview.line_commit', {sha}),
1851
2185
  '',
1852
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
2186
+ m('wf_preview.tbl_steps'),
1853
2187
  '|------|------|----------|',
1854
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
1855
- '| 🐳 Docker 이미지 생성 | ⏳ μ§„ν–‰ 쀑... | - |',
1856
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏸️ λŒ€κΈ° | - |',
2188
+ m('wf_preview.row_build_done', {buildDuration}),
2189
+ m('wf_preview.row_image_progress'),
2190
+ m('wf_preview.row_deploy_wait'),
1857
2191
  '',
1858
2192
  '---',
1859
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2193
+ m('wf_preview.footer_generic')
1860
2194
  ].join('\n');
1861
2195
 
1862
2196
  await github.rest.issues.updateComment({
@@ -1868,14 +2202,14 @@ jobs:
1868
2202
 
1869
2203
  core.setOutput('build_duration', buildDuration);
1870
2204
 
1871
- - name: Docker Hub 둜그인
2205
+ - name: Log in to Docker Hub
1872
2206
  if: steps.check_branch.outputs.exists == 'true'
1873
2207
  uses: docker/login-action@v3
1874
2208
  with:
1875
2209
  username: ${{ secrets.DOCKERHUB_USERNAME }}
1876
2210
  password: ${{ secrets.DOCKERHUB_TOKEN }}
1877
2211
 
1878
- - name: Docker 이미지 λΉŒλ“œ & ν‘Έμ‹œ
2212
+ - name: Build & push Docker image
1879
2213
  working-directory: ${{ env.PROJECT_PATH }}
1880
2214
  if: steps.check_branch.outputs.exists == 'true'
1881
2215
  id: docker
@@ -1890,12 +2224,15 @@ jobs:
1890
2224
  echo "duration=${DOCKER_DURATION}s" >> $GITHUB_OUTPUT
1891
2225
  echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT
1892
2226
 
1893
- - name: Docker μ™„λ£Œ μ§„ν–‰ 상황 μ—…λ°μ΄νŠΈ
2227
+ - name: Update progress (Docker done)
1894
2228
  if: steps.check_branch.outputs.exists == 'true'
1895
2229
  id: docker_progress
1896
2230
  uses: actions/github-script@v9
1897
2231
  with:
1898
2232
  script: |
2233
+ const M = JSON.parse(process.env.PAW_MSG);
2234
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2235
+
1899
2236
  const commentId = ${{ steps.progress.outputs.comment_id }};
1900
2237
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1901
2238
  const branchName = '${{ steps.extract.outputs.branch_name }}';
@@ -1904,19 +2241,19 @@ jobs:
1904
2241
  const dockerDuration = '${{ steps.docker.outputs.duration }}';
1905
2242
 
1906
2243
  const body = [
1907
- '## πŸš€ Custom Branch Preview 배포 μ‹œμž‘',
2244
+ m('wf_preview.title_custom_started'),
1908
2245
  '',
1909
- `**브랜치**: \`${branchName}\``,
1910
- `**컀밋**: \`${sha}\``,
2246
+ m('wf_preview.line_branch', {branchName}),
2247
+ m('wf_preview.line_commit', {sha}),
1911
2248
  '',
1912
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
2249
+ m('wf_preview.tbl_steps'),
1913
2250
  '|------|------|----------|',
1914
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
1915
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
1916
- '| πŸš€ μ„œλ²„ 배포 & Health Check | ⏳ μ§„ν–‰ 쀑... | - |',
2251
+ m('wf_preview.row_build_done', {buildDuration}),
2252
+ m('wf_preview.row_image_done', {dockerDuration}),
2253
+ m('wf_preview.row_deploy_progress'),
1917
2254
  '',
1918
2255
  '---',
1919
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2256
+ m('wf_preview.footer_generic')
1920
2257
  ].join('\n');
1921
2258
 
1922
2259
  await github.rest.issues.updateComment({
@@ -1928,7 +2265,7 @@ jobs:
1928
2265
 
1929
2266
  core.setOutput('docker_duration', dockerDuration);
1930
2267
 
1931
- - name: μ„œλ²„ 배포
2268
+ - name: Deploy to server
1932
2269
  if: steps.check_branch.outputs.exists == 'true'
1933
2270
  id: deploy
1934
2271
  uses: appleboy/ssh-action@v1.0.3
@@ -1939,6 +2276,8 @@ jobs:
1939
2276
  key: ${{ secrets.SSH_KEY }}
1940
2277
  port: ${{ env.SSH_PORT }}
1941
2278
  script: |
2279
+ ${{ env.PAW_REMOTE }}
2280
+
1942
2281
  export PATH=$PATH:/usr/local/bin
1943
2282
 
1944
2283
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -1949,18 +2288,18 @@ jobs:
1949
2288
  DOMAIN_SUFFIX="${{ env.PREVIEW_DOMAIN_SUFFIX }}"
1950
2289
 
1951
2290
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1952
- echo "πŸš€ Custom Branch Preview 배포"
2291
+ m wf_preview.r_deploy_head_custom_short
1953
2292
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1954
2293
 
1955
2294
  docker pull $IMAGE_TAG
1956
2295
 
1957
2296
  if docker ps -a --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
1958
- echo "πŸ—‘οΈ κΈ°μ‘΄ μ»¨ν…Œμ΄λ„ˆ μ‚­μ œ: ${CONTAINER_NAME}"
2297
+ m wf_preview.r_existing_removed container_name="${CONTAINER_NAME}"
1959
2298
  docker stop ${CONTAINER_NAME} || true
1960
2299
  docker rm ${CONTAINER_NAME} || true
1961
2300
  fi
1962
2301
 
1963
- echo "🐳 μƒˆ μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘: ${CONTAINER_NAME}"
2302
+ m wf_preview.r_container_new_named container_name="${CONTAINER_NAME}"
1964
2303
  docker run -d \
1965
2304
  --name ${CONTAINER_NAME} \
1966
2305
  --network traefik-network \
@@ -1970,34 +2309,40 @@ jobs:
1970
2309
  -l "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=${PREVIEW_PORT}" \
1971
2310
  ${IMAGE_TAG}
1972
2311
 
1973
- echo "⏳ Health Check λŒ€κΈ° (μ΅œλŒ€ 60초)..."
2312
+ m wf_preview.r_hc_wait_60
1974
2313
  for i in {1..12}; do
1975
2314
  if docker ps --filter "name=${CONTAINER_NAME}" --filter "status=running" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
1976
- echo "βœ… μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰ 확인됨"
2315
+ m wf_preview.r_container_confirmed
1977
2316
  break
1978
2317
  fi
1979
- echo "λŒ€κΈ° 쀑... ($i/12)"
2318
+ m wf_preview.r_wait_i i="$i"
1980
2319
  sleep 5
1981
2320
  done
1982
2321
 
1983
2322
  if ! docker ps --filter "name=${CONTAINER_NAME}" --filter "status=running" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
1984
- echo "❌ 배포 μ‹€νŒ¨: μ»¨ν…Œμ΄λ„ˆκ°€ μ‹€ν–‰λ˜μ§€ μ•ŠμŒ"
2323
+ m wf_preview.r_deploy_failed_not_running
1985
2324
  exit 1
1986
2325
  fi
1987
2326
 
1988
- echo "βœ… 배포 μ™„λ£Œ!"
2327
+ m wf_preview.r_deploy_done
1989
2328
  echo "🌐 Preview URL: http://${CONTAINER_NAME}.${DOMAIN_SUFFIX}:${PREVIEW_PORT}"
1990
2329
 
1991
- - name: 배포 μ™„λ£Œ μ‹œκ°„ 기둝
2330
+ - name: Record deploy completion
1992
2331
  if: steps.check_branch.outputs.exists == 'true'
1993
2332
  id: deploy_time
1994
- run: echo "duration=μ™„λ£Œ" >> $GITHUB_OUTPUT
2333
+ run: |
2334
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
2335
+
2336
+ echo "duration=$(m wf_preview.done_label)" >> $GITHUB_OUTPUT
1995
2337
 
1996
- - name: 배포 μ™„λ£Œ μ½”λ©˜νŠΈ
2338
+ - name: Deploy complete comment
1997
2339
  if: steps.check_branch.outputs.exists == 'true'
1998
2340
  uses: actions/github-script@v9
1999
2341
  with:
2000
2342
  script: |
2343
+ const M = JSON.parse(process.env.PAW_MSG);
2344
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2345
+
2001
2346
  const commentId = ${{ steps.progress.outputs.comment_id }};
2002
2347
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2003
2348
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
@@ -2020,35 +2365,35 @@ jobs:
2020
2365
  if (apiDocsPath) {
2021
2366
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
2022
2367
  }
2023
- envRows.push(`| **μ»¨ν…Œμ΄λ„ˆ** | \`${containerName}\` |`);
2024
- envRows.push(`| **브랜치** | \`${branchName}\` |`);
2025
- envRows.push(`| **컀밋** | \`${sha}\` |`);
2368
+ envRows.push(m('wf_preview.env_container_name', {containerName}));
2369
+ envRows.push(m('wf_preview.env_branch', {branchName}));
2370
+ envRows.push(m('wf_preview.env_commit', {sha}));
2026
2371
 
2027
2372
  const body = [
2028
- '## βœ… Custom Branch Preview 배포 μ™„λ£Œ!',
2373
+ m('wf_preview.title_custom_deployed'),
2029
2374
  '',
2030
- `**브랜치**: \`${branchName}\``,
2375
+ m('wf_preview.line_branch', {branchName}),
2031
2376
  '',
2032
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
2377
+ m('wf_preview.tbl_steps'),
2033
2378
  '|------|------|----------|',
2034
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | βœ… μ™„λ£Œ | ${buildDuration} |`,
2035
- `| 🐳 Docker 이미지 생성 | βœ… μ™„λ£Œ | ${dockerDuration} |`,
2036
- `| πŸš€ μ„œλ²„ 배포 & Health Check | βœ… μ™„λ£Œ | ${deployDuration} |`,
2379
+ m('wf_preview.row_build_done', {buildDuration}),
2380
+ m('wf_preview.row_image_done', {dockerDuration}),
2381
+ m('wf_preview.row_deploy_done', {deployDuration}),
2037
2382
  '',
2038
- '### 🌐 Preview ν™˜κ²½',
2039
- '| ν•­λͺ© | κ°’ |',
2383
+ m('wf_preview.sec_env'),
2384
+ m('wf_preview.tbl_items'),
2040
2385
  '|------|-----|',
2041
2386
  ...envRows,
2042
2387
  '',
2043
- '### πŸ“‹ λͺ…λ Ήμ–΄',
2044
- '| λͺ…λ Ήμ–΄ | μ„€λͺ… |',
2388
+ m('wf_preview.sec_commands'),
2389
+ m('wf_preview.tbl_commands'),
2045
2390
  '|--------|------|',
2046
- `| \`/wizard server build ${branchName}\` | μ΅œμ‹  μ»€λ°‹μœΌλ‘œ 재배포 |`,
2047
- `| \`/wizard server destroy ${branchName}\` | Preview ν™˜κ²½ μ‚­μ œ |`,
2048
- `| \`/wizard server status ${branchName}\` | ν˜„μž¬ μƒνƒœ 확인 |`,
2391
+ m('wf_preview.cmd_build_branch', {branchName}),
2392
+ m('wf_preview.cmd_destroy_branch', {branchName}),
2393
+ m('wf_preview.cmd_status_branch', {branchName}),
2049
2394
  '',
2050
2395
  '---',
2051
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2396
+ m('wf_preview.footer_generic')
2052
2397
  ].join('\n');
2053
2398
 
2054
2399
  await github.rest.issues.updateComment({
@@ -2058,11 +2403,14 @@ jobs:
2058
2403
  body: body
2059
2404
  });
2060
2405
 
2061
- - name: λΉŒλ“œ/배포 μ‹€νŒ¨ μ‹œ μ—λŸ¬ μ½”λ©˜νŠΈ
2406
+ - name: Build/deploy failure comment
2062
2407
  if: failure() && steps.check_branch.outputs.exists == 'true'
2063
2408
  uses: actions/github-script@v9
2064
2409
  with:
2065
2410
  script: |
2411
+ const M = JSON.parse(process.env.PAW_MSG);
2412
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2413
+
2066
2414
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
2067
2415
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2068
2416
  const branchName = '${{ steps.extract.outputs.branch_name }}';
@@ -2075,48 +2423,48 @@ jobs:
2075
2423
  let buildTime = '-', dockerTime = '-';
2076
2424
 
2077
2425
  if (!commentId) {
2078
- buildStatus = '⚠️ μ΄ˆκΈ°ν™” μ‹€νŒ¨';
2426
+ buildStatus = m('wf_preview.status_init_failed');
2079
2427
  dockerStatus = '-';
2080
2428
  deployStatus = '-';
2081
2429
  } else if (!buildDuration || buildDuration === '') {
2082
- buildStatus = '❌ μ‹€νŒ¨';
2083
- dockerStatus = '⏸️ λŒ€κΈ°';
2084
- deployStatus = '⏸️ λŒ€κΈ°';
2430
+ buildStatus = m('wf_preview.status_failed');
2431
+ dockerStatus = m('wf_preview.status_waiting');
2432
+ deployStatus = m('wf_preview.status_waiting');
2085
2433
  } else if (!dockerDuration || dockerDuration === '') {
2086
- buildStatus = 'βœ… μ™„λ£Œ';
2087
- dockerStatus = '❌ μ‹€νŒ¨';
2088
- deployStatus = '⏸️ λŒ€κΈ°';
2434
+ buildStatus = m('wf_preview.status_done');
2435
+ dockerStatus = m('wf_preview.status_failed');
2436
+ deployStatus = m('wf_preview.status_waiting');
2089
2437
  buildTime = buildDuration;
2090
2438
  } else {
2091
- buildStatus = 'βœ… μ™„λ£Œ';
2092
- dockerStatus = 'βœ… μ™„λ£Œ';
2093
- deployStatus = '❌ μ‹€νŒ¨';
2439
+ buildStatus = m('wf_preview.status_done');
2440
+ dockerStatus = m('wf_preview.status_done');
2441
+ deployStatus = m('wf_preview.status_failed');
2094
2442
  buildTime = buildDuration;
2095
2443
  dockerTime = dockerDuration;
2096
2444
  }
2097
2445
 
2098
2446
  const body = [
2099
- '## ❌ Custom Branch Preview 배포 μ‹€νŒ¨!',
2447
+ m('wf_preview.title_custom_failed'),
2100
2448
  '',
2101
- `**브랜치**: \`${branchName}\``,
2449
+ m('wf_preview.line_branch', {branchName}),
2102
2450
  '',
2103
- '| 단계 | μƒνƒœ | μ†Œμš” μ‹œκ°„ |',
2451
+ m('wf_preview.tbl_steps'),
2104
2452
  '|------|------|----------|',
2105
- `| πŸ”¨ ν”„λ‘œμ νŠΈ λΉŒλ“œ | ${buildStatus} | ${buildTime} |`,
2106
- `| 🐳 Docker 이미지 생성 | ${dockerStatus} | ${dockerTime} |`,
2107
- `| πŸš€ μ„œλ²„ 배포 & Health Check | ${deployStatus} | - |`,
2453
+ m('wf_preview.row_build_status', {buildStatus, buildTime}),
2454
+ m('wf_preview.row_image_status', {dockerStatus, dockerTime}),
2455
+ m('wf_preview.row_deploy_status', {deployStatus}),
2108
2456
  '',
2109
- `**[πŸ“‹ λΉŒλ“œ/배포 둜그 확인](${runUrl})**`,
2457
+ m('wf_preview.link_build_logs', {runUrl}),
2110
2458
  '',
2111
- '### πŸ” κ°€λŠ₯ν•œ 원인',
2112
- '- ν•„μˆ˜ Secret(DOCKERHUB_*, SERVER_*) 미등둝 λ˜λŠ” Dockerfile μ—†μŒ β†’ 둜그의 "배포 사전 점검" 단계 확인',
2113
- '- Gradle λΉŒλ“œ μ‹€νŒ¨',
2114
- '- Docker 이미지 λΉŒλ“œ μ‹€νŒ¨',
2115
- '- μ„œλ²„ μ—°κ²° 문제',
2116
- '- μ»¨ν…Œμ΄λ„ˆ μ‹œμž‘ μ‹€νŒ¨',
2459
+ m('wf_preview.sec_causes'),
2460
+ m('wf_preview.hint_secrets'),
2461
+ m('wf_preview.hint_gradle'),
2462
+ m('wf_preview.hint_docker'),
2463
+ m('wf_preview.hint_server_conn'),
2464
+ m('wf_preview.hint_start'),
2117
2465
  '',
2118
2466
  '---',
2119
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2467
+ m('wf_preview.footer_generic')
2120
2468
  ].join('\n');
2121
2469
 
2122
2470
  if (commentId) {
@@ -2136,7 +2484,7 @@ jobs:
2136
2484
  }
2137
2485
 
2138
2486
  destroy-preview-custom-branch:
2139
- name: Preview μ‚­μ œ (Custom Branch)
2487
+ name: Destroy preview (Custom Branch)
2140
2488
  needs: check-command
2141
2489
  if: |
2142
2490
  needs.check-command.outputs.is_valid == 'true' &&
@@ -2144,7 +2492,51 @@ jobs:
2144
2492
  needs.check-command.outputs.is_custom_branch == 'true'
2145
2493
  runs-on: ubuntu-latest
2146
2494
  steps:
2147
- - name: 브랜치λͺ…μ—μ„œ μ»¨ν…Œμ΄λ„ˆ suffix μΆ”μΆœ
2495
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
2496
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
2497
+ - name: Checkout messages
2498
+ uses: actions/checkout@v7
2499
+ with:
2500
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2501
+ sparse-checkout: |
2502
+ .github/scripts/messages.py
2503
+ version.yml
2504
+ sparse-checkout-cone-mode: false
2505
+
2506
+ - name: Load messages
2507
+ run: |
2508
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2509
+ mkdir -p "$RUNNER_TEMP/paw"
2510
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2511
+ {
2512
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2513
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2514
+ } >> "$GITHUB_ENV"
2515
+ # The deploy script runs on the remote server, where messages.py does not exist:
2516
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
2517
+ python3 - >> "$GITHUB_ENV" <<'PY'
2518
+ import json, os, shlex, subprocess
2519
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
2520
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
2521
+ head = ["m() {", ' case "$1" in']
2522
+ for key, text in msgs.items():
2523
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
2524
+ tail = r''' *) _t="$1" ;;
2525
+ esac
2526
+ shift
2527
+ while [ "$#" -gt 0 ]; do
2528
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
2529
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
2530
+ shift
2531
+ done
2532
+ printf '%s\n' "$_t"
2533
+ }'''
2534
+ print("PAW_REMOTE<<PAW_EOF")
2535
+ print("\n".join(head) + "\n" + tail)
2536
+ print("PAW_EOF")
2537
+ PY
2538
+
2539
+ - name: Extract container suffix from branch name
2148
2540
  id: extract
2149
2541
  uses: actions/github-script@v9
2150
2542
  with:
@@ -2167,7 +2559,7 @@ jobs:
2167
2559
 
2168
2560
  core.setOutput('branch_name', branchName);
2169
2561
 
2170
- - name: μ»¨ν…Œμ΄λ„ˆ μ‚­μ œ
2562
+ - name: Remove container
2171
2563
  id: destroy
2172
2564
  uses: appleboy/ssh-action@v1.0.3
2173
2565
  with:
@@ -2177,6 +2569,8 @@ jobs:
2177
2569
  key: ${{ secrets.SSH_KEY }}
2178
2570
  port: ${{ env.SSH_PORT }}
2179
2571
  script: |
2572
+ ${{ env.PAW_REMOTE }}
2573
+
2180
2574
  export PATH=$PATH:/usr/local/bin
2181
2575
 
2182
2576
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -2184,40 +2578,43 @@ jobs:
2184
2578
  CONTAINER_NAME="${PROJECT_NAME}-${CONTAINER_SUFFIX}"
2185
2579
 
2186
2580
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2187
- echo "πŸ—‘οΈ Custom Branch Preview μ‚­μ œ"
2581
+ m wf_preview.r_destroy_head_custom
2188
2582
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2189
2583
 
2190
2584
  if docker ps -a --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
2191
- echo "πŸ—‘οΈ μ»¨ν…Œμ΄λ„ˆ μ‚­μ œ 쀑: ${CONTAINER_NAME}"
2585
+ m wf_preview.r_container_removing_named container_name="${CONTAINER_NAME}"
2192
2586
  docker stop ${CONTAINER_NAME} || true
2193
2587
  docker rm ${CONTAINER_NAME} || true
2194
2588
  echo "DESTROYED=true"
2195
2589
  else
2196
- echo "ℹ️ μ»¨ν…Œμ΄λ„ˆκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: ${CONTAINER_NAME}"
2590
+ m wf_preview.r_container_missing_info container_name="${CONTAINER_NAME}"
2197
2591
  echo "DESTROYED=false"
2198
2592
  fi
2199
2593
 
2200
- - name: μ‚­μ œ 확인 μ½”λ©˜νŠΈ
2594
+ - name: Destroy confirmation comment
2201
2595
  uses: actions/github-script@v9
2202
2596
  with:
2203
2597
  script: |
2598
+ const M = JSON.parse(process.env.PAW_MSG);
2599
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2600
+
2204
2601
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2205
2602
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2206
2603
  const branchName = '${{ steps.extract.outputs.branch_name }}';
2207
2604
  const containerName = `${projectName}-${containerSuffix}`;
2208
2605
 
2209
2606
  const body = [
2210
- '## πŸ—‘οΈ Custom Branch Preview μ‚­μ œ μ™„λ£Œ',
2607
+ m('wf_preview.title_custom_destroyed_short'),
2211
2608
  '',
2212
- '| ν•­λͺ© | κ°’ |',
2609
+ m('wf_preview.tbl_items'),
2213
2610
  '|------|-----|',
2214
- `| **브랜치** | \`${branchName}\` |`,
2215
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${containerName}\` |`,
2611
+ m('wf_preview.env_branch', {branchName}),
2612
+ m('wf_preview.env_container_name', {containerName}),
2216
2613
  '',
2217
- `λ‹€μ‹œ λ°°ν¬ν•˜λ €λ©΄: \`/wizard server build ${branchName}\``,
2614
+ m('wf_preview.redeploy_hint_branch', {branchName}),
2218
2615
  '',
2219
2616
  '---',
2220
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2617
+ m('wf_preview.footer_generic')
2221
2618
  ].join('\n');
2222
2619
 
2223
2620
  await github.rest.issues.createComment({
@@ -2228,7 +2625,7 @@ jobs:
2228
2625
  });
2229
2626
 
2230
2627
  check-status-custom-branch:
2231
- name: Preview μƒνƒœ 확인 (Custom Branch)
2628
+ name: Preview status (Custom Branch)
2232
2629
  needs: check-command
2233
2630
  if: |
2234
2631
  needs.check-command.outputs.is_valid == 'true' &&
@@ -2236,7 +2633,28 @@ jobs:
2236
2633
  needs.check-command.outputs.is_custom_branch == 'true'
2237
2634
  runs-on: ubuntu-latest
2238
2635
  steps:
2239
- - name: 브랜치λͺ…μ—μ„œ μ»¨ν…Œμ΄λ„ˆ suffix μΆ”μΆœ
2636
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
2637
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
2638
+ - name: Checkout messages
2639
+ uses: actions/checkout@v7
2640
+ with:
2641
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2642
+ sparse-checkout: |
2643
+ .github/scripts/messages.py
2644
+ version.yml
2645
+ sparse-checkout-cone-mode: false
2646
+
2647
+ - name: Load messages
2648
+ run: |
2649
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2650
+ mkdir -p "$RUNNER_TEMP/paw"
2651
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2652
+ {
2653
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2654
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2655
+ } >> "$GITHUB_ENV"
2656
+
2657
+ - name: Extract container suffix from branch name
2240
2658
  id: extract
2241
2659
  uses: actions/github-script@v9
2242
2660
  with:
@@ -2259,7 +2677,7 @@ jobs:
2259
2677
 
2260
2678
  core.setOutput('branch_name', branchName);
2261
2679
 
2262
- - name: μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인
2680
+ - name: Check container status
2263
2681
  id: check_running
2264
2682
  uses: appleboy/ssh-action@v1.0.3
2265
2683
  continue-on-error: true
@@ -2278,10 +2696,13 @@ jobs:
2278
2696
 
2279
2697
  docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
2280
2698
 
2281
- - name: μƒνƒœ μ½”λ©˜νŠΈ μž‘μ„±
2699
+ - name: Post status comment
2282
2700
  uses: actions/github-script@v9
2283
2701
  with:
2284
2702
  script: |
2703
+ const M = JSON.parse(process.env.PAW_MSG);
2704
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2705
+
2285
2706
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2286
2707
  const containerSuffix = '${{ steps.extract.outputs.container_suffix }}';
2287
2708
  const branchName = '${{ steps.extract.outputs.branch_name }}';
@@ -2295,38 +2716,38 @@ jobs:
2295
2716
  let body;
2296
2717
  if (isRunning) {
2297
2718
  body = [
2298
- '## βœ… Custom Branch Preview ν™˜κ²½ μ‹€ν–‰ 쀑',
2719
+ m('wf_preview.title_custom_running'),
2299
2720
  '',
2300
- '| ν•­λͺ© | κ°’ |',
2721
+ m('wf_preview.tbl_items'),
2301
2722
  '|------|-----|',
2302
- `| **브랜치** | \`${branchName}\` |`,
2723
+ m('wf_preview.env_branch', {branchName}),
2303
2724
  `| **Preview URL** | ${previewUrl} |`,
2304
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${containerName}\` |`,
2305
- '| **μƒνƒœ** | 🟒 Running |',
2725
+ m('wf_preview.env_container_name', {containerName}),
2726
+ m('wf_preview.env_status_running'),
2306
2727
  '',
2307
- '### πŸ“‹ λͺ…λ Ήμ–΄',
2308
- '| λͺ…λ Ήμ–΄ | μ„€λͺ… |',
2728
+ m('wf_preview.sec_commands'),
2729
+ m('wf_preview.tbl_commands'),
2309
2730
  '|--------|------|',
2310
- `| \`/wizard server build ${branchName}\` | μ΅œμ‹  μ»€λ°‹μœΌλ‘œ 재배포 |`,
2311
- `| \`/wizard server destroy ${branchName}\` | Preview ν™˜κ²½ μ‚­μ œ |`,
2731
+ m('wf_preview.cmd_build_branch', {branchName}),
2732
+ m('wf_preview.cmd_destroy_branch', {branchName}),
2312
2733
  '',
2313
2734
  '---',
2314
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2735
+ m('wf_preview.footer_generic')
2315
2736
  ].join('\n');
2316
2737
  } else {
2317
2738
  body = [
2318
- '## ❌ Custom Branch Preview ν™˜κ²½ μ—†μŒ',
2739
+ m('wf_preview.title_custom_absent'),
2319
2740
  '',
2320
- '| ν•­λͺ© | κ°’ |',
2741
+ m('wf_preview.tbl_items'),
2321
2742
  '|------|-----|',
2322
- `| **브랜치** | \`${branchName}\` |`,
2323
- `| **μ»¨ν…Œμ΄λ„ˆ** | \`${containerName}\` |`,
2324
- '| **μƒνƒœ** | πŸ”΄ Not Found |',
2743
+ m('wf_preview.env_branch', {branchName}),
2744
+ m('wf_preview.env_container_name', {containerName}),
2745
+ m('wf_preview.env_status_absent'),
2325
2746
  '',
2326
- `λ°°ν¬ν•˜λ €λ©΄: \`/wizard server build ${branchName}\``,
2747
+ m('wf_preview.deploy_hint_branch', {branchName}),
2327
2748
  '',
2328
2749
  '---',
2329
- '*πŸ€– 이 λŒ“κΈ€μ€ Preview μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.*'
2750
+ m('wf_preview.footer_generic')
2330
2751
  ].join('\n');
2331
2752
  }
2332
2753