project-auto-wizard 0.10.0 → 0.12.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 (51) hide show
  1. package/README.md +104 -18
  2. package/package.json +2 -2
  3. package/payload/config/breaking-changes.json +24 -1
  4. package/payload/flutter-app/android/fastlane/Fastfile.playstore +86 -0
  5. package/payload/flutter-app/ios/ExportOptions.plist +31 -0
  6. package/payload/flutter-app/ios/fastlane/Fastfile +117 -0
  7. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  8. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  9. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  10. package/payload/scripts/changelog_manager.py +83 -30
  11. package/payload/version.yml.template +5 -0
  12. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +30 -7
  13. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +36 -8
  14. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +25 -1
  15. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +48 -20
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +44 -19
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +28 -36
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +32 -45
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +56 -41
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
  22. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
  23. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
  24. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
  25. package/payload/workflows/react/PROJECT-REACT-CI.yaml +47 -0
  26. package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
  27. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +47 -0
  28. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
  29. package/src/cli/args.js +44 -0
  30. package/src/cli/help.js +6 -1
  31. package/src/commands/doctor.js +59 -5
  32. package/src/commands/dry-run.js +10 -0
  33. package/src/commands/full.js +28 -3
  34. package/src/commands/interactive-flutter.js +95 -0
  35. package/src/commands/interactive.js +60 -5
  36. package/src/commands/status.js +21 -2
  37. package/src/context.js +7 -0
  38. package/src/core/breaking.js +7 -3
  39. package/src/core/copy/flutter-app.js +42 -0
  40. package/src/core/copy/workflows.js +21 -9
  41. package/src/core/detect-fs.js +9 -1
  42. package/src/core/flutter-options.js +118 -0
  43. package/src/core/installed-stores.js +12 -0
  44. package/src/core/verify.js +1 -1
  45. package/src/core/version-yml.js +50 -6
  46. package/src/core/wizard-env.js +26 -4
  47. package/src/index.js +34 -1
  48. package/src/ui/env-plan.js +8 -5
  49. package/src/ui/prompts.js +88 -3
  50. package/src/ui/status-cards.js +13 -1
  51. package/src/ui/summary.js +28 -9
@@ -27,7 +27,9 @@ import html
27
27
  import json
28
28
  import os
29
29
  import re
30
+ import subprocess
30
31
  import sys
32
+ import tempfile
31
33
  import traceback
32
34
  import urllib.error
33
35
  import urllib.request
@@ -123,7 +125,7 @@ def _parse_markdown_sections(md_content: str) -> dict:
123
125
  """
124
126
  섹션 파서: `### 카테고리` 헤딩 + `- 항목` 불릿 형식.
125
127
 
126
- AI 엔진 체인(사용자 지정 API → GitHub Models)과 규칙 기반 폴백이
128
+ AI 엔진 체인(사용자 지정 API → Copilot CLI)과 규칙 기반 폴백이
127
129
  동일하게 생성하는 형식이므로 1순위로 시도한다.
128
130
  """
129
131
  detected: dict[str, dict] = {}
@@ -389,21 +391,19 @@ def _ai_assisted_minor_upgrade(unclassified_lines: list[str]) -> bool:
389
391
  return False
390
392
  prompt = _BUMP_AI_PROMPT_PREFIX + "\n".join(f"- {line}" for line in unclassified_lines)
391
393
 
392
- ai_api_key = os.environ.get('AI_API_KEY')
393
- github_token = os.environ.get('GITHUB_TOKEN')
394
- candidates = [
395
- (ai_api_key, os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL, os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL),
396
- (github_token, _AI_DEFAULT_BASE_URL, _AI_DEFAULT_MODEL),
397
- ]
398
- for token, base_url, model in candidates:
399
- if not token:
400
- continue
394
+ settings = _user_api_settings()
395
+ if settings:
396
+ api_key, base_url, model = settings
401
397
  try:
402
- response = call_openai_compatible(base_url, token, model, prompt)
403
- return response.strip() == 'MINOR'
398
+ return call_openai_compatible(base_url, api_key, model, prompt).strip() == 'MINOR'
404
399
  except Exception as e:
405
400
  print(f"[warn] bump AI assist failed: {e}", file=sys.stderr)
406
- continue
401
+
402
+ if _copilot_enabled():
403
+ try:
404
+ return call_copilot_cli(prompt).strip() == 'MINOR'
405
+ except Exception as e:
406
+ print(f"[warn] bump AI assist (copilot) failed: {e}", file=sys.stderr)
407
407
  return False
408
408
 
409
409
 
@@ -694,8 +694,67 @@ def cmd_export_release_notes(version: str, output_path: str | None) -> int:
694
694
 
695
695
  # ------------------------ ai-summary 엔진 체인 ------------------------
696
696
 
697
- _AI_DEFAULT_BASE_URL = "https://models.github.ai/inference"
698
- _AI_DEFAULT_MODEL = "openai/gpt-4o-mini"
697
+ _COPILOT_DEFAULT_MODEL = "claude-haiku-4.5"
698
+ _COPILOT_TIMEOUT_SECONDS = 90
699
+
700
+
701
+ def _user_api_settings() -> tuple[str, str, str] | None:
702
+ """사용자 지정 AI 티어 설정. AI_API_KEY와 AI_API_BASE_URL, AI_MODEL이 모두 있어야 한다.
703
+
704
+ 키만 있고 URL·모델이 비어 있으면 그 키를 어디로도 보내지 않고 경고 후 건너뛴다
705
+ (종료된 기본 엔드포인트으로 사용자 키가 흘러가던 문제 방지)."""
706
+ api_key = os.environ.get('AI_API_KEY')
707
+ if not api_key:
708
+ return None
709
+ base_url = os.environ.get('AI_API_BASE_URL')
710
+ model = os.environ.get('AI_MODEL')
711
+ if not base_url or not model:
712
+ print(
713
+ "::warning::AI_API_KEY가 설정됐지만 AI_API_BASE_URL/AI_MODEL이 없어 사용자 API 티어를 건너뜁니다",
714
+ file=sys.stderr,
715
+ )
716
+ return None
717
+ return api_key, base_url, model
718
+
719
+
720
+ def _copilot_enabled() -> bool:
721
+ """version.yml의 copilot_ai가 켜져 있고(워크플로우가 COPILOT_AI로 전달) 토큰이 있을 때만 True."""
722
+ return os.environ.get('COPILOT_AI', '').strip().lower() == 'true' and bool(os.environ.get('GITHUB_TOKEN'))
723
+
724
+
725
+ def call_copilot_cli(prompt: str) -> str:
726
+ """Copilot CLI를 텍스트 생성 전용으로 호출해 응답 텍스트를 반환.
727
+
728
+ 프롬프트에 필요한 정보가 이미 다 들어 있으므로 에이전트 기능은 전부 막는다:
729
+ 빈 임시 디렉터리에서 실행하고, shell/write/url 도구를 거부하며, 내장 MCP와
730
+ 커스텀 지침 로딩을 끈다.
731
+ 실패(비정상 종료·타임아웃·CLI 없음)는 예외로 올려 호출부가 fallback한다."""
732
+ model = os.environ.get('COPILOT_MODEL') or _COPILOT_DEFAULT_MODEL
733
+ with tempfile.TemporaryDirectory() as workdir:
734
+ result = subprocess.run(
735
+ [
736
+ 'copilot', '-p', prompt, '-s',
737
+ '--no-ask-user', '--no-color', '--no-custom-instructions', '--disable-builtin-mcps',
738
+ '--deny-tool=shell', '--deny-tool=write', '--deny-tool=url',
739
+ '--model', model,
740
+ ],
741
+ cwd=workdir, stdin=subprocess.DEVNULL,
742
+ capture_output=True, text=True, timeout=_COPILOT_TIMEOUT_SECONDS,
743
+ )
744
+ if result.returncode != 0:
745
+ raise RuntimeError(f"copilot exited with {result.returncode}: {result.stderr.strip()[:200]}")
746
+ return result.stdout
747
+
748
+
749
+ def _is_valid_copilot_summary(text: str) -> bool:
750
+ """프롬프트가 요구한 Markdown 형식인지 최소한만 검사한다.
751
+
752
+ 섹션 헤딩('### ')이 하나도 없거나 코드펜스로 시작하는(통째로 감싼) 응답은 릴리스 노트로
753
+ 쓰지 않는다."""
754
+ stripped = text.strip()
755
+ if not stripped or stripped.startswith('```'):
756
+ return False
757
+ return any(line.startswith('### ') for line in stripped.splitlines())
699
758
 
700
759
 
701
760
  def _build_ai_prompt(commit_lines: list[str], pr_title: str | None, version: str, diff_stat: str | None = None) -> str:
@@ -760,16 +819,13 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
760
819
  except Exception:
761
820
  diff_stat = None
762
821
 
763
- ai_api_key = os.environ.get('AI_API_KEY')
764
- ai_base_url = os.environ.get('AI_API_BASE_URL') or _AI_DEFAULT_BASE_URL
765
- ai_model = os.environ.get('AI_MODEL') or _AI_DEFAULT_MODEL
766
- github_token = os.environ.get('GITHUB_TOKEN')
767
-
768
822
  engine = None
769
823
  summary_text = None
770
824
  prompt = _build_ai_prompt(commit_lines, pr_title, version, diff_stat)
771
825
 
772
- if ai_api_key:
826
+ settings = _user_api_settings()
827
+ if settings:
828
+ ai_api_key, ai_base_url, ai_model = settings
773
829
  try:
774
830
  candidate = call_openai_compatible(ai_base_url, ai_api_key, ai_model, prompt)
775
831
  if candidate and candidate.strip():
@@ -780,19 +836,16 @@ def cmd_ai_summary(commits_file: str, version: str, output_path: str, pr_title:
780
836
  except Exception as e:
781
837
  print(f"[warn] user-api failed: {e}", file=sys.stderr)
782
838
 
783
- if summary_text is None and github_token:
839
+ if summary_text is None and _copilot_enabled():
784
840
  try:
785
- # GitHub Models는 자체 모델 카탈로그만 서빙한다 — 사용자 API용으로
786
- # AI_MODEL이 오버라이드돼 있어도 여기서는 기본 모델을 쓴다
787
- # (커스텀 모델명은 models.github.ai에서 404).
788
- candidate = call_openai_compatible(_AI_DEFAULT_BASE_URL, github_token, _AI_DEFAULT_MODEL, prompt)
789
- if candidate and candidate.strip():
841
+ candidate = call_copilot_cli(prompt)
842
+ if _is_valid_copilot_summary(candidate):
790
843
  summary_text = candidate
791
- engine = "github-models"
844
+ engine = "copilot"
792
845
  else:
793
- print("[warn] github-models failed: empty content in response", file=sys.stderr)
846
+ print("[warn] copilot failed: empty or not in the requested Markdown format", file=sys.stderr)
794
847
  except Exception as e:
795
- print(f"[warn] github-models failed: {e}", file=sys.stderr)
848
+ print(f"[warn] copilot failed: {e}", file=sys.stderr)
796
849
 
797
850
  if summary_text is None:
798
851
  classified = classify_commits(commit_lines)
@@ -6,6 +6,9 @@
6
6
  # (default) — feat: commits bump minor, a `!` breaking marker on any commit
7
7
  # type bumps major. Set semver_auto: false to always patch-bump instead
8
8
  # (then edit major/minor manually, e.g., 1.0.5 -> 1.1.0 or 2.0.0).
9
+ # - copilot_ai: opt-in (default false) — true lets the release/PR summary
10
+ # workflows call GitHub Copilot CLI, which consumes GitHub Copilot AI Credits.
11
+ # Falls back to rule-based summaries whenever Copilot is off or unavailable.
9
12
  # ===
10
13
  # Supported project types: spring, flutter, next, react, react-native,
11
14
  # react-native-expo, node, python, basic, go
@@ -47,5 +50,7 @@ metadata:
47
50
  nexus: {{OPT_NEXUS}}
48
51
  secret_backup: {{OPT_SECRET_BACKUP}}
49
52
  semver_auto: {{OPT_SEMVER_AUTO}}
53
+ copilot_ai: {{OPT_COPILOT_AI}}
50
54
  deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (서버 배포 워크플로우)
55
+ {{FLUTTER_OPTIONS}}
51
56
  {{DEPLOY}}
@@ -4,9 +4,9 @@
4
4
  # Self-hosted AI PR summary bot — no third-party service required.
5
5
  # ===================================================================
6
6
  #
7
- # Posts an AI-generated summary comment on PRs targeting the default
8
- # branch, using the same zero-API-key engine chain as the release
9
- # changelog (user API key -> GitHub Models -> rule-based fallback).
7
+ # Posts a summary comment on PRs targeting the default branch, using the
8
+ # same engine chain as the release changelog (user API key -> Copilot CLI,
9
+ # opt-in via version.yml copilot_ai -> rule-based fallback).
10
10
  # The prompt includes commit subjects plus a capped `git diff --stat`.
11
11
  #
12
12
  # On release PRs (head branch == {{DEVELOP_BRANCH}}) this job is skipped.
@@ -30,7 +30,11 @@ on:
30
30
  permissions:
31
31
  contents: read
32
32
  pull-requests: write
33
- models: read
33
+ copilot-requests: write
34
+
35
+ concurrency:
36
+ group: ai-pr-summary-${{ github.event.pull_request.number }}
37
+ cancel-in-progress: true
34
38
 
35
39
  jobs:
36
40
  ai-pr-summary:
@@ -46,6 +50,20 @@ jobs:
46
50
  fetch-depth: 0
47
51
  ref: ${{ github.event.pull_request.head.ref }}
48
52
 
53
+ - name: Read copilot_ai option from version.yml
54
+ id: copilot_options
55
+ run: |
56
+ # scoped to the metadata: -> template: -> options: chain so an
57
+ # unrelated "copilot_ai:" key can never hijack the value
58
+ COPILOT_AI=$(python3 -c 'import re; t=open("version.yml",encoding="utf-8").read(); m=re.search(r"metadata:.*?template:.*?options:.*?copilot_ai:\s*\"?(true|false)", t, re.S); print(m.group(1) if m else "false")' 2>/dev/null || echo "false")
59
+ echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
60
+ echo "copilot_ai option: $COPILOT_AI"
61
+
62
+ - name: Install Copilot CLI
63
+ if: steps.copilot_options.outputs.copilot_ai == 'true'
64
+ continue-on-error: true
65
+ run: npm install -g @github/copilot@1.0.88
66
+
49
67
  - name: Generate and post AI summary
50
68
  env:
51
69
  PR_TITLE: ${{ github.event.pull_request.title }}
@@ -53,6 +71,8 @@ jobs:
53
71
  AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
54
72
  AI_MODEL: ${{ vars.AI_MODEL }}
55
73
  GITHUB_TOKEN: ${{ github.token }}
74
+ COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
75
+ COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
56
76
  run: |
57
77
  PR_NUMBER=${{ github.event.pull_request.number }}
58
78
  VERSION=$(python3 .github/scripts/version_manager.py get 2>/dev/null | tail -n 1)
@@ -68,17 +88,20 @@ jobs:
68
88
  fi
69
89
  rm -f _full_diff_stat.txt
70
90
 
71
- python3 .github/scripts/changelog_manager.py ai-summary \
91
+ SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
72
92
  --commits-file commits.txt \
73
93
  --version "$VERSION" \
74
94
  --output summary.md \
75
95
  --pr-title "$PR_TITLE" \
76
- --diff-stat-file diff_stat.txt
96
+ --diff-stat-file diff_stat.txt | tail -n 1)
97
+ ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
77
98
 
78
99
  {
79
- echo "🤖 **AI Summary (project-auto-wizard)**"
100
+ echo "📋 **PR Summary (project-auto-wizard)**"
80
101
  echo ""
81
102
  cat summary.md
103
+ echo ""
104
+ echo "<sub>engine: ${ENGINE}</sub>"
82
105
  } > comment_body.md
83
106
 
84
107
  python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json
@@ -12,8 +12,8 @@
12
12
  # commits) and bumps accordingly.
13
13
  # - Otherwise: always patch+1 (legacy behavior).
14
14
  # 2. Generates the release summary locally with changelog_manager.py
15
- # ai-summary (engine chain: user API key -> GitHub Models -> rule-based
16
- # fallback; always succeeds, no third-party service involved)
15
+ # ai-summary (engine chain: user API key -> Copilot CLI, opt-in via
16
+ # version.yml copilot_ai -> rule-based fallback; always succeeds)
17
17
  # - The AI prompt includes commit subjects plus a capped
18
18
  # `git diff --stat` (file-level change summary, no diff body)
19
19
  # for richer context without token-cost blowup.
@@ -34,8 +34,10 @@
34
34
  # - secrets.WORKFLOW_PAT: PAT used for the merge. Without it the merge is
35
35
  # performed with GITHUB_TOKEN, and GitHub will NOT trigger downstream
36
36
  # {{MAIN_BRANCH}}-push workflows (e.g. RELEASE-PUBLISH) for that merge.
37
- # - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL): preferred
38
- # summary engine. Falls back to GitHub Models, then to rule-based.
37
+ # - secrets.AI_API_KEY (+ vars.AI_API_BASE_URL / vars.AI_MODEL, all three
38
+ # required together): preferred summary engine. Falls back to Copilot CLI
39
+ # (only when version.yml copilot_ai is true; optional vars.COPILOT_MODEL),
40
+ # then to rule-based.
39
41
  #
40
42
  # Issue #90: without WORKFLOW_PAT, a wait-for-merge-and-trigger-release job
41
43
  # polls until the automerge actually lands, then wakes RELEASE-PUBLISH via
@@ -60,7 +62,7 @@ on:
60
62
  permissions:
61
63
  contents: write
62
64
  pull-requests: write
63
- models: read
65
+ copilot-requests: write
64
66
 
65
67
  jobs:
66
68
  changelog-and-merge:
@@ -94,6 +96,20 @@ jobs:
94
96
  echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
95
97
  echo "semver_auto option: $SEMVER_AUTO"
96
98
 
99
+ - name: Read copilot_ai option from version.yml
100
+ id: copilot_options
101
+ run: |
102
+ # scoped to the metadata: -> template: -> options: chain so an
103
+ # unrelated "copilot_ai:" key can never hijack the value
104
+ 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")
105
+ echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
106
+ echo "copilot_ai option: $COPILOT_AI"
107
+
108
+ - name: Install Copilot CLI
109
+ if: steps.copilot_options.outputs.copilot_ai == 'true'
110
+ continue-on-error: true
111
+ run: npm install -g @github/copilot@1.0.88
112
+
97
113
  - name: Collect commits since last release
98
114
  id: collect
99
115
  run: |
@@ -149,6 +165,8 @@ jobs:
149
165
  AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
150
166
  AI_MODEL: ${{ vars.AI_MODEL }}
151
167
  GITHUB_TOKEN: ${{ github.token }}
168
+ COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
169
+ COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
152
170
  run: |
153
171
  # Idempotency: if the PR head is already this workflow's own
154
172
  # version-confirm commit (re-run / synchronize retrigger), do not
@@ -175,6 +193,7 @@ jobs:
175
193
  echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
176
194
 
177
195
  - name: Generate summary with the AI engine chain
196
+ id: summary
178
197
  env:
179
198
  # PR title goes through the env block — never inline-interpolate
180
199
  # untrusted text into the shell string (quote injection).
@@ -183,28 +202,37 @@ jobs:
183
202
  AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
184
203
  AI_MODEL: ${{ vars.AI_MODEL }}
185
204
  GITHUB_TOKEN: ${{ github.token }}
205
+ COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
206
+ COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
186
207
  run: |
187
208
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
188
209
 
189
- python3 .github/scripts/changelog_manager.py ai-summary \
210
+ SUMMARY_RESULT=$(python3 .github/scripts/changelog_manager.py ai-summary \
190
211
  --commits-file commits.txt \
191
212
  --version "$VERSION" \
192
213
  --output summary.md \
193
214
  --pr-title "$PR_TITLE" \
194
- --diff-stat-file diff_stat.txt
215
+ --diff-stat-file diff_stat.txt | tail -n 1)
216
+ ENGINE=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1]).get("engine") or "unknown")' "$SUMMARY_RESULT" 2>/dev/null || echo "unknown")
217
+ echo "summary engine: $ENGINE"
218
+ echo "engine=$ENGINE" >> $GITHUB_OUTPUT
195
219
 
196
220
  # update-from-summary reads ./pr_body.md
197
221
  cp summary.md pr_body.md
198
222
 
199
223
  - name: Post AI summary comment
200
224
  continue-on-error: true
225
+ env:
226
+ ENGINE: ${{ steps.summary.outputs.engine }}
201
227
  run: |
202
228
  PR_NUMBER=${{ github.event.pull_request.number }}
203
229
 
204
230
  {
205
- echo "🤖 **AI Summary (project-auto-wizard)**"
231
+ echo "📋 **PR Summary (project-auto-wizard)**"
206
232
  echo ""
207
233
  cat summary.md
234
+ echo ""
235
+ echo "<sub>engine: ${ENGINE:-unknown}</sub>"
208
236
  } > comment_body.md
209
237
 
210
238
  python3 -c 'import json; print(json.dumps({"body": open("comment_body.md", encoding="utf-8").read()}))' > comment_payload.json
@@ -59,7 +59,7 @@ on:
59
59
 
60
60
  permissions:
61
61
  contents: write
62
- models: read
62
+ copilot-requests: write
63
63
 
64
64
  jobs:
65
65
  release:
@@ -160,6 +160,28 @@ jobs:
160
160
  echo "semver_auto=$SEMVER_AUTO" >> $GITHUB_OUTPUT
161
161
  echo "semver_auto option: $SEMVER_AUTO"
162
162
 
163
+ - name: Read copilot_ai option from version.yml
164
+ id: copilot_options
165
+ if: >-
166
+ steps.gate.outputs.proceed == 'true' &&
167
+ steps.mode.outputs.mode == 'trunk-based' &&
168
+ github.event_name == 'push'
169
+ run: |
170
+ # scoped to the metadata: -> template: -> options: chain so an
171
+ # unrelated "copilot_ai:" key can never hijack the value
172
+ 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")
173
+ echo "copilot_ai=$COPILOT_AI" >> $GITHUB_OUTPUT
174
+ echo "copilot_ai option: $COPILOT_AI"
175
+
176
+ - name: Install Copilot CLI
177
+ if: >-
178
+ steps.gate.outputs.proceed == 'true' &&
179
+ steps.mode.outputs.mode == 'trunk-based' &&
180
+ github.event_name == 'push' &&
181
+ steps.copilot_options.outputs.copilot_ai == 'true'
182
+ continue-on-error: true
183
+ run: npm install -g @github/copilot@1.0.88
184
+
163
185
  - name: Trunk-based version bump + changelog
164
186
  if: >-
165
187
  steps.gate.outputs.proceed == 'true' &&
@@ -170,6 +192,8 @@ jobs:
170
192
  AI_API_BASE_URL: ${{ vars.AI_API_BASE_URL }}
171
193
  AI_MODEL: ${{ vars.AI_MODEL }}
172
194
  GITHUB_TOKEN: ${{ github.token }}
195
+ COPILOT_AI: ${{ steps.copilot_options.outputs.copilot_ai }}
196
+ COPILOT_MODEL: ${{ vars.COPILOT_MODEL }}
173
197
  run: |
174
198
  # Idempotency: on a re-run of a failed job the checked-out HEAD may
175
199
  # already be this workflow's own docs commit — never bump twice.
@@ -26,8 +26,8 @@
26
26
  # ENV_FILE 또는 ENV: .env 파일 내용 (둘 중 하나, ENV_FILE 우선)
27
27
  # GOOGLE_SERVICES_JSON: Firebase google-services.json
28
28
  #
29
- # 🪄 빠른 설정: .github/util/flutter/firebase-wizard/firebase-wizard.html을
30
- # 브라우저에서 열어 5단계 마법사로 Firebase 배포를 자동 설정할 수 있습니다.
29
+ # ※ 위 Secrets는 저장소 Settings → Secrets and variables → Actions에 등록하세요.
30
+ # FIREBASE_APP_ID·FIREBASE_TESTER_GROUP은 아래 env 섹션 값을 직접 수정합니다.
31
31
  #
32
32
  # ===================================================================
33
33
  #
@@ -53,6 +53,7 @@ name: PROJECT-FLUTTER-ANDROID-FIREBASE-APPDIST-CICD
53
53
  on:
54
54
  push:
55
55
  branches: ["{{MAIN_BRANCH}}"]
56
+ # @wizard paths-anchor (모노레포일 때 integrator가 paths 필터를 여기 주입)
56
57
  workflow_dispatch:
57
58
 
58
59
  env:
@@ -65,6 +66,10 @@ env:
65
66
  FLUTTER_VERSION: "3.35.5"
66
67
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
67
68
  PROJECT_TYPE: "flutter"
69
+ # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등 — 설치 시 자동 설정
70
+ FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
71
+ # 환경변수 전달 방식 (dart-define | dotenv) — 설치 시 선택값으로 자동 설정
72
+ ENV_MODE: "dart-define" # @wizard auto:flutter-env-mode
68
73
  # Firebase App Distribution 설정
69
74
  FIREBASE_APP_ID: "your-firebase-app-id"
70
75
  FIREBASE_TESTER_GROUP: "testers"
@@ -73,6 +78,9 @@ jobs:
73
78
  prepare-build:
74
79
  name: 환경 설정 및 준비
75
80
  runs-on: ubuntu-latest
81
+ defaults:
82
+ run:
83
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
76
84
  # CHANGELOG 워크플로우가 성공한 경우에만 실행
77
85
  if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }}
78
86
 
@@ -91,12 +99,19 @@ jobs:
91
99
  - name: Pull latest changes
92
100
  run: git pull origin main
93
101
 
94
- - name: Create .env file
102
+ - name: Prepare env file
95
103
  env:
96
104
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
97
105
  run: |
98
- printf '%s\n' "$ENV_CONTENT" > .env
99
- echo ".env file created"
106
+ if [ "$ENV_MODE" = "dotenv" ]; then
107
+ printf '%s\n' "$ENV_CONTENT" > .env
108
+ echo ".env file created"
109
+ elif [ -n "$ENV_CONTENT" ]; then
110
+ DART_DEFINE_FILE="$RUNNER_TEMP/dart-define.env"
111
+ printf '%s\n' "$ENV_CONTENT" > "$DART_DEFINE_FILE"
112
+ echo "DART_DEFINE_FILE=$DART_DEFINE_FILE" >> "$GITHUB_ENV"
113
+ echo "dart-define file prepared"
114
+ fi
100
115
 
101
116
  - name: Set up Flutter
102
117
  uses: subosito/flutter-action@v2
@@ -108,7 +123,7 @@ jobs:
108
123
  uses: actions/cache@v6
109
124
  with:
110
125
  path: ~/.pub-cache
111
- key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
126
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles(format('{0}/**/pubspec.lock', env.FLUTTER_PROJECT_DIR)) }}
112
127
  restore-keys: ${{ runner.os }}-flutter-pub-
113
128
 
114
129
  - name: Install dependencies
@@ -121,6 +136,7 @@ jobs:
121
136
  # version_manager.py를 사용하여 현재 버전 정보 가져오기
122
137
  - name: 현재 버전 정보 가져오기
123
138
  id: current_version
139
+ working-directory: ${{ github.workspace }} # version_manager.py가 레포 루트 version.yml을 읽어야 함
124
140
  run: |
125
141
  # VERSION 가져오기 (x.y.z 형식)
126
142
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
@@ -148,6 +164,7 @@ jobs:
148
164
  # CHANGELOG.json에서 릴리즈 노트 추출 - changelog_manager.py 사용
149
165
  - name: 릴리즈 노트 생성
150
166
  id: release_notes
167
+ working-directory: ${{ github.workspace }} # CHANGELOG.json·changelog_manager.py·final_release_notes.txt(upload 짝)는 레포 루트 기준
151
168
  run: |
152
169
  VERSION="${{ steps.current_version.outputs.version }}"
153
170
 
@@ -192,15 +209,18 @@ jobs:
192
209
  with:
193
210
  name: project-files
194
211
  path: |
195
- pubspec.yaml
196
- lib/
197
- assets/
212
+ ${{ env.FLUTTER_PROJECT_DIR }}/pubspec.yaml
213
+ ${{ env.FLUTTER_PROJECT_DIR }}/lib/
214
+ ${{ env.FLUTTER_PROJECT_DIR }}/assets/
198
215
  retention-days: 1
199
216
 
200
217
  build-android:
201
218
  name: Android AAB 빌드
202
219
  runs-on: ubuntu-latest
203
220
  needs: prepare-build
221
+ defaults:
222
+ run:
223
+ working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
204
224
 
205
225
  steps:
206
226
  - name: Checkout repository
@@ -217,15 +237,22 @@ jobs:
217
237
  uses: actions/download-artifact@v8
218
238
  with:
219
239
  name: project-files
220
- path: .
240
+ path: ${{ env.FLUTTER_PROJECT_DIR }}
221
241
 
222
- # .env 파일 생성 (보안을 위해 아티팩트가 아닌 시크릿에서 생성)
223
- - name: Create .env file
242
+ # 환경변수 준비 (보안을 위해 아티팩트가 아닌 시크릿에서 생성) — ENV_MODE=dotenv면 .env 파일, dart-define이면 임시 파일
243
+ - name: Prepare env file
224
244
  env:
225
245
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
226
246
  run: |
227
- printf '%s\n' "$ENV_CONTENT" > .env
228
- echo "✅ .env 파일 생성됨 (크기: $(wc -c < .env) bytes)"
247
+ if [ "$ENV_MODE" = "dotenv" ]; then
248
+ printf '%s\n' "$ENV_CONTENT" > .env
249
+ echo ".env file created"
250
+ elif [ -n "$ENV_CONTENT" ]; then
251
+ DART_DEFINE_FILE="$RUNNER_TEMP/dart-define.env"
252
+ printf '%s\n' "$ENV_CONTENT" > "$DART_DEFINE_FILE"
253
+ echo "DART_DEFINE_FILE=$DART_DEFINE_FILE" >> "$GITHUB_ENV"
254
+ echo "dart-define file prepared"
255
+ fi
229
256
 
230
257
  # Release Keystore 설정
231
258
  - name: Setup Release Keystore
@@ -265,7 +292,7 @@ jobs:
265
292
  uses: actions/cache@v6
266
293
  with:
267
294
  path: ~/.pub-cache
268
- key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
295
+ key: ${{ runner.os }}-flutter-pub-${{ hashFiles(format('{0}/**/pubspec.lock', env.FLUTTER_PROJECT_DIR)) }}
269
296
  restore-keys: ${{ runner.os }}-flutter-pub-
270
297
 
271
298
  - name: Install dependencies
@@ -287,7 +314,7 @@ jobs:
287
314
  path: |
288
315
  ~/.gradle/caches
289
316
  ~/.gradle/wrapper
290
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle*', '**/gradle-wrapper.properties') }}
317
+ key: ${{ runner.os }}-gradle-${{ hashFiles(format('{0}/android/**/build.gradle*', env.FLUTTER_PROJECT_DIR), format('{0}/android/**/gradle-wrapper.properties', env.FLUTTER_PROJECT_DIR)) }}
291
318
  restore-keys: |
292
319
  ${{ runner.os }}-gradle-
293
320
 
@@ -374,7 +401,8 @@ jobs:
374
401
  --release \
375
402
  --build-name="$VERSION_NAME" \
376
403
  --build-number="$VERSION_CODE" \
377
- --verbose
404
+ --verbose \
405
+ ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
378
406
 
379
407
  BUILD_EXIT_CODE=$?
380
408
 
@@ -524,7 +552,7 @@ jobs:
524
552
  uses: actions/upload-artifact@v7
525
553
  with:
526
554
  name: android-aab
527
- path: build/app/outputs/bundle/release/app-release.aab
555
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/build/app/outputs/bundle/release/app-release.aab
528
556
  retention-days: 1
529
557
 
530
558
  deploy-firebase:
@@ -544,7 +572,7 @@ jobs:
544
572
  uses: actions/download-artifact@v8
545
573
  with:
546
574
  name: android-aab
547
- path: build/app/outputs/bundle/release/
575
+ path: ${{ env.FLUTTER_PROJECT_DIR }}/build/app/outputs/bundle/release/
548
576
 
549
577
  # 릴리즈 노트 다운로드
550
578
  - name: Download release notes
@@ -575,7 +603,7 @@ jobs:
575
603
  appId: ${{ env.FIREBASE_APP_ID }}
576
604
  serviceCredentialsFile: firebase-service-account.json
577
605
  groups: ${{ env.FIREBASE_TESTER_GROUP }}
578
- file: build/app/outputs/bundle/release/app-release.aab
606
+ file: ${{ env.FLUTTER_PROJECT_DIR }}/build/app/outputs/bundle/release/app-release.aab
579
607
  releaseNotesFile: final_release_notes.txt
580
608
 
581
609
  # Firebase 인증 파일 삭제 (보안)