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.
- package/README.md +104 -18
- package/package.json +2 -2
- package/payload/config/breaking-changes.json +24 -1
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +86 -0
- package/payload/flutter-app/ios/ExportOptions.plist +31 -0
- package/payload/flutter-app/ios/fastlane/Fastfile +117 -0
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
- package/payload/scripts/changelog_manager.py +83 -30
- package/payload/version.yml.template +5 -0
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +30 -7
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +36 -8
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +25 -1
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +48 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +44 -19
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +28 -36
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +32 -45
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +56 -41
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +47 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +47 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
- package/src/cli/args.js +44 -0
- package/src/cli/help.js +6 -1
- package/src/commands/doctor.js +59 -5
- package/src/commands/dry-run.js +10 -0
- package/src/commands/full.js +28 -3
- package/src/commands/interactive-flutter.js +95 -0
- package/src/commands/interactive.js +60 -5
- package/src/commands/status.js +21 -2
- package/src/context.js +7 -0
- package/src/core/breaking.js +7 -3
- package/src/core/copy/flutter-app.js +42 -0
- package/src/core/copy/workflows.js +21 -9
- package/src/core/detect-fs.js +9 -1
- package/src/core/flutter-options.js +118 -0
- package/src/core/installed-stores.js +12 -0
- package/src/core/verify.js +1 -1
- package/src/core/version-yml.js +50 -6
- package/src/core/wizard-env.js +26 -4
- package/src/index.js +34 -1
- package/src/ui/env-plan.js +8 -5
- package/src/ui/prompts.js +88 -3
- package/src/ui/status-cards.js +13 -1
- 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 →
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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
|
|
839
|
+
if summary_text is None and _copilot_enabled():
|
|
784
840
|
try:
|
|
785
|
-
|
|
786
|
-
|
|
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 = "
|
|
844
|
+
engine = "copilot"
|
|
792
845
|
else:
|
|
793
|
-
print("[warn]
|
|
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]
|
|
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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
|
|
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 "
|
|
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 ->
|
|
16
|
-
# fallback; always succeeds
|
|
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
|
|
38
|
-
# summary engine. Falls back to
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
#
|
|
30
|
-
#
|
|
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:
|
|
102
|
+
- name: Prepare env file
|
|
95
103
|
env:
|
|
96
104
|
ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
|
|
97
105
|
run: |
|
|
98
|
-
|
|
99
|
-
|
|
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('
|
|
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
|
-
#
|
|
223
|
-
- name:
|
|
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
|
-
|
|
228
|
-
|
|
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('
|
|
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('
|
|
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 인증 파일 삭제 (보안)
|