project-auto-wizard 0.13.3 → 0.15.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 (115) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +2 -0
  5. package/bin/project-auto-wizard.js +18 -4
  6. package/package.json +2 -2
  7. package/payload/config/breaking-changes.json +28 -14
  8. package/payload/config/wizard-prompts.yml +102 -46
  9. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  10. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  11. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  12. package/payload/scripts/changelog_manager.py +304 -265
  13. package/payload/scripts/issue_helper.py +54 -50
  14. package/payload/scripts/messages.py +2585 -0
  15. package/payload/scripts/truncate_release_notes.py +7 -6
  16. package/payload/scripts/version_manager.py +110 -107
  17. package/payload/version.yml.template +2 -1
  18. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
  19. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
  20. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  21. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
  22. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
  23. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  29. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  30. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  31. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  32. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  33. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  34. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  35. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  36. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  37. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  38. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  39. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  40. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  41. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  42. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  44. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  45. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  46. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  47. package/src/cli/args.js +98 -52
  48. package/src/cli/help.js +8 -41
  49. package/src/commands/doctor.js +90 -89
  50. package/src/commands/dry-run.js +64 -43
  51. package/src/commands/full.js +79 -77
  52. package/src/commands/install-settings.js +16 -15
  53. package/src/commands/interactive-flutter.js +20 -19
  54. package/src/commands/interactive.js +135 -132
  55. package/src/commands/purge.js +47 -42
  56. package/src/commands/status.js +36 -36
  57. package/src/commands/uninstall.js +43 -41
  58. package/src/context.js +12 -11
  59. package/src/core/assets.js +12 -11
  60. package/src/core/baseline.js +27 -27
  61. package/src/core/branches.js +29 -27
  62. package/src/core/branding.js +7 -6
  63. package/src/core/breaking-check.js +22 -21
  64. package/src/core/breaking.js +15 -8
  65. package/src/core/copy/app-files.js +5 -4
  66. package/src/core/copy/flutter-app.js +8 -8
  67. package/src/core/copy/gitignore.js +58 -50
  68. package/src/core/copy/readme.js +102 -36
  69. package/src/core/copy/simple.js +16 -15
  70. package/src/core/copy/workflows.js +131 -124
  71. package/src/core/deploy-style.js +51 -48
  72. package/src/core/detect-fs.js +57 -57
  73. package/src/core/detect.js +67 -65
  74. package/src/core/errors.js +2 -2
  75. package/src/core/flutter-doctor.js +17 -16
  76. package/src/core/flutter-hooks.js +33 -32
  77. package/src/core/flutter-options.js +34 -33
  78. package/src/core/fsutil.js +12 -12
  79. package/src/core/installed-stores.js +4 -4
  80. package/src/core/logger.js +42 -40
  81. package/src/core/paths-resolve.js +88 -86
  82. package/src/core/paths.js +10 -10
  83. package/src/core/release-options.js +8 -8
  84. package/src/core/removal-exec.js +14 -13
  85. package/src/core/removal-plan.js +37 -37
  86. package/src/core/types.js +47 -47
  87. package/src/core/verify.js +44 -38
  88. package/src/core/version-yml.js +76 -68
  89. package/src/core/wizard-env.js +46 -46
  90. package/src/core/wizard-labels.js +63 -41
  91. package/src/i18n/catalog/en/cli.js +50 -0
  92. package/src/i18n/catalog/en/commands.js +188 -0
  93. package/src/i18n/catalog/en/copy.js +20 -0
  94. package/src/i18n/catalog/en/core.js +74 -0
  95. package/src/i18n/catalog/en/core2.js +15 -0
  96. package/src/i18n/catalog/en/ui.js +221 -0
  97. package/src/i18n/catalog/en.js +12 -0
  98. package/src/i18n/catalog/index.js +5 -0
  99. package/src/i18n/catalog/ko/cli.js +50 -0
  100. package/src/i18n/catalog/ko/commands.js +188 -0
  101. package/src/i18n/catalog/ko/copy.js +20 -0
  102. package/src/i18n/catalog/ko/core.js +74 -0
  103. package/src/i18n/catalog/ko/core2.js +15 -0
  104. package/src/i18n/catalog/ko/ui.js +221 -0
  105. package/src/i18n/catalog/ko.js +10 -0
  106. package/src/i18n/index.js +44 -0
  107. package/src/i18n/languages.js +9 -0
  108. package/src/index.js +132 -97
  109. package/src/ui/ansi.js +7 -7
  110. package/src/ui/banner.js +7 -6
  111. package/src/ui/env-plan.js +82 -81
  112. package/src/ui/prompts.js +97 -122
  113. package/src/ui/readline-engine.js +59 -58
  114. package/src/ui/status-cards.js +37 -31
  115. package/src/ui/summary.js +90 -89
@@ -53,11 +53,12 @@ jobs:
53
53
  - name: Read copilot_ai option from version.yml
54
54
  id: copilot_options
55
55
  run: |
56
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
56
57
  # scoped to the metadata: -> template: -> options: chain so an
57
58
  # unrelated "copilot_ai:" key can never hijack the value
58
59
  COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
59
60
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
60
- echo "copilot_ai option: $COPILOT_AI"
61
+ echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
61
62
 
62
63
  - name: Install Copilot CLI
63
64
  if: steps.copilot_options.outputs.copilot_ai == 'true'
@@ -74,12 +75,14 @@ jobs:
74
75
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
75
76
  GH_TOKEN: ${{ github.token }}
76
77
  run: |
78
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
77
79
  PR_NUMBER=${{ github.event.pull_request.number }}
78
80
 
79
81
  git fetch origin {{MAIN_BRANCH}}
80
82
  git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
81
- # 본문 푸터 BREAKING CHANGE:는 제목 목록 뒤에 한 줄씩 덧붙인다 — 스크립트는 한 줄을
82
- # 한 항목으로 읽으므로 본문 전체(%B)를 넣으면 설명 줄까지 변경 항목으로 잡힌다.
83
+ # Body-footer BREAKING CHANGE: lines are appended after the subject list, one per
84
+ # line. The script reads each line as one entry, so passing the whole body (%B)
85
+ # would turn description lines into change entries.
83
86
  git log --pretty=%b "origin/{{MAIN_BRANCH}}..HEAD" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
84
87
  git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
85
88
  if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
@@ -89,10 +92,12 @@ jobs:
89
92
  fi
90
93
  rm -f _full_diff_stat.txt
91
94
 
92
- # 헤더에는 이미 발행된 현재 버전이 아니라 이 PR이 병합되면 붙을 예상 버전을 보인다.
93
- # 병합 뒤 버전을 올리는 쪽과 같은 규칙: trunk-based는 RELEASE-PUBLISH가 semver_auto면
94
- # 커밋으로 승격 폭을 정하고, pr-flow에서 {{MAIN_BRANCH}}로 바로 들어가는 PR은
95
- # VERSION-CONTROL 안전망이 patch를 올린다. 예상치라 AI 보조 판정 없이 규칙만 쓴다.
95
+ # The header shows the version this PR will get once merged, not the version
96
+ # that is already published. Same rules as whatever bumps the version after the
97
+ # merge: in trunk-based mode RELEASE-PUBLISH picks the bump size from the commits
98
+ # when semver_auto is on, and in pr-flow a PR going straight into {{MAIN_BRANCH}}
99
+ # gets a patch bump from the VERSION-CONTROL safety net. It is only an estimate,
100
+ # so rules alone are used, without any AI-assisted classification.
96
101
  CURRENT_VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
97
102
  MODE=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?branches:.*?mode:\s*\"?([A-Za-z-]+)", t, re.S); print(m.group(1) if m else "pr-flow")' 2>/dev/null || echo "pr-flow")
98
103
  SEMVER_AUTO=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?semver_auto:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
@@ -102,7 +107,7 @@ jobs:
102
107
  fi
103
108
  VERSION=$(python3 -c 'import sys; sys.path.insert(0, ".github/scripts"); from version_manager import increment_version; print(increment_version(sys.argv[1], sys.argv[2]))' "$CURRENT_VERSION" "${BUMP:-patch}" 2>/dev/null || echo "")
104
109
  VERSION=${VERSION:-Unreleased}
105
- echo "expected next version: $VERSION (current: ${CURRENT_VERSION:-none}, mode: $MODE, bump: ${BUMP:-patch})"
110
+ echo "$(m wf_aisum.expected_version version="$VERSION" current_version="${CURRENT_VERSION:-none}" mode="$MODE" bump="${BUMP:-patch}")"
106
111
 
107
112
  SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
108
113
  --commits-file commits.txt \
@@ -111,12 +116,12 @@ jobs:
111
116
  --pr-title "$PR_TITLE" \
112
117
  --diff-stat-file diff_stat.txt | tail -n 1)
113
118
  ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
114
- # fallback이면 사유(스크립트가 ::warning::으로도 남김)를 engine 줄에 붙인다 — 잡 로그를 열지 않아도 원인이 보이게
119
+ # On fallback, append the reason (the script also emits it as ::warning::) to the engine line so the cause is visible without opening the job log
115
120
  FALLBACK_REASON=$(python3 -c 'import html,json,sys; print(html.escape((json.loads(sys.argv[1]).get("fallback_reason") or "")[:200]))' "$SUMMARY_RESULT" 2>/dev/null || echo "")
116
121
  ENGINE_LINE="$ENGINE${FALLBACK_REASON:+ ($FALLBACK_REASON)}"
117
122
  echo "engine: $ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
118
123
 
119
- # push마다 댓글이 쌓이지 않도록 이 봇이 남긴 요약 댓글을 표식으로 찾아 최신 내용으로 교체한다
124
+ # Find this bot's earlier summary comment by its marker and replace it with the latest content so pushes do not pile up comments
120
125
  MARKER="<!-- project-auto-wizard:pr-summary -->"
121
126
  {
122
127
  echo "$MARKER"
@@ -133,25 +138,27 @@ jobs:
133
138
  --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$MARKER\"))) | .id" 2>/dev/null | tail -n 1 || true)
134
139
  if [ -n "$COMMENT_ID" ]; then
135
140
  gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --input comment_payload.json > /dev/null \
136
- || echo "comment update failed — continuing (this workflow never blocks the PR)"
141
+ || echo "$(m wf_aisum.comment_update_failed)"
137
142
  else
138
143
  gh api -X POST "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --input comment_payload.json > /dev/null \
139
- || echo "comment post failed — continuing (this workflow never blocks the PR)"
144
+ || echo "$(m wf_aisum.comment_post_failed)"
140
145
  fi
141
146
 
142
147
  - name: Link related issue via branch name
143
148
  continue-on-error: true
144
149
  env:
145
150
  GITHUB_TOKEN: ${{ github.token }}
146
- # 브랜치명을 run: 스크립트에 직접 ${{ }} 보간하지 않고 env로 전달한다 —
147
- # git ref 이름은 $, 백틱, 세미콜론 등을 포함할 수 있어 직접 보간하면
148
- # 셸 인젝션 경로가 된다(AUTO-CHANGELOG-CONTROL의 PR_TITLE과 동일한 이유).
151
+ # Pass the branch name through env instead of interpolating ${{ }} into the run:
152
+ # script - git ref names can contain $, backticks, semicolons and so on, so direct
153
+ # interpolation would be a shell-injection path (same reason as PR_TITLE in
154
+ # AUTO-CHANGELOG-CONTROL).
149
155
  HEAD_REF: ${{ github.event.pull_request.head.ref }}
150
156
  run: |
157
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
151
158
  ISSUE_NUM=$(python3 .github/scripts/issue_helper.py extract-branch-issue "$HEAD_REF")
152
159
 
153
160
  if [ -z "$ISSUE_NUM" ]; then
154
- echo "브랜치명에서 이슈 번호를 찾지 못함 — 건너뜀: $HEAD_REF"
161
+ echo "$(m wf_aisum.no_issue_in_branch ref="$HEAD_REF")"
155
162
  exit 0
156
163
  fi
157
164
 
@@ -69,7 +69,7 @@ permissions:
69
69
  copilot-requests: write
70
70
 
71
71
  env:
72
- # 스크립트 import가 .github/scripts/__pycache__를 만들어 봇 커밋에 섞이지 않게 한다
72
+ # Keep script imports from creating .github/scripts/__pycache__ so it never ends up in bot commits
73
73
  PYTHONDONTWRITEBYTECODE: "1"
74
74
 
75
75
  jobs:
@@ -85,27 +85,40 @@ jobs:
85
85
  github.event.pull_request.head.ref == '{{DEVELOP_BRANCH}}'
86
86
  runs-on: ubuntu-latest
87
87
  permissions:
88
+ contents: read
88
89
  pull-requests: read
89
90
  outputs:
90
91
  open: ${{ steps.state.outputs.open }}
91
92
  steps:
93
+ # Only the message catalog and language setting are needed for the log wording
94
+ - name: Checkout message catalog
95
+ uses: actions/checkout@v7
96
+ with:
97
+ sparse-checkout: |
98
+ .github/scripts/messages.py
99
+ version.yml
100
+ sparse-checkout-cone-mode: false
101
+
92
102
  - name: Read the current PR state
93
103
  id: state
94
104
  env:
95
105
  GH_TOKEN: ${{ github.token }}
96
106
  GH_REPO: ${{ github.repository }}
97
107
  run: |
108
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
98
109
  PR_NUMBER=${{ github.event.pull_request.number }}
99
- # 파이프라인 도중 {{DEVELOP_BRANCH}}에 push하면 그 실행은 대기열에서 기다렸다가 이미 병합된
100
- # PR을 다시 처리한다 — 빈 요약 댓글, 중복 CHANGELOG 항목, {{DEVELOP_BRANCH}}에만 남는 확정 커밋이 생긴다.
101
- # 이벤트 시점이 아니라 지금 상태를 본다. 조회에 실패하면 릴리스를 막지 않고 진행한다.
110
+ # A push to {{DEVELOP_BRANCH}} mid-pipeline queues a run that later reprocesses the
111
+ # already-merged PR - an empty summary comment, duplicate CHANGELOG entries and a
112
+ # confirm commit that lands only on {{DEVELOP_BRANCH}}.
113
+ # Look at the current state, not the event-time state. If the lookup fails,
114
+ # do not block the release and carry on.
102
115
  STATE=$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null || echo "UNKNOWN")
103
116
  if [ "$STATE" = "MERGED" ] || [ "$STATE" = "CLOSED" ]; then
104
117
  echo "open=false" >> "$GITHUB_OUTPUT"
105
- echo "::notice::PR #$PR_NUMBER is already $STATE — skipping the release pipeline"
118
+ echo "::notice::$(m wf_changelog.pr_not_open pr_number="$PR_NUMBER" state="$STATE")"
106
119
  else
107
120
  echo "open=true" >> "$GITHUB_OUTPUT"
108
- echo "PR #$PR_NUMBER state: $STATE"
121
+ echo "$(m wf_changelog.pr_state pr_number="$PR_NUMBER" state="$STATE")"
109
122
  fi
110
123
 
111
124
  changelog-and-merge:
@@ -129,18 +142,20 @@ jobs:
129
142
  - name: Read semver_auto option from version.yml
130
143
  id: semver_options
131
144
  run: |
145
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
132
146
  SEMVER_AUTO=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?semver_auto:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
133
147
  echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
134
- echo "semver_auto option: $SEMVER_AUTO"
148
+ echo "$(m wf_common.semver_auto_option semver_auto="$SEMVER_AUTO")"
135
149
 
136
150
  - name: Read copilot_ai option from version.yml
137
151
  id: copilot_options
138
152
  run: |
153
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
139
154
  # scoped to the metadata: -> template: -> options: chain so an
140
155
  # unrelated "copilot_ai:" key can never hijack the value
141
156
  COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
142
157
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
143
- echo "copilot_ai option: $COPILOT_AI"
158
+ echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
144
159
 
145
160
  - name: Install Copilot CLI
146
161
  if: steps.copilot_options.outputs.copilot_ai == 'true'
@@ -150,10 +165,12 @@ jobs:
150
165
  - name: Collect commits since last release
151
166
  id: collect
152
167
  run: |
168
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
153
169
  git fetch origin {{MAIN_BRANCH}}
154
170
  git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
155
- # 본문 푸터 BREAKING CHANGE:는 제목 목록 뒤에 한 줄씩 덧붙인다 — 스크립트는 한 줄을
156
- # 한 항목으로 읽으므로 본문 전체(%B)를 넣으면 설명 줄까지 변경 항목으로 잡힌다.
171
+ # Body-footer BREAKING CHANGE: lines are appended after the subject list, one per
172
+ # line. The script reads each line as one entry, so passing the whole body (%B)
173
+ # would turn description lines into change entries.
157
174
  git log --pretty=%b "origin/{{MAIN_BRANCH}}..HEAD" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
158
175
  git diff --stat "origin/{{MAIN_BRANCH}}...HEAD" > _full_diff_stat.txt
159
176
  if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
@@ -162,7 +179,7 @@ jobs:
162
179
  cp _full_diff_stat.txt diff_stat.txt
163
180
  fi
164
181
  rm -f _full_diff_stat.txt
165
- echo "commits to summarize: $(wc -l < commits.txt)"
182
+ echo "$(m wf_changelog.commits_to_summarize n="$(wc -l < commits.txt)")"
166
183
 
167
184
  - name: Collect issues merged into {{DEVELOP_BRANCH}}
168
185
  continue-on-error: true
@@ -170,6 +187,7 @@ jobs:
170
187
  GH_TOKEN: ${{ github.token }}
171
188
  GITHUB_TOKEN: ${{ github.token }}
172
189
  run: |
190
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
173
191
  git log --pretty=%H "origin/{{MAIN_BRANCH}}..HEAD" > commit_shas.txt
174
192
 
175
193
  gh pr list --state merged --base {{DEVELOP_BRANCH}} \
@@ -177,7 +195,7 @@ jobs:
177
195
 
178
196
  MERGED_PR_COUNT=$(python3 -c "import json; print(len(json.load(open('merged_prs.json'))))")
179
197
  if [ "$MERGED_PR_COUNT" -ge 200 ]; then
180
- echo "::warning::gh pr list가 --limit 200에 도달했습니다 — 오래된 병합 PR이 누락됐을 수 있습니다 (조회된 PR: $MERGED_PR_COUNT)"
198
+ echo "::warning::$(m wf_changelog.pr_list_limit count="$MERGED_PR_COUNT")"
181
199
  fi
182
200
 
183
201
  ISSUE_NUMBERS=$(python3 .github/scripts/changelog_manager.py collect-issue-closes \
@@ -187,7 +205,7 @@ jobs:
187
205
  rm -f commit_shas.txt merged_prs.json
188
206
 
189
207
  if [ -z "$ISSUE_NUMBERS" ]; then
190
- echo "이번 릴리스에 연결할 이슈 없음 — 건너뜀"
208
+ echo "$(m wf_changelog.no_issues)"
191
209
  exit 0
192
210
  fi
193
211
 
@@ -196,7 +214,7 @@ jobs:
196
214
  --issue-numbers "$ISSUE_NUMBERS" \
197
215
  --replace
198
216
 
199
- echo "이슈 연결: $ISSUE_NUMBERS"
217
+ echo "$(m wf_changelog.issues_linked issues="$ISSUE_NUMBERS")"
200
218
 
201
219
  - name: Confirm release version (bump + sync)
202
220
  id: bump
@@ -207,9 +225,12 @@ jobs:
207
225
  GITHUB_TOKEN: ${{ github.token }}
208
226
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
209
227
  run: |
210
- # 멱등: 이 PR에서 이미 확정한(아직 발행 전) 버전을 다시 올리지 않는다.
211
- # HEAD 커밋 메시지만 보면 확정 커밋 위에 커밋이 더 쌓일 때({{DEVELOP_BRANCH}} 추가 push,
212
- # {{MAIN_BRANCH}} 역병합) 가드가 풀려 발행되지 않은 버전이 생기므로 버전과 태그로 판정한다.
228
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
229
+ # Idempotency: never raise a version this PR already confirmed (not yet published).
230
+ # Looking only at the HEAD commit message lets the guard slip when more commits
231
+ # pile on top of the confirm commit (extra {{DEVELOP_BRANCH}} pushes, a
232
+ # {{MAIN_BRANCH}} back-merge) and an unpublished version appears, so decide by
233
+ # version and tag instead.
213
234
  HEAD_MSG=$(git log -1 --pretty=%s)
214
235
  HEAD_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
215
236
  MAIN_VERSION=$(git show "origin/{{MAIN_BRANCH}}:version.yml" 2>/dev/null | grep -m1 '^version:' | sed 's/version:[ \t]*"\{0,1\}\([0-9][0-9.]*\)"\{0,1\}.*/\1/' || true)
@@ -221,30 +242,32 @@ jobs:
221
242
  else
222
243
  BUMP="patch"
223
244
  fi
224
- echo "bump level: $BUMP"
245
+ echo "$(m wf_common.bump_level bump="$BUMP")"
225
246
 
226
247
  if [ -n "$MAIN_VERSION" ] && [ -n "$HEAD_VERSION" ] && version_gt "$HEAD_VERSION" "$MAIN_VERSION" && ! is_tagged "$HEAD_VERSION"; then
227
- # 이미 확정된 버전 — {{MAIN_BRANCH}} 버전 기준으로 다시 계산해 더 높아질 때만(예: 확정 뒤 feat 추가) 올린다.
228
- # 버전을 건너뛰지 않고, AI 보조 판정이 재실행마다 달라도 낮아지지 않는다.
248
+ # Already-confirmed version - recompute from the {{MAIN_BRANCH}} version and raise it
249
+ # only when the result is higher (e.g. a feat added after confirming).
250
+ # Versions are never skipped, and they never drop even if an AI-assisted
251
+ # classification differs between re-runs.
229
252
  TARGET=$(python3 -c 'import sys; sys.path.insert(0, ".github/scripts"); from version_manager import increment_version; print(increment_version(sys.argv[1], sys.argv[2]))' "$MAIN_VERSION" "$BUMP")
230
253
  if version_gt "$TARGET" "$HEAD_VERSION"; then
231
254
  NEW_VERSION=$(python3 .github/scripts/version_manager.py set "$TARGET" | tail -n 1)
232
- echo "confirmed version raised: $HEAD_VERSION -> $NEW_VERSION ($BUMP from {{MAIN_BRANCH}} $MAIN_VERSION)"
255
+ echo "$(m wf_changelog.version_raised head_version="$HEAD_VERSION" new_version="$NEW_VERSION" bump="$BUMP" main_branch="{{MAIN_BRANCH}}" main_version="$MAIN_VERSION")"
233
256
  else
234
257
  NEW_VERSION="$HEAD_VERSION"
235
- echo "version already confirmed and not yet released — reusing $NEW_VERSION ({{MAIN_BRANCH}} is $MAIN_VERSION)"
258
+ echo "$(m wf_changelog.version_reuse_unreleased new_version="$NEW_VERSION" main_branch="{{MAIN_BRANCH}}" main_version="$MAIN_VERSION")"
236
259
  fi
237
260
  elif [ -z "$MAIN_VERSION" ] && printf '%s\n' "$HEAD_MSG" | grep -q "^chore(version): confirm v.* \[skip ci\]"; then
238
- # {{MAIN_BRANCH}}에 아직 version.yml이 없는 첫 릴리스의 재실행
261
+ # Re-run of a first release where {{MAIN_BRANCH}} has no version.yml yet
239
262
  NEW_VERSION="$HEAD_VERSION"
240
- echo "version already confirmed by a previous run — reusing $NEW_VERSION (no re-increment)"
263
+ echo "$(m wf_changelog.version_reuse_previous new_version="$NEW_VERSION")"
241
264
  else
242
265
  NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
243
266
  python3 .github/scripts/version_manager.py sync
244
- echo "release version confirmed: $NEW_VERSION ($BUMP)"
267
+ echo "$(m wf_changelog.version_confirmed new_version="$NEW_VERSION" bump="$BUMP")"
245
268
  fi
246
269
  if [ -z "$NEW_VERSION" ]; then
247
- echo "version bump failed"
270
+ echo "$(m wf_common.version_bump_failed)"
248
271
  exit 1
249
272
  fi
250
273
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
@@ -261,6 +284,7 @@ jobs:
261
284
  GITHUB_TOKEN: ${{ github.token }}
262
285
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
263
286
  run: |
287
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
264
288
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
265
289
 
266
290
  SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
@@ -270,10 +294,10 @@ jobs:
270
294
  --pr-title "$PR_TITLE" \
271
295
  --diff-stat-file diff_stat.txt | tail -n 1)
272
296
  ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
273
- # fallback이면 사유(스크립트가 ::warning::으로도 남김)를 engine 줄에 붙인다 — 잡 로그를 열지 않아도 원인이 보이게
297
+ # On fallback, append the reason (the script also emits it as ::warning::) to the engine line so the cause is visible without opening the job log
274
298
  FALLBACK_REASON=$(python3 -c 'import html,json,sys; print(html.escape((json.loads(sys.argv[1]).get("fallback_reason") or "")[:200]))' "$SUMMARY_RESULT" 2>/dev/null || echo "")
275
299
  ENGINE_LINE="$ENGINE${FALLBACK_REASON:+ ($FALLBACK_REASON)}"
276
- echo "summary engine: $ENGINE_LINE"
300
+ echo "$(m wf_changelog.summary_engine engine_line="$ENGINE_LINE")"
277
301
  echo "engine: $ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
278
302
  echo "engine=$ENGINE" >> $GITHUB_OUTPUT
279
303
  echo "fallback_reason=$FALLBACK_REASON" >> $GITHUB_OUTPUT
@@ -288,8 +312,9 @@ jobs:
288
312
  FALLBACK_REASON: ${{ steps.summary.outputs.fallback_reason }}
289
313
  GH_TOKEN: ${{ github.token }}
290
314
  run: |
315
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
291
316
  PR_NUMBER=${{ github.event.pull_request.number }}
292
- # 재실행·재동기화 때 댓글이 쌓이지 않도록 이 봇이 남긴 요약 댓글을 표식으로 찾아 교체한다
317
+ # Find this bot's earlier summary comment by its marker and replace it so re-runs and re-syncs do not pile up comments
293
318
  MARKER="<!-- project-auto-wizard:pr-summary -->"
294
319
 
295
320
  {
@@ -307,12 +332,12 @@ jobs:
307
332
  --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$MARKER\"))) | .id" 2>/dev/null | tail -n 1 || true)
308
333
  if [ -n "$COMMENT_ID" ]; then
309
334
  gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --input comment_payload.json > /dev/null \
310
- && echo "summary comment updated (#${COMMENT_ID})" \
311
- || echo "comment update failed — continuing (this workflow never blocks the release)"
335
+ && echo "$(m wf_changelog.comment_updated comment_id="${COMMENT_ID}")" \
336
+ || echo "$(m wf_changelog.comment_update_failed)"
312
337
  else
313
338
  gh api -X POST "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --input comment_payload.json > /dev/null \
314
- && echo "summary comment posted" \
315
- || echo "comment post failed — continuing (this workflow never blocks the release)"
339
+ && echo "$(m wf_changelog.comment_posted)" \
340
+ || echo "$(m wf_changelog.comment_post_failed)"
316
341
  fi
317
342
 
318
343
  rm -f comment_body.md comment_payload.json
@@ -337,28 +362,30 @@ jobs:
337
362
  env:
338
363
  GH_TOKEN: ${{ github.token }}
339
364
  run: |
365
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
340
366
  HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
341
367
  VERSION="${{ steps.bump.outputs.new_version }}"
342
368
 
343
369
  # Working files must not land in the repo
344
370
  rm -f pr_body.md summary.md commits.txt diff_stat.txt comment_body.md comment_payload.json commit_shas.txt merged_prs.json
345
371
 
346
- # "Re-run failed jobs"는 precheck 결과를 재사용한다 — 그사이 PR이 병합·종료됐다면
347
- # 확정 커밋을 올리지 않는다 (병합 뒤 {{DEVELOP_BRANCH}}에만 남는 커밋이 된다).
372
+ # "Re-run failed jobs" reuses the precheck result - if the PR was merged or closed
373
+ # in the meantime, do not push the confirm commit (it would exist only on
374
+ # {{DEVELOP_BRANCH}} after the merge).
348
375
  PR_STATE=$(gh pr view "${{ github.event.pull_request.number }}" --json state -q .state 2>/dev/null || echo "UNKNOWN")
349
376
  if [ "$PR_STATE" = "MERGED" ] || [ "$PR_STATE" = "CLOSED" ]; then
350
- echo "::notice::PR is already $PR_STATE — not pushing release docs"
377
+ echo "::notice::$(m wf_changelog.pr_closed_skip_docs pr_state="$PR_STATE")"
351
378
  exit 0
352
379
  fi
353
380
 
354
- # -A는 러너가 만든 임시 파일·캐시까지 사용자 레포에 커밋한다.
355
- # 버전 파일은 이미 추적 중이라 -u로 충분하고, 첫 릴리스에 새로 생기는
356
- # CHANGELOG만 따로 올린다 (.gitignore에 있으면 건너뛴다).
381
+ # -A would also commit runner temp files and caches into the user's repo.
382
+ # Version files are already tracked, so -u is enough; only a CHANGELOG
383
+ # created by the first release is added separately (skipped if ignored).
357
384
  git add -u
358
385
  git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
359
386
 
360
387
  if git diff --staged --quiet; then
361
- echo "no changes to commit"
388
+ echo "$(m wf_common.no_changes)"
362
389
  else
363
390
  git commit -m "chore(version): confirm v${VERSION} and update release docs [skip ci]"
364
391
 
@@ -369,18 +396,18 @@ jobs:
369
396
 
370
397
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
371
398
  RETRY_COUNT=$((RETRY_COUNT + 1))
372
- echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
399
+ echo "$(m wf_common.push_attempt retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES")"
373
400
 
374
401
  if git push origin HEAD:$HEAD_BRANCH; then
375
402
  PUSH_SUCCESS=true
376
- echo "release docs pushed to $HEAD_BRANCH"
403
+ echo "$(m wf_changelog.docs_pushed head_branch="$HEAD_BRANCH")"
377
404
  break
378
405
  else
379
- echo "push failed, syncing with remote..."
406
+ echo "$(m wf_common.push_failed_syncing)"
380
407
  if git pull --rebase origin $HEAD_BRANCH; then
381
- echo "rebase ok, retrying push..."
408
+ echo "$(m wf_common.rebase_ok)"
382
409
  else
383
- echo "rebase failed, manual conflict resolution required"
410
+ echo "$(m wf_common.rebase_failed)"
384
411
  git rebase --abort 2>/dev/null || true
385
412
  exit 1
386
413
  fi
@@ -388,7 +415,7 @@ jobs:
388
415
  done
389
416
 
390
417
  if [ "$PUSH_SUCCESS" = false ]; then
391
- echo "push failed after $MAX_RETRIES attempts"
418
+ echo "$(m wf_common.push_gave_up max_retries="$MAX_RETRIES")"
392
419
  exit 1
393
420
  fi
394
421
  fi
@@ -399,6 +426,7 @@ jobs:
399
426
  # (RELEASE-PUBLISH etc.); GITHUB_TOKEN merges do not.
400
427
  GH_TOKEN: ${{ secrets.WORKFLOW_PAT || github.token }}
401
428
  run: |
429
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
402
430
  PR_NUMBER=${{ github.event.pull_request.number }}
403
431
  VERSION="${{ steps.bump.outputs.new_version }}"
404
432
 
@@ -407,39 +435,41 @@ jobs:
407
435
  SUBJECT="chore(release): v${VERSION} (PR #${PR_NUMBER})"
408
436
  ERR_FILE="${RUNNER_TEMP:-/tmp}/merge_err.txt"
409
437
 
410
- # 확정 커밋 push 직후에는 GitHub가 PR head 갱신을 아직 반영하지 못해 "Head branch is
411
- # out of date"·"not mergeable"로 간헐 실패한다. 간격을 늘려 가며 다시 시도한다.
438
+ # Right after the confirm commit is pushed, GitHub may not have picked up the new PR
439
+ # head yet and intermittently fails with "Head branch is out of date" / "not
440
+ # mergeable". Retry with growing intervals.
412
441
  MERGED=false
413
442
  MAX_ATTEMPTS=4
414
443
  for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
415
444
  if [ "$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null)" = "MERGED" ]; then
416
- echo "PR #$PR_NUMBER is already merged"
445
+ echo "$(m wf_changelog.pr_already_merged pr_number="$PR_NUMBER")"
417
446
  MERGED=true
418
447
  break
419
448
  fi
420
449
  if gh pr merge "$PR_NUMBER" --auto --merge --subject "$SUBJECT" 2> "$ERR_FILE"; then
421
- echo "automerge enabled: $SUBJECT"
450
+ echo "$(m wf_changelog.automerge_enabled subject="$SUBJECT")"
422
451
  MERGED=true
423
452
  break
424
453
  fi
425
454
  cat "$ERR_FILE" >&2
426
455
  if gh pr merge "$PR_NUMBER" --merge --subject "$SUBJECT" 2> "$ERR_FILE"; then
427
- echo "automerge unavailable — merged directly: $SUBJECT"
456
+ echo "$(m wf_changelog.automerge_unavailable subject="$SUBJECT")"
428
457
  MERGED=true
429
458
  break
430
459
  fi
431
460
  cat "$ERR_FILE" >&2
432
461
  [ "$ATTEMPT" -ge "$MAX_ATTEMPTS" ] && break
433
- # 충돌은 기다려도 풀리지 않는다
462
+ # A conflict never resolves by waiting
434
463
  [ "$(gh pr view "$PR_NUMBER" --json mergeable -q .mergeable 2>/dev/null)" = "CONFLICTING" ] && break
435
464
  if grep -qi "out of date" "$ERR_FILE"; then
436
- # 브랜치 보호의 "최신 상태 요구"로 실제로 뒤처졌다면 base를 head에 병합해 따라잡는다
437
- # (단순 반영 지연이면 "새 커밋 없음"으로 거절되고 그대로 재시도한다)
465
+ # If the head really fell behind because of branch protection's "require up to
466
+ # date" rule, merge base into head to catch up (a mere propagation delay is
467
+ # rejected as "no new commits" and simply retried)
438
468
  gh api -X PUT "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/update-branch" > /dev/null 2>&1 \
439
469
  && echo "head branch updated from base" || true
440
470
  fi
441
471
  WAIT=$((ATTEMPT * 10))
442
- echo "merge attempt $ATTEMPT/$MAX_ATTEMPTS failed — retrying in ${WAIT}s"
472
+ echo "$(m wf_changelog.merge_retry attempt="$ATTEMPT" max_attempts="$MAX_ATTEMPTS" wait="${WAIT}")"
443
473
  sleep "$WAIT"
444
474
  done
445
475
 
@@ -448,31 +478,43 @@ jobs:
448
478
  MERGEABLE=$(gh pr view "$PR_NUMBER" --json mergeable -q .mergeable 2>/dev/null || echo "unknown")
449
479
  ALLOW_MERGE_COMMIT=$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.allow_merge_commit' 2>/dev/null || echo "unknown")
450
480
  if [ "$MERGEABLE" = "CONFLICTING" ]; then
451
- # 주로 {{MAIN_BRANCH}} 직접 push 뒤 안전망 버전 bump가 {{DEVELOP_BRANCH}}에 반영되지 않았을 때다
452
- echo "::error::릴리스 PR이 {{MAIN_BRANCH}}와 충돌합니다 (버전 파일·CHANGELOG 등) — {{MAIN_BRANCH}}에만 있는 커밋을 {{DEVELOP_BRANCH}}에 병합한 뒤 push하세요: git checkout {{DEVELOP_BRANCH}} && git merge origin/{{MAIN_BRANCH}} (버전 줄 충돌은 어느 쪽을 택해도 다음 실행이 알맞게 다시 확정합니다)"
481
+ # Mostly happens when a safety-net version bump after a direct {{MAIN_BRANCH}} push never reached {{DEVELOP_BRANCH}}
482
+ echo "::error::$(m wf_changelog.merge_conflict prod="{{MAIN_BRANCH}}" dev="{{DEVELOP_BRANCH}}")"
453
483
  elif [ "$ALLOW_MERGE_COMMIT" = "false" ]; then
454
- echo "::error::이 레포는 merge commit이 비활성화되어 있습니다 — Settings에서 'Allow merge commits'를 켜세요; 릴리스 감지는 merge commit subject를 사용합니다"
484
+ echo "::error::$(m wf_changelog.merge_commit_disabled)"
455
485
  else
456
- echo "::error::PR merge failed after $MAX_ATTEMPTS attempts (allow_merge_commit=$ALLOW_MERGE_COMMIT) — check branch protection rules, required checks, and token permissions"
486
+ echo "::error::$(m wf_changelog.merge_gave_up max_attempts="$MAX_ATTEMPTS" allow_merge_commit="$ALLOW_MERGE_COMMIT")"
457
487
  fi
458
488
  exit 1
459
489
  fi
460
490
 
461
491
  wait-for-merge-and-trigger-release:
462
- name: Wait for merge and trigger RELEASE-PUBLISH (WORKFLOW_PAT 없이도 동작)
492
+ name: Wait for merge and trigger RELEASE-PUBLISH (works without WORKFLOW_PAT)
463
493
  needs: changelog-and-merge
464
494
  runs-on: ubuntu-latest
465
495
  timeout-minutes: 35
466
496
  permissions:
497
+ # contents: read is needed for the sparse checkout that loads the message catalog
498
+ contents: read
467
499
  pull-requests: read
468
500
  actions: write
469
501
  steps:
502
+ # Only the message catalog and version.yml (language) are needed here
503
+ - name: Checkout message catalog
504
+ uses: actions/checkout@v7
505
+ with:
506
+ sparse-checkout: |
507
+ .github/scripts/messages.py
508
+ version.yml
509
+ sparse-checkout-cone-mode: false
510
+
470
511
  - name: Poll for merge completion
471
512
  id: wait
472
513
  env:
473
514
  GH_TOKEN: ${{ github.token }}
474
515
  GH_REPO: ${{ github.repository }}
475
516
  run: |
517
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
476
518
  PR_NUMBER=${{ github.event.pull_request.number }}
477
519
  MAX_WAIT=1800
478
520
  INTERVAL=20
@@ -483,7 +525,7 @@ jobs:
483
525
  PR_STATE=$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null || echo "")
484
526
  if [ "$PR_STATE" = "MERGED" ]; then
485
527
  MERGED=true
486
- echo "PR #$PR_NUMBER 병합 확인 (경과 ${ELAPSED}s)"
528
+ echo "$(m wf_changelog.merge_confirmed pr="$PR_NUMBER" elapsed="$ELAPSED")"
487
529
  break
488
530
  fi
489
531
  sleep $INTERVAL
@@ -491,7 +533,7 @@ jobs:
491
533
  done
492
534
 
493
535
  if [ "$MERGED" != "true" ]; then
494
- echo "::warning::PR #$PR_NUMBER 병합을 ${MAX_WAIT}s 내에 확인하지 못했습니다 — RELEASE-PUBLISH 자동 트리거를 건너뜁니다. WORKFLOW_PAT이 등록되어 있다면 기존 경로로 정상 진행되며, 없다면 필요 시 RELEASE-PUBLISH를 수동으로 workflow_dispatch 하세요."
536
+ echo "::warning::$(m wf_changelog.merge_timeout pr="$PR_NUMBER" max="$MAX_WAIT")"
495
537
  fi
496
538
 
497
539
  echo "merged=$MERGED" >> "$GITHUB_OUTPUT"
@@ -503,5 +545,6 @@ jobs:
503
545
  GH_TOKEN: ${{ github.token }}
504
546
  GH_REPO: ${{ github.repository }}
505
547
  run: |
548
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
506
549
  gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
507
- echo "RELEASE-PUBLISH workflow_dispatch 트리거 요청 완료 (WORKFLOW_PAT 없이도 동작)"
550
+ echo "$(m wf_changelog.release_publish_triggered)"