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,66 +1,66 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # Spring Boot 무중단 배포 (Nginx Blue-Green, SSH + Docker)
2
+ # Spring Boot zero-downtime deploy (Nginx Blue-Green, SSH + Docker)
3
3
  #
4
- # deploy 브랜치 푸시 시 Docker 이미지를 빌드하고
5
- # nginx config 의 proxy_pass 포트 토글로 Blue-Green 무중단 배포합니다.
4
+ # On a push to the deploy branch, builds a Docker image and
5
+ # deploys it Blue-Green with no downtime by toggling the proxy_pass port in the nginx config.
6
6
  #
7
- # 본 워크플로우는 무중단 배포 옵션입니다.
8
- # 기본 배포는 PROJECT-SPRING-SIMPLE-CICD.yaml 입니다.
9
- # 전환하려면 아래 push.branches 주석 해제 + SIMPLE-CICD 의 push 트리거 주석 처리.
7
+ # This workflow is the zero-downtime deploy option.
8
+ # The default deploy is PROJECT-SPRING-SIMPLE-CICD.yaml.
9
+ # To switch, uncomment push.branches below and comment out the push trigger in SIMPLE-CICD.
10
10
  #
11
- # 필수 GitHub Secrets
12
- # APPLICATION_PROD_YML (선택): application-prod.yml 내용
13
- # DOCKERHUB_USERNAME: DockerHub 사용자명
14
- # DOCKERHUB_TOKEN: DockerHub 액세스 토큰
15
- # SERVER_HOST: 배포 서버 주소
16
- # SERVER_USER: SSH 사용자명
17
- # SERVER_PASSWORD: SSH 비밀번호 (SSH_AUTH_METHOD=password일 때)
18
- # SSH_KEY: SSH 개인키 .pem 내용 (SSH_AUTH_METHOD=key일 때, AWS EC2 등)
11
+ # Required GitHub Secrets
12
+ # APPLICATION_PROD_YML (optional): contents of application-prod.yml
13
+ # DOCKERHUB_USERNAME: DockerHub username
14
+ # DOCKERHUB_TOKEN: DockerHub access token
15
+ # SERVER_HOST: deployment server address
16
+ # SERVER_USER: SSH username
17
+ # SERVER_PASSWORD: SSH password (when SSH_AUTH_METHOD=password)
18
+ # SSH_KEY: contents of the SSH private key .pem (when SSH_AUTH_METHOD=key, e.g. AWS EC2)
19
19
  #
20
- # 환경변수 설정 (env 섹션에서 설정)
20
+ # Environment variables (set in the env section)
21
21
  #
22
- # 포트 및 컨테이너:
23
- # PROJECT_NAME: 프로젝트명 (컨테이너/이미지 이름)
24
- # SERVICE_DOMAIN: 외부 도메인 (nginx server_name 과 일치)
25
- # CONTAINER_INTERNAL_PORT: Spring Boot 내부 포트 (기본: 8080)
26
- # BLUE_PORT: Blue 호스트 노출 포트 (기본: 8080)
27
- # GREEN_PORT: Green 호스트 노출 포트 (기본: 8081)
22
+ # Ports and containers:
23
+ # PROJECT_NAME: project name (container/image name)
24
+ # SERVICE_DOMAIN: external domain (must match the nginx server_name)
25
+ # CONTAINER_INTERNAL_PORT: Spring Boot internal port (default: 8080)
26
+ # BLUE_PORT: host port exposed by Blue (default: 8080)
27
+ # GREEN_PORT: host port exposed by Green (default: 8081)
28
28
  #
29
29
  # Nginx:
30
- # NGINX_RP_CONF: nginx 역방향 프록시 config 파일 경로
31
- # NGINX_BACKUP_DIR: nginx config 백업 디렉토리
32
- # NGINX_BACKUP_KEEP: 백업 보존 개수 (기본: 10)
30
+ # NGINX_RP_CONF: path of the nginx reverse proxy config file
31
+ # NGINX_BACKUP_DIR: directory for nginx config backups
32
+ # NGINX_BACKUP_KEEP: number of backups to keep (default: 10)
33
33
  #
34
- # 볼륨/스토리지:
35
- # ENABLE_VOLUME_MOUNT: 볼륨 마운트 여부 (true/false)
36
- # VOLUME_HOST_PATH: 호스트 볼륨 경로
37
- # VOLUME_CONTAINER_PATH: 컨테이너 내부 마운트 경로
34
+ # Volume/storage:
35
+ # ENABLE_VOLUME_MOUNT: whether to mount a volume (true/false)
36
+ # VOLUME_HOST_PATH: host volume path
37
+ # VOLUME_CONTAINER_PATH: mount path inside the container
38
38
  #
39
- # SSH 연결:
40
- # SSH_PORT: SSH 포트 (기본: 2022)
41
- # SSH_COMMAND_TIMEOUT: 명령 타임아웃 (기본: 600s)
42
- # SSH_CONNECTION_TIMEOUT: 연결 타임아웃 (기본: 60s)
39
+ # SSH connection:
40
+ # SSH_PORT: SSH port (default: 2022)
41
+ # SSH_COMMAND_TIMEOUT: command timeout (default: 600s)
42
+ # SSH_CONNECTION_TIMEOUT: connection timeout (default: 60s)
43
43
  #
44
- # 헬스체크:
45
- # HEALTHCHECK_PATH: HTTP 경로 (기본: /actuator/health, fallback: /)
46
- # HEALTHCHECK_MAX_RETRIES: 컨테이너 헬스체크 최대 횟수 (기본: 120)
47
- # HEALTHCHECK_RETRY_INTERVAL: 재시도 간격 초 (기본: 1)
48
- # DOMAIN_CHECK_RETRIES: nginx reload 후 도메인 검증 횟수 (기본: 10)
49
- # DOMAIN_CHECK_INTERVAL: 도메인 검증 간격 초 (기본: 2)
50
- # IN_FLIGHT_WAIT: old 컨테이너 제거 전 대기 초 (기본: 5)
44
+ # Health check:
45
+ # HEALTHCHECK_PATH: HTTP path (default: /actuator/health, fallback: /)
46
+ # HEALTHCHECK_MAX_RETRIES: max container health check attempts (default: 120)
47
+ # HEALTHCHECK_RETRY_INTERVAL: retry interval in seconds (default: 1)
48
+ # DOMAIN_CHECK_RETRIES: domain check attempts after the nginx reload (default: 10)
49
+ # DOMAIN_CHECK_INTERVAL: domain check interval in seconds (default: 2)
50
+ # IN_FLIGHT_WAIT: seconds to wait before removing the old container (default: 5)
51
51
  #
52
- # 사전 조건:
53
- # - 서버에 nginx 설치 + sites-enabled config 존재
54
- # - 해당 config 의 server 블록에 다음 라인 존재:
52
+ # Prerequisites:
53
+ # - nginx is installed on the server and a sites-enabled config exists
54
+ # - the server block of that config contains these lines:
55
55
  # server_name ${SERVICE_DOMAIN};
56
- # proxy_pass http://127.0.0.1:${BLUE_PORT}; # 또는 ${GREEN_PORT}
57
- # - SSH 사용자가 sudo 로 nginx -t / reload, docker 명령 실행 가능
56
+ # proxy_pass http://127.0.0.1:${BLUE_PORT}; # or ${GREEN_PORT}
57
+ # - the SSH user can run nginx -t / reload and docker commands via sudo
58
58
 
59
59
  name: PROJECT-SPRING-NONSTOP-NGINX-CICD
60
60
 
61
- # 트리거 설정
62
- # deploy 브랜치 push 자동 트리거는 주석 처리되어 있습니다.
63
- # 본 워크플로우로 전환하려면 아래 push.branches 주석을 해제하세요.
61
+ # Trigger settings
62
+ # The automatic trigger on a push to the deploy branch is commented out.
63
+ # To switch to this workflow, uncomment push.branches below.
64
64
  on:
65
65
  # push:
66
66
  # branches:
@@ -68,51 +68,51 @@ on:
68
68
  workflow_dispatch:
69
69
 
70
70
  env:
71
- # 프로젝트 설정 - 실제 프로젝트명으로 변경하세요
71
+ # Project settings - change to your actual project name
72
72
  PROJECT_NAME: "__PROJECT_NAME__" # @wizard ask:@repo
73
- # 모노레포(--paths)에서 이 프로젝트가 있는 하위 경로. 빌드·Docker context의 기준 ('.'이면 레포 루트)
73
+ # Subdirectory of this project in a monorepo (--paths). Base for the build and Docker context ('.' = repo root)
74
74
  PROJECT_PATH: "." # @wizard auto:project-path
75
- # 모노레포(PROJECT_PATH가 '.'이 아님)에서는 이미지·컨테이너·도메인 이름에 '-spring'를 붙여
76
- # 같은 레포의 다른 타입 서비스와 이름이 겹치지 않게 한다 (예: my-app-spring)
75
+ # In a monorepo (PROJECT_PATH is not '.') '-spring' is appended to image, container and domain names
76
+ # so they do not collide with services of other types in the same repo (e.g. my-app-spring)
77
77
  SERVICE_DOMAIN: "__SERVICE_DOMAIN__" # @wizard ask:example.com
78
78
 
79
- # Spring Boot 설정
79
+ # Spring Boot settings
80
80
  SPRING_PROFILE: "prod"
81
81
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:@jdk
82
82
  GRADLE_OPTS: "-Dspring.profiles.active=prod"
83
83
 
84
- # 경로 설정
84
+ # Path settings
85
85
  APPLICATION_YML_DIR: "__APPLICATION_YML_DIR__" # @wizard auto:spring-app-yml-dir
86
- DOCKERFILE_PATH: "./Dockerfile" # PROJECT_PATH 기준 상대 경로
86
+ DOCKERFILE_PATH: "./Dockerfile" # relative to PROJECT_PATH
87
87
 
88
- # 포트 및 컨테이너 설정
89
- # BLUE/GREEN은 서버에서 번갈아 뜨는 두 컨테이너의 호스트 포트다 — 다른 서비스와 겹치면 배포가 실패한다.
88
+ # Port and container settings
89
+ # BLUE/GREEN are the host ports of the two containers that alternate on the server - the deploy fails if they clash with another service.
90
90
  CONTAINER_INTERNAL_PORT: "8080"
91
91
  BLUE_PORT: "__BLUE_PORT__" # @wizard ask:8080
92
92
  GREEN_PORT: "__GREEN_PORT__" # @wizard ask:8081
93
93
 
94
- # Nginx 설정
95
- # NGINX_RP_CONF 경로가 틀리면 무중단 전환(proxy_pass 포트 토글) 자체가 동작하지 않는다.
94
+ # Nginx settings
95
+ # If the NGINX_RP_CONF path is wrong, the zero-downtime switch (proxy_pass port toggle) cannot work at all.
96
96
  NGINX_RP_CONF: "__NGINX_RP_CONF__" # @wizard ask:/etc/nginx/sites-enabled/__PROJECT_NAME__.conf
97
97
  NGINX_BACKUP_DIR: "__NGINX_BACKUP_DIR__" # @wizard ask:/volume1/projects/nginx/backups
98
98
  NGINX_BACKUP_KEEP: "10"
99
99
 
100
- # 볼륨 마운트 설정
100
+ # Volume mount settings
101
101
  ENABLE_VOLUME_MOUNT: "false" # @wizard ask:false
102
102
  VOLUME_HOST_PATH: "/volume1/projects/__PROJECT_NAME__" # @wizard ask:/volume1/projects/__PROJECT_NAME__
103
103
  VOLUME_CONTAINER_PATH: "/app" # @wizard ask:/app
104
104
 
105
- # SSH 인증 방식: "password"(Synology·일반 서버) | "key"(AWS EC2·.pem)
106
- # password → SERVER_PASSWORD secret 사용
107
- # key → SSH_KEY secret 사용 (.pem 파일 전체 내용을 secret 값으로)
105
+ # SSH auth method: "password" (Synology / generic servers) | "key" (AWS EC2 / .pem)
106
+ # password -> uses the SERVER_PASSWORD secret
107
+ # key -> uses the SSH_KEY secret (the full .pem contents as the secret value)
108
108
  SSH_AUTH_METHOD: "password" # @wizard ask:password
109
109
 
110
- # SSH 연결 설정
110
+ # SSH connection settings
111
111
  SSH_PORT: "__SSH_PORT__" # @wizard ask:2022
112
112
  SSH_COMMAND_TIMEOUT: "600s"
113
113
  SSH_CONNECTION_TIMEOUT: "60s"
114
114
 
115
- # 헬스체크 설정
115
+ # Health check settings
116
116
  HEALTHCHECK_PATH: "/actuator/health"
117
117
  HEALTHCHECK_MAX_RETRIES: "120"
118
118
  HEALTHCHECK_RETRY_INTERVAL: "1"
@@ -121,17 +121,17 @@ env:
121
121
  IN_FLIGHT_WAIT: "5"
122
122
 
123
123
  jobs:
124
- # 빌드 작업
124
+ # Build job
125
125
  build:
126
- name: Spring Boot 애플리케이션 빌드
126
+ name: Build Spring Boot application
127
127
  runs-on: ubuntu-latest
128
128
 
129
129
  steps:
130
- - name: 코드 체크아웃
130
+ - name: Checkout code
131
131
  uses: actions/checkout@v7
132
132
 
133
- # 필수 Secret·Dockerfile이 없으면 docker 로그인 같은 서드파티 오류 대신 빠진 항목을 알려 주고 멈춘다
134
- - name: 배포 사전 점검 (Secret·Dockerfile)
133
+ # If a required Secret or the Dockerfile is missing, name it and stop instead of failing later in a third-party step such as docker login
134
+ - name: Deploy pre-check (Secrets and Dockerfile)
135
135
  working-directory: ${{ env.PROJECT_PATH }}
136
136
  env:
137
137
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -142,11 +142,12 @@ jobs:
142
142
  SSH_KEY: ${{ secrets.SSH_KEY }}
143
143
  DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
144
144
  run: |
145
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
145
146
  MISSING=""
146
147
  for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
147
148
  [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
148
149
  done
149
- # SSH_AUTH_METHOD=key면 SSH_KEY, 아니면 SERVER_PASSWORD가 필요하다
150
+ # SSH_AUTH_METHOD=key needs SSH_KEY, anything else needs SERVER_PASSWORD
150
151
  if [ "${SSH_AUTH_METHOD:-password}" = "key" ]; then
151
152
  [ -n "$SSH_KEY" ] || MISSING="$MISSING SSH_KEY"
152
153
  else
@@ -154,45 +155,46 @@ jobs:
154
155
  fi
155
156
  FAILED=false
156
157
  if [ -n "$MISSING" ]; then
157
- echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
158
+ echo "::error title=$(m cicd.precheck_secret_title)::$(m cicd.precheck_secret_body missing="$MISSING")"
158
159
  FAILED=true
159
160
  fi
160
161
  if [ ! -f "$DOCKERFILE" ]; then
161
- echo "::error title=Dockerfile 없음::${PROJECT_PATH}/${DOCKERFILE#./} 파일이 없습니다. 이 워크플로우는 Docker 이미지로 배포하므로 Dockerfile이 필요합니다 (서버 배포를 하지 않는다면 이 워크플로우 파일을 삭제하세요)"
162
+ echo "::error title=$(m cicd.precheck_dockerfile_title)::$(m cicd.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
162
163
  FAILED=true
163
164
  fi
164
165
  if [ "$FAILED" = "true" ]; then
165
166
  exit 1
166
167
  fi
167
- echo "✅ 사전 점검 통과: 필수 Secret과 Dockerfile이 모두 있습니다"
168
+ m cicd.precheck_ok
168
169
 
169
- - name: Java 설정
170
+ - name: Set up Java
170
171
  uses: actions/setup-java@v5
171
172
  with:
172
173
  java-version: ${{ env.JAVA_VERSION }}
173
174
  distribution: 'temurin'
174
175
  cache: 'gradle'
175
176
 
176
- - name: Gradle Wrapper 실행권한 부여
177
+ - name: Make Gradle wrapper executable
177
178
  working-directory: ${{ env.PROJECT_PATH }}
178
179
  run: chmod +x gradlew
179
180
 
180
- - name: application-prod.yml 생성
181
+ - name: Generate application-prod.yml
181
182
  run: |
183
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
182
184
  mkdir -p ${{ env.APPLICATION_YML_DIR }}
183
185
  cat << 'EOF' > ./${{ env.APPLICATION_YML_DIR }}/application-prod.yml
184
186
  ${{ secrets.APPLICATION_PROD_YML }}
185
187
  EOF
186
- echo "✅ application-prod.yml 파일 생성 완료 (${{ env.APPLICATION_YML_DIR }})"
188
+ m wf_spring_deploy.prod_yml_created dir="${{ env.APPLICATION_YML_DIR }}"
187
189
 
188
190
  - name: Build with Gradle
189
191
  working-directory: ${{ env.PROJECT_PATH }}
190
192
  run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
191
193
 
192
- - name: Docker 빌드환경 설정
194
+ - name: Set up Docker Buildx
193
195
  uses: docker/setup-buildx-action@v3
194
196
 
195
- - name: DockerHub 로그인
197
+ - name: Log in to DockerHub
196
198
  uses: docker/login-action@v3
197
199
  with:
198
200
  username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -206,7 +208,7 @@ jobs:
206
208
  restore-keys: |
207
209
  ${{ runner.os }}-buildx-
208
210
 
209
- - name: Docker 이미지 빌드 및 푸시
211
+ - name: Build and push Docker image
210
212
  uses: docker/build-push-action@v5
211
213
  with:
212
214
  context: ${{ env.PROJECT_PATH }}
@@ -221,16 +223,34 @@ jobs:
221
223
  rm -rf /tmp/.buildx-cache
222
224
  mv /tmp/.buildx-cache-new /tmp/.buildx-cache
223
225
 
224
- # 배포 작업 (Blue-Green 무중단)
226
+ # Deploy job (Blue-Green, zero downtime)
225
227
  deploy:
226
- name: Blue-Green 무중단 배포
228
+ name: Blue-Green zero-downtime deploy
227
229
  needs: build
228
230
  runs-on: ubuntu-latest
229
231
 
230
232
  steps:
233
+ # The deploy script runs on the server, which has no messages.py:
234
+ # render the message catalog here and pass it along as a base64 env var
235
+ - name: Checkout message catalog
236
+ uses: actions/checkout@v7
237
+ with:
238
+ sparse-checkout: |
239
+ .github/scripts/messages.py
240
+ version.yml
241
+ sparse-checkout-cone-mode: false
242
+
243
+ - name: Load messages
244
+ run: |
245
+ MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
246
+ | python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
247
+ | base64 | tr -d '\n')"
248
+ echo "PAW_MSG_B64=$MSGS" >> "$GITHUB_ENV"
249
+
231
250
  - name: Deploy
232
251
  uses: appleboy/ssh-action@v1.0.3
233
252
  env:
253
+ PAW_MSG_B64: ${{ env.PAW_MSG_B64 }}
234
254
  PROJECT_NAME: ${{ env.PROJECT_PATH != '.' && format('{0}-spring', env.PROJECT_NAME) || env.PROJECT_NAME }}
235
255
  SERVICE_DOMAIN: ${{ env.SERVICE_DOMAIN }}
236
256
  SPRING_PROFILE: ${{ env.SPRING_PROFILE }}
@@ -261,49 +281,59 @@ jobs:
261
281
  port: ${{ env.SSH_PORT }}
262
282
  command_timeout: ${{ env.SSH_COMMAND_TIMEOUT }}
263
283
  timeout: ${{ env.SSH_CONNECTION_TIMEOUT }}
264
- envs: PROJECT_NAME,SERVICE_DOMAIN,SPRING_PROFILE,CONTAINER_INTERNAL_PORT,BLUE_PORT,GREEN_PORT,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,NGINX_RP_CONF,NGINX_BACKUP_DIR,NGINX_BACKUP_KEEP,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,HEALTHCHECK_PATH,DOMAIN_CHECK_RETRIES,DOMAIN_CHECK_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD
284
+ envs: PROJECT_NAME,SERVICE_DOMAIN,SPRING_PROFILE,CONTAINER_INTERNAL_PORT,BLUE_PORT,GREEN_PORT,ENABLE_VOLUME_MOUNT,VOLUME_HOST_PATH,VOLUME_CONTAINER_PATH,NGINX_RP_CONF,NGINX_BACKUP_DIR,NGINX_BACKUP_KEEP,HEALTHCHECK_MAX_RETRIES,HEALTHCHECK_RETRY_INTERVAL,HEALTHCHECK_PATH,DOMAIN_CHECK_RETRIES,DOMAIN_CHECK_INTERVAL,IN_FLIGHT_WAIT,BRANCH_REF,DOCKERHUB_USERNAME,SERVER_PASSWORD,SSH_AUTH_METHOD,PAW_MSG_B64
265
285
  script: |
266
286
  set -e
267
287
  export PATH=$PATH:/usr/local/bin
268
288
  export PW="${SERVER_PASSWORD}"
269
289
 
270
- # SSH 인증 방식에 따른 sudo 추상화
290
+ # Messages arrive as base64 shell assignments (the server has no messages.py)
291
+ eval "$(printf '%s' "$PAW_MSG_B64" | base64 -d)"
292
+ m() {
293
+ local _v="M_wf_spring_deploy_$1" _kv
294
+ local _s="${!_v}"
295
+ shift
296
+ for _kv in "$@"; do _s=${_s//\{${_kv%%=*}\}/${_kv#*=}}; done
297
+ printf '%s\n' "$_s"
298
+ }
299
+
300
+ # Abstract sudo by SSH auth method
271
301
  SSH_AUTH_METHOD="${SSH_AUTH_METHOD:-password}"
272
302
  if [ "${SSH_AUTH_METHOD}" = "key" ]; then
273
303
  SUDO() { sudo "$@"; }
274
304
  else
275
305
  SUDO() { echo "$PW" | sudo -S "$@"; }
276
306
  fi
277
- echo "🔐 SSH 인증 방식: ${SSH_AUTH_METHOD}"
307
+ m ssh_auth_method method="${SSH_AUTH_METHOD}"
278
308
 
279
309
  IMAGE="${DOCKERHUB_USERNAME}/${PROJECT_NAME}:${BRANCH_REF}"
280
310
  BLUE_CONTAINER_NAME="${PROJECT_NAME}-blue"
281
311
  GREEN_CONTAINER_NAME="${PROJECT_NAME}-green"
282
312
 
283
313
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
284
- echo "🚀 Nginx Blue-Green 무중단 배포 시작"
314
+ m nginx_deploy_start
285
315
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
286
- echo "📦 프로젝트: ${PROJECT_NAME}"
287
- echo "🌐 도메인: ${SERVICE_DOMAIN}"
288
- echo "🖼️ 이미지: ${IMAGE}"
316
+ m banner_project name="${PROJECT_NAME}"
317
+ m banner_domain domain="${SERVICE_DOMAIN}"
318
+ m banner_image image="${IMAGE}"
289
319
  echo "🎨 Blue=${BLUE_PORT} | Green=${GREEN_PORT}"
290
320
  echo "🔧 Nginx config: ${NGINX_RP_CONF}"
291
321
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
292
322
 
293
- # 1. 최신 이미지 Pull
294
- echo "📥 이미지 Pull: ${IMAGE}"
323
+ # 1. Pull the latest image
324
+ m image_pull image="${IMAGE}"
295
325
  SUDO docker pull "${IMAGE}"
296
326
 
297
- # 2. nginx 설정 백업
298
- echo "📁 nginx 백업 디렉토리 생성: ${NGINX_BACKUP_DIR}"
327
+ # 2. Back up the nginx config
328
+ m nginx_backup_dir dir="${NGINX_BACKUP_DIR}"
299
329
  SUDO mkdir -p "${NGINX_BACKUP_DIR}"
300
330
 
301
331
  BACKUP_FILE="${NGINX_BACKUP_DIR}/server.ReverseProxy.conf.$(date +%Y%m%d_%H%M%S).bak"
302
- echo "💾 nginx config 백업: ${BACKUP_FILE}"
332
+ m nginx_backup_file file="${BACKUP_FILE}"
303
333
  SUDO cp "${NGINX_RP_CONF}" "${BACKUP_FILE}"
304
334
 
305
- # 3. 현재 활성 포트 판별 (nginx config proxy_pass 파싱)
306
- echo "🔍 nginx config 의 현재 활성 포트 탐색"
335
+ # 3. Determine the active port (parse proxy_pass in the nginx config)
336
+ m nginx_find_active
307
337
  ACTIVE_PORT=$(
308
338
  SUDO awk -v dom="${SERVICE_DOMAIN}" '
309
339
  BEGIN {
@@ -337,10 +367,10 @@ jobs:
337
367
  )
338
368
 
339
369
  if [ -z "${ACTIVE_PORT}" ]; then
340
- echo "⚠️ 활성 포트를 찾지 못함 → 기본값(BLUE=${BLUE_PORT})을 활성으로 간주"
370
+ m nginx_active_missing blue="${BLUE_PORT}"
341
371
  ACTIVE_PORT="${BLUE_PORT}"
342
372
  fi
343
- echo "🎨 현재 활성 포트: ${ACTIVE_PORT}"
373
+ m nginx_active_port port="${ACTIVE_PORT}"
344
374
 
345
375
  if [ "${ACTIVE_PORT}" = "${BLUE_PORT}" ]; then
346
376
  NEW_PORT="${GREEN_PORT}"
@@ -352,30 +382,30 @@ jobs:
352
382
  OLD_CONTAINER_NAME="${GREEN_CONTAINER_NAME}"
353
383
  fi
354
384
 
355
- echo "🐳 신규 배포 → 포트=${NEW_PORT} 컨테이너=${NEW_CONTAINER_NAME}"
356
- echo "🗑️ 이전 컨테이너 (제거 예정) = ${OLD_CONTAINER_NAME}"
385
+ m nginx_new_deploy port="${NEW_PORT}" name="${NEW_CONTAINER_NAME}"
386
+ m nginx_old_container name="${OLD_CONTAINER_NAME}"
357
387
 
358
- # 4. 동일 슬롯(idle 쪽) 잔존 컨테이너 정리
388
+ # 4. Clean up a leftover container in the same (idle) slot
359
389
  if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${NEW_CONTAINER_NAME}\$"; then
360
- echo "🗑️ idle 슬롯 정리: ${NEW_CONTAINER_NAME}"
390
+ m idle_slot_cleanup name="${NEW_CONTAINER_NAME}"
361
391
  SUDO docker rm -f "${NEW_CONTAINER_NAME}"
362
392
  fi
363
393
 
364
- # 5. 볼륨 마운트 옵션 구성
394
+ # 5. Build the volume mount option
365
395
  VOLUME_MOUNT_OPTION="-v /etc/localtime:/etc/localtime:ro"
366
396
  if [ "${ENABLE_VOLUME_MOUNT}" = "true" ]; then
367
397
  if [ ! -d "${VOLUME_HOST_PATH}" ]; then
368
- echo "📁 볼륨 마운트 디렉토리 생성: ${VOLUME_HOST_PATH}"
398
+ m volume_mkdir path="${VOLUME_HOST_PATH}"
369
399
  SUDO mkdir -p "${VOLUME_HOST_PATH}"
370
400
  fi
371
401
  VOLUME_MOUNT_OPTION="${VOLUME_MOUNT_OPTION} -v ${VOLUME_HOST_PATH}:${VOLUME_CONTAINER_PATH}"
372
- echo "📦 볼륨 마운트 활성화: ${VOLUME_HOST_PATH} → ${VOLUME_CONTAINER_PATH}"
402
+ m volume_enabled host="${VOLUME_HOST_PATH}" container="${VOLUME_CONTAINER_PATH}"
373
403
  else
374
- echo "ℹ️ 볼륨 마운트 비활성화됨"
404
+ m volume_disabled
375
405
  fi
376
406
 
377
- # 6. 새 컨테이너 기동
378
- echo "🐳 새 컨테이너 기동: ${NEW_CONTAINER_NAME} (포트 ${NEW_PORT})"
407
+ # 6. Start the new container
408
+ m nginx_new_container_start name="${NEW_CONTAINER_NAME}" port="${NEW_PORT}"
379
409
  SUDO docker run -d \
380
410
  -p ${NEW_PORT}:${CONTAINER_INTERNAL_PORT} \
381
411
  --name "${NEW_CONTAINER_NAME}" \
@@ -384,30 +414,30 @@ jobs:
384
414
  ${VOLUME_MOUNT_OPTION} \
385
415
  "${IMAGE}"
386
416
 
387
- # 7. 컨테이너 헬스체크 (로컬 포트 직접 호출)
388
- echo "⏳ 헬스체크 시작 (최대 ${HEALTHCHECK_MAX_RETRIES}회 × ${HEALTHCHECK_RETRY_INTERVAL}초)"
417
+ # 7. Container health check (call the local port directly)
418
+ m nginx_hc_start retries="${HEALTHCHECK_MAX_RETRIES}" interval="${HEALTHCHECK_RETRY_INTERVAL}"
389
419
  HEALTHCHECK_SUCCESS=false
390
420
  for i in $(seq 1 ${HEALTHCHECK_MAX_RETRIES}); do
391
421
  if curl -fsS "http://127.0.0.1:${NEW_PORT}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
392
422
  curl -fsS "http://127.0.0.1:${NEW_PORT}/" >/dev/null 2>&1 ; then
393
- echo "✅ 헬스체크 성공 (시도 ${i})"
423
+ m nginx_hc_ok i="${i}"
394
424
  HEALTHCHECK_SUCCESS=true
395
425
  break
396
426
  fi
397
- echo "⏳ 헬스체크 진행중... (${i}/${HEALTHCHECK_MAX_RETRIES})"
427
+ m nginx_hc_progress i="${i}" max="${HEALTHCHECK_MAX_RETRIES}"
398
428
  sleep ${HEALTHCHECK_RETRY_INTERVAL}
399
429
  done
400
430
 
401
431
  if [ "${HEALTHCHECK_SUCCESS}" != "true" ]; then
402
- echo "❌ 헬스체크 실패 → 배포 중단"
432
+ m nginx_hc_fail
403
433
  SUDO docker logs --tail 200 "${NEW_CONTAINER_NAME}" || true
404
- echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
434
+ m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
405
435
  SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
406
436
  exit 1
407
437
  fi
408
438
 
409
- # 8. nginx config 포트 토글 (proxy_pass 포트 치환)
410
- echo "🔧 nginx config 포트 토글: ${ACTIVE_PORT} → ${NEW_PORT}"
439
+ # 8. Toggle the nginx config port (replace the proxy_pass port)
440
+ m nginx_toggle old="${ACTIVE_PORT}" new="${NEW_PORT}"
411
441
  TMP_FILE="$(mktemp)"
412
442
  SUDO awk -v dom="${SERVICE_DOMAIN}" -v new_port="${NEW_PORT}" '
413
443
  BEGIN {
@@ -440,97 +470,97 @@ jobs:
440
470
  }
441
471
  ' "${NGINX_RP_CONF}" > "${TMP_FILE}"
442
472
 
443
- # 9. nginx 설정 검증 + reload (실패 시 백업 복구)
444
- echo "🔧 nginx 설정 적용 및 검증"
473
+ # 9. Validate the nginx config and reload (restore the backup on failure)
474
+ m nginx_apply
445
475
  SUDO cp "${TMP_FILE}" "${NGINX_RP_CONF}"
446
476
  if ! (SUDO nginx -t); then
447
- echo "❌ nginx 설정 검증 실패 → 백업 파일로 복구"
477
+ m nginx_test_fail
448
478
  SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
449
479
  SUDO rm -f "${BACKUP_FILE}"
450
480
  rm -f "${TMP_FILE}"
451
- echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
481
+ m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
452
482
  SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
453
483
  exit 1
454
484
  fi
455
- echo "✅ nginx 설정 검증 성공"
485
+ m nginx_test_ok
456
486
  rm -f "${TMP_FILE}"
457
487
 
458
488
  echo "🔄 nginx reload"
459
489
  if ! (SUDO systemctl reload nginx); then
460
- echo "⚠️ reload 실패 → restart 시도"
490
+ m nginx_reload_fail
461
491
  if ! (SUDO systemctl restart nginx); then
462
- echo "❌ nginx reload/restart 실패 → 백업 복구"
492
+ m nginx_restart_fail
463
493
  SUDO cp "${BACKUP_FILE}" "${NGINX_RP_CONF}"
464
494
  SUDO systemctl restart nginx
465
495
  SUDO rm -f "${BACKUP_FILE}"
466
- echo "🗑️ 신규 컨테이너 정리: ${NEW_CONTAINER_NAME}"
496
+ m nginx_new_cleanup name="${NEW_CONTAINER_NAME}"
467
497
  SUDO docker rm -f "${NEW_CONTAINER_NAME}" || true
468
498
  exit 1
469
499
  fi
470
500
  fi
471
501
 
472
- # 10. 도메인 접근 검증
473
- echo "🌐 도메인 접근 검증 (${SERVICE_DOMAIN})"
502
+ # 10. Verify domain access
503
+ m nginx_domain_check domain="${SERVICE_DOMAIN}"
474
504
  DOMAIN_OK=false
475
505
  for i in $(seq 1 ${DOMAIN_CHECK_RETRIES}); do
476
506
  if curl -fsS "https://${SERVICE_DOMAIN}${HEALTHCHECK_PATH}" >/dev/null 2>&1 || \
477
507
  curl -fsS "https://${SERVICE_DOMAIN}/" >/dev/null 2>&1 ; then
478
- echo "✅ 도메인 접근 검증 성공 (시도 ${i})"
508
+ m nginx_domain_ok i="${i}"
479
509
  DOMAIN_OK=true
480
510
  break
481
511
  fi
482
- echo "⏳ 도메인 접근 검증 중... (${i}/${DOMAIN_CHECK_RETRIES})"
512
+ m nginx_domain_progress i="${i}" max="${DOMAIN_CHECK_RETRIES}"
483
513
  sleep ${DOMAIN_CHECK_INTERVAL}
484
514
  done
485
515
 
486
516
  if [ "${DOMAIN_OK}" != "true" ]; then
487
- echo "⚠️ 도메인 접근 검증 실패: 배포 진행 (DNS 전파 지연 가능성)"
517
+ m nginx_domain_fail
488
518
  fi
489
519
 
490
- # 11. 이전 컨테이너 제거
491
- echo "⏱️ in-flight 요청 처리 대기 (${IN_FLIGHT_WAIT}초)"
520
+ # 11. Remove the previous container
521
+ m inflight_wait sec="${IN_FLIGHT_WAIT}"
492
522
  sleep ${IN_FLIGHT_WAIT}
493
523
 
494
524
  if SUDO docker ps -a --format '{{.Names}}' | grep -Eq "^${OLD_CONTAINER_NAME}\$"; then
495
- echo "🗑️ old=${OLD_CONTAINER_NAME} 제거"
525
+ m old_removed name="${OLD_CONTAINER_NAME}"
496
526
  SUDO docker rm -f "${OLD_CONTAINER_NAME}" || true
497
527
  else
498
- echo "ℹ️ old=${OLD_CONTAINER_NAME} 없음 — 정리 생략"
528
+ m old_absent name="${OLD_CONTAINER_NAME}"
499
529
  fi
500
530
 
501
- # 12. dangling 이미지 정리 + 백업 보존
502
- echo "🧹 dangling(<none>) 이미지 정리..."
531
+ # 12. Clean up dangling images and keep only recent backups
532
+ m prune_dangling
503
533
  SUDO docker image prune -af
504
534
 
505
- echo "🧹 백업 파일 최신 ${NGINX_BACKUP_KEEP}개 보존"
535
+ m nginx_backup_keep keep="${NGINX_BACKUP_KEEP}"
506
536
  KEEP_PLUS_ONE=$((NGINX_BACKUP_KEEP + 1))
507
537
  SUDO /bin/bash -c "ls -1t '${NGINX_BACKUP_DIR}'/server.ReverseProxy.conf.*.bak 2>/dev/null | tail -n +${KEEP_PLUS_ONE} | xargs -r rm -f"
508
538
 
509
539
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
510
- echo "✅ 무중단 배포 완료. active=${NEW_CONTAINER_NAME} (포트 ${NEW_PORT})"
511
- echo "🌐 도메인: https://${SERVICE_DOMAIN}"
540
+ m nginx_done name="${NEW_CONTAINER_NAME}" port="${NEW_PORT}"
541
+ m banner_domain domain="https://${SERVICE_DOMAIN}"
512
542
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
513
543
 
514
- # 사용 방법
544
+ # Usage
515
545
  #
516
- # 1) GitHub Secrets 등록:
546
+ # 1) Register the GitHub Secrets:
517
547
  # APPLICATION_PROD_YML, DOCKERHUB_USERNAME, DOCKERHUB_TOKEN,
518
548
  # SERVER_HOST, SERVER_USER, SERVER_PASSWORD
519
549
  #
520
- # 2) 본 워크플로우의 env 블록에서 프로젝트별 설정 수정:
550
+ # 2) Edit the project-specific settings in this workflow's env block:
521
551
  # - PROJECT_NAME, SERVICE_DOMAIN
522
552
  # - APPLICATION_YML_DIR, CONTAINER_INTERNAL_PORT
523
- # - BLUE_PORT, GREEN_PORT (서로 다른 호스트 포트)
553
+ # - BLUE_PORT, GREEN_PORT (two different host ports)
524
554
  # - NGINX_RP_CONF, NGINX_BACKUP_DIR
525
555
  # - ENABLE_VOLUME_MOUNT, VOLUME_HOST_PATH, VOLUME_CONTAINER_PATH
526
556
  #
527
- # 3) 서버 사전 준비:
528
- # - nginx 설치 + sites-enabled config 존재
529
- # - 해당 config server 블록에 다음 라인 존재:
557
+ # 3) Server preparation:
558
+ # - nginx installed and a sites-enabled config present
559
+ # - the server block of that config contains these lines:
530
560
  # server_name ${SERVICE_DOMAIN};
531
- # proxy_pass http://127.0.0.1:${BLUE_PORT}; # 또는 ${GREEN_PORT}
532
- # - SSH 사용자가 sudo 로 nginx -t / reload, docker 명령 실행 가능
561
+ # proxy_pass http://127.0.0.1:${BLUE_PORT}; # or ${GREEN_PORT}
562
+ # - the SSH user can run nginx -t / reload and docker commands via sudo
533
563
  #
534
- # 4) 본 워크플로우로 전환:
535
- # - 본 파일의 push.branches 주석 해제
536
- # - PROJECT-SPRING-SIMPLE-CICD.yaml 의 push 트리거 주석 처리
564
+ # 4) Switch to this workflow:
565
+ # - uncomment push.branches in this file
566
+ # - comment out the push trigger in PROJECT-SPRING-SIMPLE-CICD.yaml