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
44 changes: 8 additions & 36 deletions .claude/hooks/check-commit-msg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,47 +35,19 @@ if [[ -z "$subject" ]]; then
exit 2
fi

types='feat|fix|test|refactor|perf|docs|build|ci|chore'
errors=()
# 규칙은 .githooks/lib/ 하나에만 둔다. 여기에 복사하면 git 훅과 갈라지고,
# 그때부터 어느 쪽이 맞는지 알 수 없다.
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../../.githooks/lib/commit-subject-rules.sh
source "$here/../../.githooks/lib/commit-subject-rules.sh"

printf '%s' "$subject" | grep -qE "^($types)(\([a-z0-9-]+\))?: .+" \
|| errors+=("형식이 맞지 않는다: '<type>(<scope>): <subject>' / 허용 type: $types")

# 글자 수가 아니라 **표시 폭**으로 센다. 한글은 터미널에서 두 칸을 차지하므로
# 50자로 재면 100칸이 되어 git log 가 줄바꿈된다. 50칸은 git 의 관례다.
chars=$(printf '%s' "$subject" | wc -m)
wide=$(printf '%s' "$subject" | grep -oP '[\x{AC00}-\x{D7A3}\x{3130}-\x{318F}]' | wc -l)
width=$((chars + wide))
((width > 50)) && errors+=("제목이 ${width}칸이다 (한글은 두 칸). 50칸 이내로 줄인다")

printf '%s' "$subject" | grep -qE '\.$' \
&& errors+=("제목 끝에 마침표를 쓰지 않는다")

# 제목은 요약이지 문장이 아니다. 한글 종결어미로 끝나면 서술문이다 —
# 명사형은 '다' 로 끝나지 않으므로 이 한 글자로 갈린다.
printf '%s' "$subject" | grep -qP '다$' \
&& errors+=("제목을 명사형으로 끝낸다. '~했다/한다' 는 요약이 아니라 문장이다")

# 제목은 한글로 쓴다. type·scope 만 영문이다.
printf '%s' "$subject" | grep -qP '[\x{AC00}-\x{D7A3}]' \
|| errors+=("제목을 한글로 쓴다 (type·scope 는 영문)")

printf '%s' "$subject" | grep -qE '\bCY-[0-9]+' \
&& errors+=("Jira 키는 제목이 아니라 'Refs: CY-###' 푸터에 둔다")

if ((${#errors[@]} > 0)); then
if ! violations=$(check_commit_subject "$subject"); then
{
echo "커밋 메시지 규약 위반"
echo " 제목: $subject"
echo
printf ' - %s\n' "${errors[@]}"
echo
echo "예시:"
echo " test(admission): 한산한 쿠폰의 무대기 통과 검증"
echo " feat(admission): 전역 크레딧 기반 통과 상한 산출"
echo
echo "푸터에 'Refs: CY-###' 를 남긴다."
echo "규칙 전문: ai/rules/60-workflow.md"
echo "$violations"
commit_rule_help
} >&2
exit 2
fi
Expand Down
40 changes: 40 additions & 0 deletions .claude/hooks/self-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
set -uo pipefail

HOOKS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$HOOKS/../.." && pwd)"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

Expand Down Expand Up @@ -214,6 +215,45 @@ bash_case check-commit-msg.sh "git commit --amend --no-edit" allow '--amend --no
bash_case check-commit-msg.sh "git status" allow 'commit 아닌 명령'
bash_case check-commit-msg.sh "echo 'nothing to do with version control'" allow '무관한 명령'

# ── git commit-msg 훅 ────────────────────────────────────────────────────────
# 도구 훅만 검증하면 터미널 직접 커밋 경로가 비어 있다.
echo
echo ".githooks/commit-msg"

# git 훅은 0 통과 / 1 차단이다. Claude 훅의 exit 2 규약과 달라서
# verdict 를 그대로 쓰면 차단을 전부 통과로 읽는다.
git_case() { # 메시지 기대 설명
local msg=$1 expect=$2 label=$3
local f; f=$(mktemp)
printf '%s\n' "$msg" > "$f"
"$ROOT/.githooks/commit-msg" "$f" >/dev/null 2>&1
local rc=$?
rm -f "$f"
local actual=allow
((rc != 0)) && actual=block
if [[ "$actual" == "$expect" ]]; then
printf ' ok %s\n' "$label"; pass=$((pass + 1))
else
printf ' FAIL %s (기대 %s, 실제 %s)\n' "$label" "$expect" "$actual"; fail=$((fail + 1))
fi
}

git_case 'feat(app): 진입점 추가
Refs: CY-18' allow '정상'
git_case '진입점 추가
Refs: CY-18' block '형식 위반'
git_case 'feat(app): add entrypoint
Refs: CY-18' block '영문 제목'
git_case 'feat(app): 진입점을 추가했다
Refs: CY-18' block '종결어미'
git_case 'feat(app): 진입점 추가' block 'Refs 푸터 없음'
git_case 'feat(app): 진입점 추가
Refs: CY-18
Plan: 1.2.1' block '계획서 ID (커밋에 남기지 않는다)'
git_case 'feat(app): CY-18 진입점 추가
Refs: CY-18' block '제목에 Jira 키'
git_case 'Merge branch develop' allow '병합 커밋은 대상 아님'

echo
printf '통과 %d · 실패 %d\n' "$pass" "$fail"
((fail == 0))
49 changes: 49 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# git commit-msg 훅 — 터미널에서 직접 커밋해도 규약이 걸린다.
#
# Claude Code 의 PreToolUse 훅만 두면 `git commit` 을 손으로 치는 순간
# 검사가 통째로 우회된다. 실제로 초기 커밋 전부가 그렇게 빠져나갔다.
#
# 설치: .githooks/install.sh (core.hooksPath 를 이 디렉터리로 잡는다)
set -uo pipefail

msg_file="$1"
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=lib/commit-subject-rules.sh
source "$here/lib/commit-subject-rules.sh"

# 주석과 빈 줄을 걷어낸 첫 줄이 제목이다.
subject=$(grep -vE '^[[:space:]]*#' "$msg_file" | grep -vE '^[[:space:]]*$' | head -1)

# 병합·리버트 커밋은 git 이 문구를 정하므로 규약 대상이 아니다.
case "$subject" in
Merge\ *|Revert\ *|fixup!\ *|squash!\ *) exit 0 ;;
esac

[[ -z "$subject" ]] && exit 0 # 빈 메시지는 git 이 알아서 막는다

if ! violations=$(check_commit_subject "$subject"); then
{
echo "커밋 메시지 규약 위반"
echo " 제목: $subject"
echo
echo "$violations"
commit_rule_help
} >&2
exit 1
fi

# 푸터 — Jira 키는 여기 하나로만 추적한다.
if ! grep -qE '^Refs: CY-[0-9]+' "$msg_file"; then
echo "[WF-1] 푸터에 'Refs: CY-###' 를 남긴다." >&2
exit 1
fi

# 계획서 ID 는 커밋에 남기지 않는다. 문서를 개편하면 바뀌는데 커밋 이력은
# 못 고쳐서, 박아두면 죽은 참조만 남는다 (60-workflow.md 3절).
if grep -qE '^Plan:' "$msg_file"; then
echo "[WF-1] 계획서 ID 를 커밋에 남기지 않는다. 추적은 'Refs:' 하나로." >&2
exit 1
fi

exit 0
22 changes: 22 additions & 0 deletions .githooks/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# git 훅 설치. 저장소를 클론한 뒤 한 번 실행한다.
#
# ./.githooks/install.sh
#
# `.git/hooks/` 에 복사하지 않고 core.hooksPath 를 쓴다 — 복사하면 훅을
# 고칠 때마다 각자 다시 깔아야 하고, 누가 안 깔았는지 알 수 없다.
set -euo pipefail

here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
root=$(git -C "$here" rev-parse --show-toplevel)

chmod +x "$here"/commit-msg "$here"/lib/*.sh
git -C "$root" config core.hooksPath .githooks

echo "설치됨 — core.hooksPath = .githooks"
echo
echo "확인:"
echo " git -C '$root' config core.hooksPath"
echo
echo "해제하려면:"
echo " git -C '$root' config --unset core.hooksPath"
62 changes: 62 additions & 0 deletions .githooks/lib/commit-subject-rules.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# 커밋 제목 규약 — 검증 규칙의 유일한 출처. ai/rules/60-workflow.md (WF-1)
#
# 이 파일을 두 곳이 쓴다.
# - .claude/hooks/check-commit-msg.sh Claude Code 가 부르는 PreToolUse 훅
# - .githooks/commit-msg git 이 부르는 훅 (터미널 직접 커밋)
#
# **규칙을 양쪽에 복사하지 않는다.** 사본이 생기면 한쪽만 고쳐지고, 그때부터
# 어느 쪽이 맞는지 알 수 없다. 도구 훅만 두면 터미널 커밋이 우회하고,
# 한쪽만 막으면 막지 않은 것과 같다.
#
# 사용: check_commit_subject "<제목>" → 위반 메시지를 stdout 으로, 있으면 1

check_commit_subject() {
local subject="$1"
local types='feat|fix|test|refactor|perf|docs|build|ci|chore'
local errors=()

printf '%s' "$subject" | grep -qE "^($types)(\([a-z0-9-]+\))?: .+" \
|| errors+=("형식이 맞지 않는다: '<type>(<scope>): <subject>' / 허용 type: $types")

# 글자 수가 아니라 **표시 폭**으로 센다. 한글은 터미널에서 두 칸을 차지하므로
# 50자로 재면 100칸이 되어 git log 가 줄바꿈된다. 50칸은 git 의 관례다.
local chars wide width
chars=$(printf '%s' "$subject" | wc -m)
wide=$(printf '%s' "$subject" | grep -oP '[\x{AC00}-\x{D7A3}\x{3130}-\x{318F}]' | wc -l)
width=$((chars + wide))
((width > 50)) && errors+=("제목이 ${width}칸이다 (한글은 두 칸). 50칸 이내로 줄인다")

printf '%s' "$subject" | grep -qE '\.$' \
&& errors+=("제목 끝에 마침표를 쓰지 않는다")

# 제목은 요약이지 문장이 아니다. 한글 종결어미로 끝나면 서술문이다 —
# 명사형은 '다' 로 끝나지 않으므로 이 한 글자로 갈린다.
printf '%s' "$subject" | grep -qP '다$' \
&& errors+=("제목을 명사형으로 끝낸다. '~했다/한다' 는 요약이 아니라 문장이다")

# 제목은 한글로 쓴다. type·scope 만 영문이다.
printf '%s' "$subject" | grep -qP '[\x{AC00}-\x{D7A3}]' \
|| errors+=("제목을 한글로 쓴다 (type·scope 는 영문)")

printf '%s' "$subject" | grep -qE '\bCY-[0-9]+' \
&& errors+=("Jira 키는 제목이 아니라 'Refs: CY-###' 푸터에 둔다")

((${#errors[@]} == 0)) && return 0

printf ' - %s\n' "${errors[@]}"
return 1
}

# 위반 시 함께 보여줄 안내. 두 훅이 같은 문구를 쓴다.
commit_rule_help() {
cat <<'EOF'

예시:
test(admission): 한산한 쿠폰의 무대기 통과 검증
feat(admission): 전역 크레딧 기반 통과 상한 산출

푸터에 'Refs: CY-###' 를 남긴다.
규칙 전문: ai/rules/60-workflow.md
EOF
}
4 changes: 2 additions & 2 deletions .github/actions/setup-gradle/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ runs:
fi

- if: steps.probe.outputs.buildable == 'true'
uses: actions/setup-java@v4
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
distribution: temurin
java-version: ${{ inputs.java-version }}

# 브랜치가 아니라 역할로 가른다. PR 의 ref 는 refs/pull/N/merge 라
# 브랜치로 가르면 PR 에서는 아무도 캐시를 못 쓴다.
- if: steps.probe.outputs.buildable == 'true'
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4
with:
cache-read-only: ${{ inputs.cache-write != 'true' }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

Expand All @@ -36,7 +36,7 @@ jobs:
run: ./gradlew --no-daemon assemble testClasses

- if: inputs.upload-artifact && steps.setup.outputs.buildable == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-libs
path: build/libs/
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/_load-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
passed: ${{ steps.evaluate.outputs.passed }}
summary: ${{ steps.evaluate.outputs.summary }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
run: docker compose -f test/load/compose.yml up -d --wait

- if: steps.probe.outputs.ready == 'true'
uses: grafana/setup-k6-action@v1
uses: grafana/setup-k6-action@c00053c996b4061198c9c24a00474b748abdf8b7 # v1

- name: k6 실행
id: run
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
run: docker compose -f test/load/compose.yml logs --tail=200 || true

- if: always() && hashFiles('k6-summary.json') != ''
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: load-${{ inputs.scenario }}
path: k6-summary.json
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
HAS_CONFLUENCE: ${{ secrets.CONFLUENCE_SPACE_KEY != '' && secrets.ATLASSIAN_BASE_URL != '' }}
HAS_JIRA: ${{ secrets.ATLASSIAN_BASE_URL != '' && secrets.ATLASSIAN_API_TOKEN != '' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/_security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

# 이미지 빌드 전이라 fs 모드. 차단과 업로드에 SARIF 하나를 같이 쓴다 —
# 나누면 같은 스캔이 두 번 돈다 (DB 내려받기까지).
- name: Trivy
uses: aquasecurity/trivy-action@v0.36.0
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
with:
scan-type: fs
scan-ref: .
Expand All @@ -72,7 +72,7 @@ jobs:
- if: >-
always() && hashFiles('trivy.sarif') != '' &&
github.event.pull_request.head.repo.fork != true
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@d6317709a54fd87078d323eeb0e48ec331c8e621 # v3
with:
sarif_file: trivy.sarif
category: trivy-fs
Expand Down Expand Up @@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
fetch-depth: 0 # 이력 전체를 봐야 과거 커밋의 유출을 잡는다
Expand Down Expand Up @@ -160,7 +160,7 @@ jobs:
pull-requests: write
steps:
# 체크아웃하지 않는다 — 이 액션은 GitHub API 로 의존성 변화를 읽는다
- uses: actions/dependency-review-action@v4
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: high
comment-summary-in-pr: ${{ github.event.pull_request.head.repo.fork && 'never' || 'on-failure' }}
4 changes: 2 additions & 2 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
outputs:
result: ${{ steps.run.outcome }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

Expand All @@ -53,7 +53,7 @@ jobs:

# 실패해도 리포트는 올린다 — 로그만으로는 어느 단언이 깨졌는지 찾기 어렵다
- if: always() && steps.setup.outputs.buildable == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: report-${{ inputs.layer }}
path: |
Expand Down
Loading
Loading