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.
- package/README.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +7 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +397 -97
- package/payload/version.yml.template +4 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +17 -10
- package/src/commands/dry-run.js +54 -1
- package/src/commands/full.js +127 -50
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +124 -56
- package/src/commands/purge.js +59 -17
- package/src/commands/status.js +16 -1
- package/src/commands/uninstall.js +58 -13
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +90 -47
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +131 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +113 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +122 -49
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- 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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
246
|
+
ref: "{{MAIN_BRANCH}}"
|
|
215
247
|
fetch-depth: 0
|
|
216
248
|
|
|
217
249
|
- name: Pull latest changes
|
|
218
|
-
run: git pull origin
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
# google-services.json 생성 (선택) — 비어 있으면 빈 파일을 만들지 않는다
|
|
309
|
+
- name: Create google-services.json
|
|
310
|
+
env:
|
|
311
|
+
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
|
|
260
312
|
run: |
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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:
|
|
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
|
-
|
|
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 길이 제한 (안전망)
|