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
@@ -35,6 +35,13 @@
35
35
  # trigger and workflow_dispatch is dispatched too. A PAT or human merge
36
36
  # already fired the push event, so those are left alone (no double deploy).
37
37
  #
38
+ # The trunk-based docs commit is pushed with fetch -> rebase -> push retries.
39
+ # If README.md conflicts with a concurrent README commit (a date refresh),
40
+ # the remote README is kept and the released version line is reapplied, so
41
+ # a README conflict never stops the tag and the Release from being created.
42
+ # README-VERSION-UPDATE skips its own push run in trunk-based mode; this
43
+ # workflow dispatches it after publishing instead.
44
+ #
38
45
  # Release-confirm pattern: "chore(release):" — keep in sync with
39
46
  # PROJECT-COMMON-AUTO-CHANGELOG-CONTROL and PROJECT-COMMON-VERSION-CONTROL.
40
47
  #
@@ -94,15 +101,18 @@ jobs:
94
101
  - name: Detect branch mode
95
102
  id: mode
96
103
  run: |
104
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
105
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
97
106
  # scoped to the metadata: -> template: -> branches: chain so an
98
107
  # unrelated top-level "mode:" key can never hijack the value
99
108
  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")
100
109
  echo "mode=$MODE" >> $GITHUB_OUTPUT
101
- echo "branch mode: $MODE"
110
+ echo "$(m wf_release.branch_mode mode="$MODE")"
102
111
 
103
112
  - name: Release gate
104
113
  id: gate
105
114
  run: |
115
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
106
116
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
107
117
  MODE="${{ steps.mode.outputs.mode }}"
108
118
  EVENT="${{ github.event_name }}"
@@ -123,11 +133,11 @@ jobs:
123
133
 
124
134
  echo "proceed=$PROCEED" >> $GITHUB_OUTPUT
125
135
  if [ "$PROCEED" = "true" ]; then
126
- echo "release gate open (mode=$MODE, event=$EVENT)"
136
+ echo "$(m wf_release.gate_open mode="$MODE" event="$EVENT")"
127
137
  exit 0
128
138
  fi
129
139
 
130
- echo "not a release-confirm push in pr-flow mode — skipping"
140
+ echo "$(m wf_release.gate_closed)"
131
141
 
132
142
  # Drift guard: skipping is normal, but a version.yml that
133
143
  # has run ahead of the newest tag is not — it means the version was
@@ -138,13 +148,13 @@ jobs:
138
148
  LATEST_VERSION="${LATEST_TAG#v}"
139
149
 
140
150
  if [ -z "$FILE_VERSION" ] || [ -z "$LATEST_VERSION" ]; then
141
- echo "drift guard skipped (no version.yml version or no tag yet)"
151
+ echo "$(m wf_release.drift_skipped)"
142
152
  exit 0
143
153
  fi
144
154
 
145
155
  HIGHER=$(printf '%s\n%s\n' "$FILE_VERSION" "$LATEST_VERSION" | sort -V | tail -n 1)
146
156
  if [ "$FILE_VERSION" != "$LATEST_VERSION" ] && [ "$HIGHER" = "$FILE_VERSION" ]; then
147
- echo "::error::version.yml($FILE_VERSION) is ahead of the newest tag($LATEST_TAG) but no release was published — this version will never reach npm. Recover with: Actions → PROJECT-RELEASE-PUBLISH → Run workflow (workflow_dispatch bypasses this gate)."
157
+ echo "::error::$(m wf_release.drift_error file_version="$FILE_VERSION" latest_tag="$LATEST_TAG")"
148
158
  {
149
159
  echo "$(m wf_release.drift_heading)"
150
160
  echo ""
@@ -157,7 +167,7 @@ jobs:
157
167
  exit 1
158
168
  fi
159
169
 
160
- echo "drift guard ok (version.yml=$FILE_VERSION, latest tag=$LATEST_TAG)"
170
+ echo "$(m wf_release.drift_ok file_version="$FILE_VERSION" latest_tag="$LATEST_TAG")"
161
171
 
162
172
  - name: Read semver_auto option from version.yml
163
173
  id: semver_options
@@ -166,11 +176,13 @@ jobs:
166
176
  steps.mode.outputs.mode == 'trunk-based' &&
167
177
  github.event_name == 'push'
168
178
  run: |
179
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
180
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
169
181
  # scoped to the metadata: -> template: -> options: chain so an
170
182
  # unrelated "semver_auto:" key can never hijack the value
171
183
  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")
172
184
  echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
173
- echo "semver_auto option: $SEMVER_AUTO"
185
+ echo "$(m wf_common.semver_auto_option semver_auto="$SEMVER_AUTO")"
174
186
 
175
187
  - name: Read copilot_ai option from version.yml
176
188
  id: copilot_options
@@ -179,11 +191,13 @@ jobs:
179
191
  steps.mode.outputs.mode == 'trunk-based' &&
180
192
  github.event_name == 'push'
181
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; }
195
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
182
196
  # scoped to the metadata: -> template: -> options: chain so an
183
197
  # unrelated "copilot_ai:" key can never hijack the value
184
198
  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")
185
199
  echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
186
- echo "copilot_ai option: $COPILOT_AI"
200
+ echo "$(m wf_common.copilot_ai_option copilot_ai="$COPILOT_AI")"
187
201
 
188
202
  - name: Install Copilot CLI
189
203
  if: >-
@@ -206,13 +220,15 @@ jobs:
206
220
  GITHUB_TOKEN: ${{ github.token }}
207
221
  COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
208
222
  run: |
223
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
224
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
209
225
  # Idempotency: on a re-run of a failed job the checked-out HEAD may
210
226
  # already be this workflow's own docs commit — never bump twice.
211
227
  # Jump straight to the common tail (tag + release for the current
212
228
  # version).
213
229
  HEAD_MSG=$(git log -1 --pretty=%s)
214
230
  if printf '%s\n' "$HEAD_MSG" | grep -q "^chore(version): release v.* docs \[skip ci\]"; then
215
- echo "HEAD is already this workflow's release docs commit — skipping re-increment (idempotent re-run)"
231
+ echo "$(m wf_release.head_is_docs_commit)"
216
232
  exit 0
217
233
  fi
218
234
 
@@ -222,11 +238,11 @@ jobs:
222
238
  # release (no previous tag to diff against).
223
239
  LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
224
240
  if [ -n "$LAST_TAG" ]; then
225
- echo "collecting commits since $LAST_TAG"
241
+ echo "$(m wf_release.collect_since last_tag="$LAST_TAG")"
226
242
  LOG_RANGE="${LAST_TAG}..HEAD"
227
243
  DIFF_BASE="$LAST_TAG"
228
244
  else
229
- echo "no previous tag — collecting the full history"
245
+ echo "$(m wf_release.collect_all)"
230
246
  LOG_RANGE="HEAD"
231
247
  DIFF_BASE="4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git empty tree hash
232
248
  fi
@@ -248,15 +264,15 @@ jobs:
248
264
  else
249
265
  BUMP="patch"
250
266
  fi
251
- echo "bump level: $BUMP"
267
+ echo "$(m wf_common.bump_level bump="$BUMP")"
252
268
 
253
269
  NEW_VERSION=$(python3 .github/scripts/version_manager.py increment --bump "$BUMP" | tail -n 1)
254
270
  if [ -z "$NEW_VERSION" ]; then
255
- echo "version bump failed"
271
+ echo "$(m wf_common.version_bump_failed)"
256
272
  exit 1
257
273
  fi
258
274
  python3 .github/scripts/version_manager.py sync
259
- echo "release version: $NEW_VERSION ($BUMP)"
275
+ echo "$(m wf_release.release_version new_version="$NEW_VERSION" bump="$BUMP")"
260
276
 
261
277
  python3 .github/scripts/changelog_manager.py ai-summary \
262
278
  --commits-file commits.txt \
@@ -289,39 +305,57 @@ jobs:
289
305
  git add -u
290
306
  git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
291
307
  if git diff --staged --quiet; then
292
- echo "no changes to commit"
308
+ echo "$(m wf_common.no_changes)"
293
309
  else
294
310
  git commit -m "chore(version): release v${NEW_VERSION} docs [skip ci]"
295
311
 
296
- # Race-condition guard: pull-rebase then push (up to 3 retries)
297
- MAX_RETRIES=3
312
+ # >>> push-with-retry
313
+ # Race-condition guard: fetch -> rebase -> push, retried. A concurrent
314
+ # README-only commit (date refresh) touches the same version line and
315
+ # conflicts on rebase; the remote README is kept and this run's version
316
+ # line is reapplied, so a README conflict never blocks the release.
317
+ MAX_RETRIES=5
298
318
  RETRY_COUNT=0
299
319
  PUSH_SUCCESS=false
300
320
 
301
321
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
302
322
  RETRY_COUNT=$((RETRY_COUNT + 1))
303
- echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
323
+ echo "$(m wf_common.push_attempt retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES")"
304
324
 
305
325
  if git push origin HEAD:{{MAIN_BRANCH}}; then
306
326
  PUSH_SUCCESS=true
307
- echo "release docs pushed"
327
+ echo "$(m wf_release.docs_pushed)"
308
328
  break
309
- else
310
- echo "push failed, syncing with remote..."
311
- if git pull --rebase origin {{MAIN_BRANCH}}; then
312
- echo "rebase ok, retrying push..."
313
- else
314
- echo "rebase failed, manual conflict resolution required"
315
- git rebase --abort 2>/dev/null || true
316
- exit 1
329
+ fi
330
+
331
+ echo "$(m wf_common.push_failed_syncing)"
332
+ git fetch origin {{MAIN_BRANCH}}
333
+ if git rebase origin/{{MAIN_BRANCH}}; then
334
+ echo "$(m wf_common.rebase_ok)"
335
+ continue
336
+ fi
337
+
338
+ # Only README.md conflicting: "ours" is the remote side during a rebase
339
+ if [ "$(git diff --name-only --diff-filter=U)" = "README.md" ]; then
340
+ git checkout --ours -- README.md
341
+ python3 -c 'import sys; sys.path.insert(0, ".github/scripts"); import version_manager as vm; vm.update_readme_version(vm.get_current_version())' || true
342
+ git add README.md
343
+ if GIT_EDITOR=true git rebase --continue; then
344
+ echo "$(m wf_common.readme_conflict_resolved)"
345
+ continue
317
346
  fi
318
347
  fi
348
+
349
+ echo "$(m wf_common.rebase_failed)"
350
+ git rebase --abort 2>/dev/null || true
351
+ exit 1
319
352
  done
320
353
 
321
354
  if [ "$PUSH_SUCCESS" = false ]; then
322
- echo "push failed after $MAX_RETRIES attempts"
355
+ echo "$(m wf_common.push_gave_up max_retries="$MAX_RETRIES")"
323
356
  exit 1
324
357
  fi
358
+ # <<< push-with-retry
325
359
  fi
326
360
 
327
361
  - name: Resolve version + idempotency guard
@@ -330,9 +364,11 @@ jobs:
330
364
  env:
331
365
  GH_TOKEN: ${{ github.token }}
332
366
  run: |
367
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
368
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
333
369
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
334
370
  if [ -z "$VERSION" ]; then
335
- echo "failed to read version"
371
+ echo "$(m wf_release.read_version_failed)"
336
372
  exit 1
337
373
  fi
338
374
  echo "version=$VERSION" >> $GITHUB_OUTPUT
@@ -344,32 +380,36 @@ jobs:
344
380
  # "release created" can be re-run and still publish the release.
345
381
  if git rev-parse "v$VERSION" >/dev/null 2>&1; then
346
382
  echo "tag_exists=true" >> $GITHUB_OUTPUT
347
- echo "tag v$VERSION already exists — skipping tag push"
383
+ echo "$(m wf_release.tag_exists version="$VERSION")"
348
384
  else
349
385
  echo "tag_exists=false" >> $GITHUB_OUTPUT
350
386
  fi
351
387
 
352
388
  if gh release view "v$VERSION" >/dev/null 2>&1; then
353
389
  echo "release_exists=true" >> $GITHUB_OUTPUT
354
- echo "release v$VERSION already exists — nothing to publish"
390
+ echo "$(m wf_release.release_exists version="$VERSION")"
355
391
  else
356
392
  echo "release_exists=false" >> $GITHUB_OUTPUT
357
- echo "publishing release v$VERSION"
393
+ echo "$(m wf_release.publishing version="$VERSION")"
358
394
  fi
359
395
 
360
396
  - name: Create and push tag
361
397
  if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.tag_exists != 'true'
362
398
  run: |
399
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
400
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
363
401
  VERSION="${{ steps.version.outputs.version }}"
364
402
  git tag "v$VERSION"
365
403
  git push origin "v$VERSION"
366
- echo "tag v$VERSION pushed"
404
+ echo "$(m wf_release.tag_pushed version="$VERSION")"
367
405
 
368
406
  - name: Build release notes (changelog export + GitHub generate-notes)
369
407
  if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.release_exists != 'true'
370
408
  env:
371
409
  GH_TOKEN: ${{ github.token }}
372
410
  run: |
411
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
412
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
373
413
  VERSION="${{ steps.version.outputs.version }}"
374
414
 
375
415
  # AI/changelog notes for this version
@@ -397,7 +437,7 @@ jobs:
397
437
  echo "Release v$VERSION" > notes.md
398
438
  fi
399
439
 
400
- echo "release notes assembled ($(wc -c < notes.md) bytes)"
440
+ echo "$(m wf_release.notes_assembled n="$(wc -c < notes.md)")"
401
441
 
402
442
  - name: Create GitHub Release
403
443
  if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.release_exists != 'true'
@@ -408,9 +448,11 @@ jobs:
408
448
  # default token.
409
449
  GH_TOKEN: ${{ secrets.WORKFLOW_PAT || github.token }}
410
450
  run: |
451
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
452
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
411
453
  VERSION="${{ steps.version.outputs.version }}"
412
454
  gh release create "v$VERSION" --title "v$VERSION" --notes-file notes.md
413
- echo "GitHub Release v$VERSION published"
455
+ echo "$(m wf_release.published version="$VERSION")"
414
456
 
415
457
  - name: Trigger README-VERSION-UPDATE
416
458
  # Pushes made with the bot token (GITHUB_TOKEN) never wake the README update
@@ -422,6 +464,7 @@ jobs:
422
464
  env:
423
465
  GH_TOKEN: ${{ github.token }}
424
466
  run: |
467
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
425
468
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
426
469
  if gh workflow run PROJECT-COMMON-README-VERSION-UPDATE.yaml --ref {{MAIN_BRANCH}}; then
427
470
  echo "$(m wf_release.readme_dispatch_ok)"
@@ -449,6 +492,7 @@ jobs:
449
492
  RELEASE_VERSION: ${{ steps.version.outputs.version }}
450
493
  MAIN_BRANCH: "{{MAIN_BRANCH}}"
451
494
  run: |
495
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
452
496
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
453
497
  # Merge commit of this version's release PR - releases that are not merges
454
498
  # (safety-net bump, manual republish) were already deployed by a human push,
@@ -66,20 +66,22 @@ jobs:
66
66
  - name: Release merge / mode guard
67
67
  id: release_guard
68
68
  run: |
69
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
70
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
69
71
  SKIP="false"
70
72
 
71
73
  # trunk-based mode: RELEASE-PUBLISH owns versioning — skip safety net
72
74
  MODE=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"^\s*mode:\s*\"?([A-Za-z-]+)", t, re.M); print(m.group(1) if m else "pr-flow")' 2>/dev/null || echo "pr-flow")
73
75
  if [ "$MODE" = "trunk-based" ]; then
74
76
  SKIP="true"
75
- echo "trunk-based mode detected — RELEASE-PUBLISH owns version bumping, skipping"
77
+ echo "$(m wf_version.trunk_skip)"
76
78
  fi
77
79
 
78
80
  # release-confirm pattern: the merge commit of a release PR
79
81
  HEAD_MSG=$(git log -1 --pretty=%B)
80
82
  if echo "$HEAD_MSG" | grep -q "chore(release):"; then
81
83
  SKIP="true"
82
- echo "release-confirm commit detected (chore(release):) — skipping safety-net bump"
84
+ echo "$(m wf_version.confirm_skip)"
83
85
  fi
84
86
 
85
87
  # release merge detection: the release push includes a version.yml change
@@ -91,51 +93,57 @@ jobs:
91
93
  fi
92
94
  if git diff --name-only $RANGE 2>/dev/null | grep -qx "version.yml"; then
93
95
  SKIP="true"
94
- echo "release merge detected (push contains a version.yml change) — skipping safety-net bump"
96
+ echo "$(m wf_version.release_merge_skip)"
95
97
  fi
96
98
 
97
99
  echo "skip=$SKIP" >> $GITHUB_OUTPUT
98
100
  if [ "$SKIP" = "false" ]; then
99
- echo "direct push to {{MAIN_BRANCH}} detected — running safety-net version bump"
101
+ echo "$(m wf_version.direct_push main_branch="{{MAIN_BRANCH}}")"
100
102
  fi
101
103
 
102
104
  - name: Read current version
103
105
  id: current_version
104
106
  if: steps.release_guard.outputs.skip != 'true'
105
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 "$@"; }
106
110
  CURRENT_VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
107
111
  if [ -z "$CURRENT_VERSION" ]; then
108
- echo "failed to read current version"
112
+ echo "$(m wf_version.read_current_failed)"
109
113
  exit 1
110
114
  fi
111
115
  echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
112
- echo "current version: $CURRENT_VERSION"
116
+ echo "$(m wf_version.current_version current_version="$CURRENT_VERSION")"
113
117
 
114
118
  - name: Bump patch version and sync
115
119
  id: version
116
120
  if: steps.release_guard.outputs.skip != 'true'
117
121
  run: |
122
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
123
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
118
124
  NEW_VERSION=$(python3 .github/scripts/version_manager.py increment | tail -n 1)
119
125
  if [ -z "$NEW_VERSION" ]; then
120
- echo "version bump failed"
126
+ echo "$(m wf_common.version_bump_failed)"
121
127
  exit 1
122
128
  fi
123
129
  python3 .github/scripts/version_manager.py sync
124
130
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
125
- echo "new version: $NEW_VERSION"
131
+ echo "$(m wf_version.new_version new_version="$NEW_VERSION")"
126
132
 
127
133
  - name: Read project types
128
134
  id: project_info
129
135
  if: steps.release_guard.outputs.skip != 'true'
130
136
  run: |
137
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
138
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
131
139
  if [ -f "version.yml" ]; then
132
140
  PROJECT_TYPES=$(grep "^project_types:" version.yml 2>/dev/null | sed -E 's/.*\[([^]]*)\].*/\1/' | tr -d '" ')
133
141
  PROJECT_TYPES=${PROJECT_TYPES:-unknown}
134
142
  echo "project_types=$PROJECT_TYPES" >> $GITHUB_OUTPUT
135
- echo "project types: $PROJECT_TYPES"
143
+ echo "$(m wf_version.project_types project_types="$PROJECT_TYPES")"
136
144
  else
137
145
  echo "project_types=unknown" >> $GITHUB_OUTPUT
138
- echo "version.yml not found"
146
+ echo "$(m wf_version.yml_not_found)"
139
147
  fi
140
148
 
141
149
  - name: Update CHANGELOG for the safety-net release
@@ -150,6 +158,8 @@ jobs:
150
158
  AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
151
159
  AI_MODEL: ${{ vars.AI_MODEL }}
152
160
  run: |
161
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
162
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
153
163
  NEW_VERSION="${{ steps.version.outputs.new_version }}"
154
164
 
155
165
  # Commits pushed straight to {{MAIN_BRANCH}} since the last release tag (full history if there is no tag)
@@ -160,7 +170,7 @@ jobs:
160
170
  # line. The script reads each line as one entry, so passing the whole body (%B)
161
171
  # would turn description lines into change entries.
162
172
  git log --pretty=%b "$LOG_RANGE" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
163
- echo "commits since ${LAST_TAG:-the beginning}: $(wc -l < commits.txt)"
173
+ echo "$(m wf_version.commits_since last_tag="${LAST_TAG:-the beginning}" n="$(wc -l < commits.txt)")"
164
174
 
165
175
  python3 .github/scripts/changelog_manager.py ai-summary \
166
176
  --commits-file commits.txt \
@@ -183,6 +193,8 @@ jobs:
183
193
  id: commit_push
184
194
  if: steps.release_guard.outputs.skip != 'true'
185
195
  run: |
196
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
197
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
186
198
  git config --local user.email "action@github.com"
187
199
  git config --local user.name "GitHub Action"
188
200
 
@@ -193,45 +205,63 @@ jobs:
193
205
  git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
194
206
 
195
207
  if git diff --staged --quiet; then
196
- echo "no changes to commit (already in sync)"
208
+ echo "$(m wf_version.no_changes_synced)"
197
209
  echo "pushed=false" >> "$GITHUB_OUTPUT"
198
210
  else
199
211
  NEW_VERSION="${{ steps.version.outputs.new_version }}"
200
212
  COMMIT_MSG="chore(version): bump to v${NEW_VERSION} [skip ci]"
201
213
 
202
214
  if git commit -m "$COMMIT_MSG"; then
203
- # Race-condition guard: pull-rebase then push (up to 3 retries)
204
- MAX_RETRIES=3
215
+ # >>> push-with-retry
216
+ # Race-condition guard: fetch -> rebase -> push, retried. A concurrent
217
+ # README-only commit (date refresh) touches the same version line and
218
+ # conflicts on rebase; the remote README is kept and this run's version
219
+ # line is reapplied, so a README conflict never blocks the release.
220
+ MAX_RETRIES=5
205
221
  RETRY_COUNT=0
206
222
  PUSH_SUCCESS=false
207
223
 
208
224
  while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
209
225
  RETRY_COUNT=$((RETRY_COUNT + 1))
210
- echo "push attempt $RETRY_COUNT/$MAX_RETRIES..."
226
+ echo "$(m wf_common.push_attempt retry_count="$RETRY_COUNT" max_retries="$MAX_RETRIES")"
211
227
 
212
228
  if git push origin HEAD:{{MAIN_BRANCH}}; then
213
229
  PUSH_SUCCESS=true
214
- echo "version bump commit pushed"
230
+ echo "$(m wf_version.bump_pushed)"
215
231
  break
216
- else
217
- echo "push failed, syncing with remote..."
218
- if git pull --rebase origin {{MAIN_BRANCH}}; then
219
- echo "rebase ok, retrying push..."
220
- else
221
- echo "rebase failed, manual conflict resolution required"
222
- git rebase --abort 2>/dev/null || true
223
- exit 1
232
+ fi
233
+
234
+ echo "$(m wf_common.push_failed_syncing)"
235
+ git fetch origin {{MAIN_BRANCH}}
236
+ if git rebase origin/{{MAIN_BRANCH}}; then
237
+ echo "$(m wf_common.rebase_ok)"
238
+ continue
239
+ fi
240
+
241
+ # Only README.md conflicting: "ours" is the remote side during a rebase
242
+ if [ "$(git diff --name-only --diff-filter=U)" = "README.md" ]; then
243
+ git checkout --ours -- README.md
244
+ python3 -c 'import sys; sys.path.insert(0, ".github/scripts"); import version_manager as vm; vm.update_readme_version(vm.get_current_version())' || true
245
+ git add README.md
246
+ if GIT_EDITOR=true git rebase --continue; then
247
+ echo "$(m wf_common.readme_conflict_resolved)"
248
+ continue
224
249
  fi
225
250
  fi
251
+
252
+ echo "$(m wf_common.rebase_failed)"
253
+ git rebase --abort 2>/dev/null || true
254
+ exit 1
226
255
  done
227
256
 
228
257
  if [ "$PUSH_SUCCESS" = false ]; then
229
- echo "push failed after $MAX_RETRIES attempts"
258
+ echo "$(m wf_common.push_gave_up max_retries="$MAX_RETRIES")"
230
259
  exit 1
231
260
  fi
261
+ # <<< push-with-retry
232
262
  echo "pushed=true" >> "$GITHUB_OUTPUT"
233
263
  else
234
- echo "commit failed"
264
+ echo "$(m wf_version.commit_failed)"
235
265
  exit 1
236
266
  fi
237
267
  fi
@@ -242,6 +272,7 @@ jobs:
242
272
  env:
243
273
  GH_TOKEN: ${{ github.token }}
244
274
  run: |
275
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
245
276
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
246
277
  gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
247
278
  echo "$(m wf_version.release_publish_triggered)"
@@ -254,13 +285,14 @@ jobs:
254
285
  # so only a warning is left.
255
286
  if: steps.release_guard.outputs.skip != 'true' && steps.commit_push.outputs.pushed == 'true'
256
287
  run: |
288
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
257
289
  m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
258
290
  DEVELOP_BRANCH="{{DEVELOP_BRANCH}}"
259
291
  NEW_VERSION="${{ steps.version.outputs.new_version }}"
260
292
  MANUAL="git checkout $DEVELOP_BRANCH && git pull && git merge origin/{{MAIN_BRANCH}} && git push"
261
293
 
262
294
  if ! git ls-remote --exit-code --heads origin "$DEVELOP_BRANCH" >/dev/null 2>&1; then
263
- echo "branch $DEVELOP_BRANCH not found — nothing to back-merge"
295
+ echo "$(m wf_version.no_develop develop_branch="$DEVELOP_BRANCH")"
264
296
  exit 0
265
297
  fi
266
298
 
@@ -268,7 +300,7 @@ jobs:
268
300
  git fetch -q origin "$DEVELOP_BRANCH" {{MAIN_BRANCH}}
269
301
  git checkout -q -B safety-net-back-merge "origin/$DEVELOP_BRANCH"
270
302
  if git merge-base --is-ancestor "origin/{{MAIN_BRANCH}}" HEAD; then
271
- echo "$DEVELOP_BRANCH already contains {{MAIN_BRANCH}}"
303
+ echo "$(m wf_version.already_contains develop_branch="$DEVELOP_BRANCH" main_branch="{{MAIN_BRANCH}}")"
272
304
  exit 0
273
305
  fi
274
306
  if ! git merge --no-edit -m "chore(version): merge v${NEW_VERSION} safety-net bump into ${DEVELOP_BRANCH} [skip ci]" "origin/{{MAIN_BRANCH}}"; then
@@ -277,19 +309,23 @@ jobs:
277
309
  exit 0
278
310
  fi
279
311
  if git push origin "HEAD:$DEVELOP_BRANCH"; then
280
- echo "safety-net bump v$NEW_VERSION merged into $DEVELOP_BRANCH"
312
+ echo "$(m wf_version.backmerged new_version="$NEW_VERSION" develop_branch="$DEVELOP_BRANCH")"
281
313
  exit 0
282
314
  fi
283
- echo "push attempt $ATTEMPT failed — retrying"
315
+ echo "$(m wf_version.push_retry attempt="$ATTEMPT")"
284
316
  sleep 5
285
317
  done
286
318
  echo "::warning::$(m wf_version.backmerge_push_failed dev="$DEVELOP_BRANCH" manual="$MANUAL")"
287
319
 
288
320
  - name: Summary
289
321
  if: always()
322
+ env:
323
+ PAW_REF_NAME: ${{ github.ref_name }}
290
324
  run: |
291
- echo "version-control workflow finished"
292
- echo " skipped (release merge / trunk-based): ${{ steps.release_guard.outputs.skip }}"
293
- echo " previous version: ${{ steps.current_version.outputs.current_version }}"
294
- echo " new version: ${{ steps.version.outputs.new_version }}"
295
- echo " branch: ${{ github.ref_name }}"
325
+ [ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
326
+ m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
327
+ echo "$(m wf_version.finished)"
328
+ echo "$(m wf_version.sum_skipped skip="${{ steps.release_guard.outputs.skip }}")"
329
+ echo "$(m wf_version.sum_previous current_version="${{ steps.current_version.outputs.current_version }}")"
330
+ echo "$(m wf_version.sum_new new_version="${{ steps.version.outputs.new_version }}")"
331
+ echo "$(m wf_version.sum_branch ref_name="$PAW_REF_NAME")"