project-auto-wizard 0.14.0 → 0.15.1

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 (64) hide show
  1. package/README.ja.md +3 -1
  2. package/README.ko.md +3 -1
  3. package/README.md +4 -2
  4. package/README.zh-CN.md +3 -1
  5. package/package.json +1 -1
  6. package/payload/config/breaking-changes.json +31 -5
  7. package/payload/config/wizard-prompts.yml +0 -4
  8. package/payload/scripts/changelog_manager.py +8 -8
  9. package/payload/scripts/messages.py +460 -24
  10. package/payload/scripts/version_manager.py +102 -73
  11. package/payload/version.yml.template +2 -2
  12. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +9 -4
  13. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +62 -30
  14. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +96 -44
  15. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +79 -35
  16. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +71 -35
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
  25. package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
  26. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
  27. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
  28. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
  29. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
  30. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
  31. package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
  32. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
  33. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
  38. package/src/cli/args.js +17 -7
  39. package/src/commands/doctor.js +16 -0
  40. package/src/commands/dry-run.js +10 -1
  41. package/src/commands/full.js +5 -2
  42. package/src/commands/interactive.js +4 -1
  43. package/src/commands/status.js +14 -1
  44. package/src/core/copy/readme.js +58 -2
  45. package/src/core/copy/simple.js +31 -0
  46. package/src/core/copy/workflows.js +2 -2
  47. package/src/core/deploy-style.js +1 -1
  48. package/src/core/detect.js +3 -2
  49. package/src/core/types.js +15 -8
  50. package/src/core/version-yml.js +52 -6
  51. package/src/i18n/catalog/en/cli.js +1 -0
  52. package/src/i18n/catalog/en/commands.js +9 -0
  53. package/src/i18n/catalog/en/copy.js +1 -0
  54. package/src/i18n/catalog/en/core2.js +3 -0
  55. package/src/i18n/catalog/ko/cli.js +1 -0
  56. package/src/i18n/catalog/ko/commands.js +9 -0
  57. package/src/i18n/catalog/ko/copy.js +1 -0
  58. package/src/i18n/catalog/ko/core2.js +3 -0
  59. package/src/index.js +8 -4
  60. package/src/ui/env-plan.js +1 -1
  61. package/src/ui/prompts.js +7 -3
  62. package/src/ui/summary.js +1 -1
  63. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
  64. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
@@ -85,16 +85,28 @@ 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
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
109
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
98
110
  PR_NUMBER=${{ github.event.pull_request.number }}
99
111
  # A push to {{DEVELOP_BRANCH}} mid-pipeline queues a run that later reprocesses the
100
112
  # already-merged PR - an empty summary comment, duplicate CHANGELOG entries and a
@@ -104,10 +116,10 @@ jobs:
104
116
  STATE=$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null || echo "UNKNOWN")
105
117
  if [ "$STATE" = "MERGED" ] || [ "$STATE" = "CLOSED" ]; then
106
118
  echo "open=false" >> "$GITHUB_OUTPUT"
107
- echo "::notice::PR #$PR_NUMBER is already $STATE — skipping the release pipeline"
119
+ echo "::notice::$(m wf_changelog.pr_not_open pr_number="$PR_NUMBER" state="$STATE")"
108
120
  else
109
121
  echo "open=true" >> "$GITHUB_OUTPUT"
110
- echo "PR #$PR_NUMBER state: $STATE"
122
+ echo "$(m wf_changelog.pr_state pr_number="$PR_NUMBER" state="$STATE")"
111
123
  fi
112
124
 
113
125
  changelog-and-merge:
@@ -131,18 +143,22 @@ jobs:
131
143
  - name: Read semver_auto option from version.yml
132
144
  id: semver_options
133
145
  run: |
146
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
147
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
134
148
  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")
135
149
  echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
136
- echo "semver_auto option: $SEMVER_AUTO"
150
+ echo "$(m wf_common.semver_auto_option semver_auto="$SEMVER_AUTO")"
137
151
 
138
152
  - name: Read copilot_ai option from version.yml
139
153
  id: copilot_options
140
154
  run: |
155
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
156
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
141
157
  # scoped to the metadata: -> template: -> options: chain so an
142
158
  # unrelated "copilot_ai:" key can never hijack the value
143
159
  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")
144
160
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
145
- echo "copilot_ai option: $COPILOT_AI"
161
+ echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
146
162
 
147
163
  - name: Install Copilot CLI
148
164
  if: steps.copilot_options.outputs.copilot_ai == 'true'
@@ -152,6 +168,8 @@ jobs:
152
168
  - name: Collect commits since last release
153
169
  id: collect
154
170
  run: |
171
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
172
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
155
173
  git fetch origin {{MAIN_BRANCH}}
156
174
  git log --pretty=%s "origin/{{MAIN_BRANCH}}..HEAD" > commits.txt
157
175
  # Body-footer BREAKING CHANGE: lines are appended after the subject list, one per
@@ -165,7 +183,7 @@ jobs:
165
183
  cp _full_diff_stat.txt diff_stat.txt
166
184
  fi
167
185
  rm -f _full_diff_stat.txt
168
- echo "commits to summarize: $(wc -l < commits.txt)"
186
+ echo "$(m wf_changelog.commits_to_summarize n="$(wc -l < commits.txt)")"
169
187
 
170
188
  - name: Collect issues merged into {{DEVELOP_BRANCH}}
171
189
  continue-on-error: true
@@ -173,6 +191,7 @@ jobs:
173
191
  GH_TOKEN: ${{ github.token }}
174
192
  GITHUB_TOKEN: ${{ github.token }}
175
193
  run: |
194
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
176
195
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
177
196
  git log --pretty=%H "origin/{{MAIN_BRANCH}}..HEAD" > commit_shas.txt
178
197
 
@@ -211,6 +230,8 @@ jobs:
211
230
  GITHUB_TOKEN: ${{ github.token }}
212
231
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
213
232
  run: |
233
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
234
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
214
235
  # Idempotency: never raise a version this PR already confirmed (not yet published).
215
236
  # Looking only at the HEAD commit message lets the guard slip when more commits
216
237
  # pile on top of the confirm commit (extra {{DEVELOP_BRANCH}} pushes, a
@@ -227,7 +248,7 @@ jobs:
227
248
  else
228
249
  BUMP="patch"
229
250
  fi
230
- echo "bump level: $BUMP"
251
+ echo "$(m wf_common.bump_level bump="$BUMP")"
231
252
 
232
253
  if [ -n "$MAIN_VERSION" ] && [ -n "$HEAD_VERSION" ] && version_gt "$HEAD_VERSION" "$MAIN_VERSION" && ! is_tagged "$HEAD_VERSION"; then
233
254
  # Already-confirmed version - recompute from the {{MAIN_BRANCH}} version and raise it
@@ -237,22 +258,22 @@ jobs:
237
258
  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")
238
259
  if version_gt "$TARGET" "$HEAD_VERSION"; then
239
260
  NEW_VERSION=$(python3 .github/scripts/version_manager.py set "$TARGET" | tail -n 1)
240
- echo "confirmed version raised: $HEAD_VERSION -> $NEW_VERSION ($BUMP from {{MAIN_BRANCH}} $MAIN_VERSION)"
261
+ echo "$(m wf_changelog.version_raised head_version="$HEAD_VERSION" new_version="$NEW_VERSION" bump="$BUMP" main_branch="{{MAIN_BRANCH}}" main_version="$MAIN_VERSION")"
241
262
  else
242
263
  NEW_VERSION="$HEAD_VERSION"
243
- echo "version already confirmed and not yet released — reusing $NEW_VERSION ({{MAIN_BRANCH}} is $MAIN_VERSION)"
264
+ echo "$(m wf_changelog.version_reuse_unreleased new_version="$NEW_VERSION" main_branch="{{MAIN_BRANCH}}" main_version="$MAIN_VERSION")"
244
265
  fi
245
266
  elif [ -z "$MAIN_VERSION" ] && printf '%s\n' "$HEAD_MSG" | grep -q "^chore(version): confirm v.* \[skip ci\]"; then
246
267
  # Re-run of a first release where {{MAIN_BRANCH}} has no version.yml yet
247
268
  NEW_VERSION="$HEAD_VERSION"
248
- echo "version already confirmed by a previous run — reusing $NEW_VERSION (no re-increment)"
269
+ echo "$(m wf_changelog.version_reuse_previous new_version="$NEW_VERSION")"
249
270
  else
250
271
  NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
251
272
  python3 .github/scripts/version_manager.py sync
252
- echo "release version confirmed: $NEW_VERSION ($BUMP)"
273
+ echo "$(m wf_changelog.version_confirmed new_version="$NEW_VERSION" bump="$BUMP")"
253
274
  fi
254
275
  if [ -z "$NEW_VERSION" ]; then
255
- echo "version bump failed"
276
+ echo "$(m wf_common.version_bump_failed)"
256
277
  exit 1
257
278
  fi
258
279
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
@@ -269,6 +290,8 @@ jobs:
269
290
  GITHUB_TOKEN: ${{ github.token }}
270
291
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
271
292
  run: |
293
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
294
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
272
295
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
273
296
 
274
297
  SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
@@ -281,7 +304,7 @@ jobs:
281
304
  # 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
282
305
  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 "")
283
306
  ENGINE_LINE="$ENGINE${FALLBACK_REASON:+ ($FALLBACK_REASON)}"
284
- echo "summary engine: $ENGINE_LINE"
307
+ echo "$(m wf_changelog.summary_engine engine_line="$ENGINE_LINE")"
285
308
  echo "engine: $ENGINE_LINE" >> "$GITHUB_STEP_SUMMARY"
286
309
  echo "engine=$ENGINE" >> $GITHUB_OUTPUT
287
310
  echo "fallback_reason=$FALLBACK_REASON" >> $GITHUB_OUTPUT
@@ -296,6 +319,8 @@ jobs:
296
319
  FALLBACK_REASON: ${{ steps.summary.outputs.fallback_reason }}
297
320
  GH_TOKEN: ${{ github.token }}
298
321
  run: |
322
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
323
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
299
324
  PR_NUMBER=${{ github.event.pull_request.number }}
300
325
  # Find this bot's earlier summary comment by its marker and replace it so re-runs and re-syncs do not pile up comments
301
326
  MARKER="<!-- project-auto-wizard:pr-summary -->"
@@ -315,12 +340,12 @@ jobs:
315
340
  --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$MARKER\"))) | .id" 2>/dev/null | tail -n 1 || true)
316
341
  if [ -n "$COMMENT_ID" ]; then
317
342
  gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --input comment_payload.json > /dev/null \
318
- && echo "summary comment updated (#${COMMENT_ID})" \
319
- || echo "comment update failed — continuing (this workflow never blocks the release)"
343
+ && echo "$(m wf_changelog.comment_updated comment_id="${COMMENT_ID}")" \
344
+ || echo "$(m wf_changelog.comment_update_failed)"
320
345
  else
321
346
  gh api -X POST "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --input comment_payload.json > /dev/null \
322
- && echo "summary comment posted" \
323
- || echo "comment post failed — continuing (this workflow never blocks the release)"
347
+ && echo "$(m wf_changelog.comment_posted)" \
348
+ || echo "$(m wf_changelog.comment_post_failed)"
324
349
  fi
325
350
 
326
351
  rm -f comment_body.md comment_payload.json
@@ -344,8 +369,12 @@ jobs:
344
369
  - name: Commit release docs to the PR head branch
345
370
  env:
346
371
  GH_TOKEN: ${{ github.token }}
372
+ # Passed through env so shell metacharacters in the branch name are never parsed as commands
373
+ PAW_HEAD_REF: ${{ github.event.pull_request.head.ref }}
347
374
  run: |
348
- HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
375
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
376
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
377
+ HEAD_BRANCH="$PAW_HEAD_REF"
349
378
  VERSION="${{ steps.bump.outputs.new_version }}"
350
379
 
351
380
  # Working files must not land in the repo
@@ -356,7 +385,7 @@ jobs:
356
385
  # {{DEVELOP_BRANCH}} after the merge).
357
386
  PR_STATE=$(gh pr view "${{ github.event.pull_request.number }}" --json state -q .state 2>/dev/null || echo "UNKNOWN")
358
387
  if [ "$PR_STATE" = "MERGED" ] || [ "$PR_STATE" = "CLOSED" ]; then
359
- echo "::notice::PR is already $PR_STATE — not pushing release docs"
388
+ echo "::notice::$(m wf_changelog.pr_closed_skip_docs pr_state="$PR_STATE")"
360
389
  exit 0
361
390
  fi
362
391
 
@@ -367,7 +396,7 @@ jobs:
367
396
  git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
368
397
 
369
398
  if git diff --staged --quiet; then
370
- echo "no changes to commit"
399
+ echo "$(m wf_common.no_changes)"
371
400
  else
372
401
  git commit -m "chore(version): confirm v${VERSION} and update release docs [skip ci]"
373
402
 
@@ -378,18 +407,18 @@ jobs:
378
407
 
379
408
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
380
409
  RETRY_COUNT=$((RETRY_COUNT + 1))
381
- echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
410
+ echo "$(m wf_common.push_attempt retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES")"
382
411
 
383
412
  if git push origin HEAD:$HEAD_BRANCH; then
384
413
  PUSH_SUCCESS=true
385
- echo "release docs pushed to $HEAD_BRANCH"
414
+ echo "$(m wf_changelog.docs_pushed head_branch="$HEAD_BRANCH")"
386
415
  break
387
416
  else
388
- echo "push failed, syncing with remote..."
417
+ echo "$(m wf_common.push_failed_syncing)"
389
418
  if git pull --rebase origin $HEAD_BRANCH; then
390
- echo "rebase ok, retrying push..."
419
+ echo "$(m wf_common.rebase_ok)"
391
420
  else
392
- echo "rebase failed, manual conflict resolution required"
421
+ echo "$(m wf_common.rebase_failed)"
393
422
  git rebase --abort 2>/dev/null || true
394
423
  exit 1
395
424
  fi
@@ -397,7 +426,7 @@ jobs:
397
426
  done
398
427
 
399
428
  if [ "$PUSH_SUCCESS" = false ]; then
400
- echo "push failed after $MAX_RETRIES attempts"
429
+ echo "$(m wf_common.push_gave_up max_retries="$MAX_RETRIES")"
401
430
  exit 1
402
431
  fi
403
432
  fi
@@ -408,6 +437,7 @@ jobs:
408
437
  # (RELEASE-PUBLISH etc.); GITHUB_TOKEN merges do not.
409
438
  GH_TOKEN: ${{ secrets.WORKFLOW_PAT || github.token }}
410
439
  run: |
440
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
411
441
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
412
442
  PR_NUMBER=${{ github.event.pull_request.number }}
413
443
  VERSION="${{ steps.bump.outputs.new_version }}"
@@ -424,18 +454,18 @@ jobs:
424
454
  MAX_ATTEMPTS=4
425
455
  for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
426
456
  if [ "$(gh pr view "$PR_NUMBER" --json state -q .state 2>/dev/null)" = "MERGED" ]; then
427
- echo "PR #$PR_NUMBER is already merged"
457
+ echo "$(m wf_changelog.pr_already_merged pr_number="$PR_NUMBER")"
428
458
  MERGED=true
429
459
  break
430
460
  fi
431
461
  if gh pr merge "$PR_NUMBER" --auto --merge --subject "$SUBJECT" 2> "$ERR_FILE"; then
432
- echo "automerge enabled: $SUBJECT"
462
+ echo "$(m wf_changelog.automerge_enabled subject="$SUBJECT")"
433
463
  MERGED=true
434
464
  break
435
465
  fi
436
466
  cat "$ERR_FILE" >&2
437
467
  if gh pr merge "$PR_NUMBER" --merge --subject "$SUBJECT" 2> "$ERR_FILE"; then
438
- echo "automerge unavailable — merged directly: $SUBJECT"
468
+ echo "$(m wf_changelog.automerge_unavailable subject="$SUBJECT")"
439
469
  MERGED=true
440
470
  break
441
471
  fi
@@ -451,7 +481,7 @@ jobs:
451
481
  && echo "head branch updated from base" || true
452
482
  fi
453
483
  WAIT=$((ATTEMPT * 10))
454
- echo "merge attempt $ATTEMPT/$MAX_ATTEMPTS failed — retrying in ${WAIT}s"
484
+ echo "$(m wf_changelog.merge_retry attempt="$ATTEMPT" max_attempts="$MAX_ATTEMPTS" wait="${WAIT}")"
455
485
  sleep "$WAIT"
456
486
  done
457
487
 
@@ -465,7 +495,7 @@ jobs:
465
495
  elif [ "$ALLOW_MERGE_COMMIT" = "false" ]; then
466
496
  echo "::error::$(m wf_changelog.merge_commit_disabled)"
467
497
  else
468
- echo "::error::PR merge failed after $MAX_ATTEMPTS attempts (allow_merge_commit=$ALLOW_MERGE_COMMIT) — check branch protection rules, required checks, and token permissions"
498
+ echo "::error::$(m wf_changelog.merge_gave_up max_attempts="$MAX_ATTEMPTS" allow_merge_commit="$ALLOW_MERGE_COMMIT")"
469
499
  fi
470
500
  exit 1
471
501
  fi
@@ -496,6 +526,7 @@ jobs:
496
526
  GH_TOKEN: ${{ github.token }}
497
527
  GH_REPO: ${{ github.repository }}
498
528
  run: |
529
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
499
530
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
500
531
  PR_NUMBER=${{ github.event.pull_request.number }}
501
532
  MAX_WAIT=1800
@@ -527,6 +558,7 @@ jobs:
527
558
  GH_TOKEN: ${{ github.token }}
528
559
  GH_REPO: ${{ github.repository }}
529
560
  run: |
561
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
530
562
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
531
563
  gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
532
564
  echo "$(m wf_changelog.release_publish_triggered)"
@@ -39,6 +39,15 @@
39
39
  # README always ends on the released version. Run it manually
40
40
  # (workflow_dispatch) to catch up at any other time.
41
41
  #
42
+ # trunk-based mode: RELEASE-PUBLISH bumps the version, rewrites the README
43
+ # line in its own commit and then dispatches this workflow, so the push
44
+ # run is skipped there. Two workflows committing to the same README line
45
+ # at the same time is what made releases fail on a rebase conflict.
46
+ #
47
+ # Push conflicts: if the README on {{MAIN_BRANCH}} moved in the meantime
48
+ # (a release commit rewrote the same line), a run that shows the same
49
+ # version, or whose version is already stale, is skipped instead of failing.
50
+ #
42
51
  # The version line under the AUTO-VERSION-SECTION marker is already
43
52
  # rewritten inside the release commit itself (version_manager.py
44
53
  # increment/set), so the release tag snapshot shows the released
@@ -83,25 +92,48 @@ jobs:
83
92
  git config --local user.email "action@github.com"
84
93
  git config --local user.name "GitHub Action"
85
94
 
95
+ - name: Detect branch mode
96
+ id: mode
97
+ run: |
98
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
99
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
100
+ # scoped to the metadata: -> template: -> branches: chain so an
101
+ # unrelated top-level "mode:" key can never hijack the value
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")
103
+ # A push in trunk-based mode is a release: RELEASE-PUBLISH rewrites the
104
+ # README itself and dispatches this workflow afterwards. Running on the
105
+ # push too would race it on the same README line.
106
+ if [ "$MODE" = "trunk-based" ] && [ "${{ github.event_name }}" = "push" ]; then
107
+ echo "skip=true" >> "$GITHUB_OUTPUT"
108
+ echo "$(m wf_readme.trunk_skip)"
109
+ else
110
+ echo "skip=false" >> "$GITHUB_OUTPUT"
111
+ fi
112
+
86
113
  - name: Check README.md exists
87
114
  id: precheck
115
+ if: steps.mode.outputs.skip != 'true'
88
116
  run: |
117
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
118
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
89
119
  if [ -f README.md ]; then
90
120
  echo "readme_exists=true" >> $GITHUB_OUTPUT
91
121
  else
92
122
  echo "readme_exists=false" >> $GITHUB_OUTPUT
93
- echo "README.md not found — nothing to update, skipping"
123
+ echo "$(m wf_readme.not_found)"
94
124
  fi
95
125
 
96
126
  - name: Read latest version via version_manager.py
97
127
  id: version_info
98
128
  if: steps.precheck.outputs.readme_exists == 'true'
99
129
  run: |
130
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
131
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
100
132
  LATEST_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
101
133
  # Guard: an empty version must never reach the README rewrite —
102
134
  # it would commit a corrupted "v (date)" line.
103
135
  if [ -z "$LATEST_VERSION" ]; then
104
- echo "failed to read the current version"
136
+ echo "$(m wf_readme.read_version_failed)"
105
137
  exit 1
106
138
  fi
107
139
  RELEASE_DATE=$(date '+%Y-%m-%d')
@@ -112,6 +144,7 @@ jobs:
112
144
  - name: Update version line in README.md
113
145
  if: steps.precheck.outputs.readme_exists == 'true'
114
146
  run: |
147
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
115
148
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
116
149
  LATEST_VERSION="${{ steps.version_info.outputs.latest_version }}"
117
150
  RELEASE_DATE="${{ steps.version_info.outputs.release_date }}"
@@ -130,28 +163,28 @@ jobs:
130
163
  local version_text="$4"
131
164
 
132
165
  echo ""
133
- echo "README.md version update failed"
134
- echo " error type: $error_type"
135
- echo " current version line: $version_line"
136
- echo " extracted header: $header_part"
137
- echo " new version text: $version_text"
166
+ echo "$(m wf_readme.update_failed)"
167
+ echo "$(m wf_readme.err_type error_type="$error_type")"
168
+ echo "$(m wf_readme.err_current_line version_line="$version_line")"
169
+ echo "$(m wf_readme.err_header header_part="$header_part")"
170
+ echo "$(m wf_readme.err_new_text version_text="$version_text")"
138
171
  echo ""
139
- echo "Make sure the README version line matches one of:"
172
+ echo "$(m wf_readme.hint_formats)"
140
173
  echo " $(m wf_readme.hint_first_example)"
141
174
  echo " ## Current Version : v1.0.0"
142
175
  echo " ## Version : v1.0.0"
143
- echo "Notes:"
144
- echo " - no markdown bold (**) characters"
145
- echo " - no regex special characters (*, [, ], ^, $)"
146
- echo " - the version must come right after the colon (:)"
176
+ echo "$(m wf_readme.hint_notes)"
177
+ echo "$(m wf_readme.hint_no_bold)"
178
+ echo "$(m wf_readme.hint_no_regex)"
179
+ echo "$(m wf_readme.hint_after_colon)"
147
180
  }
148
181
 
149
182
  if [ "$SHOW_DATE" = "true" ]; then
150
183
  VERSION_TEXT="v${LATEST_VERSION} (${RELEASE_DATE})"
151
- echo "version text with date: $VERSION_TEXT"
184
+ echo "$(m wf_readme.text_with_date version_text="$VERSION_TEXT")"
152
185
  else
153
186
  VERSION_TEXT="v${LATEST_VERSION}"
154
- echo "version text: $VERSION_TEXT"
187
+ echo "$(m wf_readme.text version_text="$VERSION_TEXT")"
155
188
  fi
156
189
 
157
190
  # Patterns for recognized version lines (case-insensitive, flexible spacing)
@@ -172,14 +205,14 @@ jobs:
172
205
  if grep -qi "$pattern" README.md; then
173
206
  VERSION_LINE=$(grep -i "$pattern" README.md | head -n 1)
174
207
  PATTERN_FOUND="$pattern"
175
- echo "version line found: $VERSION_LINE"
208
+ echo "$(m wf_readme.line_found version_line="$VERSION_LINE")"
176
209
  break
177
210
  fi
178
211
  done
179
212
 
180
213
  # No version line yet — insert a default one
181
214
  if [ -z "$VERSION_LINE" ]; then
182
- echo "no version line found — inserting a new one"
215
+ echo "$(m wf_readme.line_inserting)"
183
216
  # Heading wording follows the configured language; an existing line keeps its own wording
184
217
  DEFAULT_HEADING=$(m wf_readme.default_heading)
185
218
 
@@ -190,16 +223,16 @@ jobs:
190
223
  # at the very top of the file
191
224
  sed -i '1i\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n## '"$DEFAULT_HEADING"' : '"$VERSION_TEXT"'\n' README.md
192
225
  fi
193
- echo "default version line inserted: $VERSION_TEXT"
226
+ echo "$(m wf_readme.line_inserted version_text="$VERSION_TEXT")"
194
227
  else
195
228
  # Update the existing version line
196
229
 
197
230
  # Validate the line format
198
231
  if [[ "$VERSION_LINE" =~ \*\* ]]; then
199
232
  echo ""
200
- echo "warning: the version line contains markdown bold (**) characters"
201
- echo " current line: $VERSION_LINE"
202
- echo " this may break the version update"
233
+ echo "$(m wf_readme.warn_bold)"
234
+ echo "$(m wf_readme.warn_current_line version_line="$VERSION_LINE")"
235
+ echo "$(m wf_readme.warn_may_break)"
203
236
  echo ""
204
237
  fi
205
238
 
@@ -211,12 +244,12 @@ jobs:
211
244
  PREV_LINE=$(sed -n "${PREV_LINE_NUMBER}p" README.md)
212
245
  if [[ "$PREV_LINE" != *"AUTO-VERSION-SECTION"* ]]; then
213
246
  sed -i "${PREV_LINE_NUMBER}a\\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->" README.md
214
- echo "auto-update marker comment added"
247
+ echo "$(m wf_readme.marker_added)"
215
248
  LINE_NUMBER=$((LINE_NUMBER + 1))
216
249
  fi
217
250
  else
218
251
  sed -i '1i\<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->' README.md
219
- echo "auto-update marker comment added"
252
+ echo "$(m wf_readme.marker_added)"
220
253
  LINE_NUMBER=$((LINE_NUMBER + 1))
221
254
  fi
222
255
 
@@ -224,7 +257,7 @@ jobs:
224
257
  HEADER_PART=$(printf '%s\n' "$VERSION_LINE" | sed 's/\(.*:\).*/\1 /')
225
258
 
226
259
  if [ -z "$HEADER_PART" ] || [[ "$HEADER_PART" == "$VERSION_LINE" ]]; then
227
- print_error_message "header extraction failed - no colon (:) found" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
260
+ print_error_message "$(m wf_readme.err_no_colon)" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
228
261
  exit 1
229
262
  fi
230
263
 
@@ -237,62 +270,81 @@ jobs:
237
270
  ESCAPED_LINE=$(printf '%s' "$NEW_LINE" | sed 's/[&\\|]/\\&/g')
238
271
 
239
272
  if ! sed -i "${LINE_NUMBER}s|.*|${ESCAPED_LINE}|" README.md 2>/tmp/sed_error.log; then
240
- SED_ERROR=$(cat /tmp/sed_error.log 2>/dev/null || echo "unknown sed error")
241
- print_error_message "sed command failed - $SED_ERROR" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
273
+ SED_ERROR=$(cat /tmp/sed_error.log 2>/dev/null || m wf_readme.err_sed_unknown)
274
+ print_error_message "$(m wf_readme.err_sed error="$SED_ERROR")" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
242
275
  exit 1
243
276
  fi
244
277
 
245
278
  # Verify the replacement
246
279
  UPDATED_LINE=$(sed -n "${LINE_NUMBER}p" README.md)
247
280
  if [[ "$UPDATED_LINE" != *"$VERSION_TEXT"* ]]; then
248
- print_error_message "version replacement verification failed" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
249
- echo " updated line: $UPDATED_LINE"
281
+ print_error_message "$(m wf_readme.err_verify)" "$VERSION_LINE" "$HEADER_PART" "$VERSION_TEXT"
282
+ echo "$(m wf_readme.updated_line_detail updated_line="$UPDATED_LINE")"
250
283
  exit 1
251
284
  fi
252
285
 
253
- echo "version line updated: $VERSION_TEXT"
286
+ echo "$(m wf_readme.line_updated version_text="$VERSION_TEXT")"
254
287
  fi
255
288
 
256
289
  - name: Commit and push changes
257
290
  if: steps.precheck.outputs.readme_exists == 'true'
258
291
  run: |
292
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
293
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
259
294
  git add README.md
260
295
 
261
296
  if git diff --staged --quiet; then
262
- echo "no changes in README.md"
297
+ echo "$(m wf_readme.no_changes)"
263
298
  else
264
299
  VERSION="${{ steps.version_info.outputs.latest_version }}"
265
300
 
266
301
  git commit -m "docs(readme): update version to v$VERSION [skip ci]"
267
302
 
268
- # Race-condition guard: pull-rebase then push (up to 3 retries)
269
- MAX_RETRIES=3
303
+ # >>> push-with-retry
304
+ # Race-condition guard: fetch -> rebase -> push, retried
305
+ MAX_RETRIES=5
270
306
  RETRY_COUNT=0
271
307
  PUSH_SUCCESS=false
272
308
 
273
309
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
274
310
  RETRY_COUNT=$((RETRY_COUNT + 1))
275
- echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
311
+ echo "$(m wf_common.push_attempt retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES")"
276
312
 
277
313
  if git push origin HEAD:{{MAIN_BRANCH}}; then
278
314
  PUSH_SUCCESS=true
279
- echo "README.md version update pushed"
315
+ echo "$(m wf_readme.pushed)"
280
316
  break
281
- else
282
- echo "push failed, syncing with remote..."
283
-
284
- if git pull --rebase origin {{MAIN_BRANCH}}; then
285
- echo "rebase ok, retrying push..."
286
- else
287
- echo "rebase failed, manual conflict resolution required"
288
- git rebase --abort 2>/dev/null || true
289
- exit 1
290
- fi
291
317
  fi
318
+
319
+ echo "$(m wf_common.push_failed_syncing)"
320
+ git fetch origin {{MAIN_BRANCH}}
321
+ if git rebase origin/{{MAIN_BRANCH}}; then
322
+ echo "$(m wf_common.rebase_ok)"
323
+ continue
324
+ fi
325
+ git rebase --abort 2>/dev/null || true
326
+
327
+ # The same README line was rewritten on the remote (a release commit).
328
+ # Nothing is lost when it already shows this version or is ahead of it.
329
+ REMOTE_VERSION=$(git show origin/{{MAIN_BRANCH}}:version.yml | grep -m1 '^version:' | sed 's/version:[ \t]*"\{0,1\}\([0-9][0-9.]*\)"\{0,1\}.*/\1/')
330
+ if [ -n "$REMOTE_VERSION" ] && [ "$REMOTE_VERSION" != "$VERSION" ]; then
331
+ echo "$(m wf_readme.conflict_skip_stale version="$VERSION" remote_version="$REMOTE_VERSION")"
332
+ PUSH_SUCCESS=true
333
+ break
334
+ fi
335
+ if git show origin/{{MAIN_BRANCH}}:README.md | grep -qF "v${VERSION}"; then
336
+ echo "$(m wf_readme.conflict_skip_same version="$VERSION")"
337
+ PUSH_SUCCESS=true
338
+ break
339
+ fi
340
+
341
+ echo "$(m wf_common.rebase_failed)"
342
+ exit 1
292
343
  done
293
344
 
294
345
  if [ "$PUSH_SUCCESS" = false ]; then
295
- echo "push failed after $MAX_RETRIES attempts"
346
+ echo "$(m wf_common.push_gave_up max_retries="$MAX_RETRIES")"
296
347
  exit 1
297
348
  fi
349
+ # <<< push-with-retry
298
350
  fi