Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .claude/hooks/guard-preview-branch.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
//
// 비개발자가 Claude로 작업할 때 반드시 .claude/worktrees 아래의 preview/ 브랜치 워크트리에서만
// 비개발자가 Claude로 작업할 때 반드시 .claude/worktrees 아래의 work/ 브랜치 워크트리에서만
// 파일을 고치고 커밋하게 강제하는 PreToolUse 훅이다.
//
// 이 훅은 .claude/settings.json에 등록되어 있어 기본으로 켜져 있다. 개발자는 개인 파일인
Expand All @@ -12,10 +12,10 @@
//
// 막는 범위는 다음과 같다.
// - 이 저장소 안의 파일을 Edit/Write할 때, 그 파일이 .claude/worktrees/<이름>/ 아래에 있고
// 그 워크트리의 브랜치가 preview/로 시작하지 않으면 막는다. 메인 워킹 디렉토리는 항상 막는다.
// 그 워크트리의 브랜치가 work/로 시작하지 않으면 막는다. 메인 워킹 디렉토리는 항상 막는다.
// - 저장소의 .claude/hooks, .claude/scripts, .claude/settings.json은 워크트리 안이라도 막는다.
// - Bash의 git commit, merge, rebase, reset, cherry-pick은 preview/ 워크트리 안에서만 허용한다.
// - git push는 preview/ 브랜치로만 허용하고, force push와 develop, main으로의 push, gh pr merge는 막는다.
// - Bash의 git commit, merge, rebase, reset, cherry-pick은 work/ 워크트리 안에서만 허용한다.
// - git push는 work/ 브랜치로만 허용하고, force push와 preview, main으로의 push, gh pr merge는 막는다.
//
// 저장소 밖의 파일(스크래치패드, 메모리 등)은 검사하지 않는다.

Expand All @@ -26,9 +26,11 @@ import { fileURLToPath } from 'node:url';

if (process.env.FI_ADMIN_DEVELOPER === '1') process.exit(0);

// 작업 브랜치 이름 앞부분. 저장소에 preview 브랜치가 있어 git이 preview/<이름> 브랜치를 만들 수 없으므로 work/를 쓴다.
const BRANCH_PREFIX = 'work/';

const GUIDE =
'작업은 preview/ 브랜치 워크트리에서만 할 수 있습니다. 먼저 `bash .claude/scripts/new-preview-worktree.sh <영문-작업-이름>`을 실행해 ' +
'작업은 work/ 브랜치 워크트리에서만 할 수 있습니다. 먼저 `bash .claude/scripts/new-preview-worktree.sh <영문-작업-이름>`을 실행해 ' +
'.claude/worktrees/<작업-이름>/ 워크트리를 만들고, 그 안의 파일만 수정하십시오.';

const block = (reason) => {
Expand Down Expand Up @@ -67,13 +69,13 @@ const existingDir = (path) => {
return realpathSync(dir);
};

// 주어진 디렉토리가 이 저장소의 preview/ 워크트리 안이면 그 워크트리 경로를, 아니면 이유를 돌려준다.
// 주어진 디렉토리가 이 저장소의 work/ 워크트리 안이면 그 워크트리 경로를, 아니면 이유를 돌려준다.
const checkLocation = (dir) => {
if (!isInside(dir, WORKTREES)) return { error: '메인 워킹 디렉토리에서는 작업할 수 없습니다.' };
const top = git(dir, 'rev-parse', '--show-toplevel');
if (!top || realpathSync(top) === ROOT) return { error: '워크트리 밖입니다.' };
const branch = git(dir, 'rev-parse', '--abbrev-ref', 'HEAD');
if (!branch.startsWith('preview/')) return { error: `현재 브랜치(${branch || '알 수 없음'})가 preview/로 시작하지 않습니다.` };
if (!branch.startsWith(BRANCH_PREFIX)) return { error: `현재 브랜치(${branch || '알 수 없음'})가 ${BRANCH_PREFIX}로 시작하지 않습니다.` };
return { top: realpathSync(top), branch };
};

Expand Down Expand Up @@ -116,7 +118,8 @@ if (tool === 'Bash') {

if (/\bgit\s+(-C\s+\S+\s+)?push\b/.test(command)) {
if (/\s(--force\S*|-f|--mirror|--delete|-d)\b|\s\+\S/.test(command)) block('강제 push와 브랜치 삭제는 할 수 없습니다.');
if (/\b(develop|main|master)\b/.test(command)) block('develop, main으로는 push할 수 없습니다.');
// preview 브랜치 자체로 가는 push만 막도록 preview 뒤에 공백이나 명령 끝이 오는 경우로 좁힌다.
if (/\b(main|master)\b|(^|[\s:])preview(?=\s|$)/.test(command)) block('preview, main으로는 push할 수 없습니다.');
const location = checkLocation(runDir);
if (location.error) block(location.error);
}
Expand Down
9 changes: 5 additions & 4 deletions .claude/scripts/new-preview-worktree.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# 최신 origin/develop에서 preview/<작업-이름> 브랜치를 만들고 .claude/worktrees/<작업-이름>/에
# 최신 origin/preview에서 work/<작업-이름> 브랜치를 만들고 .claude/worktrees/<작업-이름>/에
# 워크트리로 체크아웃한다. 비개발자가 Claude로 작업을 시작할 때 Claude가 이 스크립트를 실행한다.
#
# bash .claude/scripts/new-preview-worktree.sh notice-banner-text
Expand All @@ -18,7 +18,8 @@ fi

root=$(cd "$(dirname "$0")/../.." && pwd)
dir="$root/.claude/worktrees/$name"
branch="preview/$name"
# 저장소에 preview 브랜치가 있으면 git이 preview/<이름> 브랜치를 만들 수 없으므로 work/를 쓴다.
branch="work/$name"

if [ -e "$dir" ]; then
echo "이미 있는 워크트리입니다: $dir" >&2
Expand All @@ -29,8 +30,8 @@ if git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
exit 1
fi

git -C "$root" fetch --quiet origin develop
git -C "$root" worktree add --quiet --no-track -b "$branch" "$dir" origin/develop
git -C "$root" fetch --quiet origin preview
git -C "$root" worktree add --quiet --no-track -b "$branch" "$dir" origin/preview

copied=0
for file in "$root"/.env* "$root/next-env.d.ts"; do
Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/create-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ description: 사용자가 PR(풀 리퀘스트) 생성을 요청했을 때 이

# create-pr

`.github/pull_request_template.md` 구조(관련 이슈 / 작업 내용 / 참고 사항 / 체크리스트)를 그대로 따라 PR 본문을 작성하고 `gh pr create --title "<title>" --body "<body>"`로 전달한다.
`.github/pull_request_template.md` 구조(PR 종류 / 관련 이슈 / 작업 내용 / 참고 사항 / 체크리스트)를 그대로 따라 PR 본문을 작성하고 `gh pr create --title "<title>" --body "<body>"`로 전달한다.

## 절차

1. `.github/pull_request_template.md`를 읽어 최신 템플릿 구조를 확인한다.
2. 대상 브랜치의 커밋 히스토리와 diff(`git log`, `git diff <base>...HEAD`)를 확인한다 — 마지막 커밋만이 아니라 브랜치에 포함된 모든 커밋을 근거로 삼는다.
3. 제목은 `브랜치명의 앞 키워드: 간단한 요약` 형식으로 짓는다. 예를 들어 브랜치가 `feat/dashboard`면 `feat: 대시보드 응답 시간 차트 기간별 필터링 추가`처럼 `/` 앞부분을 그대로 키워드로 쓰고, 콜론 뒤에 브랜치 전체 작업을 온전한 문장 대신 간단한 명사구로 요약한다.
- 제목 맨 앞에 PR 종류를 붙인다. 테스트 코드(Jest, `tests/e2e`)만 바꾼 PR은 `[테스트]`, 그 밖의 기능 PR은 `[기능]`이다. 예: `[기능] feat: 대시보드 기간 필터 추가`, `[테스트] test: 대시보드 기간 필터 테스트 추가`.
- diff에 기능 코드와 테스트 코드가 섞여 있으면 PR을 만들지 말고, 루트 CLAUDE.md의 PR 분리 규칙에 따라 어떻게 나눌지 사용자에게 먼저 확인한다.
4. 템플릿 섹션별로 본문을 작성한다:
- **PR 종류**: 제목에 붙인 종류와 같은 항목 하나만 `[x]`로 체크한다.
- **관련 이슈**: 사용자가 이슈 번호를 알려준 경우에만 채운다. 모르면 템플릿 형식만 남기고 비워둔다 (번호를 추측해서 채우지 않는다).
- **작업 내용**: 커밋 히스토리와 diff를 근거로 실제 변경된 내용을 온전한 문장으로 요약한다.
- **참고 사항**: 리뷰어가 알아야 할 맥락(브레이킹 체인지, 후속 작업 필요 여부, 임시 조치 등)이 있으면 적고 없으면 생략한다.
Expand Down
8 changes: 8 additions & 0 deletions .github/auto_assign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
addReviewers: true

addAssignees: author

reviewers:
- wlrnjs

numberOfReviewers: 0
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Pull Request

## PR 종류

- [ ] 기능 개발
- [ ] 테스트 코드
<!-- 둘 중 하나만 체크합니다. 기능과 테스트는 PR을 나눠서 올립니다. -->

## 관련 이슈

- close #이슈번호
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/auto-assign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Auto Assign Reviewers
on:
pull_request:
types: [opened, reopened, ready_for_review]

permissions:
contents: read
pull-requests: write

jobs:
addReviewers:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- uses: kentaro-m/auto-assign-action@v2.0.1
with:
repo-token: ${{ steps.app-token.outputs.token }}
configuration-path: .github/auto_assign.yml
2 changes: 1 addition & 1 deletion .github/workflows/jest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI - Jest

on:
pull_request:
branches: [develop]
branches: [preview]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Playwright Tests

on:
pull_request:
branches: [develop]
branches: [preview]

permissions:
contents: read
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/preview-promotion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Auto Main PR

on:
push:
branches:
- preview

# PR 생성과 수정은 GitHub App 토큰으로 하므로 기본 토큰은 체크아웃용 읽기 권한만 둔다.
permissions:
contents: read

# 연달아 머지되면 PR 생성이 겹칠 수 있으므로, 목록을 매번 새로 계산하는 최신 실행만 남긴다.
concurrency:
group: preview-promotion
cancel-in-progress: true

jobs:
create-or-update-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Create or Update Pull Request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
# if 조건 안에서는 rev-list가 실패해도 멈추지 않으므로 변수에 먼저 담는다.
count=$(git rev-list --count origin/main..origin/preview)
if [ "$count" -eq 0 ]; then
echo "main에 반영할 변경 사항이 없습니다."
exit 0
fi

# preview에 머지된 PR만 모으기 위해 preview의 first-parent 머지 커밋에서 PR 번호를 뽑는다.
pr_numbers=$(git log origin/main..origin/preview --first-parent --merges --format='%s' \
| sed -n 's/^Merge pull request #\([0-9]*\) .*/\1/p')

{
echo "## 작업 요약"
echo "Preview 브랜치의 최신 변경 사항을 Main 브랜치로 반영합니다."
echo
echo "## 포함된 PR"
if [ -z "$pr_numbers" ]; then
echo "- 없음"
fi
for number in $pr_numbers; do
gh pr view "$number" --json number,title,author \
--jq '"- #\(.number) \(.title) (@\(.author.login))"'
done
} > pr-body.md

existing=$(gh pr list --base main --head preview --state open --json number --jq '.[0].number // empty')

if [ -n "$existing" ]; then
gh pr edit "$existing" --body-file pr-body.md
else
gh pr create --base main --head preview \
--title "[배포 대기] main 최신화" \
--body-file pr-body.md \
--label "🌿 main"
fi
Loading
Loading