project-auto-wizard 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.ja.md +165 -0
  2. package/README.ko.md +165 -0
  3. package/README.md +107 -286
  4. package/README.zh-CN.md +165 -0
  5. package/bin/project-auto-wizard.js +1 -2
  6. package/package.json +7 -3
  7. package/payload/config/breaking-changes.json +24 -0
  8. package/payload/config/wizard-prompts.yml +45 -0
  9. package/payload/scripts/changelog_manager.py +141 -46
  10. package/payload/scripts/issue_helper.py +34 -5
  11. package/payload/scripts/version_manager.py +397 -97
  12. package/payload/version.yml.template +4 -2
  13. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
  14. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
  15. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
  16. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
  17. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
  25. package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
  26. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
  27. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
  28. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
  29. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
  30. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
  31. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
  32. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
  33. package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
  34. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
  35. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
  38. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
  39. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
  40. package/src/cli/args.js +33 -24
  41. package/src/cli/help.js +16 -4
  42. package/src/commands/doctor.js +17 -10
  43. package/src/commands/dry-run.js +54 -1
  44. package/src/commands/full.js +127 -50
  45. package/src/commands/interactive-flutter.js +1 -1
  46. package/src/commands/interactive.js +124 -56
  47. package/src/commands/purge.js +59 -17
  48. package/src/commands/status.js +16 -1
  49. package/src/commands/uninstall.js +58 -13
  50. package/src/context.js +5 -5
  51. package/src/core/baseline.js +20 -5
  52. package/src/core/branches.js +19 -1
  53. package/src/core/branding.js +1 -1
  54. package/src/core/breaking-check.js +14 -19
  55. package/src/core/breaking.js +7 -3
  56. package/src/core/copy/gitignore.js +20 -12
  57. package/src/core/copy/readme.js +24 -5
  58. package/src/core/copy/simple.js +52 -13
  59. package/src/core/copy/workflows.js +90 -47
  60. package/src/core/deploy-style.js +83 -11
  61. package/src/core/detect-fs.js +127 -24
  62. package/src/core/detect.js +131 -47
  63. package/src/core/errors.js +3 -0
  64. package/src/core/flutter-options.js +4 -3
  65. package/src/core/fsutil.js +23 -6
  66. package/src/core/logger.js +55 -19
  67. package/src/core/paths-resolve.js +50 -47
  68. package/src/core/paths.js +26 -1
  69. package/src/core/release-options.js +23 -0
  70. package/src/core/removal-plan.js +75 -19
  71. package/src/core/types.js +113 -0
  72. package/src/core/verify.js +85 -19
  73. package/src/core/version-yml.js +42 -8
  74. package/src/core/wizard-env.js +21 -14
  75. package/src/core/wizard-labels.js +34 -25
  76. package/src/index.js +122 -49
  77. package/src/ui/ansi.js +3 -1
  78. package/src/ui/banner.js +12 -8
  79. package/src/ui/env-plan.js +68 -44
  80. package/src/ui/prompts.js +22 -12
  81. package/src/ui/readline-engine.js +109 -22
  82. package/src/ui/status-cards.js +10 -5
  83. package/src/ui/summary.js +71 -33
  84. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  85. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  86. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
@@ -18,8 +18,8 @@
18
18
  # CHANGELOG.json / CHANGELOG.md, and commits everything back with
19
19
  # [skip ci]. Same as pr-flow's AUTO-CHANGELOG-CONTROL:
20
20
  # - If version.yml metadata.template.options.semver_auto is true
21
- # (default): classifies commits (feat -> minor, `!` marker -> major,
22
- # else patch; AI-assisted patch->minor upgrade for unclassified
21
+ # (default): classifies commits (feat -> minor, `!` marker or a
22
+ # `BREAKING CHANGE:` body footer -> major, else patch; AI-assisted patch->minor upgrade for unclassified
23
23
  # commits) and bumps accordingly.
24
24
  # - Otherwise: always patch+1 (legacy behavior).
25
25
  #
@@ -27,7 +27,13 @@
27
27
  # release checked separately so a half-published release can be finished
28
28
  # on re-run), tag push, release notes = changelog export (ai_notes) +
29
29
  # GitHub generate-notes API (contributors/compare links), then
30
- # gh release create.
30
+ # gh release create, then a workflow_dispatch of README-VERSION-UPDATE
31
+ # (bot-token pushes never wake it, so the README would stay behind).
32
+ # pr-flow only: when the release PR was merged by the bot token (no
33
+ # WORKFLOW_PAT), the merge push woke none of the {{MAIN_BRANCH}} deploy
34
+ # workflows either, so every installed workflow with a {{MAIN_BRANCH}} push
35
+ # trigger and workflow_dispatch is dispatched too. A PAT or human merge
36
+ # already fired the push event, so those are left alone (no double deploy).
31
37
  #
32
38
  # Release-confirm pattern: "chore(release):" — keep in sync with
33
39
  # PROJECT-COMMON-AUTO-CHANGELOG-CONTROL and PROJECT-COMMON-VERSION-CONTROL.
@@ -56,8 +62,16 @@ on:
56
62
 
57
63
  permissions:
58
64
  contents: write
65
+ # README-VERSION-UPDATE·배포 워크플로우를 workflow_dispatch로 깨운다
66
+ actions: write
67
+ # 릴리스 PR을 누가 병합했는지 읽어 배포를 다시 깨울지 정한다
68
+ pull-requests: read
59
69
  copilot-requests: write
60
70
 
71
+ env:
72
+ # 스크립트 import가 .github/scripts/__pycache__를 만들어 봇 커밋에 섞이지 않게 한다
73
+ PYTHONDONTWRITEBYTECODE: "1"
74
+
61
75
  jobs:
62
76
  release:
63
77
  name: Tag and publish GitHub Release
@@ -208,13 +222,17 @@ jobs:
208
222
  LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
209
223
  if [ -n "$LAST_TAG" ]; then
210
224
  echo "collecting commits since $LAST_TAG"
211
- git log --pretty=%s "${LAST_TAG}..HEAD" > commits.txt
225
+ LOG_RANGE="${LAST_TAG}..HEAD"
212
226
  DIFF_BASE="$LAST_TAG"
213
227
  else
214
228
  echo "no previous tag — collecting the full history"
215
- git log --pretty=%s > commits.txt
229
+ LOG_RANGE="HEAD"
216
230
  DIFF_BASE="4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git empty tree hash
217
231
  fi
232
+ git log --pretty=%s "$LOG_RANGE" > commits.txt
233
+ # 본문 푸터 BREAKING CHANGE:는 제목 목록 뒤에 한 줄씩 덧붙인다 — 스크립트는 한 줄을
234
+ # 한 항목으로 읽으므로 본문 전체(%B)를 넣으면 설명 줄까지 변경 항목으로 잡힌다.
235
+ git log --pretty=%b "$LOG_RANGE" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
218
236
  git diff --stat "${DIFF_BASE}..HEAD" > _full_diff_stat.txt
219
237
  if [ "$(wc -l < _full_diff_stat.txt)" -gt 49 ]; then
220
238
  { head -49 _full_diff_stat.txt; tail -1 _full_diff_stat.txt; } > diff_stat.txt
@@ -263,7 +281,11 @@ jobs:
263
281
 
264
282
  rm -f pr_body.md summary.md commits.txt diff_stat.txt
265
283
 
266
- git add -A
284
+ # -A는 러너가 만든 임시 파일·캐시까지 사용자 레포에 커밋한다.
285
+ # 버전 파일은 이미 추적 중이라 -u로 충분하고, 첫 릴리스에 새로 생기는
286
+ # CHANGELOG만 따로 올린다 (.gitignore에 있으면 건너뛴다).
287
+ git add -u
288
+ git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
267
289
  if git diff --staged --quiet; then
268
290
  echo "no changes to commit"
269
291
  else
@@ -386,3 +408,195 @@ jobs:
386
408
  VERSION="${{ steps.version.outputs.version }}"
387
409
  gh release create "v$VERSION" --title "v$VERSION" --notes-file notes.md
388
410
  echo "GitHub Release v$VERSION published"
411
+
412
+ - name: Trigger README-VERSION-UPDATE
413
+ # 봇 토큰(GITHUB_TOKEN)이 만든 push는 README 갱신 워크플로우를 깨우지 못한다 —
414
+ # PAT 없는 릴리스 PR 병합, 안전망·trunk-based의 [skip ci] 버전 커밋이 그렇다.
415
+ # 릴리스를 새로 발행했을 때 확정된 버전으로 한 번 직접 깨운다 (중복 실행돼도 변경 없으면 커밋 안 함).
416
+ if: steps.gate.outputs.proceed == 'true' && steps.version.outputs.release_exists != 'true'
417
+ env:
418
+ GH_TOKEN: ${{ github.token }}
419
+ run: |
420
+ if gh workflow run PROJECT-COMMON-README-VERSION-UPDATE.yaml --ref {{MAIN_BRANCH}}; then
421
+ echo "README-VERSION-UPDATE workflow_dispatch 트리거 요청 완료"
422
+ else
423
+ echo "::notice::README-VERSION-UPDATE 워크플로우를 깨우지 못했습니다 (삭제했거나 비활성화됨) — README 버전 갱신을 건너뜁니다"
424
+ fi
425
+
426
+ - name: Trigger deploy workflows
427
+ # 봇 토큰(GITHUB_TOKEN)이 병합한 릴리스 PR의 push는 {{MAIN_BRANCH}}의 배포 워크플로우(on: push)를
428
+ # 깨우지 못한다 — PAT 없는 기본 설치에서 서버·스토어 배포가 한 번도 돌지 않는다.
429
+ # 그런 병합일 때만 배포 워크플로우를 workflow_dispatch로 직접 깨운다. PAT·사람이 병합했다면
430
+ # push 이벤트로 이미 실행됐으므로 다시 깨우지 않는다 (중복 배포 방지).
431
+ # 대상: 마법사가 설치한 워크플로우 중 push.branches에 {{MAIN_BRANCH}}가 있고 workflow_dispatch가
432
+ # 있는 것. 모노레포 paths 필터는 push와 같은 범위(병합 커밋의 변경 파일)로 평가한다.
433
+ if: >-
434
+ steps.gate.outputs.proceed == 'true' &&
435
+ steps.version.outputs.release_exists != 'true' &&
436
+ steps.mode.outputs.mode == 'pr-flow'
437
+ continue-on-error: true
438
+ env:
439
+ GH_TOKEN: ${{ github.token }}
440
+ HAS_WORKFLOW_PAT: ${{ secrets.WORKFLOW_PAT != '' }}
441
+ RELEASE_VERSION: ${{ steps.version.outputs.version }}
442
+ MAIN_BRANCH: "{{MAIN_BRANCH}}"
443
+ run: |
444
+ # 이 버전의 릴리스 PR 병합 커밋 — 안전망 bump·수동 재발행처럼 병합이 아닌 릴리스는
445
+ # 사람의 push가 이미 배포를 깨웠으므로 건너뛴다.
446
+ MERGE_LINE=$(git log --first-parent --pretty='%H %s' -n 100 | grep -m1 -F " chore(release): v${RELEASE_VERSION} (PR #" || true)
447
+ if [ -z "$MERGE_LINE" ]; then
448
+ echo "v${RELEASE_VERSION}은 릴리스 PR 병합으로 들어온 릴리스가 아닙니다 — 배포는 push 이벤트가 맡습니다"
449
+ exit 0
450
+ fi
451
+ MERGE_SHA=${MERGE_LINE%% *}
452
+ PR_NUMBER=$(printf '%s\n' "$MERGE_LINE" | sed -n 's/.*(PR #\([0-9][0-9]*\)).*/\1/p')
453
+
454
+ # 병합 주체로 push 이벤트가 떴는지 판정한다. 조회에 실패하면 PAT 등록 여부로 대신 판단한다.
455
+ MERGED_BY=""
456
+ if [ -n "$PR_NUMBER" ]; then
457
+ MERGED_BY=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.merged_by.login // ""' 2>/dev/null || true)
458
+ fi
459
+ case "$MERGED_BY" in
460
+ # GITHUB_TOKEN 병합자는 정확히 이 로그인이다 — 접두사만 같은 다른 계정은 사람 병합으로 본다
461
+ "github-actions[bot]") echo "PR #${PR_NUMBER}을 봇 토큰이 병합했습니다 — push 이벤트가 배포를 깨우지 못했습니다" ;;
462
+ "")
463
+ if [ "$HAS_WORKFLOW_PAT" = "true" ]; then
464
+ echo "병합 주체를 확인하지 못했고 WORKFLOW_PAT이 있습니다 — push 이벤트가 배포를 실행했다고 보고 건너뜁니다"
465
+ exit 0
466
+ fi
467
+ echo "병합 주체를 확인하지 못했고 WORKFLOW_PAT이 없습니다 — 봇 토큰 병합으로 봅니다"
468
+ ;;
469
+ *)
470
+ echo "PR #${PR_NUMBER}을 ${MERGED_BY}이(가) 병합했습니다 — push 이벤트가 배포를 이미 실행했으므로 건너뜁니다"
471
+ exit 0
472
+ ;;
473
+ esac
474
+
475
+ CHANGED_FILES="${RUNNER_TEMP:-/tmp}/release_changed_files.txt"
476
+ git diff --name-only "${MERGE_SHA}^1" "$MERGE_SHA" > "$CHANGED_FILES" || rm -f "$CHANGED_FILES"
477
+ export CHANGED_FILES
478
+
479
+ # 명령 치환 안의 heredoc은 따옴표 해석이 어긋나므로 파일로 먼저 쓴다
480
+ FIND_DEPLOY="${RUNNER_TEMP:-/tmp}/find_deploy_workflows.py"
481
+ cat > "$FIND_DEPLOY" <<'PY'
482
+ import os, re
483
+
484
+ MARKER = "project-auto-wizard:managed-workflow"
485
+ BRANCH = os.environ["MAIN_BRANCH"]
486
+ CHANGED = None
487
+ if os.path.exists(os.environ.get("CHANGED_FILES", "")):
488
+ with open(os.environ["CHANGED_FILES"], encoding="utf-8") as f:
489
+ CHANGED = [l.strip() for l in f if l.strip()]
490
+
491
+ # YAML 라이브러리 없이 on 블록만 읽는다 — 러너 파이썬에 PyYAML이 있다고 가정하지 않는다.
492
+ def strip_comment(s):
493
+ q = None
494
+ for i, c in enumerate(s):
495
+ if q:
496
+ q = None if c == q else q
497
+ elif c in "\"'":
498
+ q = c
499
+ elif c == "#" and (i == 0 or s[i - 1] in " \t"):
500
+ return s[:i].rstrip()
501
+ return s.rstrip()
502
+
503
+ def unquote(v):
504
+ v = v.strip()
505
+ return v[1:-1] if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'" else v
506
+
507
+ # GitHub 필터 문법: ** 는 / 포함 전부, * 와 ? 는 / 를 넘지 않는다. ! 는 뒤 패턴이 앞을 뒤집는다.
508
+ def glob_re(p):
509
+ out, i = "", 0
510
+ while i < len(p):
511
+ if p.startswith("**", i):
512
+ out, i = out + ".*", i + 2
513
+ elif p[i] in "*?":
514
+ out, i = out + ("[^/]*" if p[i] == "*" else "[^/]"), i + 1
515
+ else:
516
+ out, i = out + re.escape(p[i]), i + 1
517
+ return re.compile(out + r"\Z")
518
+
519
+ def matches(pats, value):
520
+ hit = False
521
+ for p in pats:
522
+ neg = p.startswith("!")
523
+ if glob_re(p[1:] if neg else p).match(value):
524
+ hit = not neg
525
+ return hit
526
+
527
+ def mapping(lines):
528
+ if not lines:
529
+ return {}
530
+ base = min(i for i, _ in lines)
531
+ res = {}
532
+ for n, (ind, text) in enumerate(lines):
533
+ m = re.match(r"""^["']?([\w-]+)["']?\s*:(?:\s+(.*))?$""", text)
534
+ if ind != base or text.startswith("- ") or not m:
535
+ continue
536
+ sub = []
537
+ for ind2, text2 in lines[n + 1:]:
538
+ if ind2 < ind or (ind2 == ind and not text2.startswith("- ")):
539
+ break
540
+ sub.append((ind2, text2))
541
+ res[m.group(1)] = (m.group(2) or "", sub)
542
+ return res
543
+
544
+ def items(entry):
545
+ inline, sub = entry
546
+ if inline.startswith("["):
547
+ return [unquote(x) for x in inline.strip("[]").split(",") if x.strip()]
548
+ if inline:
549
+ return [unquote(inline)]
550
+ return [unquote(t[2:]) for _, t in sub if t.startswith("- ")]
551
+
552
+ def is_target(path):
553
+ with open(path, encoding="utf-8") as f:
554
+ raw = f.read()
555
+ if MARKER not in raw:
556
+ return False
557
+ lines = []
558
+ for l in raw.splitlines():
559
+ s = strip_comment(l)
560
+ if s.strip():
561
+ lines.append((len(s) - len(s.lstrip()), s.strip()))
562
+ on = mapping(lines).get("on")
563
+ events = mapping(on[1]) if on else {}
564
+ push = events.get("push")
565
+ if "workflow_dispatch" not in events or not push:
566
+ return False
567
+ f = mapping(push[1])
568
+ if "branches" not in f or not matches(items(f["branches"]), BRANCH):
569
+ return False
570
+ if CHANGED is not None:
571
+ if "paths" in f and not any(matches(items(f["paths"]), c) for c in CHANGED):
572
+ return False
573
+ if "paths-ignore" in f and all(matches(items(f["paths-ignore"]), c) for c in CHANGED):
574
+ return False
575
+ return True
576
+
577
+ root = ".github/workflows"
578
+ for name in sorted(os.listdir(root)) if os.path.isdir(root) else []:
579
+ # 공통 워크플로우(릴리스·README·안전망)는 배포가 아니고, 필요한 것은 각자 따로 깨운다
580
+ if name.startswith("PROJECT-COMMON-") or not re.search(r"\.ya?ml$", name):
581
+ continue
582
+ if is_target(os.path.join(root, name)):
583
+ print(name)
584
+ PY
585
+ TARGETS=$(python3 "$FIND_DEPLOY")
586
+
587
+ if [ -z "$TARGETS" ]; then
588
+ echo "{{MAIN_BRANCH}} push로 도는 배포 워크플로우가 없습니다 — 건너뜁니다"
589
+ exit 0
590
+ fi
591
+
592
+ FAILED=0
593
+ for WF in $TARGETS; do
594
+ # 입력 없이 실행하면 workflow_dispatch 입력 기본값(설치 때 고른 배포 모드 등)을 쓴다
595
+ if gh workflow run "$WF" --ref {{MAIN_BRANCH}}; then
596
+ echo "$WF workflow_dispatch 트리거 요청 완료"
597
+ else
598
+ FAILED=1
599
+ echo "::warning::$WF 배포 워크플로우를 깨우지 못했습니다 — Actions 탭에서 Run workflow로 직접 실행하세요"
600
+ fi
601
+ done
602
+ exit "$FAILED"
@@ -4,7 +4,9 @@
4
4
  #
5
5
  # [Safety net] Only when a direct push (one that did not go through a
6
6
  # release PR) lands on {{MAIN_BRANCH}} (production), this workflow bumps
7
- # the patch version and syncs every version file.
7
+ # the patch version, syncs every version file, and adds a CHANGELOG entry
8
+ # built from the commits since the last release tag (these become the
9
+ # release notes RELEASE-PUBLISH publishes).
8
10
  #
9
11
  # Normal releases ({{DEVELOP_BRANCH}} -> {{MAIN_BRANCH}} PR) get their
10
12
  # version confirmed by AUTO-CHANGELOG-CONTROL before the merge. The merge
@@ -20,7 +22,9 @@
20
22
  #
21
23
  # After pushing the safety-net bump, this workflow also triggers
22
24
  # RELEASE-PUBLISH via workflow_dispatch so a version drift is
23
- # auto-recovered instead of requiring a manual re-run.
25
+ # auto-recovered instead of requiring a manual re-run, and merges
26
+ # {{MAIN_BRANCH}} back into {{DEVELOP_BRANCH}} so the next release PR does
27
+ # not conflict on the version files.
24
28
 
25
29
  name: PROJECT-VERSION-CONTROL
26
30
 
@@ -42,6 +46,10 @@ permissions:
42
46
  contents: write
43
47
  actions: write
44
48
 
49
+ env:
50
+ # 스크립트 import가 .github/scripts/__pycache__를 만들어 봇 커밋에 섞이지 않게 한다
51
+ PYTHONDONTWRITEBYTECODE: "1"
52
+
45
53
  jobs:
46
54
  version-bump:
47
55
  name: Safety-net version bump
@@ -130,6 +138,44 @@ jobs:
130
138
  echo "version.yml not found"
131
139
  fi
132
140
 
141
+ - name: Update CHANGELOG for the safety-net release
142
+ # 안전망 릴리스도 실제로 들어간 커밋으로 CHANGELOG 항목을 만든다. 항목이 없으면
143
+ # RELEASE-PUBLISH가 고정 문구로 릴리스 노트를 채우고 변경 내역이 어디에도 남지 않는다.
144
+ # 요약 엔진: 사용자 API(설정된 경우) -> 규칙 기반 (Copilot은 릴리스 PR·trunk 경로에서만 쓴다)
145
+ if: steps.release_guard.outputs.skip != 'true'
146
+ env:
147
+ AI_API_KEY: ${{ secrets.AI_API_KEY }}
148
+ AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
149
+ AI_MODEL: ${{ vars.AI_MODEL }}
150
+ run: |
151
+ NEW_VERSION="${{ steps.version.outputs.new_version }}"
152
+
153
+ # 마지막 릴리스 태그 이후 {{MAIN_BRANCH}}에 직접 들어온 커밋 (태그가 없으면 전체 이력)
154
+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
155
+ LOG_RANGE="${LAST_TAG:+${LAST_TAG}..}HEAD"
156
+ git log --pretty=%s "$LOG_RANGE" > commits.txt
157
+ # 본문 푸터 BREAKING CHANGE:는 제목 목록 뒤에 한 줄씩 덧붙인다 — 스크립트는 한 줄을
158
+ # 한 항목으로 읽으므로 본문 전체(%B)를 넣으면 설명 줄까지 변경 항목으로 잡힌다.
159
+ git log --pretty=%b "$LOG_RANGE" | grep -E '^BREAKING[ -]CHANGE[[:space:]]*:' >> commits.txt || true
160
+ echo "commits since ${LAST_TAG:-the beginning}: $(wc -l < commits.txt)"
161
+
162
+ python3 .github/scripts/changelog_manager.py ai-summary \
163
+ --commits-file commits.txt \
164
+ --version "$NEW_VERSION" \
165
+ --output summary.md
166
+
167
+ # update-from-summary reads ./pr_body.md
168
+ cp summary.md pr_body.md
169
+ VERSION="$NEW_VERSION"
170
+ PROJECT_TYPES="${{ steps.project_info.outputs.project_types }}"
171
+ TODAY=$(date '+%Y-%m-%d')
172
+ TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%SZ')
173
+ export VERSION PROJECT_TYPES TODAY TIMESTAMP
174
+ python3 .github/scripts/changelog_manager.py update-from-summary
175
+ python3 .github/scripts/changelog_manager.py generate-md
176
+
177
+ rm -f commits.txt summary.md pr_body.md
178
+
133
179
  - name: Commit and push changes
134
180
  id: commit_push
135
181
  if: steps.release_guard.outputs.skip != 'true'
@@ -137,7 +183,11 @@ jobs:
137
183
  git config --local user.email "action@github.com"
138
184
  git config --local user.name "GitHub Action"
139
185
 
140
- git add -A
186
+ # -A는 러너가 만든 임시 파일·캐시까지 사용자 레포에 커밋한다.
187
+ # 버전 파일은 이미 추적 중이라 -u로 충분하고, 새로 생기는 CHANGELOG만
188
+ # 따로 올린다 (.gitignore에 있으면 건너뛴다).
189
+ git add -u
190
+ git ls-files -z --others --exclude-standard -- CHANGELOG.json CHANGELOG.md | xargs -0 -r git add --
141
191
 
142
192
  if git diff --staged --quiet; then
143
193
  echo "no changes to commit (already in sync)"
@@ -192,6 +242,42 @@ jobs:
192
242
  gh workflow run PROJECT-COMMON-RELEASE-PUBLISH.yaml --ref {{MAIN_BRANCH}}
193
243
  echo "RELEASE-PUBLISH workflow_dispatch 트리거 요청 완료 (WORKFLOW_PAT 없이도 동작, 이슈 #61 드리프트 자동 복구)"
194
244
 
245
+ - name: Back-merge the safety-net bump into {{DEVELOP_BRANCH}}
246
+ # 안전망 bump는 {{MAIN_BRANCH}}에만 올라간다. {{DEVELOP_BRANCH}}가 이를 모르면 다음 릴리스 PR에서
247
+ # 양쪽이 같은 버전 줄을 고쳐 충돌하므로 곧바로 {{DEVELOP_BRANCH}}에도 병합해 둔다.
248
+ # 실패(충돌·브랜치 보호)해도 안전망 릴리스는 이미 끝났으므로 경고만 남긴다.
249
+ if: steps.release_guard.outputs.skip != 'true' && steps.commit_push.outputs.pushed == 'true'
250
+ run: |
251
+ DEVELOP_BRANCH="{{DEVELOP_BRANCH}}"
252
+ NEW_VERSION="${{ steps.version.outputs.new_version }}"
253
+ MANUAL="git checkout $DEVELOP_BRANCH && git pull && git merge origin/{{MAIN_BRANCH}} && git push"
254
+
255
+ if ! git ls-remote --exit-code --heads origin "$DEVELOP_BRANCH" >/dev/null 2>&1; then
256
+ echo "branch $DEVELOP_BRANCH not found — nothing to back-merge"
257
+ exit 0
258
+ fi
259
+
260
+ for ATTEMPT in 1 2 3; do
261
+ git fetch -q origin "$DEVELOP_BRANCH" {{MAIN_BRANCH}}
262
+ git checkout -q -B safety-net-back-merge "origin/$DEVELOP_BRANCH"
263
+ if git merge-base --is-ancestor "origin/{{MAIN_BRANCH}}" HEAD; then
264
+ echo "$DEVELOP_BRANCH already contains {{MAIN_BRANCH}}"
265
+ exit 0
266
+ fi
267
+ if ! git merge --no-edit -m "chore(version): merge v${NEW_VERSION} safety-net bump into ${DEVELOP_BRANCH} [skip ci]" "origin/{{MAIN_BRANCH}}"; then
268
+ git merge --abort 2>/dev/null || true
269
+ echo "::warning::{{MAIN_BRANCH}}을 ${DEVELOP_BRANCH}에 자동 병합하지 못했습니다 (충돌). 다음 릴리스 PR 전에 직접 병합하세요: $MANUAL"
270
+ exit 0
271
+ fi
272
+ if git push origin "HEAD:$DEVELOP_BRANCH"; then
273
+ echo "safety-net bump v$NEW_VERSION merged into $DEVELOP_BRANCH"
274
+ exit 0
275
+ fi
276
+ echo "push attempt $ATTEMPT failed — retrying"
277
+ sleep 5
278
+ done
279
+ echo "::warning::${DEVELOP_BRANCH}에 push하지 못했습니다 (브랜치 보호 등). 다음 릴리스 PR 전에 직접 병합하세요: $MANUAL"
280
+
195
281
  - name: Summary
196
282
  if: always()
197
283
  run: |
@@ -20,14 +20,14 @@
20
20
  #
21
21
  # 환경 설정 (선택):
22
22
  # ENV_FILE 또는 ENV: .env 파일 내용 (둘 중 하나, ENV_FILE 우선)
23
- # GOOGLE_SERVICES_JSON: Firebase google-services.json
23
+ # GOOGLE_SERVICES_JSON (선택): Firebase google-services.json
24
24
  #
25
25
  # ※ 위 Secrets는 저장소 Settings → Secrets and variables → Actions에 등록하세요.
26
26
  # FIREBASE_APP_ID·FIREBASE_TESTER_GROUP은 아래 env 섹션 값을 직접 수정합니다.
27
27
  #
28
28
  # 프로젝트별 설정 (env 섹션에서 수정)
29
29
  # APP_ARTIFACT_NAME : 프로젝트 이름
30
- # FLUTTER_VERSION : Flutter SDK 버전
30
+ # FLUTTER_VERSION : Flutter SDK 버전 (빈 값 = stable 최신)
31
31
  # JAVA_VERSION : Java 버전
32
32
  # FIREBASE_APP_ID : Firebase 앱 ID (Firebase Console에서 확인)
33
33
  # FIREBASE_TESTER_GROUP : Firebase 테스터 그룹명
@@ -51,7 +51,9 @@ env:
51
51
  # 프로젝트 이름
52
52
  APP_ARTIFACT_NAME: "__APP_ARTIFACT_NAME__" # @wizard ask:@repo
53
53
  # Flutter/Java 버전
54
- FLUTTER_VERSION: "3.35.5"
54
+ # Flutter SDK 버전 — 비워 두면 stable 채널 최신을 쓴다(최신 flutter create 프로젝트의 SDK 제약과 맞음).
55
+ # 특정 버전에 고정하려면 "3.38.9"처럼 적는다. pubspec.yaml의 environment.sdk 범위를 만족해야 한다.
56
+ FLUTTER_VERSION: ""
55
57
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
56
58
  PROJECT_TYPE: "flutter"
57
59
  # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등 — 설치 시 자동 설정
@@ -81,11 +83,40 @@ jobs:
81
83
  - name: Checkout repository
82
84
  uses: actions/checkout@v7
83
85
  with:
84
- ref: main
86
+ ref: "{{MAIN_BRANCH}}"
85
87
  fetch-depth: 0
86
88
 
89
+ # 서명·업로드 Secret이 없으면 빌드를 한참 진행한 뒤에야 실패하므로, 빠진 이름을 먼저 모두 알려 주고 멈춘다
90
+ # ENV_FILE·GOOGLE_SERVICES_JSON은 선택이라 점검하지 않는다
91
+ - name: 배포 사전 점검 (Secret)
92
+ env:
93
+ RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
94
+ RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
95
+ RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
96
+ RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
97
+ FIREBASE_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
98
+ run: |
99
+ MISSING=""
100
+ for NAME in RELEASE_KEYSTORE_BASE64 RELEASE_KEYSTORE_PASSWORD RELEASE_KEY_ALIAS RELEASE_KEY_PASSWORD FIREBASE_SERVICE_ACCOUNT_JSON_BASE64; do
101
+ [ -n "${!NAME}" ] || MISSING="$MISSING $NAME"
102
+ done
103
+ FAILED=false
104
+ if [ -n "$MISSING" ]; then
105
+ echo "::error title=필수 Secret 누락::등록되지 않은 GitHub Secret:$MISSING — Settings → Secrets and variables → Actions에서 등록하세요"
106
+ FAILED=true
107
+ fi
108
+ # 업로드 대상 앱 ID는 Secret이 아니라 이 파일 env 섹션 값이다
109
+ if [ -z "$FIREBASE_APP_ID" ] || [ "$FIREBASE_APP_ID" = "your-firebase-app-id" ]; then
110
+ echo "::error title=FIREBASE_APP_ID 미설정::워크플로우 env 섹션의 FIREBASE_APP_ID를 Firebase Console의 앱 ID로 바꾸세요"
111
+ FAILED=true
112
+ fi
113
+ if [ "$FAILED" = "true" ]; then
114
+ exit 1
115
+ fi
116
+ echo "✅ 사전 점검 통과: 필수 Secret과 FIREBASE_APP_ID가 모두 있습니다"
117
+
87
118
  - name: Pull latest changes
88
- run: git pull origin main
119
+ run: git pull origin {{MAIN_BRANCH}}
89
120
 
90
121
  - name: Prepare env file
91
122
  env:
@@ -104,6 +135,7 @@ jobs:
104
135
  - name: Set up Flutter
105
136
  uses: subosito/flutter-action@v2
106
137
  with:
138
+ channel: stable
107
139
  flutter-version: ${{ env.FLUTTER_VERSION }}
108
140
  cache: true
109
141
 
@@ -211,11 +243,11 @@ jobs:
211
243
  - name: Checkout repository
212
244
  uses: actions/checkout@v7
213
245
  with:
214
- ref: main
246
+ ref: "{{MAIN_BRANCH}}"
215
247
  fetch-depth: 0
216
248
 
217
249
  - name: Pull latest changes
218
- run: git pull origin main
250
+ run: git pull origin {{MAIN_BRANCH}}
219
251
 
220
252
  - name: Download project files
221
253
  uses: actions/download-artifact@v8
@@ -239,9 +271,27 @@ jobs:
239
271
  fi
240
272
 
241
273
  - name: Setup Release Keystore
274
+ env:
275
+ RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
276
+ RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
277
+ RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
278
+ RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
242
279
  run: |
280
+ # 빈 Secret으로 빈 keystore를 만들면 어떤 키로 서명됐는지 알 수 없거나 뒤 단계에서 엉뚱한 오류로 끝난다
281
+ MISSING=""
282
+ for name in RELEASE_KEYSTORE_BASE64 RELEASE_KEYSTORE_PASSWORD RELEASE_KEY_ALIAS RELEASE_KEY_PASSWORD; do
283
+ [ -n "${!name}" ] || MISSING="$MISSING $name"
284
+ done
285
+ if [ -n "$MISSING" ]; then
286
+ echo "::error::서명 Secret이 비어 있습니다:$MISSING (워크플로우 상단 주석 참고)"
287
+ exit 1
288
+ fi
243
289
  mkdir -p android/app/keystore
244
- echo "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore/key.jks
290
+ printf '%s' "$RELEASE_KEYSTORE_BASE64" | base64 -d > android/app/keystore/key.jks
291
+ if [ ! -s android/app/keystore/key.jks ]; then
292
+ echo "::error::RELEASE_KEYSTORE_BASE64를 디코딩한 keystore가 비어 있습니다"
293
+ exit 1
294
+ fi
245
295
  echo "✅ Release Keystore 생성 완료"
246
296
  ls -la android/app/keystore/
247
297
 
@@ -254,18 +304,24 @@ jobs:
254
304
  keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}
255
305
  EOF
256
306
  echo "✅ key.properties 생성 완료"
257
- cat android/key.properties
258
307
 
259
- - name: Create Google-services.json
308
+ # google-services.json 생성 (선택) — 비어 있으면 빈 파일을 만들지 않는다
309
+ - name: Create google-services.json
310
+ env:
311
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
260
312
  run: |
261
- cat << 'EOF' > android/app/google-services.json
262
- ${{ secrets.GOOGLE_SERVICES_JSON }}
263
- EOF
264
- echo "✅ Google-services.json 생성 완료"
313
+ if [ -n "$GOOGLE_SERVICES_JSON" ]; then
314
+ mkdir -p android/app
315
+ printf '%s' "$GOOGLE_SERVICES_JSON" > android/app/google-services.json
316
+ echo "✅ google-services.json created"
317
+ else
318
+ echo "ℹ️ GOOGLE_SERVICES_JSON secret not provided, skipping"
319
+ fi
265
320
 
266
321
  - name: Set up Flutter
267
322
  uses: subosito/flutter-action@v2
268
323
  with:
324
+ channel: stable
269
325
  flutter-version: ${{ env.FLUTTER_VERSION }}
270
326
  cache: true
271
327
 
@@ -542,7 +598,7 @@ jobs:
542
598
  - name: Checkout repository
543
599
  uses: actions/checkout@v7
544
600
  with:
545
- ref: main
601
+ ref: "{{MAIN_BRANCH}}"
546
602
  fetch-depth: 0
547
603
 
548
604
  - name: Download AAB artifact
@@ -558,8 +614,14 @@ jobs:
558
614
  path: .
559
615
 
560
616
  - name: Setup Firebase credentials file
617
+ env:
618
+ SERVICE_ACCOUNT_BASE64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
561
619
  run: |
562
- echo "${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}" | base64 --decode > firebase-service-account.json
620
+ if [ -z "$SERVICE_ACCOUNT_BASE64" ]; then
621
+ echo "::error::FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 Secret이 비어 있습니다"
622
+ exit 1
623
+ fi
624
+ printf '%s' "$SERVICE_ACCOUNT_BASE64" | base64 --decode > firebase-service-account.json
563
625
  echo "✅ Firebase 서비스 계정 JSON 파일 생성 완료"
564
626
 
565
627
  # Firebase App Distribution release notes 길이 제한 (안전망)