project-auto-wizard 0.16.0 → 0.16.2

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 (47) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +1 -1
  5. package/package.json +1 -1
  6. package/payload/scripts/messages.py +48 -6
  7. package/payload/scripts/version_manager.py +76 -3
  8. package/payload/version.yml.template +3 -2
  9. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +7 -7
  10. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +111 -46
  11. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +10 -1
  12. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +13 -3
  13. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +10 -1
  14. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +2 -2
  15. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +10 -10
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +10 -10
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +14 -12
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +35 -18
  19. package/payload/workflows/go/PROJECT-GO-CI.yaml +3 -1
  20. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +81 -61
  21. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +3 -1
  22. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +81 -61
  23. package/payload/workflows/react/PROJECT-REACT-CI.yaml +3 -2
  24. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +4 -4
  25. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +86 -68
  26. package/src/commands/doctor.js +17 -23
  27. package/src/commands/dry-run.js +4 -3
  28. package/src/commands/full.js +6 -6
  29. package/src/commands/interactive.js +5 -1
  30. package/src/commands/status.js +9 -2
  31. package/src/core/copy/gitignore.js +3 -3
  32. package/src/core/copy/readme.js +11 -10
  33. package/src/core/detect-fs.js +9 -4
  34. package/src/core/options.js +36 -0
  35. package/src/core/version-yml.js +37 -13
  36. package/src/i18n/catalog/en/cli.js +1 -0
  37. package/src/i18n/catalog/en/commands.js +4 -1
  38. package/src/i18n/catalog/en/core.js +1 -0
  39. package/src/i18n/catalog/en/core2.js +2 -0
  40. package/src/i18n/catalog/en/ui.js +3 -0
  41. package/src/i18n/catalog/ko/cli.js +1 -0
  42. package/src/i18n/catalog/ko/commands.js +4 -1
  43. package/src/i18n/catalog/ko/core.js +1 -0
  44. package/src/i18n/catalog/ko/core2.js +2 -0
  45. package/src/i18n/catalog/ko/ui.js +3 -0
  46. package/src/index.js +19 -7
  47. package/src/ui/summary.js +15 -6
@@ -125,10 +125,8 @@ jobs:
125
125
  rm -rf .paw-msg
126
126
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
127
127
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
128
- {
129
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
130
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
131
- } >> "$GITHUB_ENV"
128
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
129
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
132
130
 
133
131
  - name: Add 👀 reaction to comment
134
132
  if: contains(github.event.comment.body, '/wizard') && contains(github.event.comment.body, 'server')
@@ -235,10 +233,8 @@ jobs:
235
233
  rm -rf .paw-msg
236
234
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
237
235
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
238
- {
239
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
240
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
241
- } >> "$GITHUB_ENV"
236
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
237
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
242
238
  # The deploy script runs on the remote server, where messages.py does not exist:
243
239
  # ship the messages it needs as a small shell function (m KEY name=value ...)
244
240
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -283,7 +279,7 @@ jobs:
283
279
  uses: actions/github-script@v9
284
280
  with:
285
281
  script: |
286
- const M = JSON.parse(process.env.PAW_MSG);
282
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
287
283
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
288
284
 
289
285
  const prNumber = context.issue.number;
@@ -322,6 +318,7 @@ jobs:
322
318
 
323
319
  # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
324
320
  - name: Deploy pre-check (Secrets and Dockerfile)
321
+ id: precheck
325
322
  working-directory: ${{ env.PROJECT_PATH }}
326
323
  env:
327
324
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -345,15 +342,21 @@ jobs:
345
342
  [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
346
343
  fi
347
344
  FAILED=false
345
+ ITEMS=""
348
346
  if [ -n "$MISSING" ]; then
349
347
  echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
348
+ ITEMS="${MISSING# }"
349
+ ITEMS="${ITEMS// /, }"
350
350
  FAILED=true
351
351
  fi
352
352
  if [ ! -f "$DOCKERFILE" ]; then
353
353
  echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
354
+ ITEMS="${ITEMS:+$ITEMS, }Dockerfile"
354
355
  FAILED=true
355
356
  fi
356
357
  if [ "$FAILED" = "true" ]; then
358
+ # The failure comment names what blocked the later steps instead of showing them as waiting
359
+ echo "missing=$ITEMS" >> "$GITHUB_OUTPUT"
357
360
  exit 1
358
361
  fi
359
362
  m wf_preview.precheck_ok
@@ -419,7 +422,7 @@ jobs:
419
422
  PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME: ${{ steps.progress.outputs.start_time }}
420
423
  with:
421
424
  script: |
422
- const M = JSON.parse(process.env.PAW_MSG);
425
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
423
426
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
424
427
 
425
428
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -484,7 +487,7 @@ jobs:
484
487
  PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START: ${{ steps.build_progress.outputs.docker_start }}
485
488
  with:
486
489
  script: |
487
- const M = JSON.parse(process.env.PAW_MSG);
490
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
488
491
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
489
492
 
490
493
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -676,7 +679,7 @@ jobs:
676
679
  PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DEPLOY_START: ${{ steps.docker_progress.outputs.deploy_start }}
677
680
  with:
678
681
  script: |
679
- const M = JSON.parse(process.env.PAW_MSG);
682
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
680
683
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
681
684
 
682
685
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -759,9 +762,11 @@ jobs:
759
762
  PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
760
763
  PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
761
764
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
765
+ PAW_IN_STEPS_PRECHECK_OUTCOME: ${{ steps.precheck.outcome }}
766
+ PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING: ${{ steps.precheck.outputs.missing }}
762
767
  with:
763
768
  script: |
764
- const M = JSON.parse(process.env.PAW_MSG);
769
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
765
770
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
766
771
 
767
772
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -782,16 +787,19 @@ jobs:
782
787
  buildStatus = m('wf_preview.status_init_failed');
783
788
  dockerStatus = '-';
784
789
  deployStatus = '-';
790
+ } else if (process.env.PAW_IN_STEPS_PRECHECK_OUTCOME === 'failure') {
791
+ // The pre-check stopped the run before any build step: say why nothing ran
792
+ buildStatus = dockerStatus = deployStatus = m('wf_preview.status_precheck_blocked', { missing: process.env.PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING || '-' });
785
793
  } else if (!buildDuration || buildDuration === '') {
786
794
  // Failed at the build stage
787
795
  buildStatus = m('wf_preview.status_failed');
788
- dockerStatus = m('wf_preview.status_waiting');
789
- deployStatus = m('wf_preview.status_waiting');
796
+ dockerStatus = m('wf_preview.status_not_run');
797
+ deployStatus = m('wf_preview.status_not_run');
790
798
  } else if (!dockerDuration || dockerDuration === '') {
791
799
  // Failed at the Docker stage
792
800
  buildStatus = m('wf_preview.status_done');
793
801
  dockerStatus = m('wf_preview.status_failed');
794
- deployStatus = m('wf_preview.status_waiting');
802
+ deployStatus = m('wf_preview.status_not_run');
795
803
  buildTime = buildDuration;
796
804
  } else {
797
805
  // Failed at the deploy stage
@@ -882,17 +890,15 @@ jobs:
882
890
  rm -rf .paw-msg
883
891
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
884
892
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
885
- {
886
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
887
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
888
- } >> "$GITHUB_ENV"
893
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
894
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
889
895
 
890
896
  - name: Extract branch name from issue comment
891
897
  id: extract
892
898
  uses: actions/github-script@v9
893
899
  with:
894
900
  script: |
895
- const M = JSON.parse(process.env.PAW_MSG);
901
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
896
902
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
897
903
 
898
904
  const issueNumber = context.issue.number;
@@ -937,7 +943,7 @@ jobs:
937
943
  uses: actions/github-script@v9
938
944
  with:
939
945
  script: |
940
- const M = JSON.parse(process.env.PAW_MSG);
946
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
941
947
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
942
948
 
943
949
  const issueNumber = context.issue.number;
@@ -1003,10 +1009,8 @@ jobs:
1003
1009
  rm -rf .paw-msg
1004
1010
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1005
1011
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1006
- {
1007
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1008
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1009
- } >> "$GITHUB_ENV"
1012
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
1013
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
1010
1014
  # The deploy script runs on the remote server, where messages.py does not exist:
1011
1015
  # ship the messages it needs as a small shell function (m KEY name=value ...)
1012
1016
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -1039,7 +1043,7 @@ jobs:
1039
1043
  PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1040
1044
  with:
1041
1045
  script: |
1042
- const M = JSON.parse(process.env.PAW_MSG);
1046
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1043
1047
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1044
1048
 
1045
1049
  const branchName = process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_BRANCH_NAME;
@@ -1117,7 +1121,7 @@ jobs:
1117
1121
  PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1118
1122
  with:
1119
1123
  script: |
1120
- const M = JSON.parse(process.env.PAW_MSG);
1124
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1121
1125
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1122
1126
 
1123
1127
  const issueNumber = parseInt(process.env.PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER, 10) || 0;
@@ -1160,6 +1164,7 @@ jobs:
1160
1164
 
1161
1165
  # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
1162
1166
  - name: Deploy pre-check (Secrets and Dockerfile)
1167
+ id: precheck
1163
1168
  if: steps.check_branch.outputs.exists == 'true'
1164
1169
  working-directory: ${{ env.PROJECT_PATH }}
1165
1170
  env:
@@ -1184,15 +1189,21 @@ jobs:
1184
1189
  [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
1185
1190
  fi
1186
1191
  FAILED=false
1192
+ ITEMS=""
1187
1193
  if [ -n "$MISSING" ]; then
1188
1194
  echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
1195
+ ITEMS="${MISSING# }"
1196
+ ITEMS="${ITEMS// /, }"
1189
1197
  FAILED=true
1190
1198
  fi
1191
1199
  if [ ! -f "$DOCKERFILE" ]; then
1192
1200
  echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
1201
+ ITEMS="${ITEMS:+$ITEMS, }Dockerfile"
1193
1202
  FAILED=true
1194
1203
  fi
1195
1204
  if [ "$FAILED" = "true" ]; then
1205
+ # The failure comment names what blocked the later steps instead of showing them as waiting
1206
+ echo "missing=$ITEMS" >> "$GITHUB_OUTPUT"
1196
1207
  exit 1
1197
1208
  fi
1198
1209
  m wf_preview.precheck_ok
@@ -1235,7 +1246,7 @@ jobs:
1235
1246
  PAW_IN_STEPS_PROGRESS_OUTPUTS_START_TIME: ${{ steps.progress.outputs.start_time }}
1236
1247
  with:
1237
1248
  script: |
1238
- const M = JSON.parse(process.env.PAW_MSG);
1249
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1239
1250
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1240
1251
 
1241
1252
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -1308,7 +1319,7 @@ jobs:
1308
1319
  PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_DOCKER_START: ${{ steps.build_progress.outputs.docker_start }}
1309
1320
  with:
1310
1321
  script: |
1311
- const M = JSON.parse(process.env.PAW_MSG);
1322
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1312
1323
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1313
1324
 
1314
1325
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -1506,7 +1517,7 @@ jobs:
1506
1517
  PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1507
1518
  with:
1508
1519
  script: |
1509
- const M = JSON.parse(process.env.PAW_MSG);
1520
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1510
1521
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1511
1522
 
1512
1523
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -1592,10 +1603,12 @@ jobs:
1592
1603
  PAW_IN_STEPS_BUILD_PROGRESS_OUTPUTS_BUILD_DURATION: ${{ steps.build_progress.outputs.build_duration }}
1593
1604
  PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
1594
1605
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
1606
+ PAW_IN_STEPS_PRECHECK_OUTCOME: ${{ steps.precheck.outcome }}
1607
+ PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING: ${{ steps.precheck.outputs.missing }}
1595
1608
  PAW_IN_NEEDS_GET_BRANCH_FROM_ISSUE_OUTPUTS_ISSUE_NUMBER: ${{ needs.get-branch-from-issue.outputs.issue_number }}
1596
1609
  with:
1597
1610
  script: |
1598
- const M = JSON.parse(process.env.PAW_MSG);
1611
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1599
1612
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1600
1613
 
1601
1614
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -1614,14 +1627,17 @@ jobs:
1614
1627
  buildStatus = m('wf_preview.status_init_failed');
1615
1628
  dockerStatus = '-';
1616
1629
  deployStatus = '-';
1630
+ } else if (process.env.PAW_IN_STEPS_PRECHECK_OUTCOME === 'failure') {
1631
+ // The pre-check stopped the run before any build step: say why nothing ran
1632
+ buildStatus = dockerStatus = deployStatus = m('wf_preview.status_precheck_blocked', { missing: process.env.PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING || '-' });
1617
1633
  } else if (!buildDuration || buildDuration === '') {
1618
1634
  buildStatus = m('wf_preview.status_failed');
1619
- dockerStatus = m('wf_preview.status_waiting');
1620
- deployStatus = m('wf_preview.status_waiting');
1635
+ dockerStatus = m('wf_preview.status_not_run');
1636
+ deployStatus = m('wf_preview.status_not_run');
1621
1637
  } else if (!dockerDuration || dockerDuration === '') {
1622
1638
  buildStatus = m('wf_preview.status_done');
1623
1639
  dockerStatus = m('wf_preview.status_failed');
1624
- deployStatus = m('wf_preview.status_waiting');
1640
+ deployStatus = m('wf_preview.status_not_run');
1625
1641
  buildTime = buildDuration;
1626
1642
  } else {
1627
1643
  buildStatus = m('wf_preview.status_done');
@@ -1708,10 +1724,8 @@ jobs:
1708
1724
  rm -rf .paw-msg
1709
1725
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1710
1726
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1711
- {
1712
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1713
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1714
- } >> "$GITHUB_ENV"
1727
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
1728
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
1715
1729
  # The deploy script runs on the remote server, where messages.py does not exist:
1716
1730
  # ship the messages it needs as a small shell function (m KEY name=value ...)
1717
1731
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -1841,7 +1855,7 @@ jobs:
1841
1855
  PAW_IN_STEPS_PR_NUMBER_OUTPUTS_NUMBER: ${{ steps.pr_number.outputs.number }}
1842
1856
  with:
1843
1857
  script: |
1844
- const M = JSON.parse(process.env.PAW_MSG);
1858
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
1845
1859
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1846
1860
 
1847
1861
  const number = parseInt(process.env.PAW_IN_STEPS_PR_NUMBER_OUTPUTS_NUMBER, 10) || 0;
@@ -1898,10 +1912,8 @@ jobs:
1898
1912
  rm -rf .paw-msg
1899
1913
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
1900
1914
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
1901
- {
1902
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
1903
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
1904
- } >> "$GITHUB_ENV"
1915
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
1916
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
1905
1917
  # The deploy script runs on the remote server, where messages.py does not exist:
1906
1918
  # ship the messages it needs as a small shell function (m KEY name=value ...)
1907
1919
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -2008,7 +2020,7 @@ jobs:
2008
2020
  PAW_IN_STEPS_CHECK_RUNNING_OUTCOME: ${{ steps.check_running.outcome }}
2009
2021
  with:
2010
2022
  script: |
2011
- const M = JSON.parse(process.env.PAW_MSG);
2023
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2012
2024
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2013
2025
 
2014
2026
  const number = context.issue.number;
@@ -2101,10 +2113,8 @@ jobs:
2101
2113
  rm -rf .paw-msg
2102
2114
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2103
2115
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
2104
- {
2105
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
2106
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2107
- } >> "$GITHUB_ENV"
2116
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
2117
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
2108
2118
  # The deploy script runs on the remote server, where messages.py does not exist:
2109
2119
  # ship the messages it needs as a small shell function (m KEY name=value ...)
2110
2120
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -2188,7 +2198,7 @@ jobs:
2188
2198
  PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2189
2199
  with:
2190
2200
  script: |
2191
- const M = JSON.parse(process.env.PAW_MSG);
2201
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2192
2202
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2193
2203
 
2194
2204
  const branchName = process.env.PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME;
@@ -2226,6 +2236,7 @@ jobs:
2226
2236
 
2227
2237
  # If a required secret or the Dockerfile is missing, report the missing items and stop instead of surfacing a third-party error such as a docker login failure
2228
2238
  - name: Deploy pre-check (Secrets and Dockerfile)
2239
+ id: precheck
2229
2240
  if: steps.check_branch.outputs.exists == 'true'
2230
2241
  working-directory: ${{ env.PROJECT_PATH }}
2231
2242
  env:
@@ -2250,15 +2261,21 @@ jobs:
2250
2261
  [ -n "$SERVER_PASSWORD" ] || MISSING="$MISSING SERVER_PASSWORD"
2251
2262
  fi
2252
2263
  FAILED=false
2264
+ ITEMS=""
2253
2265
  if [ -n "$MISSING" ]; then
2254
2266
  echo "::error title=$(m wf_preview.precheck_secret_title)::$(m wf_preview.precheck_secret_body missing="$MISSING")"
2267
+ ITEMS="${MISSING# }"
2268
+ ITEMS="${ITEMS// /, }"
2255
2269
  FAILED=true
2256
2270
  fi
2257
2271
  if [ ! -f "$DOCKERFILE" ]; then
2258
2272
  echo "::error title=$(m wf_preview.precheck_dockerfile_title)::$(m wf_preview.precheck_dockerfile_body path="${PROJECT_PATH}/${DOCKERFILE#./}")"
2273
+ ITEMS="${ITEMS:+$ITEMS, }Dockerfile"
2259
2274
  FAILED=true
2260
2275
  fi
2261
2276
  if [ "$FAILED" = "true" ]; then
2277
+ # The failure comment names what blocked the later steps instead of showing them as waiting
2278
+ echo "missing=$ITEMS" >> "$GITHUB_OUTPUT"
2262
2279
  exit 1
2263
2280
  fi
2264
2281
  m wf_preview.precheck_ok
@@ -2278,7 +2295,7 @@ jobs:
2278
2295
  PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2279
2296
  with:
2280
2297
  script: |
2281
- const M = JSON.parse(process.env.PAW_MSG);
2298
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2282
2299
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2283
2300
 
2284
2301
  const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
@@ -2352,7 +2369,7 @@ jobs:
2352
2369
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2353
2370
  with:
2354
2371
  script: |
2355
- const M = JSON.parse(process.env.PAW_MSG);
2372
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2356
2373
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2357
2374
 
2358
2375
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -2420,7 +2437,7 @@ jobs:
2420
2437
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2421
2438
  with:
2422
2439
  script: |
2423
- const M = JSON.parse(process.env.PAW_MSG);
2440
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2424
2441
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2425
2442
 
2426
2443
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -2538,7 +2555,7 @@ jobs:
2538
2555
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2539
2556
  with:
2540
2557
  script: |
2541
- const M = JSON.parse(process.env.PAW_MSG);
2558
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2542
2559
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2543
2560
 
2544
2561
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -2609,9 +2626,11 @@ jobs:
2609
2626
  PAW_IN_STEPS_DOCKER_PROGRESS_OUTPUTS_DOCKER_DURATION: ${{ steps.docker_progress.outputs.docker_duration }}
2610
2627
  PAW_IN_STEPS_EXTRACT_OUTPUTS_BRANCH_NAME: ${{ steps.extract.outputs.branch_name }}
2611
2628
  PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID: ${{ steps.progress.outputs.comment_id }}
2629
+ PAW_IN_STEPS_PRECHECK_OUTCOME: ${{ steps.precheck.outcome }}
2630
+ PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING: ${{ steps.precheck.outputs.missing }}
2612
2631
  with:
2613
2632
  script: |
2614
- const M = JSON.parse(process.env.PAW_MSG);
2633
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2615
2634
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2616
2635
 
2617
2636
  const commentId = parseInt(process.env.PAW_IN_STEPS_PROGRESS_OUTPUTS_COMMENT_ID, 10) || 0;
@@ -2629,14 +2648,17 @@ jobs:
2629
2648
  buildStatus = m('wf_preview.status_init_failed');
2630
2649
  dockerStatus = '-';
2631
2650
  deployStatus = '-';
2651
+ } else if (process.env.PAW_IN_STEPS_PRECHECK_OUTCOME === 'failure') {
2652
+ // The pre-check stopped the run before any build step: say why nothing ran
2653
+ buildStatus = dockerStatus = deployStatus = m('wf_preview.status_precheck_blocked', { missing: process.env.PAW_IN_STEPS_PRECHECK_OUTPUTS_MISSING || '-' });
2632
2654
  } else if (!buildDuration || buildDuration === '') {
2633
2655
  buildStatus = m('wf_preview.status_failed');
2634
- dockerStatus = m('wf_preview.status_waiting');
2635
- deployStatus = m('wf_preview.status_waiting');
2656
+ dockerStatus = m('wf_preview.status_not_run');
2657
+ deployStatus = m('wf_preview.status_not_run');
2636
2658
  } else if (!dockerDuration || dockerDuration === '') {
2637
2659
  buildStatus = m('wf_preview.status_done');
2638
2660
  dockerStatus = m('wf_preview.status_failed');
2639
- deployStatus = m('wf_preview.status_waiting');
2661
+ deployStatus = m('wf_preview.status_not_run');
2640
2662
  buildTime = buildDuration;
2641
2663
  } else {
2642
2664
  buildStatus = m('wf_preview.status_done');
@@ -2718,10 +2740,8 @@ jobs:
2718
2740
  rm -rf .paw-msg
2719
2741
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2720
2742
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
2721
- {
2722
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
2723
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2724
- } >> "$GITHUB_ENV"
2743
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
2744
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
2725
2745
  # The deploy script runs on the remote server, where messages.py does not exist:
2726
2746
  # ship the messages it needs as a small shell function (m KEY name=value ...)
2727
2747
  python3 - >> "$GITHUB_ENV" <<'PY'
@@ -2837,7 +2857,7 @@ jobs:
2837
2857
  PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2838
2858
  with:
2839
2859
  script: |
2840
- const M = JSON.parse(process.env.PAW_MSG);
2860
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2841
2861
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2842
2862
 
2843
2863
  const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
@@ -2898,10 +2918,8 @@ jobs:
2898
2918
  rm -rf .paw-msg
2899
2919
  # The workspace root has no version.yml; pass the language resolved above to every later messages.py call
2900
2920
  export PROJECT_AUTO_WIZARD_LANG="$LANG_CODE"
2901
- {
2902
- echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE"
2903
- echo "PAW_MSG=$(python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview.)"
2904
- } >> "$GITHUB_ENV"
2921
+ python3 "$RUNNER_TEMP/paw/messages.py" dump wf_preview. > "$RUNNER_TEMP/paw-msg.json"
2922
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
2905
2923
 
2906
2924
  # Report the missing secrets by name instead of surfacing a raw SSH action error
2907
2925
  - name: Deploy pre-check (Secrets)
@@ -2981,7 +2999,7 @@ jobs:
2981
2999
  PAW_IN_STEPS_EXTRACT_OUTPUTS_CONTAINER_SUFFIX: ${{ steps.extract.outputs.container_suffix }}
2982
3000
  with:
2983
3001
  script: |
2984
- const M = JSON.parse(process.env.PAW_MSG);
3002
+ const M = JSON.parse(require('fs').readFileSync(`${process.env.RUNNER_TEMP}/paw-msg.json`, 'utf8'));
2985
3003
  const m = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
2986
3004
 
2987
3005
  const projectName = process.env.PROJECT_PATH !== '.' ? process.env.PROJECT_NAME + '-spring' : process.env.PROJECT_NAME;
@@ -18,6 +18,7 @@ import { findMissingScripts } from "../core/copy/simple.js";
18
18
  import { resolvePayloadRoot } from "../core/assets.js";
19
19
  import { parseExisting } from "../core/version-yml.js";
20
20
  import { hooksFor } from "../core/types.js";
21
+ import { OPTIONS } from "../core/options.js";
21
22
  import { t } from "../i18n/index.js";
22
23
 
23
24
  const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
@@ -172,29 +173,22 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
172
173
  }
173
174
 
174
175
  // Show the actual setting: telling a user who turned it on that it is off would misrepresent the current state.
175
- const copilotAi = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.copilotAi : null;
176
- const copilotState = copilotAi === true
177
- ? t("cmd.doctor.copilot.on")
178
- : copilotAi === false
179
- ? t("cmd.doctor.copilot.off")
180
- : t("cmd.doctor.copilot.default");
181
- add({
182
- name: t("cmd.doctor.copilot.name"), label: t("cmd.doctor.copilot.name"), purpose: t("cmd.doctor.copilot.purpose"), status: "INFO",
183
- note: [
184
- copilotState,
185
- t(copilotAi === true ? "cmd.doctor.copilot.creditsOn" : "cmd.doctor.copilot.creditsOff"),
186
- t("cmd.doctor.copilot.fallback"),
187
- ],
188
- });
189
-
190
- // release_automerge is on unless version.yml says false (a missing key also means on), so only an explicit false is reported as off.
191
- const releaseAutomerge = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.releaseAutomerge : null;
192
- add({
193
- name: t("cmd.doctor.automerge.name"), label: t("cmd.doctor.automerge.name"), purpose: t("cmd.doctor.automerge.purpose"), status: "INFO",
194
- note: releaseAutomerge === false
195
- ? [t("cmd.doctor.automerge.off"), t("cmd.doctor.automerge.offHow")]
196
- : [t("cmd.doctor.automerge.on"), t("cmd.doctor.automerge.onHow")],
197
- });
176
+ // One item per option that declares a `doctor` spec in the registry; version.yml is read once.
177
+ const savedParsed = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")) : null;
178
+ const savedOptions = savedParsed ? savedParsed.options : null;
179
+ for (const o of OPTIONS.filter((opt) => opt.doctor)) {
180
+ const { prefix, always, notes } = o.doctor;
181
+ // Without an install there is no saved setting to report
182
+ if (!savedOptions && !always) continue;
183
+ // An unrecognized value is a problem to fix (the workflow reads it as off), not just information.
184
+ const invalid = notes.invalid ? savedParsed?.invalidOptions.find((i) => i.key === o.key) : null;
185
+ const keys = invalid ? notes.invalid : notes[String(savedOptions?.[o.name] ?? null)];
186
+ add({
187
+ name: t(`${prefix}.name`), label: t(`${prefix}.name`), purpose: t(`${prefix}.purpose`),
188
+ status: invalid ? "WARN" : "INFO",
189
+ note: keys.map((k) => t(`${prefix}.${k}`, invalid ? { value: invalid.value } : {})),
190
+ });
191
+ }
198
192
 
199
193
  return results;
200
194
  }
@@ -23,11 +23,12 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
23
23
 
24
24
  const vyPath = join(targetRoot, PATHS.versionFile);
25
25
  const existingRaw = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
26
- const extraTopLevel = existingRaw !== null ? parseExisting(existingRaw).extraTopLevel : [];
26
+ const prevParsed = existingRaw !== null ? parseExisting(existingRaw) : null;
27
+ const extraTopLevel = prevParsed ? prevParsed.extraTopLevel : [];
27
28
 
28
29
  // Use the same render function as the real install; assembling separately drifts the preview whenever an option is added.
29
30
  const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
30
- pathMarkers, extraTopLevel,
31
+ pathMarkers, extraTopLevel, lastUpdatedBy: prevParsed?.lastUpdatedBy || "",
31
32
  });
32
33
  // Compare by the same rule as the real install: if only the timestamp lines differ, the install does not rewrite the file either.
33
34
  return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
@@ -63,7 +64,7 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
63
64
  droppedPathLines: existingDroppedLines(targetRoot, context.paths),
64
65
  // Files the real install also changes; scripts that overwrite existing files in particular must be shown in advance.
65
66
  scripts: planScripts(payloadRoot, targetRoot),
66
- readme: planVersionSection(targetRoot),
67
+ readme: planVersionSection(targetRoot, context.language),
67
68
  baselineExists: existsSync(join(targetRoot, BASELINE_PATH)),
68
69
  };
69
70
  }
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
6
6
  import { writeText, findUnwritable } from "../core/fsutil.js";
7
7
  import { CliError } from "../core/errors.js";
8
8
  import { PATHS } from "../core/paths.js";
9
- import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines } from "../core/version-yml.js";
9
+ import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines, invalidOptionLines } from "../core/version-yml.js";
10
10
  import { readVersionYmlTemplate } from "../core/assets.js";
11
11
  import { existingMarkerInDir } from "../core/paths-resolve.js";
12
12
  import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
@@ -28,8 +28,7 @@ import { t } from "../i18n/index.js";
28
28
  // payloadRoot: the package's payload/ root. targetRoot: the install target.
29
29
  export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
30
30
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
31
- force = true, now, today, templateVersion = "unknown",
32
- includeSemverAuto } = context;
31
+ force = true, now, today, templateVersion = "unknown" } = context;
33
32
 
34
33
  // Check permissions before writing anything: stopping midway would leave a half-installed state.
35
34
  const blocked = findUnwritable(targetRoot,
@@ -75,10 +74,11 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
75
74
  const extraTopLevel = prevParsed ? prevParsed.extraTopLevel : [];
76
75
  // Folders folded into one type entry are not managed after this rewrite; leave a trace in the install log.
77
76
  for (const line of droppedPathLines(prevParsed?.droppedPaths, paths)) log.warn("version", "paths", line);
77
+ for (const line of invalidOptionLines(prevParsed?.invalidOptions)) log.warn("version", "options", line);
78
78
 
79
79
  // 2. Generate version.yml (render payload/version.yml.template; full regeneration)
80
80
  // A re-run where only the date lines differ does not rewrite it (idempotent).
81
- const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel });
81
+ const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel, lastUpdatedBy: prevParsed?.lastUpdatedBy || "" });
82
82
  if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
83
83
  writeText(vyPath, vyText);
84
84
  log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
@@ -87,7 +87,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
87
87
  }
88
88
 
89
89
  // 3. README version section
90
- const readme = addVersionSectionToReadme(version, targetRoot);
90
+ const readme = addVersionSectionToReadme(version, targetRoot, context.language);
91
91
  log.info("readme", readme === "added" ? "append" : "skip", README_STATUS_LABEL[readme] || readme);
92
92
 
93
93
  // 4. scripts (payload/scripts/*.py -> .github/scripts/): always overwritten, so also record that user edits are lost.
@@ -123,7 +123,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
123
123
  const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0 || storeCleanup.backedUp.length > 0
124
124
  || staleCleanup.backedUp.length > 0;
125
125
  if (gitignoreUpdated) {
126
- const gi = ensureGitignore(targetRoot);
126
+ const gi = ensureGitignore(targetRoot, context.language);
127
127
  log.info("gitignore", gi.created ? "create" : gi.added.length ? "append" : "skip",
128
128
  gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : t("cmd.full.log.gitignoreExisting"));
129
129
  }
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
8
8
  import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "../core/detect-fs.js";
9
- import { parseExisting, droppedPathLines } from "../core/version-yml.js";
9
+ import { parseExisting, droppedPathLines, invalidOptionLines } from "../core/version-yml.js";
10
10
  import { pickReleaseOptions, resolveReleaseOptions } from "../core/release-options.js";
11
11
  import { OPTIONS, askableOptions, explicitFromContext } from "../core/options.js";
12
12
  import { runBreakingCheck } from "../core/breaking-check.js";
@@ -292,6 +292,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
292
292
  // Announce merged folders only now: the message must follow the folders actually chosen, not the saved ones.
293
293
  if (existing?.droppedPaths?.length) io.note?.(droppedPathLines(existing.droppedPaths, paths).join("\n"), t("core.versionYml.pathMergedTitle"));
294
294
 
295
+ // Option values that could not be read are rewritten as false by this run; tell the user before they confirm.
296
+ if (existing?.invalidOptions?.length) io.note?.(invalidOptionLines(existing.invalidOptions).join("\n"), t("core.versionYml.optionInvalidTitle"));
297
+
295
298
  // In a monorepo the version/build-number files live inside the type folder - detect there now that paths are settled.
296
299
  if (versionAutoDetected && [...paths.values()].some((p) => p && p !== ".")) {
297
300
  version = detectVersion(cwd, { types, paths, warn: () => {} });
@@ -376,6 +379,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
376
379
  // Completion summary
377
380
  io.summary?.({
378
381
  mode, types, version, versionCode, branches, developMissing,
382
+ releaseAutomerge: ctx.includeReleaseAutomerge !== false,
379
383
  copiedFiles: result?.workflows?.copiedFiles ?? [],
380
384
  autoUpdated: result?.workflows?.autoUpdated ?? [],
381
385
  gitignoreUpdated: result?.gitignoreUpdated === true,