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,84 +1,84 @@
1
1
  # project-auto-wizard:managed-workflow
2
2
  # Go PR Preview (SSH + Docker + Traefik)
3
- # PR 또는 Issue 코멘트 명령어를 통해 Preview 환경을 자동으로 관리합니다. Traefik 리버스 프록시로 동적 라우팅됩니다.
3
+ # Manages the preview environment automatically through PR or issue comment commands. Routing is dynamic via the Traefik reverse proxy.
4
4
  #
5
- # 필수 GitHub Secrets
6
- # ENV_FILE: .env 파일 전체 내용
7
- # DOCKERHUB_USERNAME / DOCKERHUB_TOKEN: Docker Hub 사용자명 / 액세스 토큰
8
- # SERVER_HOST / SERVER_USER: 서버 호스트(SSH 접속 주소) / SSH 사용자명
9
- # SERVER_PASSWORD: SSH 비밀번호 (SSH_AUTH_METHOD=password일 때)
10
- # SSH_KEY: SSH 개인키 .pem 내용 (SSH_AUTH_METHOD=key일 때, AWS EC2 등)
5
+ # Required GitHub Secrets
6
+ # ENV_FILE: full contents of the .env file
7
+ # DOCKERHUB_USERNAME / DOCKERHUB_TOKEN: Docker Hub username / access token
8
+ # SERVER_HOST / SERVER_USER: server host (SSH address) / SSH username
9
+ # SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
10
+ # SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
11
11
  #
12
- # 사용법
13
- # /wizard server build - Preview 빌드 및 배포
14
- # /wizard server destroy - Preview 환경 삭제
15
- # /wizard server status - 현재 상태 확인
12
+ # Usage
13
+ # /wizard server build - Build and deploy the preview
14
+ # /wizard server destroy - Delete the preview environment
15
+ # /wizard server status - Check the current status
16
16
  #
17
- # 리소스 네이밍
18
- # - 컨테이너: {PROJECT_NAME}-pr-{번호}
19
- # - 도메인: {PROJECT_NAME}-pr-{번호}.pr.example.com
17
+ # Resource naming
18
+ # - Container: {PROJECT_NAME}-pr-{number}
19
+ # - Domain: {PROJECT_NAME}-pr-{number}.pr.example.com
20
20
  #
21
- # 사전 요구사항
22
- # - Traefik 컨테이너 실행 중 (traefik-network)
23
- # - 와일드카드 DNS 설정: *.pr.example.com → 서버
21
+ # Prerequisites
22
+ # - Traefik container running (traefik-network)
23
+ # - Wildcard DNS: *.pr.example.com → server
24
24
 
25
25
  name: PROJECT-GO-PR-PREVIEW
26
26
 
27
- # [영역 1] 프로젝트별 설정 - 다른 프로젝트에서 사용 시 이 섹션만 수정하세요
27
+ # [Section 1] Project settings - when reusing this in another project, edit only this section
28
28
  env:
29
- # 프로젝트 고유 식별자 (컨테이너명, 이미지명, 도메인에 사용)
29
+ # Unique project identifier (used in container, image and domain names)
30
30
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
31
- # 모노레포(--paths)에서 이 프로젝트가 있는 하위 경로. 빌드·Docker context의 기준 ('.'이면 레포 루트)
31
+ # Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' means the repo root)
32
32
  PROJECT_PATH: "." # @wizard auto:project-path
33
- # 모노레포(PROJECT_PATH가 '.'이 아님)에서는 이미지·컨테이너·도메인 이름에 '-go'를 붙여
34
- # 같은 레포의 다른 타입 서비스와 이름이 겹치지 않게 한다 (예: my-app-go)
33
+ # In a monorepo (PROJECT_PATH is not '.') '-go' is appended to image, container and domain names
34
+ # so they do not collide with services of other types in the same repo (e.g. my-app-go)
35
35
 
36
- # Docker 설정
37
- DOCKERFILE_PATH: './Dockerfile' # PROJECT_PATH 기준 상대 경로
36
+ # Docker settings
37
+ DOCKERFILE_PATH: './Dockerfile' # path relative to PROJECT_PATH
38
38
  INTERNAL_PORT: '8080'
39
39
 
40
- # 볼륨 마운트 설정 (프로젝트별 수정 필요, 빈값이면 마운트 안함)
40
+ # Volume mount settings (edit per project; leave empty to skip mounting)
41
41
  PROJECT_TARGET_DIR: "__PROJECT_TARGET_DIR__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
42
42
  PROJECT_MNT_DIR: "__PROJECT_MNT_DIR__" # @wizard ask:/mnt/__PROJECT_NAME__
43
43
 
44
- # Traefik & Preview 도메인 설정 (환경 구축 후 수정 금지)
44
+ # Traefik & preview domain settings (do not change after the environment is set up)
45
45
  TRAEFIK_NETWORK: traefik-network
46
46
  PREVIEW_DOMAIN_SUFFIX: "__PREVIEW_DOMAIN_SUFFIX__" # @wizard ask:pr.example.com
47
47
  PREVIEW_PORT: '8079'
48
48
 
49
- # SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
50
- # password → SERVER_PASSWORD secret 사용
51
- # key → SSH_KEY secret 사용 (.pem 파일 전체 내용을 secret 값으로)
49
+ # SSH auth method: "password" (Synology / regular servers) | "key" (AWS EC2 / .pem)
50
+ # password → uses the SERVER_PASSWORD secret
51
+ # key → uses the SSH_KEY secret (store the whole .pem contents as the secret value)
52
52
  SSH_AUTH_METHOD: "password" # @wizard ask:password
53
53
 
54
- # SSH 포트 (서버 포트 환경 구축 후 수정 금지)
54
+ # SSH port (do not change after the server port is set up)
55
55
  SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
56
56
 
57
- # Issue Helper 댓글 마커 (프로젝트별 수정 필요)
58
- # Issue 댓글에서 브랜치명을 추출할 때 사용
57
+ # Issue Helper comment marker (edit per project)
58
+ # Used to extract the branch name from an issue comment
59
59
  ISSUE_HELPER_MARKER: '<!-- project-auto-wizard issue helper -->'
60
60
 
61
- # Health Check 설정 (프로젝트별 수정 필요)
62
- # - HEALTH_CHECK_PATH: HTTP Health Check 경로 (빈값이면 HTTP 체크 건너뜀)
63
- # - HEALTH_CHECK_LOG_PATTERN: 로그 패턴 매칭 (HTTP 실패 시 폴백)
64
- # - API_DOCS_PATH: API 문서 URL (빈값이면 배포 코멘트에 미표시)
61
+ # Health check settings (edit per project)
62
+ # - HEALTH_CHECK_PATH: HTTP health check path (empty skips the HTTP check)
63
+ # - HEALTH_CHECK_LOG_PATTERN: log pattern match (fallback when HTTP fails)
64
+ # - API_DOCS_PATH: API docs URL (empty hides it from the deploy comment)
65
65
  #
66
- # 프레임워크별 기본값 예시:
66
+ # Default examples per framework:
67
67
  # FastAPI: '/docs', 'Uvicorn running on|Application startup complete'
68
68
  # Spring Boot: '/actuator/health', 'Started.*Application|Tomcat started on port'
69
69
  # Express: '/health', 'Server listening on port'
70
- # Go (net/http 등): '/health', 'listening on'
70
+ # Go (net/http, etc.): '/health', 'listening on'
71
71
  HEALTH_CHECK_PATH: '/health'
72
72
  HEALTH_CHECK_LOG_PATTERN: 'listening on'
73
73
  API_DOCS_PATH: ''
74
74
 
75
75
  on:
76
76
  issue_comment:
77
- types: [created] # PR 댓글 + Issue 댓글
77
+ types: [created] # PR comments + issue comments
78
78
  issues:
79
- types: [closed] # Issue 닫힘 시 destroy
79
+ types: [closed] # destroy when the issue is closed
80
80
  pull_request:
81
- types: [closed] # PR 닫힘 시 destroy
81
+ types: [closed] # destroy when the PR is closed
82
82
 
83
83
  permissions:
84
84
  contents: read
@@ -86,9 +86,9 @@ permissions:
86
86
  issues: write
87
87
 
88
88
  jobs:
89
- # Job 1: 명령어 파싱 (PR 댓글 + Issue 댓글 모두 지원)
89
+ # Job 1: parse the command (supports both PR and issue comments)
90
90
  check-command:
91
- name: 명령어 확인
91
+ name: Check command
92
92
  if: github.event_name == 'issue_comment'
93
93
  runs-on: ubuntu-latest
94
94
  outputs:
@@ -98,7 +98,28 @@ jobs:
98
98
  custom_branch: ${{ steps.parse.outputs.custom_branch }}
99
99
  is_custom_branch: ${{ steps.parse.outputs.is_custom_branch }}
100
100
  steps:
101
- - name: 댓글에 👀 리액션 추가
101
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
102
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
103
+ - name: Checkout messages
104
+ uses: actions/checkout@v7
105
+ with:
106
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
107
+ sparse-checkout: |
108
+ .github/scripts/messages.py
109
+ version.yml
110
+ sparse-checkout-cone-mode: false
111
+
112
+ - name: Load messages
113
+ run: |
114
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
115
+ mkdir -p "$RUNNER_TEMP/paw"
116
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
117
+ {
118
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
119
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
120
+ } >> "$GITHUB_ENV"
121
+
122
+ - name: Add 👀 reaction to comment
102
123
  if: contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server')
103
124
  uses: actions/github-script@v9
104
125
  with:
@@ -111,33 +132,35 @@ jobs:
111
132
  content: 'eyes'
112
133
  });
113
134
 
114
- - name: 커맨드 파싱
135
+ - name: Parse command
115
136
  id: parse
116
137
  env:
117
138
  COMMENT: ${{ github.event.comment.body }}
118
139
  IS_PR: ${{ github.event.issue.pull_request != null }}
119
140
  run: |
120
- # PR인지 Issue인지 확인
141
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
142
+
143
+ # Check whether this is a PR or an issue
121
144
  if [[ "$IS_PR" == "true" ]]; then
122
145
  echo "is_pr=true" >> $GITHUB_OUTPUT
123
- echo "ℹ️ PR 댓글에서 명령어 감지"
146
+ m wf_preview.cmd_from_pr
124
147
  else
125
148
  echo "is_pr=false" >> $GITHUB_OUTPUT
126
- echo "ℹ️ Issue 댓글에서 명령어 감지"
149
+ m wf_preview.cmd_from_issue
127
150
  fi
128
151
 
129
- # 명령어 파싱 (브랜치 파라미터 지원)
130
- # 형식: /wizard server <command> [브랜치명]
152
+ # Parse the command (branch argument supported)
153
+ # Format: /wizard server <command> [branch]
131
154
  if [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+build([[:space:]]+([^[:space:]]+))? ]]; then
132
155
  echo "command=build" >> $GITHUB_OUTPUT
133
156
  echo "is_valid=true" >> $GITHUB_OUTPUT
134
157
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
135
158
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
136
159
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
137
- echo "✅ 명령어 감지: build (브랜치: ${BASH_REMATCH[2]})"
160
+ m wf_preview.cmd_detected_build_branch branch="${BASH_REMATCH[2]}"
138
161
  else
139
162
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
140
- echo "✅ 명령어 감지: build"
163
+ m wf_preview.cmd_detected_build
141
164
  fi
142
165
  elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+destroy([[:space:]]+([^[:space:]]+))? ]]; then
143
166
  echo "command=destroy" >> $GITHUB_OUTPUT
@@ -145,10 +168,10 @@ jobs:
145
168
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
146
169
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
147
170
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
148
- echo "✅ 명령어 감지: destroy (브랜치: ${BASH_REMATCH[2]})"
171
+ m wf_preview.cmd_detected_destroy_branch branch="${BASH_REMATCH[2]}"
149
172
  else
150
173
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
151
- echo "✅ 명령어 감지: destroy"
174
+ m wf_preview.cmd_detected_destroy
152
175
  fi
153
176
  elif [[ "$COMMENT" =~ /wizard[[:space:]]+server[[:space:]]+status([[:space:]]+([^[:space:]]+))? ]]; then
154
177
  echo "command=status" >> $GITHUB_OUTPUT
@@ -156,19 +179,19 @@ jobs:
156
179
  if [[ -n "${BASH_REMATCH[2]}" ]]; then
157
180
  echo "custom_branch=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
158
181
  echo "is_custom_branch=true" >> $GITHUB_OUTPUT
159
- echo "✅ 명령어 감지: status (브랜치: ${BASH_REMATCH[2]})"
182
+ m wf_preview.cmd_detected_status_branch branch="${BASH_REMATCH[2]}"
160
183
  else
161
184
  echo "is_custom_branch=false" >> $GITHUB_OUTPUT
162
- echo "✅ 명령어 감지: status"
185
+ m wf_preview.cmd_detected_status
163
186
  fi
164
187
  else
165
188
  echo "is_valid=false" >> $GITHUB_OUTPUT
166
- echo "ℹ️ /wizard server 명령어가 아님"
189
+ m wf_preview.cmd_not_wizard
167
190
  fi
168
191
 
169
- # Job 2-1: 빌드 & 배포 (PR 댓글에서 실행)
192
+ # Job 2-1: build & deploy (triggered from a PR comment)
170
193
  build-preview-pr:
171
- name: Preview 빌드 & 배포 (PR)
194
+ name: Preview build & deploy (PR)
172
195
  needs: check-command
173
196
  if: |
174
197
  needs.check-command.outputs.is_valid == 'true' &&
@@ -177,7 +200,51 @@ jobs:
177
200
  needs.check-command.outputs.is_custom_branch != 'true'
178
201
  runs-on: ubuntu-latest
179
202
  steps:
180
- - name: PR 정보 가져오기
203
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
204
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
205
+ - name: Checkout messages
206
+ uses: actions/checkout@v7
207
+ with:
208
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
209
+ sparse-checkout: |
210
+ .github/scripts/messages.py
211
+ version.yml
212
+ sparse-checkout-cone-mode: false
213
+
214
+ - name: Load messages
215
+ run: |
216
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
217
+ mkdir -p "$RUNNER_TEMP/paw"
218
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
219
+ {
220
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
221
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
222
+ } >> "$GITHUB_ENV"
223
+ # The deploy script runs on the remote server, where messages.py does not exist:
224
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
225
+ python3 - >> "$GITHUB_ENV" <<'PY'
226
+ import json, os, shlex, subprocess
227
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
228
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
229
+ head = ["m() {", ' case "$1" in']
230
+ for key, text in msgs.items():
231
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
232
+ tail = r''' *) _t="$1" ;;
233
+ esac
234
+ shift
235
+ while [ "$#" -gt 0 ]; do
236
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
237
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
238
+ shift
239
+ done
240
+ printf '%s\n' "$_t"
241
+ }'''
242
+ print("PAW_REMOTE<<PAW_EOF")
243
+ print("\n".join(head) + "\n" + tail)
244
+ print("PAW_EOF")
245
+ PY
246
+
247
+ - name: Get PR info
181
248
  id: pr
182
249
  uses: actions/github-script@v9
183
250
  with:
@@ -191,28 +258,31 @@ jobs:
191
258
  core.setOutput('sha', pr.data.head.sha.substring(0, 7));
192
259
  return pr.data.number;
193
260
 
194
- # 진행 상황 댓글 시스템
195
- - name: 진행 상황 댓글 생성
261
+ # Progress comment system
262
+ - name: Create progress comment
196
263
  id: progress
197
264
  uses: actions/github-script@v9
198
265
  with:
199
266
  script: |
267
+ const M = JSON.parse(process.env.PAW_MSG);
268
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
269
+
200
270
  const prNumber = context.issue.number;
201
271
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
202
272
  const startTime = Date.now();
203
273
 
204
274
  const body = [
205
- '## 🚀 PR Preview 빌드 중...',
275
+ m('wf_preview.title_pr_building'),
206
276
  '',
207
- '| 단계 | 상태 | 소요 시간 |',
277
+ m('wf_preview.tbl_steps'),
208
278
  '|------|------|----------|',
209
- '| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
210
- '| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
279
+ m('wf_preview.row_docker_progress'),
280
+ m('wf_preview.row_deploy_wait'),
211
281
  '',
212
- `**[📋 실시간 로그 보기](${runUrl})**`,
282
+ m('wf_preview.link_logs', {runUrl}),
213
283
  '',
214
284
  '---',
215
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
285
+ m('wf_preview.footer_progress')
216
286
  ].join('\n');
217
287
 
218
288
  const { data: comment } = await github.rest.issues.createComment({
@@ -226,13 +296,13 @@ jobs:
226
296
  core.setOutput('start_time', startTime);
227
297
  core.setOutput('docker_start', startTime);
228
298
 
229
- - name: 코드 체크아웃
299
+ - name: Checkout code
230
300
  uses: actions/checkout@v7
231
301
  with:
232
302
  ref: ${{ steps.pr.outputs.ref }}
233
303
 
234
- # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
235
- - name: 배포 사전 점검 (Secret·Dockerfile)
304
+ # 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
305
+ - name: Deploy pre-check (Secrets and Dockerfile)
236
306
  working-directory: ${{ env.PROJECT_PATH }}
237
307
  env:
238
308
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -243,11 +313,13 @@ jobs:
243
313
  SSH_KEY: ${{ secrets.SSH_KEY }}
244
314
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
245
315
  run: |
316
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
317
+
246
318
  MISSING=""
247
319
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
248
320
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
249
321
  done
250
- # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
322
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
251
323
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
252
324
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
253
325
  else
@@ -255,37 +327,37 @@ jobs:
255
327
  fi
256
328
  FAILED=false
257
329
  if [ -n "$MISSING" ]; then
258
- echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
330
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
259
331
  FAILED=true
260
332
  fi
261
333
  if [ ! -f "$DOCKERFILE" ]; then
262
- echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
334
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
263
335
  FAILED=true
264
336
  fi
265
337
  if [ "$FAILED" = "true" ]; then
266
338
  exit 1
267
339
  fi
268
- echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
340
+ m wf_preview.precheck_ok
269
341
 
270
- # [영역 2] 환경변수 파일 생성
271
- - name: "[필수] .env 파일 생성"
342
+ # [Section 2] Create the environment file
343
+ - name: "[Required] Create .env file"
272
344
  working-directory: ${{ env.PROJECT_PATH }}
273
345
  run: |
274
346
  cat << 'EOF' > .env
275
347
  ${{ secrets.ENV_FILE }}
276
348
  EOF
277
- # [영역 2 끝] 환경변수 파일 생성 끝
349
+ # [End of section 2] Create the environment file
278
350
 
279
- - name: Docker 로그인
351
+ - name: Log in to Docker Hub
280
352
  uses: docker/login-action@v3
281
353
  with:
282
354
  username: ${{ secrets.DOCKERHUB_USERNAME }}
283
355
  password: ${{ secrets.DOCKERHUB_TOKEN }}
284
356
 
285
- - name: Docker Buildx 설정
357
+ - name: Set up Docker Buildx
286
358
  uses: docker/setup-buildx-action@v3
287
359
 
288
- - name: Docker 이미지 빌드 & Push
360
+ - name: Build & push Docker image
289
361
  uses: docker/build-push-action@v5
290
362
  with:
291
363
  context: ${{ env.PROJECT_PATH }}
@@ -295,32 +367,35 @@ jobs:
295
367
  cache-from: type=gha
296
368
  cache-to: type=gha,mode=max
297
369
 
298
- - name: 진행 상황 - Docker 완료
370
+ - name: Progress - Docker done
299
371
  id: docker_progress
300
372
  uses: actions/github-script@v9
301
373
  with:
302
374
  script: |
375
+ const M = JSON.parse(process.env.PAW_MSG);
376
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
377
+
303
378
  const commentId = ${{ steps.progress.outputs.comment_id }};
304
379
  const dockerStart = ${{ steps.progress.outputs.docker_start }};
305
380
  const now = Date.now();
306
381
  const dockerElapsed = now - dockerStart;
307
382
  const minutes = Math.floor(dockerElapsed / 60000);
308
383
  const seconds = Math.floor((dockerElapsed % 60000) / 1000);
309
- const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
384
+ const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
310
385
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
311
386
 
312
387
  const body = [
313
- '## 🚀 PR Preview 빌드 중...',
388
+ m('wf_preview.title_pr_building'),
314
389
  '',
315
- '| 단계 | 상태 | 소요 시간 |',
390
+ m('wf_preview.tbl_steps'),
316
391
  '|------|------|----------|',
317
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
318
- '| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
392
+ m('wf_preview.row_docker_done', {dockerDuration}),
393
+ m('wf_preview.row_deploy_progress'),
319
394
  '',
320
- `**[📋 실시간 로그 보기](${runUrl})**`,
395
+ m('wf_preview.link_logs', {runUrl}),
321
396
  '',
322
397
  '---',
323
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
398
+ m('wf_preview.footer_progress')
324
399
  ].join('\n');
325
400
 
326
401
  await github.rest.issues.updateComment({
@@ -333,7 +408,7 @@ jobs:
333
408
  core.setOutput('docker_duration', dockerDuration);
334
409
  core.setOutput('deploy_start', now);
335
410
 
336
- - name: 서버에 배포
411
+ - name: Deploy to server
337
412
  uses: appleboy/ssh-action@v1.0.3
338
413
  env:
339
414
  SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
@@ -345,19 +420,21 @@ jobs:
345
420
  port: ${{ env.SSH_PORT }}
346
421
  envs: SSH_AUTH_METHOD
347
422
  script: |
423
+ ${{ env.PAW_REMOTE }}
424
+
348
425
  set -e
349
426
 
350
427
  export PATH=$PATH:/usr/local/bin
351
428
  export PW="${{ secrets.SERVER_PASSWORD }}"
352
429
 
353
- # SSH 인증 방식에 따른 sudo 추상화
430
+ # sudo wrapper that depends on the SSH auth method
354
431
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
355
432
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
356
433
  SUDO() { sudo "$@"; }
357
434
  else
358
435
  SUDO() { echo "$PW" | sudo -S "$@"; }
359
436
  fi
360
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
437
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
361
438
 
362
439
  PR_NUMBER=${{ github.event.issue.number }}
363
440
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -368,18 +445,18 @@ jobs:
368
445
  TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
369
446
 
370
447
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
371
- echo "🚀 PR Preview 배포 시작"
448
+ m wf_preview.r_deploy_head_pr
372
449
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
373
- echo "📦 프로젝트: ${PROJECT_NAME}"
374
- echo "🔢 PR 번호: #${PR_NUMBER}"
375
- echo "📛 컨테이너: ${CONTAINER_NAME}"
376
- echo "🌐 도메인: ${DOMAIN}"
450
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
451
+ m wf_preview.r_num_pr pr_number="${PR_NUMBER}"
452
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
453
+ m wf_preview.r_domain domain="${DOMAIN}"
377
454
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
378
455
 
379
- echo "📥 Docker 이미지 Pull 중..."
456
+ m wf_preview.r_image_pulling
380
457
  SUDO docker pull "${IMAGE}"
381
458
 
382
- echo "🗑️ 기존 컨테이너 정리 중..."
459
+ m wf_preview.r_existing_cleanup
383
460
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
384
461
 
385
462
  VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
@@ -387,7 +464,7 @@ jobs:
387
464
  VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
388
465
  fi
389
466
 
390
- echo "🐳 새 컨테이너 실행 중..."
467
+ m wf_preview.r_container_starting
391
468
  SUDO docker run -d \
392
469
  --name "${CONTAINER_NAME}" \
393
470
  --network "${TRAEFIK_NETWORK}" \
@@ -400,11 +477,11 @@ jobs:
400
477
  $VOLUME_OPTS \
401
478
  "${IMAGE}"
402
479
 
403
- # Health Check (최대 120초 대기) - 하이브리드 방식
404
- # 1. HEALTH_CHECK_PATH가 설정되어 있으면 HTTP 체크 시도
405
- # 2. 폴백: 로그 패턴 매칭 (HEALTH_CHECK_LOG_PATTERN)
480
+ # Health check (waits up to 120s) - hybrid approach
481
+ # 1. Try the HTTP check when HEALTH_CHECK_PATH is set
482
+ # 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
406
483
  echo ""
407
- echo "⏳ Health Check 시작 (최대 120초 대기)..."
484
+ m wf_preview.r_hc_start
408
485
  HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
409
486
  LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
410
487
  MAX_RETRIES=24
@@ -416,13 +493,13 @@ jobs:
416
493
  sleep 5
417
494
  RETRY_COUNT=$((RETRY_COUNT + 1))
418
495
 
419
- # 1. 컨테이너 상태 확인
496
+ # 1. Check the container state
420
497
  STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
421
498
 
422
499
  if [ "$STATUS" = "exited" ]; then
423
- echo "❌ 컨테이너 비정상 종료!"
500
+ m wf_preview.r_container_exited
424
501
  echo ""
425
- echo "📋 컨테이너 로그 (최근 100줄):"
502
+ m wf_preview.r_container_logs
426
503
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
427
504
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
428
505
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -430,24 +507,24 @@ jobs:
430
507
  fi
431
508
 
432
509
  if [ "$STATUS" = "running" ]; then
433
- # 2. HTTP Health Check 시도 (HEALTH_CHECK_PATH가 설정된 경우에만)
510
+ # 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
434
511
  if [ -n "$HEALTH_PATH" ]; then
435
512
  HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
436
513
 
437
514
  if [ -n "$HEALTH" ]; then
438
- echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
515
+ m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
439
516
  HEALTH_CHECK_PASSED=true
440
517
  HEALTH_CHECK_METHOD="HTTP"
441
518
  break
442
519
  fi
443
520
  fi
444
521
 
445
- # 3. HTTP 응답 없으면 로그 패턴 매칭으로 폴백
522
+ # 3. Without an HTTP response, fall back to log pattern matching
446
523
  if [ -n "$LOG_PATTERN" ]; then
447
524
  STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
448
525
 
449
526
  if [ -n "$STARTED" ]; then
450
- echo "✅ 정상 기동 확인! (로그 패턴)"
527
+ m wf_preview.r_hc_ok_log
451
528
  echo " $STARTED"
452
529
  HEALTH_CHECK_PASSED=true
453
530
  HEALTH_CHECK_METHOD="Log"
@@ -456,14 +533,14 @@ jobs:
456
533
  fi
457
534
  fi
458
535
 
459
- echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
536
+ m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
460
537
  done
461
538
 
462
539
  if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
463
540
  echo ""
464
- echo "❌ Health Check 타임아웃 (120초)"
541
+ m wf_preview.r_hc_timeout
465
542
  echo ""
466
- echo "📋 컨테이너 로그 (최근 100줄):"
543
+ m wf_preview.r_container_logs
467
544
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
468
545
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
469
546
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -471,13 +548,16 @@ jobs:
471
548
  fi
472
549
 
473
550
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
474
- echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
551
+ m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
475
552
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
476
553
 
477
- - name: 배포 완료 코멘트
554
+ - name: Deploy complete comment
478
555
  uses: actions/github-script@v9
479
556
  with:
480
557
  script: |
558
+ const M = JSON.parse(process.env.PAW_MSG);
559
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
560
+
481
561
  const commentId = ${{ steps.progress.outputs.comment_id }};
482
562
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
483
563
  const deployStart = ${{ steps.docker_progress.outputs.deploy_start }};
@@ -485,7 +565,7 @@ jobs:
485
565
  const deployElapsed = now - deployStart;
486
566
  const minutes = Math.floor(deployElapsed / 60000);
487
567
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
488
- const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
568
+ const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
489
569
 
490
570
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
491
571
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -496,40 +576,40 @@ jobs:
496
576
  const previewUrl = `http://${domain}:${previewPort}`;
497
577
  const sha = '${{ steps.pr.outputs.sha }}';
498
578
 
499
- // Preview 환경 테이블 구성 (API_DOCS_PATH가 있을 때만 API Docs 행 추가)
579
+ // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
500
580
  const envRows = [
501
581
  `| **Preview URL** | ${previewUrl} |`,
502
582
  ];
503
583
  if (apiDocsPath) {
504
584
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
505
585
  }
506
- envRows.push(`| **컨테이너** | \`${projectName}-pr-${prNumber}\` |`);
507
- envRows.push(`| **커밋** | \`${sha}\` |`);
586
+ envRows.push(m('wf_preview.env_container_pr', {projectName, prNumber}));
587
+ envRows.push(m('wf_preview.env_commit', {sha}));
508
588
 
509
589
  const branchName = '${{ steps.pr.outputs.ref }}';
510
590
 
511
591
  const body = [
512
- '## ✅ PR Preview 배포 완료!',
592
+ m('wf_preview.title_pr_deployed'),
513
593
  '',
514
- '| 단계 | 상태 | 소요 시간 |',
594
+ m('wf_preview.tbl_steps'),
515
595
  '|------|------|----------|',
516
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
517
- `| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
596
+ m('wf_preview.row_docker_done', {dockerDuration}),
597
+ m('wf_preview.row_deploy_done', {deployDuration}),
518
598
  '',
519
- '### 🌐 Preview 환경',
520
- '| 항목 | 값 |',
599
+ m('wf_preview.sec_env'),
600
+ m('wf_preview.tbl_items'),
521
601
  '|------|-----|',
522
602
  ...envRows,
523
603
  '',
524
- '### 📋 명령어',
525
- '| 명령어 | 설명 |',
604
+ m('wf_preview.sec_commands'),
605
+ m('wf_preview.tbl_commands'),
526
606
  '|--------|------|',
527
- '| `/wizard server build` | 최신 커밋으로 재배포 |',
528
- '| `/wizard server destroy` | Preview 환경 삭제 |',
529
- '| `/wizard server status` | 현재 상태 확인 |',
607
+ m('wf_preview.cmd_build'),
608
+ m('wf_preview.cmd_destroy'),
609
+ m('wf_preview.cmd_status'),
530
610
  '',
531
611
  '<details>',
532
- '<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
612
+ m('wf_preview.cmd_advanced'),
533
613
  '',
534
614
  '```',
535
615
  `/wizard server build ${branchName}`,
@@ -539,7 +619,7 @@ jobs:
539
619
  '</details>',
540
620
  '',
541
621
  '---',
542
- '*🤖 이 댓글은 PR Preview 시스템에 의해 자동 생성되었습니다.*'
622
+ m('wf_preview.footer_pr')
543
623
  ].join('\n');
544
624
 
545
625
  await github.rest.issues.updateComment({
@@ -549,64 +629,67 @@ jobs:
549
629
  body: body
550
630
  });
551
631
 
552
- - name: 빌드/배포 실패 시 에러 코멘트
632
+ - name: Build/deploy failure comment
553
633
  if: failure()
554
634
  uses: actions/github-script@v9
555
635
  with:
556
636
  script: |
637
+ const M = JSON.parse(process.env.PAW_MSG);
638
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
639
+
557
640
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
558
641
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
559
642
  const prNumber = context.issue.number;
560
643
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
561
644
 
562
- // 진행 상황에 따라 어디서 실패했는지 표시
645
+ // Show where it failed based on progress
563
646
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
564
647
 
565
- // 실패 지점 판단 - 초기 단계 실패도 구분
648
+ // Work out where it failed - early-stage failures are distinguished too
566
649
  let dockerStatus, deployStatus;
567
650
  let dockerTime = '-';
568
651
 
569
652
  if (!commentId) {
570
- // 진행 상황 댓글 생성 전 실패 (초기화 단계)
571
- dockerStatus = '⚠️ 초기화 실패';
653
+ // Failed before the progress comment was created (initialization stage)
654
+ dockerStatus = m('wf_preview.status_init_failed');
572
655
  deployStatus = '-';
573
656
  } else if (!dockerDuration || dockerDuration === '') {
574
- // Docker 빌드 단계에서 실패
575
- dockerStatus = '❌ 실패';
576
- deployStatus = '⏸️ 대기';
657
+ // Failed at the Docker build stage
658
+ dockerStatus = m('wf_preview.status_failed');
659
+ deployStatus = m('wf_preview.status_waiting');
577
660
  } else {
578
- // 배포 단계에서 실패
579
- dockerStatus = '✅ 완료';
580
- deployStatus = '❌ 실패';
661
+ // Failed at the deploy stage
662
+ dockerStatus = m('wf_preview.status_done');
663
+ deployStatus = m('wf_preview.status_failed');
581
664
  dockerTime = dockerDuration;
582
665
  }
583
666
 
584
667
  const body = [
585
- '## ❌ PR Preview 배포 실패!',
668
+ m('wf_preview.title_pr_failed'),
586
669
  '',
587
- '| 단계 | 상태 | 소요 시간 |',
670
+ m('wf_preview.tbl_steps'),
588
671
  '|------|------|----------|',
589
- `| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
590
- `| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
672
+ m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
673
+ m('wf_preview.row_deploy_status', {deployStatus}),
591
674
  '',
592
- `**[📋 빌드/배포 로그 확인](${runUrl})**`,
675
+ m('wf_preview.link_build_logs', {runUrl}),
593
676
  '',
594
- '### 🔍 가능한 원인',
595
- '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
596
- '- Docker 이미지 빌드 실패 (Go 의존성/빌드 문제)',
597
- '- 컨테이너 시작 실패 (애플리케이션 기동 오류)',
598
- '- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
599
- '- 환경변수 누락 (.env 파일 설정 확인)',
677
+ m('wf_preview.sec_causes'),
678
+ m('wf_preview.hint_secrets'),
679
+ m('wf_preview.hint_docker_go'),
680
+ m('wf_preview.hint_start_go'),
681
+ m('wf_preview.hint_health_timeout'),
682
+ m('wf_preview.hint_env_missing'),
600
683
  '',
601
- '### 💡 다음 단계',
602
- '1. 위 링크에서 빌드/배포 로그를 확인하세요',
603
- '2. 문제를 수정한 후 다시 시도하세요: `/wizard server build`',
684
+ m('wf_preview.sec_next'),
685
+ m('wf_preview.next_logs'),
686
+ m('wf_preview.next_retry'),
604
687
  '',
605
688
  '---',
606
- '*🤖 이 댓글은 PR Preview 시스템에 의해 자동 생성되었습니다.*'
689
+ m('wf_preview.footer_pr')
607
690
  ].join('\n');
608
691
 
609
- // 진행 상황 댓글이 있으면 업데이트, 없으면 새로 생성
692
+ // Update the progress comment if it exists, otherwise create a new one
610
693
  if (commentId) {
611
694
  await github.rest.issues.updateComment({
612
695
  owner: context.repo.owner,
@@ -623,9 +706,9 @@ jobs:
623
706
  });
624
707
  }
625
708
 
626
- # Job 2-2: Issue에서 브랜치명 추출
709
+ # Job 2-2: extract the branch name from the issue
627
710
  get-branch-from-issue:
628
- name: Issue에서 브랜치 추출
711
+ name: Extract branch from issue
629
712
  needs: check-command
630
713
  if: |
631
714
  needs.check-command.outputs.is_valid == 'true' &&
@@ -637,30 +720,54 @@ jobs:
637
720
  found: ${{ steps.extract.outputs.found }}
638
721
  issue_number: ${{ steps.extract.outputs.issue_number }}
639
722
  steps:
640
- - name: Issue 댓글에서 브랜치명 추출
723
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
724
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
725
+ - name: Checkout messages
726
+ uses: actions/checkout@v7
727
+ with:
728
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
729
+ sparse-checkout: |
730
+ .github/scripts/messages.py
731
+ version.yml
732
+ sparse-checkout-cone-mode: false
733
+
734
+ - name: Load messages
735
+ run: |
736
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
737
+ mkdir -p "$RUNNER_TEMP/paw"
738
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
739
+ {
740
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
741
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
742
+ } >> "$GITHUB_ENV"
743
+
744
+ - name: Extract branch name from issue comment
641
745
  id: extract
642
746
  uses: actions/github-script@v9
643
747
  with:
644
748
  script: |
749
+ const M = JSON.parse(process.env.PAW_MSG);
750
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
751
+
645
752
  const issueNumber = context.issue.number;
646
753
  const marker = '${{ env.ISSUE_HELPER_MARKER }}';
647
754
 
648
- console.log(`🔍 Issue #${issueNumber}에서 브랜치 검색 중...`);
649
- console.log(`📝 마커: ${marker}`);
755
+ console.log(m('wf_preview.log_search_branch', {issueNumber}));
756
+ console.log(m('wf_preview.log_marker', {marker}));
650
757
 
651
- // Issue의 모든 댓글 조회
758
+ // Fetch all comments of the issue
652
759
  const comments = await github.rest.issues.listComments({
653
760
  owner: context.repo.owner,
654
761
  repo: context.repo.repo,
655
762
  issue_number: issueNumber
656
763
  });
657
764
 
658
- // 브랜치 추출 패턴 (마커와 무관하게 동일한 형식)
659
- // "### 브랜치" 또는 "### 브랜치명" 둘 다 매칭
660
- const branchRegex = /###\s*브랜치(?:명)?\s*\n```\s*\n([^\n]+)\s*\n```/;
765
+ // Branch extraction pattern (same format regardless of the marker)
766
+ // Match the issue helper heading in either language: "### Branch name" (en) or the Korean "### \uBE0C\uB79C\uCE58(\uBA85)" (ko)
767
+ const branchRegex = /###\s*(?:\uBE0C\uB79C\uCE58(?:\uBA85)?|Branch(?: name)?)\s*\n```\s*\n([^\n]+)\s*\n```/i;
661
768
 
662
769
  for (const comment of comments.data) {
663
- // 마커가 포함된 댓글 찾기
770
+ // Find comments that contain the marker
664
771
  if (comment.body.includes(marker)) {
665
772
  const match = comment.body.match(branchRegex);
666
773
  if (match) {
@@ -668,40 +775,43 @@ jobs:
668
775
  core.setOutput('branch', branchName);
669
776
  core.setOutput('found', 'true');
670
777
  core.setOutput('issue_number', issueNumber);
671
- console.log(`✅ 브랜치 발견: ${branchName}`);
778
+ console.log(m('wf_preview.log_branch_found', {branchName}));
672
779
  return;
673
780
  }
674
781
  }
675
782
  }
676
783
 
677
- // 브랜치를 찾지 못한 경우 - 에러가 아님, graceful하게 처리
784
+ // Branch not found - not an error, handle it gracefully
678
785
  core.setOutput('found', 'false');
679
786
  core.setOutput('issue_number', issueNumber);
680
- console.log('⚠️ Issue Helper 댓글에서 브랜치를 찾을 수 없습니다.');
787
+ console.log(m('wf_preview.log_helper_no_branch'));
681
788
 
682
- - name: 브랜치 없음 알림
789
+ - name: Branch not found notice
683
790
  if: steps.extract.outputs.found == 'false'
684
791
  uses: actions/github-script@v9
685
792
  with:
686
793
  script: |
794
+ const M = JSON.parse(process.env.PAW_MSG);
795
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
796
+
687
797
  const issueNumber = context.issue.number;
688
798
  const marker = '${{ env.ISSUE_HELPER_MARKER }}';
689
799
 
690
800
  const body = [
691
- '## ⚠️ 브랜치를 찾을 수 없습니다',
801
+ m('wf_preview.title_branch_missing_warn'),
692
802
  '',
693
- '| 항목 | 값 |',
803
+ m('wf_preview.tbl_items'),
694
804
  '|------|-----|',
695
805
  `| **Issue** | #${issueNumber} |`,
696
- `| **마커** | \`${marker}\` |`,
806
+ m('wf_preview.env_marker', {marker}),
697
807
  '',
698
- '### 💡 확인 사항',
699
- '1. Issue Helper 댓글이 존재하는지 확인하세요',
700
- '2. 댓글에 `### 브랜치` 섹션이 있는지 확인하세요',
701
- '3. `ISSUE_HELPER_MARKER` 환경변수가 올바른지 확인하세요',
808
+ m('wf_preview.sec_check'),
809
+ m('wf_preview.chk_helper_exists'),
810
+ m('wf_preview.chk_helper_section'),
811
+ m('wf_preview.chk_helper_marker'),
702
812
  '',
703
813
  '---',
704
- '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
814
+ m('wf_preview.footer_generic')
705
815
  ].join('\n');
706
816
 
707
817
  await github.rest.issues.createComment({
@@ -711,9 +821,9 @@ jobs:
711
821
  body: body
712
822
  });
713
823
 
714
- # Job 2-3: 빌드 & 배포 (Issue 댓글에서 실행)
824
+ # Job 2-3: build & deploy (triggered from an issue comment)
715
825
  build-preview-issue:
716
- name: Preview 빌드 & 배포 (Issue)
826
+ name: Preview build & deploy (Issue)
717
827
  needs: [check-command, get-branch-from-issue]
718
828
  if: |
719
829
  needs.check-command.outputs.is_valid == 'true' &&
@@ -723,15 +833,62 @@ jobs:
723
833
  needs.get-branch-from-issue.outputs.found == 'true'
724
834
  runs-on: ubuntu-latest
725
835
  steps:
726
- - name: 브랜치 존재 확인
836
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
837
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
838
+ - name: Checkout messages
839
+ uses: actions/checkout@v7
840
+ with:
841
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
842
+ sparse-checkout: |
843
+ .github/scripts/messages.py
844
+ version.yml
845
+ sparse-checkout-cone-mode: false
846
+
847
+ - name: Load messages
848
+ run: |
849
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
850
+ mkdir -p "$RUNNER_TEMP/paw"
851
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
852
+ {
853
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
854
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
855
+ } >> "$GITHUB_ENV"
856
+ # The deploy script runs on the remote server, where messages.py does not exist:
857
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
858
+ python3 - >> "$GITHUB_ENV" <<'PY'
859
+ import json, os, shlex, subprocess
860
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
861
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
862
+ head = ["m() {", ' case "$1" in']
863
+ for key, text in msgs.items():
864
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
865
+ tail = r''' *) _t="$1" ;;
866
+ esac
867
+ shift
868
+ while [ "$#" -gt 0 ]; do
869
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
870
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
871
+ shift
872
+ done
873
+ printf '%s\n' "$_t"
874
+ }'''
875
+ print("PAW_REMOTE<<PAW_EOF")
876
+ print("\n".join(head) + "\n" + tail)
877
+ print("PAW_EOF")
878
+ PY
879
+
880
+ - name: Check branch exists
727
881
  id: check_branch
728
882
  uses: actions/github-script@v9
729
883
  with:
730
884
  script: |
885
+ const M = JSON.parse(process.env.PAW_MSG);
886
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
887
+
731
888
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
732
889
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
733
890
 
734
- console.log(`🔍 브랜치 존재 확인: ${branchName}`);
891
+ console.log(m('wf_preview.log_check_exists', {branchName}));
735
892
 
736
893
  try {
737
894
  await github.rest.repos.getBranch({
@@ -740,28 +897,28 @@ jobs:
740
897
  branch: branchName
741
898
  });
742
899
  core.setOutput('exists', 'true');
743
- console.log(`✅ 브랜치 존재: ${branchName}`);
900
+ console.log(m('wf_preview.log_branch_exists', {branchName}));
744
901
  } catch (error) {
745
902
  if (error.status === 404) {
746
903
  core.setOutput('exists', 'false');
747
- console.log(`❌ 브랜치 없음: ${branchName}`);
904
+ console.log(m('wf_preview.log_branch_missing', {branchName}));
748
905
 
749
- // 브랜치 없음 알림 댓글
906
+ // Comment for a missing branch
750
907
  const body = [
751
- '## ❌ 브랜치를 찾을 수 없습니다',
908
+ m('wf_preview.title_branch_missing'),
752
909
  '',
753
- '| 항목 | 값 |',
910
+ m('wf_preview.tbl_items'),
754
911
  '|------|-----|',
755
912
  `| **Issue** | #${issueNumber} |`,
756
- `| **브랜치** | \`${branchName}\` |`,
913
+ m('wf_preview.env_branch', {branchName}),
757
914
  '',
758
- '### 💡 확인 사항',
759
- '1. 브랜치가 push되었는지 확인하세요',
760
- '2. 브랜치명이 정확한지 확인하세요',
761
- '3. 브랜치가 삭제되지 않았는지 확인하세요',
915
+ m('wf_preview.sec_check'),
916
+ m('wf_preview.chk_pushed_1'),
917
+ m('wf_preview.chk_name_2'),
918
+ m('wf_preview.chk_deleted_3'),
762
919
  '',
763
920
  '---',
764
- '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
921
+ m('wf_preview.footer_generic')
765
922
  ].join('\n');
766
923
 
767
924
  await github.rest.issues.createComment({
@@ -775,47 +932,52 @@ jobs:
775
932
  }
776
933
  }
777
934
 
778
- - name: 브랜치 없으면 중단
935
+ - name: Skip when branch is missing
779
936
  if: steps.check_branch.outputs.exists == 'false'
780
937
  run: |
781
- echo "⚠️ 브랜치가 존재하지 않아 빌드를 건너뜁니다."
938
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
939
+
940
+ m wf_preview.skip_no_branch
782
941
  exit 0
783
942
 
784
- - name: Issue 정보 설정
943
+ - name: Set issue info
785
944
  if: steps.check_branch.outputs.exists == 'true'
786
945
  id: issue
787
946
  run: |
788
947
  echo "ref=${{ needs.get-branch-from-issue.outputs.branch_name }}" >> $GITHUB_OUTPUT
789
948
  echo "issue_number=${{ needs.get-branch-from-issue.outputs.issue_number }}" >> $GITHUB_OUTPUT
790
- # 브랜치의 최신 커밋 SHA 가져오기
949
+ # Get the latest commit SHA of the branch
791
950
  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
792
951
 
793
- # 진행 상황 댓글 시스템
794
- - name: 진행 상황 댓글 생성
952
+ # Progress comment system
953
+ - name: Create progress comment
795
954
  if: steps.check_branch.outputs.exists == 'true'
796
955
  id: progress
797
956
  uses: actions/github-script@v9
798
957
  with:
799
958
  script: |
959
+ const M = JSON.parse(process.env.PAW_MSG);
960
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
961
+
800
962
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
801
963
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
802
964
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
803
965
  const startTime = Date.now();
804
966
 
805
967
  const body = [
806
- '## 🚀 Issue Preview 빌드 중...',
968
+ m('wf_preview.title_issue_building'),
807
969
  '',
808
- `**브랜치**: \`${branchName}\``,
970
+ m('wf_preview.line_branch', {branchName}),
809
971
  '',
810
- '| 단계 | 상태 | 소요 시간 |',
972
+ m('wf_preview.tbl_steps'),
811
973
  '|------|------|----------|',
812
- '| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
813
- '| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
974
+ m('wf_preview.row_docker_progress'),
975
+ m('wf_preview.row_deploy_wait'),
814
976
  '',
815
- `**[📋 실시간 로그 보기](${runUrl})**`,
977
+ m('wf_preview.link_logs', {runUrl}),
816
978
  '',
817
979
  '---',
818
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
980
+ m('wf_preview.footer_progress')
819
981
  ].join('\n');
820
982
 
821
983
  const { data: comment } = await github.rest.issues.createComment({
@@ -829,14 +991,14 @@ jobs:
829
991
  core.setOutput('start_time', startTime);
830
992
  core.setOutput('docker_start', startTime);
831
993
 
832
- - name: 코드 체크아웃
994
+ - name: Checkout code
833
995
  if: steps.check_branch.outputs.exists == 'true'
834
996
  uses: actions/checkout@v7
835
997
  with:
836
998
  ref: ${{ needs.get-branch-from-issue.outputs.branch_name }}
837
999
 
838
- # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
839
- - name: 배포 사전 점검 (Secret·Dockerfile)
1000
+ # 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
1001
+ - name: Deploy pre-check (Secrets and Dockerfile)
840
1002
  if: steps.check_branch.outputs.exists == 'true'
841
1003
  working-directory: ${{ env.PROJECT_PATH }}
842
1004
  env:
@@ -848,11 +1010,13 @@ jobs:
848
1010
  SSH_KEY: ${{ secrets.SSH_KEY }}
849
1011
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
850
1012
  run: |
1013
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1014
+
851
1015
  MISSING=""
852
1016
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
853
1017
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
854
1018
  done
855
- # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
1019
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
856
1020
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
857
1021
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
858
1022
  else
@@ -860,40 +1024,40 @@ jobs:
860
1024
  fi
861
1025
  FAILED=false
862
1026
  if [ -n "$MISSING" ]; then
863
- echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
1027
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
864
1028
  FAILED=true
865
1029
  fi
866
1030
  if [ ! -f "$DOCKERFILE" ]; then
867
- echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
1031
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
868
1032
  FAILED=true
869
1033
  fi
870
1034
  if [ "$FAILED" = "true" ]; then
871
1035
  exit 1
872
1036
  fi
873
- echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
1037
+ m wf_preview.precheck_ok
874
1038
 
875
- # [영역 2] 환경변수 파일 생성
876
- - name: "[필수] .env 파일 생성"
1039
+ # [Section 2] Create the environment file
1040
+ - name: "[Required] Create .env file"
877
1041
  working-directory: ${{ env.PROJECT_PATH }}
878
1042
  if: steps.check_branch.outputs.exists == 'true'
879
1043
  run: |
880
1044
  cat << 'EOF' > .env
881
1045
  ${{ secrets.ENV_FILE }}
882
1046
  EOF
883
- # [영역 2 끝] 환경변수 파일 생성 끝
1047
+ # [End of section 2] Create the environment file
884
1048
 
885
- - name: Docker 로그인
1049
+ - name: Log in to Docker Hub
886
1050
  if: steps.check_branch.outputs.exists == 'true'
887
1051
  uses: docker/login-action@v3
888
1052
  with:
889
1053
  username: ${{ secrets.DOCKERHUB_USERNAME }}
890
1054
  password: ${{ secrets.DOCKERHUB_TOKEN }}
891
1055
 
892
- - name: Docker Buildx 설정
1056
+ - name: Set up Docker Buildx
893
1057
  if: steps.check_branch.outputs.exists == 'true'
894
1058
  uses: docker/setup-buildx-action@v3
895
1059
 
896
- - name: Docker 이미지 빌드 & Push
1060
+ - name: Build & push Docker image
897
1061
  if: steps.check_branch.outputs.exists == 'true'
898
1062
  uses: docker/build-push-action@v5
899
1063
  with:
@@ -904,12 +1068,15 @@ jobs:
904
1068
  cache-from: type=gha
905
1069
  cache-to: type=gha,mode=max
906
1070
 
907
- - name: 진행 상황 - Docker 완료
1071
+ - name: Progress - Docker done
908
1072
  if: steps.check_branch.outputs.exists == 'true'
909
1073
  id: docker_progress
910
1074
  uses: actions/github-script@v9
911
1075
  with:
912
1076
  script: |
1077
+ const M = JSON.parse(process.env.PAW_MSG);
1078
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1079
+
913
1080
  const commentId = ${{ steps.progress.outputs.comment_id }};
914
1081
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
915
1082
  const dockerStart = ${{ steps.progress.outputs.docker_start }};
@@ -917,23 +1084,23 @@ jobs:
917
1084
  const dockerElapsed = now - dockerStart;
918
1085
  const minutes = Math.floor(dockerElapsed / 60000);
919
1086
  const seconds = Math.floor((dockerElapsed % 60000) / 1000);
920
- const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
1087
+ const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
921
1088
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
922
1089
 
923
1090
  const body = [
924
- '## 🚀 Issue Preview 빌드 중...',
1091
+ m('wf_preview.title_issue_building'),
925
1092
  '',
926
- `**브랜치**: \`${branchName}\``,
1093
+ m('wf_preview.line_branch', {branchName}),
927
1094
  '',
928
- '| 단계 | 상태 | 소요 시간 |',
1095
+ m('wf_preview.tbl_steps'),
929
1096
  '|------|------|----------|',
930
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
931
- '| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
1097
+ m('wf_preview.row_docker_done', {dockerDuration}),
1098
+ m('wf_preview.row_deploy_progress'),
932
1099
  '',
933
- `**[📋 실시간 로그 보기](${runUrl})**`,
1100
+ m('wf_preview.link_logs', {runUrl}),
934
1101
  '',
935
1102
  '---',
936
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
1103
+ m('wf_preview.footer_progress')
937
1104
  ].join('\n');
938
1105
 
939
1106
  await github.rest.issues.updateComment({
@@ -946,7 +1113,7 @@ jobs:
946
1113
  core.setOutput('docker_duration', dockerDuration);
947
1114
  core.setOutput('deploy_start', now);
948
1115
 
949
- - name: 서버에 배포
1116
+ - name: Deploy to server
950
1117
  if: steps.check_branch.outputs.exists == 'true'
951
1118
  uses: appleboy/ssh-action@v1.0.3
952
1119
  env:
@@ -959,19 +1126,21 @@ jobs:
959
1126
  port: ${{ env.SSH_PORT }}
960
1127
  envs: SSH_AUTH_METHOD
961
1128
  script: |
1129
+ ${{ env.PAW_REMOTE }}
1130
+
962
1131
  set -e
963
1132
 
964
1133
  export PATH=$PATH:/usr/local/bin
965
1134
  export PW="${{ secrets.SERVER_PASSWORD }}"
966
1135
 
967
- # SSH 인증 방식에 따른 sudo 추상화
1136
+ # sudo wrapper that depends on the SSH auth method
968
1137
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
969
1138
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
970
1139
  SUDO() { sudo "$@"; }
971
1140
  else
972
1141
  SUDO() { echo "$PW" | sudo -S "$@"; }
973
1142
  fi
974
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
1143
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
975
1144
 
976
1145
  ISSUE_NUMBER=${{ needs.get-branch-from-issue.outputs.issue_number }}
977
1146
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -982,18 +1151,18 @@ jobs:
982
1151
  TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
983
1152
 
984
1153
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
985
- echo "🚀 Issue Preview 배포 시작"
1154
+ m wf_preview.r_deploy_head_issue
986
1155
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
987
- echo "📦 프로젝트: ${PROJECT_NAME}"
988
- echo "🔢 Issue 번호: #${ISSUE_NUMBER}"
989
- echo "📛 컨테이너: ${CONTAINER_NAME}"
990
- echo "🌐 도메인: ${DOMAIN}"
1156
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
1157
+ m wf_preview.r_num_issue issue_number="${ISSUE_NUMBER}"
1158
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
1159
+ m wf_preview.r_domain domain="${DOMAIN}"
991
1160
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
992
1161
 
993
- echo "📥 Docker 이미지 Pull 중..."
1162
+ m wf_preview.r_image_pulling
994
1163
  SUDO docker pull "${IMAGE}"
995
1164
 
996
- echo "🗑️ 기존 컨테이너 정리 중..."
1165
+ m wf_preview.r_existing_cleanup
997
1166
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
998
1167
 
999
1168
  VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
@@ -1001,7 +1170,7 @@ jobs:
1001
1170
  VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
1002
1171
  fi
1003
1172
 
1004
- echo "🐳 새 컨테이너 실행 중..."
1173
+ m wf_preview.r_container_starting
1005
1174
  SUDO docker run -d \
1006
1175
  --name "${CONTAINER_NAME}" \
1007
1176
  --network "${TRAEFIK_NETWORK}" \
@@ -1014,11 +1183,11 @@ jobs:
1014
1183
  $VOLUME_OPTS \
1015
1184
  "${IMAGE}"
1016
1185
 
1017
- # Health Check (최대 120초 대기) - 하이브리드 방식
1018
- # 1. HEALTH_CHECK_PATH가 설정되어 있으면 HTTP 체크 시도
1019
- # 2. 폴백: 로그 패턴 매칭 (HEALTH_CHECK_LOG_PATTERN)
1186
+ # Health check (waits up to 120s) - hybrid approach
1187
+ # 1. Try the HTTP check when HEALTH_CHECK_PATH is set
1188
+ # 2. Fallback: log pattern match (HEALTH_CHECK_LOG_PATTERN)
1020
1189
  echo ""
1021
- echo "⏳ Health Check 시작 (최대 120초 대기)..."
1190
+ m wf_preview.r_hc_start
1022
1191
  HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
1023
1192
  LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
1024
1193
  MAX_RETRIES=24
@@ -1030,13 +1199,13 @@ jobs:
1030
1199
  sleep 5
1031
1200
  RETRY_COUNT=$((RETRY_COUNT + 1))
1032
1201
 
1033
- # 1. 컨테이너 상태 확인
1202
+ # 1. Check the container state
1034
1203
  STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
1035
1204
 
1036
1205
  if [ "$STATUS" = "exited" ]; then
1037
- echo "❌ 컨테이너 비정상 종료!"
1206
+ m wf_preview.r_container_exited
1038
1207
  echo ""
1039
- echo "📋 컨테이너 로그 (최근 100줄):"
1208
+ m wf_preview.r_container_logs
1040
1209
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1041
1210
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1042
1211
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1044,24 +1213,24 @@ jobs:
1044
1213
  fi
1045
1214
 
1046
1215
  if [ "$STATUS" = "running" ]; then
1047
- # 2. HTTP Health Check 시도 (HEALTH_CHECK_PATH가 설정된 경우에만)
1216
+ # 2. Try the HTTP health check (only when HEALTH_CHECK_PATH is set)
1048
1217
  if [ -n "$HEALTH_PATH" ]; then
1049
1218
  HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
1050
1219
 
1051
1220
  if [ -n "$HEALTH" ]; then
1052
- echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
1221
+ m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
1053
1222
  HEALTH_CHECK_PASSED=true
1054
1223
  HEALTH_CHECK_METHOD="HTTP"
1055
1224
  break
1056
1225
  fi
1057
1226
  fi
1058
1227
 
1059
- # 3. HTTP 응답 없으면 로그 패턴 매칭으로 폴백
1228
+ # 3. Without an HTTP response, fall back to log pattern matching
1060
1229
  if [ -n "$LOG_PATTERN" ]; then
1061
1230
  STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
1062
1231
 
1063
1232
  if [ -n "$STARTED" ]; then
1064
- echo "✅ 정상 기동 확인! (로그 패턴)"
1233
+ m wf_preview.r_hc_ok_log
1065
1234
  echo " $STARTED"
1066
1235
  HEALTH_CHECK_PASSED=true
1067
1236
  HEALTH_CHECK_METHOD="Log"
@@ -1070,14 +1239,14 @@ jobs:
1070
1239
  fi
1071
1240
  fi
1072
1241
 
1073
- echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
1242
+ m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
1074
1243
  done
1075
1244
 
1076
1245
  if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
1077
1246
  echo ""
1078
- echo "❌ Health Check 타임아웃 (120초)"
1247
+ m wf_preview.r_hc_timeout
1079
1248
  echo ""
1080
- echo "📋 컨테이너 로그 (최근 100줄):"
1249
+ m wf_preview.r_container_logs
1081
1250
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1082
1251
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1083
1252
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1085,14 +1254,17 @@ jobs:
1085
1254
  fi
1086
1255
 
1087
1256
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1088
- echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
1257
+ m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
1089
1258
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1090
1259
 
1091
- - name: 배포 완료 코멘트
1260
+ - name: Deploy complete comment
1092
1261
  if: steps.check_branch.outputs.exists == 'true'
1093
1262
  uses: actions/github-script@v9
1094
1263
  with:
1095
1264
  script: |
1265
+ const M = JSON.parse(process.env.PAW_MSG);
1266
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1267
+
1096
1268
  const commentId = ${{ steps.progress.outputs.comment_id }};
1097
1269
  const branchName = '${{ needs.get-branch-from-issue.outputs.branch_name }}';
1098
1270
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
@@ -1101,7 +1273,7 @@ jobs:
1101
1273
  const deployElapsed = now - deployStart;
1102
1274
  const minutes = Math.floor(deployElapsed / 60000);
1103
1275
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
1104
- const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
1276
+ const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1105
1277
 
1106
1278
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1107
1279
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -1112,41 +1284,41 @@ jobs:
1112
1284
  const previewUrl = `http://${domain}:${previewPort}`;
1113
1285
  const sha = '${{ steps.issue.outputs.sha }}';
1114
1286
 
1115
- // Preview 환경 테이블 구성 (API_DOCS_PATH가 있을 때만 API Docs 행 추가)
1287
+ // Build the preview environment table (the API Docs row is added only when API_DOCS_PATH is set)
1116
1288
  const envRows = [
1117
1289
  `| **Preview URL** | ${previewUrl} |`,
1118
1290
  ];
1119
1291
  if (apiDocsPath) {
1120
1292
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
1121
1293
  }
1122
- envRows.push(`| **컨테이너** | \`${projectName}-pr-${issueNumber}\` |`);
1123
- envRows.push(`| **브랜치** | \`${branchName}\` |`);
1124
- envRows.push(`| **커밋** | \`${sha}\` |`);
1294
+ envRows.push(m('wf_preview.env_container_issue', {projectName, issueNumber}));
1295
+ envRows.push(m('wf_preview.env_branch', {branchName}));
1296
+ envRows.push(m('wf_preview.env_commit', {sha}));
1125
1297
 
1126
1298
  const body = [
1127
- '## ✅ Issue Preview 배포 완료!',
1299
+ m('wf_preview.title_issue_deployed'),
1128
1300
  '',
1129
- `**브랜치**: \`${branchName}\``,
1301
+ m('wf_preview.line_branch', {branchName}),
1130
1302
  '',
1131
- '| 단계 | 상태 | 소요 시간 |',
1303
+ m('wf_preview.tbl_steps'),
1132
1304
  '|------|------|----------|',
1133
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
1134
- `| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
1305
+ m('wf_preview.row_docker_done', {dockerDuration}),
1306
+ m('wf_preview.row_deploy_done', {deployDuration}),
1135
1307
  '',
1136
- '### 🌐 Preview 환경',
1137
- '| 항목 | 값 |',
1308
+ m('wf_preview.sec_env'),
1309
+ m('wf_preview.tbl_items'),
1138
1310
  '|------|-----|',
1139
1311
  ...envRows,
1140
1312
  '',
1141
- '### 📋 명령어',
1142
- '| 명령어 | 설명 |',
1313
+ m('wf_preview.sec_commands'),
1314
+ m('wf_preview.tbl_commands'),
1143
1315
  '|--------|------|',
1144
- '| `/wizard server build` | 최신 커밋으로 재배포 |',
1145
- '| `/wizard server destroy` | Preview 환경 삭제 |',
1146
- '| `/wizard server status` | 현재 상태 확인 |',
1316
+ m('wf_preview.cmd_build'),
1317
+ m('wf_preview.cmd_destroy'),
1318
+ m('wf_preview.cmd_status'),
1147
1319
  '',
1148
1320
  '<details>',
1149
- '<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
1321
+ m('wf_preview.cmd_advanced'),
1150
1322
  '',
1151
1323
  '```',
1152
1324
  `/wizard server build ${branchName}`,
@@ -1156,7 +1328,7 @@ jobs:
1156
1328
  '</details>',
1157
1329
  '',
1158
1330
  '---',
1159
- '*🤖 이 댓글은 Issue Preview 시스템에 의해 자동 생성되었습니다.*'
1331
+ m('wf_preview.footer_issue')
1160
1332
  ].join('\n');
1161
1333
 
1162
1334
  await github.rest.issues.updateComment({
@@ -1166,11 +1338,14 @@ jobs:
1166
1338
  body: body
1167
1339
  });
1168
1340
 
1169
- - name: 빌드/배포 실패 시 에러 코멘트
1341
+ - name: Build/deploy failure comment
1170
1342
  if: failure() && steps.check_branch.outputs.exists == 'true'
1171
1343
  uses: actions/github-script@v9
1172
1344
  with:
1173
1345
  script: |
1346
+ const M = JSON.parse(process.env.PAW_MSG);
1347
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1348
+
1174
1349
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1175
1350
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1176
1351
  const issueNumber = ${{ needs.get-branch-from-issue.outputs.issue_number }};
@@ -1183,42 +1358,42 @@ jobs:
1183
1358
  let dockerTime = '-';
1184
1359
 
1185
1360
  if (!commentId) {
1186
- dockerStatus = '⚠️ 초기화 실패';
1361
+ dockerStatus = m('wf_preview.status_init_failed');
1187
1362
  deployStatus = '-';
1188
1363
  } else if (!dockerDuration || dockerDuration === '') {
1189
- dockerStatus = '❌ 실패';
1190
- deployStatus = '⏸️ 대기';
1364
+ dockerStatus = m('wf_preview.status_failed');
1365
+ deployStatus = m('wf_preview.status_waiting');
1191
1366
  } else {
1192
- dockerStatus = '✅ 완료';
1193
- deployStatus = '❌ 실패';
1367
+ dockerStatus = m('wf_preview.status_done');
1368
+ deployStatus = m('wf_preview.status_failed');
1194
1369
  dockerTime = dockerDuration;
1195
1370
  }
1196
1371
 
1197
1372
  const body = [
1198
- '## ❌ Issue Preview 배포 실패!',
1373
+ m('wf_preview.title_issue_failed'),
1199
1374
  '',
1200
- `**브랜치**: \`${branchName}\``,
1375
+ m('wf_preview.line_branch', {branchName}),
1201
1376
  '',
1202
- '| 단계 | 상태 | 소요 시간 |',
1377
+ m('wf_preview.tbl_steps'),
1203
1378
  '|------|------|----------|',
1204
- `| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
1205
- `| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
1379
+ m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
1380
+ m('wf_preview.row_deploy_status', {deployStatus}),
1206
1381
  '',
1207
- `**[📋 빌드/배포 로그 확인](${runUrl})**`,
1382
+ m('wf_preview.link_build_logs', {runUrl}),
1208
1383
  '',
1209
- '### 🔍 가능한 원인',
1210
- '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
1211
- '- Docker 이미지 빌드 실패 (Go 의존성/빌드 문제)',
1212
- '- 컨테이너 시작 실패 (애플리케이션 기동 오류)',
1213
- '- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
1214
- '- 환경변수 누락 (.env 파일 설정 확인)',
1384
+ m('wf_preview.sec_causes'),
1385
+ m('wf_preview.hint_secrets'),
1386
+ m('wf_preview.hint_docker_go'),
1387
+ m('wf_preview.hint_start_go'),
1388
+ m('wf_preview.hint_health_timeout'),
1389
+ m('wf_preview.hint_env_missing'),
1215
1390
  '',
1216
- '### 💡 다음 단계',
1217
- '1. 위 링크에서 빌드/배포 로그를 확인하세요',
1218
- '2. 문제를 수정한 후 다시 시도하세요: `/wizard server build`',
1391
+ m('wf_preview.sec_next'),
1392
+ m('wf_preview.next_logs'),
1393
+ m('wf_preview.next_retry'),
1219
1394
  '',
1220
1395
  '---',
1221
- '*🤖 이 댓글은 Issue Preview 시스템에 의해 자동 생성되었습니다.*'
1396
+ m('wf_preview.footer_issue')
1222
1397
  ].join('\n');
1223
1398
 
1224
1399
  if (commentId) {
@@ -1237,17 +1412,61 @@ jobs:
1237
1412
  });
1238
1413
  }
1239
1414
 
1240
- # Job 3: Preview 삭제 (PR/Issue 닫힘 또는 destroy 명령)
1415
+ # Job 3: destroy the preview (PR/issue closed or destroy command)
1241
1416
  destroy-preview:
1242
- name: Preview 삭제
1243
- # needs 의존성 제거: PR/Issue 닫힘 이벤트에서도 독립적으로 실행되도록 함
1417
+ name: Destroy preview
1418
+ # No needs dependency: lets this run on its own for PR/issue closed events
1244
1419
  if: |
1245
1420
  (github.event_name == 'pull_request' && github.event.action == 'closed') ||
1246
1421
  (github.event_name == 'issues' && github.event.action == 'closed') ||
1247
1422
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server destroy'))
1248
1423
  runs-on: ubuntu-latest
1249
1424
  steps:
1250
- - name: PR/Issue 번호 가져오기
1425
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1426
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1427
+ - name: Checkout messages
1428
+ uses: actions/checkout@v7
1429
+ with:
1430
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1431
+ sparse-checkout: |
1432
+ .github/scripts/messages.py
1433
+ version.yml
1434
+ sparse-checkout-cone-mode: false
1435
+
1436
+ - name: Load messages
1437
+ run: |
1438
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1439
+ mkdir -p "$RUNNER_TEMP/paw"
1440
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1441
+ {
1442
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1443
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1444
+ } >> "$GITHUB_ENV"
1445
+ # The deploy script runs on the remote server, where messages.py does not exist:
1446
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1447
+ python3 - >> "$GITHUB_ENV" <<'PY'
1448
+ import json, os, shlex, subprocess
1449
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1450
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1451
+ head = ["m() {", ' case "$1" in']
1452
+ for key, text in msgs.items():
1453
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1454
+ tail = r''' *) _t="$1" ;;
1455
+ esac
1456
+ shift
1457
+ while [ "$#" -gt 0 ]; do
1458
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1459
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1460
+ shift
1461
+ done
1462
+ printf '%s\n' "$_t"
1463
+ }'''
1464
+ print("PAW_REMOTE<<PAW_EOF")
1465
+ print("\n".join(head) + "\n" + tail)
1466
+ print("PAW_EOF")
1467
+ PY
1468
+
1469
+ - name: Get PR/issue number
1251
1470
  id: pr_number
1252
1471
  run: |
1253
1472
  if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
@@ -1261,7 +1480,7 @@ jobs:
1261
1480
  echo "type=issue_closed" >> $GITHUB_OUTPUT
1262
1481
  fi
1263
1482
 
1264
- - name: 컨테이너 & 이미지 삭제
1483
+ - name: Remove container & image
1265
1484
  uses: appleboy/ssh-action@v1.0.3
1266
1485
  env:
1267
1486
  SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
@@ -1273,17 +1492,19 @@ jobs:
1273
1492
  port: ${{ env.SSH_PORT }}
1274
1493
  envs: SSH_AUTH_METHOD
1275
1494
  script: |
1495
+ ${{ env.PAW_REMOTE }}
1496
+
1276
1497
  export PATH=$PATH:/usr/local/bin
1277
1498
  export PW="${{ secrets.SERVER_PASSWORD }}"
1278
1499
 
1279
- # SSH 인증 방식에 따른 sudo 추상화
1500
+ # sudo wrapper that depends on the SSH auth method
1280
1501
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
1281
1502
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
1282
1503
  SUDO() { sudo "$@"; }
1283
1504
  else
1284
1505
  SUDO() { echo "$PW" | sudo -S "$@"; }
1285
1506
  fi
1286
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
1507
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
1287
1508
 
1288
1509
  PR_NUMBER=${{ steps.pr_number.outputs.number }}
1289
1510
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}"
@@ -1291,21 +1512,21 @@ jobs:
1291
1512
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:pr-${PR_NUMBER}"
1292
1513
 
1293
1514
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1294
- echo "🗑️ PR Preview 삭제"
1515
+ m wf_preview.r_destroy_head_pr
1295
1516
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1296
- echo "📦 프로젝트: ${PROJECT_NAME}"
1297
- echo "🔢 PR 번호: #${PR_NUMBER}"
1517
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
1518
+ m wf_preview.r_num_pr pr_number="${PR_NUMBER}"
1298
1519
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1299
1520
 
1300
- echo "🐳 컨테이너 삭제 중..."
1521
+ m wf_preview.r_container_removing
1301
1522
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
1302
1523
 
1303
- echo "🖼️ 이미지 삭제 중..."
1524
+ m wf_preview.r_image_removing
1304
1525
  SUDO docker rmi "${IMAGE}" 2>/dev/null || true
1305
1526
 
1306
- echo "✅ 삭제 완료!"
1527
+ m wf_preview.r_removed
1307
1528
 
1308
- - name: 삭제 완료 코멘트
1529
+ - name: Destroy complete comment
1309
1530
  if: |
1310
1531
  github.event_name == 'issue_comment' ||
1311
1532
  (github.event_name == 'pull_request' && github.event.action == 'closed') ||
@@ -1313,21 +1534,24 @@ jobs:
1313
1534
  uses: actions/github-script@v9
1314
1535
  with:
1315
1536
  script: |
1537
+ const M = JSON.parse(process.env.PAW_MSG);
1538
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1539
+
1316
1540
  const number = ${{ steps.pr_number.outputs.number }};
1317
1541
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1318
1542
 
1319
1543
  const body = [
1320
- '## 🗑️ Preview 환경 삭제 완료!',
1544
+ m('wf_preview.title_destroyed'),
1321
1545
  '',
1322
- '| 항목 | 값 |',
1546
+ m('wf_preview.tbl_items'),
1323
1547
  '|------|-----|',
1324
- `| **컨테이너** | \`${projectName}-pr-${number}\` |`,
1325
- '| **상태** | 삭제됨 |',
1548
+ m('wf_preview.env_container_num', {projectName, number}),
1549
+ m('wf_preview.env_status_deleted'),
1326
1550
  '',
1327
- '다시 배포하려면: `/wizard server build`',
1551
+ m('wf_preview.redeploy_hint'),
1328
1552
  '',
1329
1553
  '---',
1330
- '*🤖 이 댓글은 PR/Issue Preview 시스템에 의해 자동 생성되었습니다.*'
1554
+ m('wf_preview.footer_pr_issue')
1331
1555
  ].join('\n');
1332
1556
 
1333
1557
  await github.rest.issues.createComment({
@@ -1337,14 +1561,58 @@ jobs:
1337
1561
  body: body
1338
1562
  });
1339
1563
 
1340
- # Job 4: 상태 확인 (PR/Issue 모두 지원)
1564
+ # Job 4: status check (PR and issue)
1341
1565
  check-status:
1342
- name: Preview 상태 확인
1566
+ name: Preview status
1343
1567
  needs: check-command
1344
1568
  if: needs.check-command.outputs.is_valid == 'true' && needs.check-command.outputs.command == 'status'
1345
1569
  runs-on: ubuntu-latest
1346
1570
  steps:
1347
- - name: 컨테이너 상태 확인
1571
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1572
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1573
+ - name: Checkout messages
1574
+ uses: actions/checkout@v7
1575
+ with:
1576
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1577
+ sparse-checkout: |
1578
+ .github/scripts/messages.py
1579
+ version.yml
1580
+ sparse-checkout-cone-mode: false
1581
+
1582
+ - name: Load messages
1583
+ run: |
1584
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1585
+ mkdir -p "$RUNNER_TEMP/paw"
1586
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1587
+ {
1588
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1589
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1590
+ } >> "$GITHUB_ENV"
1591
+ # The deploy script runs on the remote server, where messages.py does not exist:
1592
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1593
+ python3 - >> "$GITHUB_ENV" <<'PY'
1594
+ import json, os, shlex, subprocess
1595
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1596
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1597
+ head = ["m() {", ' case "$1" in']
1598
+ for key, text in msgs.items():
1599
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1600
+ tail = r''' *) _t="$1" ;;
1601
+ esac
1602
+ shift
1603
+ while [ "$#" -gt 0 ]; do
1604
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1605
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1606
+ shift
1607
+ done
1608
+ printf '%s\n' "$_t"
1609
+ }'''
1610
+ print("PAW_REMOTE<<PAW_EOF")
1611
+ print("\n".join(head) + "\n" + tail)
1612
+ print("PAW_EOF")
1613
+ PY
1614
+
1615
+ - name: Check container status
1348
1616
  id: status
1349
1617
  uses: appleboy/ssh-action@v1.0.3
1350
1618
  with:
@@ -1354,6 +1622,8 @@ jobs:
1354
1622
  key: ${{ secrets.SSH_KEY }}
1355
1623
  port: ${{ env.SSH_PORT }}
1356
1624
  script: |
1625
+ ${{ env.PAW_REMOTE }}
1626
+
1357
1627
  export PATH=$PATH:/usr/local/bin
1358
1628
 
1359
1629
  NUMBER=${{ github.event.issue.number }}
@@ -1361,19 +1631,19 @@ jobs:
1361
1631
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1362
1632
 
1363
1633
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1364
- echo "🔍 Preview 상태 확인"
1634
+ m wf_preview.r_status_head
1365
1635
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1366
1636
 
1367
1637
  if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
1368
1638
  echo "STATUS=running"
1369
- echo "✅ 컨테이너 실행 중"
1639
+ m wf_preview.r_container_running
1370
1640
  docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
1371
1641
  else
1372
1642
  echo "STATUS=not_found"
1373
- echo "❌ 컨테이너 없음"
1643
+ m wf_preview.r_container_absent
1374
1644
  fi
1375
1645
 
1376
- - name: 상태 코멘트 (실행 중)
1646
+ - name: Status check (running)
1377
1647
  uses: appleboy/ssh-action@v1.0.3
1378
1648
  id: check_running
1379
1649
  continue-on-error: true
@@ -1391,10 +1661,13 @@ jobs:
1391
1661
  CONTAINER_NAME="${PROJECT_NAME}-pr-${NUMBER}"
1392
1662
  docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"
1393
1663
 
1394
- - name: 상태 코멘트 작성
1664
+ - name: Post status comment
1395
1665
  uses: actions/github-script@v9
1396
1666
  with:
1397
1667
  script: |
1668
+ const M = JSON.parse(process.env.PAW_MSG);
1669
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1670
+
1398
1671
  const number = context.issue.number;
1399
1672
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1400
1673
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -1408,36 +1681,36 @@ jobs:
1408
1681
  let body;
1409
1682
  if (isRunning) {
1410
1683
  body = [
1411
- '## ✅ Preview 환경 실행 중',
1684
+ m('wf_preview.title_running'),
1412
1685
  '',
1413
- '| 항목 | 값 |',
1686
+ m('wf_preview.tbl_items'),
1414
1687
  '|------|-----|',
1415
1688
  `| **Preview URL** | ${previewUrl} |`,
1416
- `| **컨테이너** | \`${projectName}-pr-${number}\` |`,
1417
- '| **상태** | 🟢 Running |',
1689
+ m('wf_preview.env_container_num', {projectName, number}),
1690
+ m('wf_preview.env_status_running'),
1418
1691
  '',
1419
- '### 📋 명령어',
1420
- '| 명령어 | 설명 |',
1692
+ m('wf_preview.sec_commands'),
1693
+ m('wf_preview.tbl_commands'),
1421
1694
  '|--------|------|',
1422
- '| `/wizard server build` | 최신 커밋으로 재배포 |',
1423
- '| `/wizard server destroy` | Preview 환경 삭제 |',
1695
+ m('wf_preview.cmd_build'),
1696
+ m('wf_preview.cmd_destroy'),
1424
1697
  '',
1425
1698
  '---',
1426
- `*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
1699
+ m('wf_preview.footer_context', {contextType})
1427
1700
  ].join('\n');
1428
1701
  } else {
1429
1702
  body = [
1430
- '## ❌ Preview 환경 없음',
1703
+ m('wf_preview.title_absent'),
1431
1704
  '',
1432
- '| 항목 | 값 |',
1705
+ m('wf_preview.tbl_items'),
1433
1706
  '|------|-----|',
1434
- `| **컨테이너** | \`${projectName}-pr-${number}\` |`,
1435
- '| **상태** | 🔴 Not Found |',
1707
+ m('wf_preview.env_container_num', {projectName, number}),
1708
+ m('wf_preview.env_status_absent'),
1436
1709
  '',
1437
- '배포하려면: `/wizard server build`',
1710
+ m('wf_preview.deploy_hint'),
1438
1711
  '',
1439
1712
  '---',
1440
- `*🤖 이 댓글은 ${contextType} Preview 시스템에 의해 자동 생성되었습니다.*`
1713
+ m('wf_preview.footer_context', {contextType})
1441
1714
  ].join('\n');
1442
1715
  }
1443
1716
 
@@ -1448,9 +1721,9 @@ jobs:
1448
1721
  body: body
1449
1722
  });
1450
1723
 
1451
- # Job 5: 커스텀 브랜치 빌드 & 배포
1724
+ # Job 5: custom branch build & deploy
1452
1725
  build-preview-custom-branch:
1453
- name: Preview 빌드 & 배포 (Custom Branch)
1726
+ name: Preview build & deploy (Custom Branch)
1454
1727
  needs: check-command
1455
1728
  if: |
1456
1729
  needs.check-command.outputs.is_valid == 'true' &&
@@ -1458,43 +1731,90 @@ jobs:
1458
1731
  needs.check-command.outputs.is_custom_branch == 'true'
1459
1732
  runs-on: ubuntu-latest
1460
1733
  steps:
1461
- - name: 브랜치 존재 확인 및 정보 추출
1734
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
1735
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
1736
+ - name: Checkout messages
1737
+ uses: actions/checkout@v7
1738
+ with:
1739
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
1740
+ sparse-checkout: |
1741
+ .github/scripts/messages.py
1742
+ version.yml
1743
+ sparse-checkout-cone-mode: false
1744
+
1745
+ - name: Load messages
1746
+ run: |
1747
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
1748
+ mkdir -p "$RUNNER_TEMP/paw"
1749
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
1750
+ {
1751
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
1752
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1753
+ } >> "$GITHUB_ENV"
1754
+ # The deploy script runs on the remote server, where messages.py does not exist:
1755
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
1756
+ python3 - >> "$GITHUB_ENV" <<'PY'
1757
+ import json, os, shlex, subprocess
1758
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
1759
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
1760
+ head = ["m() {", ' case "$1" in']
1761
+ for key, text in msgs.items():
1762
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
1763
+ tail = r''' *) _t="$1" ;;
1764
+ esac
1765
+ shift
1766
+ while [ "$#" -gt 0 ]; do
1767
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
1768
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
1769
+ shift
1770
+ done
1771
+ printf '%s\n' "$_t"
1772
+ }'''
1773
+ print("PAW_REMOTE<<PAW_EOF")
1774
+ print("\n".join(head) + "\n" + tail)
1775
+ print("PAW_EOF")
1776
+ PY
1777
+
1778
+ - name: Check branch exists and extract info
1462
1779
  id: branch_info
1463
1780
  uses: actions/github-script@v9
1464
1781
  with:
1465
1782
  script: |
1783
+ const M = JSON.parse(process.env.PAW_MSG);
1784
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1785
+
1466
1786
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
1467
1787
  const issueNumber = context.issue.number;
1468
1788
 
1469
- console.log(`🔍 브랜치 확인: ${branchName}`);
1789
+ console.log(m('wf_preview.log_check_branch', {branchName}));
1470
1790
 
1471
- // 브랜치 존재 여부 확인
1791
+ // Check whether the branch exists
1472
1792
  try {
1473
1793
  await github.rest.repos.getBranch({
1474
1794
  owner: context.repo.owner,
1475
1795
  repo: context.repo.repo,
1476
1796
  branch: branchName
1477
1797
  });
1478
- console.log(`✅ 브랜치 존재: ${branchName}`);
1798
+ console.log(m('wf_preview.log_branch_exists', {branchName}));
1479
1799
  } catch (error) {
1480
1800
  if (error.status === 404) {
1481
- console.log(`❌ 브랜치 없음: ${branchName}`);
1801
+ console.log(m('wf_preview.log_branch_missing', {branchName}));
1482
1802
 
1483
- // 에러 댓글 작성
1803
+ // Post the error comment
1484
1804
  const body = [
1485
- '## ❌ 브랜치를 찾을 수 없습니다',
1805
+ m('wf_preview.title_branch_missing'),
1486
1806
  '',
1487
- '| 항목 | 값 |',
1807
+ m('wf_preview.tbl_items'),
1488
1808
  '|------|-----|',
1489
- `| **브랜치** | \`${branchName}\` |`,
1809
+ m('wf_preview.env_branch', {branchName}),
1490
1810
  '',
1491
- '### 💡 확인 사항',
1492
- '1. 브랜치명이 정확한지 확인하세요',
1493
- '2. 브랜치가 push되었는지 확인하세요',
1494
- '3. 브랜치가 삭제되지 않았는지 확인하세요',
1811
+ m('wf_preview.sec_check'),
1812
+ m('wf_preview.chk_name_1'),
1813
+ m('wf_preview.chk_pushed_2'),
1814
+ m('wf_preview.chk_deleted_3'),
1495
1815
  '',
1496
1816
  '---',
1497
- '*🤖 이 댓글은 Preview 시스템에 의해 자동 생성되었습니다.*'
1817
+ m('wf_preview.footer_generic')
1498
1818
  ].join('\n');
1499
1819
 
1500
1820
  await github.rest.issues.createComment({
@@ -1504,22 +1824,22 @@ jobs:
1504
1824
  body: body
1505
1825
  });
1506
1826
 
1507
- core.setFailed('브랜치를 찾을 수 없습니다');
1827
+ core.setFailed(m('wf_preview.fail_branch_missing'));
1508
1828
  return;
1509
1829
  }
1510
1830
  throw error;
1511
1831
  }
1512
1832
 
1513
- // 브랜치명에서 Issue 번호 추출 (#번호 패턴)
1833
+ // Extract the issue number from the branch name (#number pattern)
1514
1834
  const issueMatch = branchName.match(/#(\d+)/);
1515
1835
 
1516
1836
  if (issueMatch) {
1517
- // 번호 추출 성공
1837
+ // Number extracted
1518
1838
  core.setOutput('container_number', issueMatch[1]);
1519
1839
  core.setOutput('use_hash', 'false');
1520
- console.log(`✅ Issue 번호 추출: #${issueMatch[1]}`);
1840
+ console.log(m('wf_preview.log_issue_number', {n: issueMatch[1]}));
1521
1841
  } else {
1522
- // 해시 생성
1842
+ // Generate a hash
1523
1843
  let hash = 0;
1524
1844
  for (let i = 0; i < branchName.length; i++) {
1525
1845
  const char = branchName.charCodeAt(i);
@@ -1529,44 +1849,47 @@ jobs:
1529
1849
  const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
1530
1850
  core.setOutput('container_suffix', `custom-${hashStr}`);
1531
1851
  core.setOutput('use_hash', 'true');
1532
- console.log(`✅ 해시 생성: custom-${hashStr}`);
1852
+ console.log(m('wf_preview.log_hash', {hashStr}));
1533
1853
  }
1534
1854
 
1535
1855
  core.setOutput('branch_name', branchName);
1536
1856
  core.setOutput('branch_exists', 'true');
1537
1857
 
1538
- - name: 브랜치의 최신 커밋 SHA 가져오기
1858
+ - name: Get latest commit SHA of the branch
1539
1859
  if: steps.branch_info.outputs.branch_exists == 'true'
1540
1860
  id: get_sha
1541
1861
  run: |
1542
1862
  SHA=$(git ls-remote https://github.com/${{ github.repository }}.git refs/heads/${{ needs.check-command.outputs.custom_branch }} | cut -c1-7)
1543
1863
  echo "sha=$SHA" >> $GITHUB_OUTPUT
1544
1864
 
1545
- - name: 진행 상황 댓글 생성
1865
+ - name: Create progress comment
1546
1866
  if: steps.branch_info.outputs.branch_exists == 'true'
1547
1867
  id: progress
1548
1868
  uses: actions/github-script@v9
1549
1869
  with:
1550
1870
  script: |
1871
+ const M = JSON.parse(process.env.PAW_MSG);
1872
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1873
+
1551
1874
  const issueNumber = context.issue.number;
1552
1875
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
1553
1876
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1554
1877
  const startTime = Date.now();
1555
1878
 
1556
1879
  const body = [
1557
- '## 🚀 Custom Branch Preview 빌드 중...',
1880
+ m('wf_preview.title_custom_building'),
1558
1881
  '',
1559
- `**브랜치**: \`${branchName}\``,
1882
+ m('wf_preview.line_branch', {branchName}),
1560
1883
  '',
1561
- '| 단계 | 상태 | 소요 시간 |',
1884
+ m('wf_preview.tbl_steps'),
1562
1885
  '|------|------|----------|',
1563
- '| 🐳 Docker 이미지 빌드 & Push | ⏳ 진행 중... | - |',
1564
- '| 🚀 서버 배포 & Health Check | ⏸️ 대기 | - |',
1886
+ m('wf_preview.row_docker_progress'),
1887
+ m('wf_preview.row_deploy_wait'),
1565
1888
  '',
1566
- `**[📋 실시간 로그 보기](${runUrl})**`,
1889
+ m('wf_preview.link_logs', {runUrl}),
1567
1890
  '',
1568
1891
  '---',
1569
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
1892
+ m('wf_preview.footer_progress')
1570
1893
  ].join('\n');
1571
1894
 
1572
1895
  const { data: comment } = await github.rest.issues.createComment({
@@ -1580,14 +1903,14 @@ jobs:
1580
1903
  core.setOutput('start_time', startTime);
1581
1904
  core.setOutput('docker_start', startTime);
1582
1905
 
1583
- - name: 코드 체크아웃
1906
+ - name: Checkout code
1584
1907
  if: steps.branch_info.outputs.branch_exists == 'true'
1585
1908
  uses: actions/checkout@v7
1586
1909
  with:
1587
1910
  ref: ${{ needs.check-command.outputs.custom_branch }}
1588
1911
 
1589
- # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
1590
- - name: 배포 사전 점검 (Secret·Dockerfile)
1912
+ # 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
1913
+ - name: Deploy pre-check (Secrets and Dockerfile)
1591
1914
  if: steps.branch_info.outputs.branch_exists == 'true'
1592
1915
  working-directory: ${{ env.PROJECT_PATH }}
1593
1916
  env:
@@ -1599,11 +1922,13 @@ jobs:
1599
1922
  SSH_KEY: ${{ secrets.SSH_KEY }}
1600
1923
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
1601
1924
  run: |
1925
+ m() { python3 "$RUNNER_TEMP/paw/messages.py" get "$@"; }
1926
+
1602
1927
  MISSING=""
1603
1928
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
1604
1929
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
1605
1930
  done
1606
- # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
1931
+ # SSH_KEY is required when SSH_AUTH_METHOD=key, otherwise SERVER_PASSWORD
1607
1932
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
1608
1933
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
1609
1934
  else
@@ -1611,19 +1936,19 @@ jobs:
1611
1936
  fi
1612
1937
  FAILED=false
1613
1938
  if [ -n "$MISSING" ]; then
1614
- echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
1939
+ echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
1615
1940
  FAILED=true
1616
1941
  fi
1617
1942
  if [ ! -f "$DOCKERFILE" ]; then
1618
- echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
1943
+ echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
1619
1944
  FAILED=true
1620
1945
  fi
1621
1946
  if [ "$FAILED" = "true" ]; then
1622
1947
  exit 1
1623
1948
  fi
1624
- echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
1949
+ m wf_preview.precheck_ok
1625
1950
 
1626
- - name: "[필수] .env 파일 생성"
1951
+ - name: "[Required] Create .env file"
1627
1952
  working-directory: ${{ env.PROJECT_PATH }}
1628
1953
  if: steps.branch_info.outputs.branch_exists == 'true'
1629
1954
  run: |
@@ -1631,18 +1956,18 @@ jobs:
1631
1956
  ${{ secrets.ENV_FILE }}
1632
1957
  EOF
1633
1958
 
1634
- - name: Docker 로그인
1959
+ - name: Log in to Docker Hub
1635
1960
  if: steps.branch_info.outputs.branch_exists == 'true'
1636
1961
  uses: docker/login-action@v3
1637
1962
  with:
1638
1963
  username: ${{ secrets.DOCKERHUB_USERNAME }}
1639
1964
  password: ${{ secrets.DOCKERHUB_TOKEN }}
1640
1965
 
1641
- - name: Docker Buildx 설정
1966
+ - name: Set up Docker Buildx
1642
1967
  if: steps.branch_info.outputs.branch_exists == 'true'
1643
1968
  uses: docker/setup-buildx-action@v3
1644
1969
 
1645
- - name: 컨테이너 이름 결정
1970
+ - name: Resolve container name
1646
1971
  if: steps.branch_info.outputs.branch_exists == 'true'
1647
1972
  id: container
1648
1973
  run: |
@@ -1654,7 +1979,7 @@ jobs:
1654
1979
  echo "suffix=$SUFFIX" >> $GITHUB_OUTPUT
1655
1980
  echo "name=${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}-${SUFFIX}" >> $GITHUB_OUTPUT
1656
1981
 
1657
- - name: Docker 이미지 빌드 & Push
1982
+ - name: Build & push Docker image
1658
1983
  if: steps.branch_info.outputs.branch_exists == 'true'
1659
1984
  uses: docker/build-push-action@v5
1660
1985
  with:
@@ -1665,12 +1990,15 @@ jobs:
1665
1990
  cache-from: type=gha
1666
1991
  cache-to: type=gha,mode=max
1667
1992
 
1668
- - name: 진행 상황 - Docker 완료
1993
+ - name: Progress - Docker done
1669
1994
  if: steps.branch_info.outputs.branch_exists == 'true'
1670
1995
  id: docker_progress
1671
1996
  uses: actions/github-script@v9
1672
1997
  with:
1673
1998
  script: |
1999
+ const M = JSON.parse(process.env.PAW_MSG);
2000
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2001
+
1674
2002
  const commentId = ${{ steps.progress.outputs.comment_id }};
1675
2003
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
1676
2004
  const dockerStart = ${{ steps.progress.outputs.docker_start }};
@@ -1678,23 +2006,23 @@ jobs:
1678
2006
  const dockerElapsed = now - dockerStart;
1679
2007
  const minutes = Math.floor(dockerElapsed / 60000);
1680
2008
  const seconds = Math.floor((dockerElapsed % 60000) / 1000);
1681
- const dockerDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
2009
+ const dockerDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1682
2010
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
1683
2011
 
1684
2012
  const body = [
1685
- '## 🚀 Custom Branch Preview 빌드 중...',
2013
+ m('wf_preview.title_custom_building'),
1686
2014
  '',
1687
- `**브랜치**: \`${branchName}\``,
2015
+ m('wf_preview.line_branch', {branchName}),
1688
2016
  '',
1689
- '| 단계 | 상태 | 소요 시간 |',
2017
+ m('wf_preview.tbl_steps'),
1690
2018
  '|------|------|----------|',
1691
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
1692
- '| 🚀 서버 배포 & Health Check | ⏳ 진행 중... | - |',
2019
+ m('wf_preview.row_docker_done', {dockerDuration}),
2020
+ m('wf_preview.row_deploy_progress'),
1693
2021
  '',
1694
- `**[📋 실시간 로그 보기](${runUrl})**`,
2022
+ m('wf_preview.link_logs', {runUrl}),
1695
2023
  '',
1696
2024
  '---',
1697
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
2025
+ m('wf_preview.footer_progress')
1698
2026
  ].join('\n');
1699
2027
 
1700
2028
  await github.rest.issues.updateComment({
@@ -1707,7 +2035,7 @@ jobs:
1707
2035
  core.setOutput('docker_duration', dockerDuration);
1708
2036
  core.setOutput('deploy_start', now);
1709
2037
 
1710
- - name: 서버에 배포
2038
+ - name: Deploy to server
1711
2039
  if: steps.branch_info.outputs.branch_exists == 'true'
1712
2040
  uses: appleboy/ssh-action@v1.0.3
1713
2041
  env:
@@ -1720,19 +2048,21 @@ jobs:
1720
2048
  port: ${{ env.SSH_PORT }}
1721
2049
  envs: SSH_AUTH_METHOD
1722
2050
  script: |
2051
+ ${{ env.PAW_REMOTE }}
2052
+
1723
2053
  set -e
1724
2054
 
1725
2055
  export PATH=$PATH:/usr/local/bin
1726
2056
  export PW="${{ secrets.SERVER_PASSWORD }}"
1727
2057
 
1728
- # SSH 인증 방식에 따른 sudo 추상화
2058
+ # sudo wrapper that depends on the SSH auth method
1729
2059
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
1730
2060
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
1731
2061
  SUDO() { sudo "$@"; }
1732
2062
  else
1733
2063
  SUDO() { echo "$PW" | sudo -S "$@"; }
1734
2064
  fi
1735
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
2065
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
1736
2066
 
1737
2067
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}"
1738
2068
  CONTAINER_NAME="${{ steps.container.outputs.name }}"
@@ -1743,17 +2073,17 @@ jobs:
1743
2073
  TRAEFIK_NETWORK="${{ env.TRAEFIK_NETWORK }}"
1744
2074
 
1745
2075
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1746
- echo "🚀 Custom Branch Preview 배포 시작"
2076
+ m wf_preview.r_deploy_head_custom
1747
2077
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1748
- echo "📦 프로젝트: ${PROJECT_NAME}"
1749
- echo "📛 컨테이너: ${CONTAINER_NAME}"
1750
- echo "🌐 도메인: ${DOMAIN}"
2078
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
2079
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
2080
+ m wf_preview.r_domain domain="${DOMAIN}"
1751
2081
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1752
2082
 
1753
- echo "📥 Docker 이미지 Pull 중..."
2083
+ m wf_preview.r_image_pulling
1754
2084
  SUDO docker pull "${IMAGE}"
1755
2085
 
1756
- echo "🗑️ 기존 컨테이너 정리 중..."
2086
+ m wf_preview.r_existing_cleanup
1757
2087
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
1758
2088
 
1759
2089
  VOLUME_OPTS="-v /etc/localtime:/etc/localtime:ro"
@@ -1761,7 +2091,7 @@ jobs:
1761
2091
  VOLUME_OPTS="$VOLUME_OPTS -v ${{ env.PROJECT_TARGET_DIR }}:${{ env.PROJECT_MNT_DIR }}"
1762
2092
  fi
1763
2093
 
1764
- echo "🐳 새 컨테이너 실행 중..."
2094
+ m wf_preview.r_container_starting
1765
2095
  SUDO docker run -d \
1766
2096
  --name "${CONTAINER_NAME}" \
1767
2097
  --network "${TRAEFIK_NETWORK}" \
@@ -1776,7 +2106,7 @@ jobs:
1776
2106
 
1777
2107
  # Health Check
1778
2108
  echo ""
1779
- echo "⏳ Health Check 시작 (최대 120초 대기)..."
2109
+ m wf_preview.r_hc_start
1780
2110
  HEALTH_PATH="${{ env.HEALTH_CHECK_PATH }}"
1781
2111
  LOG_PATTERN="${{ env.HEALTH_CHECK_LOG_PATTERN }}"
1782
2112
  MAX_RETRIES=24
@@ -1791,9 +2121,9 @@ jobs:
1791
2121
  STATUS=$(SUDO docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "not_found")
1792
2122
 
1793
2123
  if [ "$STATUS" = "exited" ]; then
1794
- echo "❌ 컨테이너 비정상 종료!"
2124
+ m wf_preview.r_container_exited
1795
2125
  echo ""
1796
- echo "📋 컨테이너 로그 (최근 100줄):"
2126
+ m wf_preview.r_container_logs
1797
2127
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1798
2128
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1799
2129
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1805,7 +2135,7 @@ jobs:
1805
2135
  HEALTH=$(SUDO docker exec "${CONTAINER_NAME}" curl -sf "http://localhost:${INTERNAL_PORT}${HEALTH_PATH}" 2>/dev/null || echo "")
1806
2136
 
1807
2137
  if [ -n "$HEALTH" ]; then
1808
- echo "✅ 정상 기동 확인! (HTTP 응답: ${HEALTH_PATH})"
2138
+ m wf_preview.r_hc_ok_http health_path="${HEALTH_PATH}"
1809
2139
  HEALTH_CHECK_PASSED=true
1810
2140
  HEALTH_CHECK_METHOD="HTTP"
1811
2141
  break
@@ -1816,7 +2146,7 @@ jobs:
1816
2146
  STARTED=$(SUDO docker logs --tail 50 "${CONTAINER_NAME}" 2>&1 | grep -E "$LOG_PATTERN" || echo "")
1817
2147
 
1818
2148
  if [ -n "$STARTED" ]; then
1819
- echo "✅ 정상 기동 확인! (로그 패턴)"
2149
+ m wf_preview.r_hc_ok_log
1820
2150
  echo " $STARTED"
1821
2151
  HEALTH_CHECK_PASSED=true
1822
2152
  HEALTH_CHECK_METHOD="Log"
@@ -1825,14 +2155,14 @@ jobs:
1825
2155
  fi
1826
2156
  fi
1827
2157
 
1828
- echo "⏳ 대기 중... ($RETRY_COUNT/$MAX_RETRIES) - 상태: $STATUS"
2158
+ m wf_preview.r_hc_waiting retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES" status="$STATUS"
1829
2159
  done
1830
2160
 
1831
2161
  if [ "$HEALTH_CHECK_PASSED" = "false" ]; then
1832
2162
  echo ""
1833
- echo "❌ Health Check 타임아웃 (120초)"
2163
+ m wf_preview.r_hc_timeout
1834
2164
  echo ""
1835
- echo "📋 컨테이너 로그 (최근 100줄):"
2165
+ m wf_preview.r_container_logs
1836
2166
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1837
2167
  SUDO docker logs --tail 100 "${CONTAINER_NAME}"
1838
2168
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -1840,14 +2170,17 @@ jobs:
1840
2170
  fi
1841
2171
 
1842
2172
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1843
- echo "✅ 배포 및 Health Check 완료! (방식: ${HEALTH_CHECK_METHOD})"
2173
+ m wf_preview.r_hc_done health_check_method="${HEALTH_CHECK_METHOD}"
1844
2174
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1845
2175
 
1846
- - name: 배포 완료 코멘트
2176
+ - name: Deploy complete comment
1847
2177
  if: steps.branch_info.outputs.branch_exists == 'true'
1848
2178
  uses: actions/github-script@v9
1849
2179
  with:
1850
2180
  script: |
2181
+ const M = JSON.parse(process.env.PAW_MSG);
2182
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2183
+
1851
2184
  const commentId = ${{ steps.progress.outputs.comment_id }};
1852
2185
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
1853
2186
  const dockerDuration = '${{ steps.docker_progress.outputs.docker_duration }}';
@@ -1856,7 +2189,7 @@ jobs:
1856
2189
  const deployElapsed = now - deployStart;
1857
2190
  const minutes = Math.floor(deployElapsed / 60000);
1858
2191
  const seconds = Math.floor((deployElapsed % 60000) / 1000);
1859
- const deployDuration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
2192
+ const deployDuration = minutes > 0 ? m('wf_preview.dur_min_sec', {minutes, seconds}) : m('wf_preview.dur_sec', {seconds});
1860
2193
 
1861
2194
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
1862
2195
  const domainSuffix = '${{ env.PREVIEW_DOMAIN_SUFFIX }}';
@@ -1867,41 +2200,41 @@ jobs:
1867
2200
  const previewUrl = `http://${domain}:${previewPort}`;
1868
2201
  const sha = '${{ steps.get_sha.outputs.sha }}';
1869
2202
 
1870
- // Preview 환경 테이블 구성
2203
+ // Build the preview environment table
1871
2204
  const envRows = [
1872
2205
  `| **Preview URL** | ${previewUrl} |`,
1873
2206
  ];
1874
2207
  if (apiDocsPath) {
1875
2208
  envRows.push(`| **API Docs** | ${previewUrl}${apiDocsPath} |`);
1876
2209
  }
1877
- envRows.push(`| **컨테이너** | \`${containerName}\` |`);
1878
- envRows.push(`| **브랜치** | \`${branchName}\` |`);
1879
- envRows.push(`| **커밋** | \`${sha}\` |`);
2210
+ envRows.push(m('wf_preview.env_container_name', {containerName}));
2211
+ envRows.push(m('wf_preview.env_branch', {branchName}));
2212
+ envRows.push(m('wf_preview.env_commit', {sha}));
1880
2213
 
1881
2214
  const body = [
1882
- '## ✅ Custom Branch Preview 배포 완료!',
2215
+ m('wf_preview.title_custom_deployed'),
1883
2216
  '',
1884
- `**브랜치**: \`${branchName}\``,
2217
+ m('wf_preview.line_branch', {branchName}),
1885
2218
  '',
1886
- '| 단계 | 상태 | 소요 시간 |',
2219
+ m('wf_preview.tbl_steps'),
1887
2220
  '|------|------|----------|',
1888
- `| 🐳 Docker 이미지 빌드 & Push | ✅ 완료 | ${dockerDuration} |`,
1889
- `| 🚀 서버 배포 & Health Check | ✅ 완료 | ${deployDuration} |`,
2221
+ m('wf_preview.row_docker_done', {dockerDuration}),
2222
+ m('wf_preview.row_deploy_done', {deployDuration}),
1890
2223
  '',
1891
- '### 🌐 Preview 환경',
1892
- '| 항목 | 값 |',
2224
+ m('wf_preview.sec_env'),
2225
+ m('wf_preview.tbl_items'),
1893
2226
  '|------|-----|',
1894
2227
  ...envRows,
1895
2228
  '',
1896
- '### 📋 명령어',
1897
- '| 명령어 | 설명 |',
2229
+ m('wf_preview.sec_commands'),
2230
+ m('wf_preview.tbl_commands'),
1898
2231
  '|--------|------|',
1899
- '| `/wizard server build` | 최신 커밋으로 재배포 |',
1900
- '| `/wizard server destroy` | Preview 환경 삭제 |',
1901
- '| `/wizard server status` | 현재 상태 확인 |',
2232
+ m('wf_preview.cmd_build'),
2233
+ m('wf_preview.cmd_destroy'),
2234
+ m('wf_preview.cmd_status'),
1902
2235
  '',
1903
2236
  '<details>',
1904
- '<summary>🔧 고급 명령어 (다른 Issue/PR에서 제어)</summary>',
2237
+ m('wf_preview.cmd_advanced'),
1905
2238
  '',
1906
2239
  '```',
1907
2240
  `/wizard server build ${branchName}`,
@@ -1911,7 +2244,7 @@ jobs:
1911
2244
  '</details>',
1912
2245
  '',
1913
2246
  '---',
1914
- '*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
2247
+ m('wf_preview.footer_custom')
1915
2248
  ].join('\n');
1916
2249
 
1917
2250
  await github.rest.issues.updateComment({
@@ -1921,11 +2254,14 @@ jobs:
1921
2254
  body: body
1922
2255
  });
1923
2256
 
1924
- - name: 빌드/배포 실패 시 에러 코멘트
2257
+ - name: Build/deploy failure comment
1925
2258
  if: failure() && steps.branch_info.outputs.branch_exists == 'true'
1926
2259
  uses: actions/github-script@v9
1927
2260
  with:
1928
2261
  script: |
2262
+ const M = JSON.parse(process.env.PAW_MSG);
2263
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2264
+
1929
2265
  const commentId = ${{ steps.progress.outputs.comment_id || 0 }};
1930
2266
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
1931
2267
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
@@ -1936,42 +2272,42 @@ jobs:
1936
2272
  let dockerTime = '-';
1937
2273
 
1938
2274
  if (!commentId) {
1939
- dockerStatus = '⚠️ 초기화 실패';
2275
+ dockerStatus = m('wf_preview.status_init_failed');
1940
2276
  deployStatus = '-';
1941
2277
  } else if (!dockerDuration || dockerDuration === '') {
1942
- dockerStatus = '❌ 실패';
1943
- deployStatus = '⏸️ 대기';
2278
+ dockerStatus = m('wf_preview.status_failed');
2279
+ deployStatus = m('wf_preview.status_waiting');
1944
2280
  } else {
1945
- dockerStatus = '✅ 완료';
1946
- deployStatus = '❌ 실패';
2281
+ dockerStatus = m('wf_preview.status_done');
2282
+ deployStatus = m('wf_preview.status_failed');
1947
2283
  dockerTime = dockerDuration;
1948
2284
  }
1949
2285
 
1950
2286
  const body = [
1951
- '## ❌ Custom Branch Preview 배포 실패!',
2287
+ m('wf_preview.title_custom_failed'),
1952
2288
  '',
1953
- `**브랜치**: \`${branchName}\``,
2289
+ m('wf_preview.line_branch', {branchName}),
1954
2290
  '',
1955
- '| 단계 | 상태 | 소요 시간 |',
2291
+ m('wf_preview.tbl_steps'),
1956
2292
  '|------|------|----------|',
1957
- `| 🐳 Docker 이미지 빌드 & Push | ${dockerStatus} | ${dockerTime} |`,
1958
- `| 🚀 서버 배포 & Health Check | ${deployStatus} | - |`,
2293
+ m('wf_preview.row_docker_status', {dockerStatus, dockerTime}),
2294
+ m('wf_preview.row_deploy_status', {deployStatus}),
1959
2295
  '',
1960
- `**[📋 빌드/배포 로그 확인](${runUrl})**`,
2296
+ m('wf_preview.link_build_logs', {runUrl}),
1961
2297
  '',
1962
- '### 🔍 가능한 원인',
1963
- '- 필수 Secret(DOCKERHUB_*, SERVER_*) 미등록 또는 Dockerfile 없음 → 로그의 "배포 사전 점검" 단계 확인',
1964
- '- Docker 이미지 빌드 실패 (Go 의존성/빌드 문제)',
1965
- '- 컨테이너 시작 실패 (애플리케이션 기동 오류)',
1966
- '- Health Check 타임아웃 (120초 내 기동 완료 안됨)',
1967
- '- 환경변수 누락 (.env 파일 설정 확인)',
2298
+ m('wf_preview.sec_causes'),
2299
+ m('wf_preview.hint_secrets'),
2300
+ m('wf_preview.hint_docker_go'),
2301
+ m('wf_preview.hint_start_go'),
2302
+ m('wf_preview.hint_health_timeout'),
2303
+ m('wf_preview.hint_env_missing'),
1968
2304
  '',
1969
- '### 💡 다음 단계',
1970
- '1. 위 링크에서 빌드/배포 로그를 확인하세요',
1971
- `2. 문제를 수정한 후 다시 시도하세요: \`/wizard server build ${branchName}\``,
2305
+ m('wf_preview.sec_next'),
2306
+ m('wf_preview.next_logs'),
2307
+ m('wf_preview.next_retry_branch', {branchName}),
1972
2308
  '',
1973
2309
  '---',
1974
- '*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
2310
+ m('wf_preview.footer_custom')
1975
2311
  ].join('\n');
1976
2312
 
1977
2313
  if (commentId) {
@@ -1990,9 +2326,9 @@ jobs:
1990
2326
  });
1991
2327
  }
1992
2328
 
1993
- # Job 6: 커스텀 브랜치 Preview 삭제
2329
+ # Job 6: destroy custom branch preview
1994
2330
  destroy-preview-custom-branch:
1995
- name: Preview 삭제 (Custom Branch)
2331
+ name: Destroy preview (Custom Branch)
1996
2332
  needs: check-command
1997
2333
  if: |
1998
2334
  needs.check-command.outputs.is_valid == 'true' &&
@@ -2000,23 +2336,70 @@ jobs:
2000
2336
  needs.check-command.outputs.is_custom_branch == 'true'
2001
2337
  runs-on: ubuntu-latest
2002
2338
  steps:
2003
- - name: 브랜치명에서 컨테이너 정보 추출
2339
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
2340
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
2341
+ - name: Checkout messages
2342
+ uses: actions/checkout@v7
2343
+ with:
2344
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2345
+ sparse-checkout: |
2346
+ .github/scripts/messages.py
2347
+ version.yml
2348
+ sparse-checkout-cone-mode: false
2349
+
2350
+ - name: Load messages
2351
+ run: |
2352
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2353
+ mkdir -p "$RUNNER_TEMP/paw"
2354
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2355
+ {
2356
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2357
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2358
+ } >> "$GITHUB_ENV"
2359
+ # The deploy script runs on the remote server, where messages.py does not exist:
2360
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
2361
+ python3 - >> "$GITHUB_ENV" <<'PY'
2362
+ import json, os, shlex, subprocess
2363
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
2364
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
2365
+ head = ["m() {", ' case "$1" in']
2366
+ for key, text in msgs.items():
2367
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
2368
+ tail = r''' *) _t="$1" ;;
2369
+ esac
2370
+ shift
2371
+ while [ "$#" -gt 0 ]; do
2372
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
2373
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
2374
+ shift
2375
+ done
2376
+ printf '%s\n' "$_t"
2377
+ }'''
2378
+ print("PAW_REMOTE<<PAW_EOF")
2379
+ print("\n".join(head) + "\n" + tail)
2380
+ print("PAW_EOF")
2381
+ PY
2382
+
2383
+ - name: Extract container info from branch name
2004
2384
  id: container_info
2005
2385
  uses: actions/github-script@v9
2006
2386
  with:
2007
2387
  script: |
2388
+ const M = JSON.parse(process.env.PAW_MSG);
2389
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2390
+
2008
2391
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2009
2392
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2010
2393
 
2011
- // 브랜치명에서 Issue 번호 추출 (#번호 패턴)
2394
+ // Extract the issue number from the branch name (#number pattern)
2012
2395
  const issueMatch = branchName.match(/#(\d+)/);
2013
2396
 
2014
2397
  let containerName;
2015
2398
  if (issueMatch) {
2016
2399
  containerName = `${projectName}-pr-${issueMatch[1]}`;
2017
- console.log(`✅ Issue 번호 추출: #${issueMatch[1]}`);
2400
+ console.log(m('wf_preview.log_issue_number', {n: issueMatch[1]}));
2018
2401
  } else {
2019
- // 해시 생성
2402
+ // Generate a hash
2020
2403
  let hash = 0;
2021
2404
  for (let i = 0; i < branchName.length; i++) {
2022
2405
  const char = branchName.charCodeAt(i);
@@ -2025,13 +2408,13 @@ jobs:
2025
2408
  }
2026
2409
  const hashStr = Math.abs(hash).toString(16).slice(-6).padStart(6, '0');
2027
2410
  containerName = `${projectName}-custom-${hashStr}`;
2028
- console.log(`✅ 해시 생성: custom-${hashStr}`);
2411
+ console.log(m('wf_preview.log_hash', {hashStr}));
2029
2412
  }
2030
2413
 
2031
2414
  core.setOutput('container_name', containerName);
2032
2415
  core.setOutput('branch_name', branchName);
2033
2416
 
2034
- - name: 컨테이너 & 이미지 삭제
2417
+ - name: Remove container & image
2035
2418
  uses: appleboy/ssh-action@v1.0.3
2036
2419
  env:
2037
2420
  SSH_AUTH_METHOD: ${{ env.SSH_AUTH_METHOD }}
@@ -2043,60 +2426,65 @@ jobs:
2043
2426
  port: ${{ env.SSH_PORT }}
2044
2427
  envs: SSH_AUTH_METHOD
2045
2428
  script: |
2429
+ ${{ env.PAW_REMOTE }}
2430
+
2046
2431
  export PATH=$PATH:/usr/local/bin
2047
2432
  export PW="${{ secrets.SERVER_PASSWORD }}"
2048
2433
 
2049
- # SSH 인증 방식에 따른 sudo 추상화
2434
+ # sudo wrapper that depends on the SSH auth method
2050
2435
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
2051
2436
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
2052
2437
  SUDO() { sudo "$@"; }
2053
2438
  else
2054
2439
  SUDO() { echo "$PW" | sudo -S "$@"; }
2055
2440
  fi
2056
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
2441
+ m wf_preview.r_ssh_auth ssh_auth_method="${SSH_AUTH_METHOD}"
2057
2442
 
2058
2443
  CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
2059
2444
  PROJECT_NAME="${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}"
2060
2445
 
2061
- # 컨테이너명에서 suffix 추출
2446
+ # Extract the suffix from the container name
2062
2447
  SUFFIX="${CONTAINER_NAME#${PROJECT_NAME}-}"
2063
2448
  IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/${PROJECT_NAME}:${SUFFIX}"
2064
2449
 
2065
2450
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2066
- echo "🗑️ Custom Branch Preview 삭제"
2451
+ m wf_preview.r_destroy_head_custom
2067
2452
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2068
- echo "📦 프로젝트: ${PROJECT_NAME}"
2069
- echo "📛 컨테이너: ${CONTAINER_NAME}"
2453
+ m wf_preview.r_project_name project_name="${PROJECT_NAME}"
2454
+ m wf_preview.r_container_name container_name="${CONTAINER_NAME}"
2070
2455
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2071
2456
 
2072
- echo "🐳 컨테이너 삭제 중..."
2457
+ m wf_preview.r_container_removing
2073
2458
  SUDO docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true
2074
2459
 
2075
- echo "🖼️ 이미지 삭제 중..."
2460
+ m wf_preview.r_image_removing
2076
2461
  SUDO docker rmi "${IMAGE}" 2>/dev/null || true
2077
2462
 
2078
- echo "✅ 삭제 완료!"
2463
+ m wf_preview.r_removed
2079
2464
 
2080
- - name: 삭제 완료 코멘트
2465
+ - name: Destroy complete comment
2081
2466
  uses: actions/github-script@v9
2082
2467
  with:
2083
2468
  script: |
2469
+ const M = JSON.parse(process.env.PAW_MSG);
2470
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2471
+
2084
2472
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2085
2473
  const containerName = '${{ steps.container_info.outputs.container_name }}';
2086
2474
 
2087
2475
  const body = [
2088
- '## 🗑️ Custom Branch Preview 환경 삭제 완료!',
2476
+ m('wf_preview.title_custom_destroyed'),
2089
2477
  '',
2090
- '| 항목 | 값 |',
2478
+ m('wf_preview.tbl_items'),
2091
2479
  '|------|-----|',
2092
- `| **브랜치** | \`${branchName}\` |`,
2093
- `| **컨테이너** | \`${containerName}\` |`,
2094
- '| **상태** | 삭제됨 |',
2480
+ m('wf_preview.env_branch', {branchName}),
2481
+ m('wf_preview.env_container_name', {containerName}),
2482
+ m('wf_preview.env_status_deleted'),
2095
2483
  '',
2096
- `다시 배포하려면: \`/wizard server build ${branchName}\``,
2484
+ m('wf_preview.redeploy_hint_branch', {branchName}),
2097
2485
  '',
2098
2486
  '---',
2099
- '*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
2487
+ m('wf_preview.footer_custom')
2100
2488
  ].join('\n');
2101
2489
 
2102
2490
  await github.rest.issues.createComment({
@@ -2106,9 +2494,9 @@ jobs:
2106
2494
  body: body
2107
2495
  });
2108
2496
 
2109
- # Job 7: 커스텀 브랜치 Preview 상태 확인
2497
+ # Job 7: custom branch preview status
2110
2498
  check-status-custom-branch:
2111
- name: Preview 상태 확인 (Custom Branch)
2499
+ name: Preview status (Custom Branch)
2112
2500
  needs: check-command
2113
2501
  if: |
2114
2502
  needs.check-command.outputs.is_valid == 'true' &&
@@ -2116,7 +2504,51 @@ jobs:
2116
2504
  needs.check-command.outputs.is_custom_branch == 'true'
2117
2505
  runs-on: ubuntu-latest
2118
2506
  steps:
2119
- - name: 브랜치명에서 컨테이너 정보 추출
2507
+ # Messages come from the same revision as this workflow (the PR merge commit on pull_request, otherwise the
2508
+ # default branch) so they exist even before the default branch has them, and before the PR branch is checked out
2509
+ - name: Checkout messages
2510
+ uses: actions/checkout@v7
2511
+ with:
2512
+ ref: ${{ github.event_name == 'pull_request' && github.sha || github.event.repository.default_branch }}
2513
+ sparse-checkout: |
2514
+ .github/scripts/messages.py
2515
+ version.yml
2516
+ sparse-checkout-cone-mode: false
2517
+
2518
+ - name: Load messages
2519
+ run: |
2520
+ # Keep a copy outside the workspace: checking out the PR branch later replaces its files
2521
+ mkdir -p "$RUNNER_TEMP/paw"
2522
+ cp .github/scripts/messages.py "$RUNNER_TEMP/paw/messages.py"
2523
+ {
2524
+ echo "PROJECT_AUTO_WIZARD_LANG=$(python3 .github/scripts/messages.py lang)"
2525
+ echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2526
+ } >> "$GITHUB_ENV"
2527
+ # The deploy script runs on the remote server, where messages.py does not exist:
2528
+ # ship the messages it needs as a small shell function (m KEY name=value ...)
2529
+ python3 - >> "$GITHUB_ENV" <<'PY'
2530
+ import json, os, shlex, subprocess
2531
+ script = os.path.join(os.environ["RUNNER_TEMP"], "paw", "messages.py")
2532
+ msgs = json.loads(subprocess.check_output(["python3", script, "dump", "wf_preview.r_"], text=True))
2533
+ head = ["m() {", ' case "$1" in']
2534
+ for key, text in msgs.items():
2535
+ head.append(" %s) _t=%s ;;" % (key, shlex.quote(text)))
2536
+ tail = r''' *) _t="$1" ;;
2537
+ esac
2538
+ shift
2539
+ while [ "$#" -gt 0 ]; do
2540
+ _v=$(printf '%s' "${1#*=}" | sed 's/[\\&|]/\\&/g')
2541
+ _t=$(printf '%s' "$_t" | sed "s|{${1%%=*}}|$_v|g")
2542
+ shift
2543
+ done
2544
+ printf '%s\n' "$_t"
2545
+ }'''
2546
+ print("PAW_REMOTE<<PAW_EOF")
2547
+ print("\n".join(head) + "\n" + tail)
2548
+ print("PAW_EOF")
2549
+ PY
2550
+
2551
+ - name: Extract container info from branch name
2120
2552
  id: container_info
2121
2553
  uses: actions/github-script@v9
2122
2554
  with:
@@ -2124,14 +2556,14 @@ jobs:
2124
2556
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2125
2557
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
2126
2558
 
2127
- // 브랜치명에서 Issue 번호 추출 (#번호 패턴)
2559
+ // Extract the issue number from the branch name (#number pattern)
2128
2560
  const issueMatch = branchName.match(/#(\d+)/);
2129
2561
 
2130
2562
  let containerName;
2131
2563
  if (issueMatch) {
2132
2564
  containerName = `${projectName}-pr-${issueMatch[1]}`;
2133
2565
  } else {
2134
- // 해시 생성
2566
+ // Generate a hash
2135
2567
  let hash = 0;
2136
2568
  for (let i = 0; i < branchName.length; i++) {
2137
2569
  const char = branchName.charCodeAt(i);
@@ -2145,7 +2577,7 @@ jobs:
2145
2577
  core.setOutput('container_name', containerName);
2146
2578
  core.setOutput('branch_name', branchName);
2147
2579
 
2148
- - name: 컨테이너 상태 확인
2580
+ - name: Check container status
2149
2581
  uses: appleboy/ssh-action@v1.0.3
2150
2582
  id: check_running
2151
2583
  continue-on-error: true
@@ -2156,28 +2588,33 @@ jobs:
2156
2588
  key: ${{ secrets.SSH_KEY }}
2157
2589
  port: ${{ env.SSH_PORT }}
2158
2590
  script: |
2591
+ ${{ env.PAW_REMOTE }}
2592
+
2159
2593
  export PATH=$PATH:/usr/local/bin
2160
2594
 
2161
2595
  CONTAINER_NAME="${{ steps.container_info.outputs.container_name }}"
2162
2596
 
2163
2597
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2164
- echo "🔍 Custom Branch Preview 상태 확인"
2598
+ m wf_preview.r_status_head_custom
2165
2599
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
2166
2600
 
2167
2601
  if docker ps --filter "name=${CONTAINER_NAME}" --format "{{.Names}}" | grep -q "${CONTAINER_NAME}"; then
2168
2602
  echo "STATUS=running"
2169
- echo "✅ 컨테이너 실행 중"
2603
+ m wf_preview.r_container_running
2170
2604
  docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.RunningFor}}"
2171
2605
  else
2172
2606
  echo "STATUS=not_found"
2173
- echo "❌ 컨테이너 없음"
2607
+ m wf_preview.r_container_absent
2174
2608
  exit 1
2175
2609
  fi
2176
2610
 
2177
- - name: 상태 코멘트 작성
2611
+ - name: Post status comment
2178
2612
  uses: actions/github-script@v9
2179
2613
  with:
2180
2614
  script: |
2615
+ const M = JSON.parse(process.env.PAW_MSG);
2616
+ const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2617
+
2181
2618
  const branchName = '${{ needs.check-command.outputs.custom_branch }}';
2182
2619
  const containerName = '${{ steps.container_info.outputs.container_name }}';
2183
2620
  const projectName = '${{ env.PROJECT_PATH != '.' && format('{0}-go', env.PROJECT_NAME) || env.PROJECT_NAME }}';
@@ -2190,38 +2627,38 @@ jobs:
2190
2627
  let body;
2191
2628
  if (isRunning) {
2192
2629
  body = [
2193
- '## ✅ Custom Branch Preview 환경 실행 중',
2630
+ m('wf_preview.title_custom_running'),
2194
2631
  '',
2195
- '| 항목 | 값 |',
2632
+ m('wf_preview.tbl_items'),
2196
2633
  '|------|-----|',
2197
- `| **브랜치** | \`${branchName}\` |`,
2634
+ m('wf_preview.env_branch', {branchName}),
2198
2635
  `| **Preview URL** | ${previewUrl} |`,
2199
- `| **컨테이너** | \`${containerName}\` |`,
2200
- '| **상태** | 🟢 Running |',
2636
+ m('wf_preview.env_container_name', {containerName}),
2637
+ m('wf_preview.env_status_running'),
2201
2638
  '',
2202
- '### 📋 명령어',
2203
- '| 명령어 | 설명 |',
2639
+ m('wf_preview.sec_commands'),
2640
+ m('wf_preview.tbl_commands'),
2204
2641
  '|--------|------|',
2205
- `| \`/wizard server build ${branchName}\` | 최신 커밋으로 재배포 |`,
2206
- `| \`/wizard server destroy ${branchName}\` | Preview 환경 삭제 |`,
2642
+ m('wf_preview.cmd_build_branch', {branchName}),
2643
+ m('wf_preview.cmd_destroy_branch', {branchName}),
2207
2644
  '',
2208
2645
  '---',
2209
- '*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
2646
+ m('wf_preview.footer_custom')
2210
2647
  ].join('\n');
2211
2648
  } else {
2212
2649
  body = [
2213
- '## ❌ Custom Branch Preview 환경 없음',
2650
+ m('wf_preview.title_custom_absent'),
2214
2651
  '',
2215
- '| 항목 | 값 |',
2652
+ m('wf_preview.tbl_items'),
2216
2653
  '|------|-----|',
2217
- `| **브랜치** | \`${branchName}\` |`,
2218
- `| **컨테이너** | \`${containerName}\` |`,
2219
- '| **상태** | 🔴 Not Found |',
2654
+ m('wf_preview.env_branch', {branchName}),
2655
+ m('wf_preview.env_container_name', {containerName}),
2656
+ m('wf_preview.env_status_absent'),
2220
2657
  '',
2221
- `배포하려면: \`/wizard server build ${branchName}\``,
2658
+ m('wf_preview.deploy_hint_branch', {branchName}),
2222
2659
  '',
2223
2660
  '---',
2224
- '*🤖 이 댓글은 Custom Branch Preview 시스템에 의해 자동 생성되었습니다.*'
2661
+ m('wf_preview.footer_custom')
2225
2662
  ].join('\n');
2226
2663
  }
2227
2664