저장소 자동화 설정
완전한 Conventional Commits + Release-Please + CI 자동화 스택을 모든 저장소에 구성합니다. 빈 저장소 및 기존 콘텐츠가 있는 저장소에서 작동합니다.
개요
Shift Left: 가능한 한 빨리 문제를 찾아내세요. PR 시점에 잘못된 커밋 메시지를 발견하는 데는 몇 초가 걸리지만, 같은 메시지가 릴리스 노트에 남으면 신뢰도를 떨어뜨립니다. 검증을 상류로 옮기세요 — PR 제목에서 커밋 형식을 강제하고, CI에서 설정을 검증하며, main 브랜치에 도달하기 전에 문제를 잡아내세요.
빠를수록 안전: 더 작은 배치와 더 빈번한 릴리스는 위험을 줄입니다. 3개의 변경 사항이 있는 릴리스는 30개가 있는 릴리스보다 디버깅하기 쉽습니다. 자동화된 릴리스는 릴리스 프로세스 자체에 대한 신뢰를 구축합니다.
이 스킬은 검증 메커니즘을 설정합니다: PR 제목 검증은 main에 대한 모든 병합이 의미 있고 파싱 가능한 커밋을 생성하도록 보장합니다. Release-Please는 이러한 커밋을 변경 로그가 포함된 자동 릴리스로 전환합니다. CI는 병합 전에 설정 및 코드 문제를 발견합니다.
사용 시기
- 새 저장소를 시작하고 첫날부터 릴리스 자동화가 필요한 경우
- 기존 저장소에 자동 버전 관리 또는 변경 로그 생성이 없는 경우
- 수동 릴리스 프로세스 또는 다른 릴리스 도구에서 마이그레이션하는 경우
- 저장소에 PR 제목 검증 및 자동 레이블링이 필요한 경우
사용하지 말아야 할 경우
- 저장소에 이미 Release-Please 또는 동등한 도구가 구성된 경우 (먼저 확인)
- 호환되지 않는 릴리스 도구가 있는 모노레포 (예: Lerna 독립 버전 관리가 이미 작동 중)
- 업스트림이 릴리스 프로세스를 제어하는 포크 저장소
프로세스
1단계: 구성 수집
파일을 생성하기 전에 사용자에게 다음 값을 요청하세요:
| 파라미터 | 기본값 | 설명 |
|---|---|---|
PROJECT_NAME | (필수) | 문서용 사람이 읽을 수 있는 프로젝트 이름 |
INITIAL_VERSION | 0.1.0 | 시작 시맨틱 버전 |
RELEASE_TYPE | simple | simple (단일 패키지) 또는 node / python / 등 |
REPO_LAYOUT | single | single (루트에 하나의 패키지) 또는 monorepo (여러 패키지) |
PACKAGE_PATHS | . | 모노레포의 경우: packages/api,packages/ui와 같은 쉼표로 구분된 경로 |
DEPENDABOT_ECOSYSTEMS | github-actions | 쉼표로 구분: github-actions, npm, pip, cargo 등 |
DEFAULT_BRANCH | main | 릴리스를 트리거하는 브랜치 |
적절한 RELEASE_TYPE을 자동으로 감지:
package.json이 존재함 →node제안setup.py또는pyproject.toml이 존재함 →python제안Cargo.toml이 존재함 →rust제안- 위 중 해당 사항 없음 →
simple제안
2단계: 기존 파일 감지
무엇이든 생성하기 전에 충돌을 확인하세요:
for f in .github/workflows/pull-request.yml .github/workflows/main-branch.yml \
.github/release-please-config.json .release-please-manifest.json \
.github/dependabot.yml .github/pull_request_template.md docs/DEVELOPMENT.md; do
[[ -f "$f" ]] && echo "EXISTS: $f"
done
파일이 존재하면 변경될 내용의 diff를 표시하고 덮어쓰기 전에 물어보세요. 자동으로 덮어쓰지 마세요 — 기존 파일에는 프로젝트별 사용자 지정이 포함될 수 있습니다.
3단계: 구성 파일 생성
다음 순서로 파일을 생성하세요 (의존성은 위에서 아래로 흐름):
3a. .github/release-please-config.json
단일 패키지:
{
"release-type": "{{RELEASE_TYPE}}",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"initial-version": "{{INITIAL_VERSION}}",
"packages": {
".": {}
}
}
모노레포: packages를 경로당 하나의 항목으로 바꾸세요:
{
"packages": {
"packages/api": { "release-type": "node" },
"packages/ui": { "release-type": "node" }
}
}
각 모노레포 패키지는 자체 release-type을 가집니다 — 패키지가 다를 경우 사용자에게 물어보세요.
3b. .release-please-manifest.json
단일 패키지:
{
".": "{{INITIAL_VERSION}}"
}
모노레포: 패키지 경로당 하나의 항목, 각각 자체 버전.
3c. .github/workflows/pull-request.yml
name: Pull Request Validation
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
pull-requests: write
contents: read
statuses: write
jobs:
validate-pr-title:
name: Validate PR Title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
feat
fix
docs
chore
refactor
test
ci
requireScope: false
subjectPattern: ^[a-z].+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
starts with a lowercase character (per conventional commits).
auto-label:
name: Auto Label PR
runs-on: ubuntu-latest
steps:
- name: Label based on PR title
uses: actions/github-script@v8
with:
script: |
const title = context.payload.pull_request.title;
const labelMap = {
'feat': 'enhancement',
'fix': 'bug',
'docs': 'documentation',
'chore': 'chore',
'refactor': 'refactor',
'test': 'test',
'ci': 'ci'
};
for (const [type, label] of Object.entries(labelMap)) {
if (title.startsWith(`${type}:`) || title.startsWith(`${type}(`)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: [label]
});
break;
}
}
no-ai-attribution:
name: No AI Attribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Scan commit messages and PR body
env:
PR_BODY: ${{ github.event.pull_request.body }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Trailer/footer forms only: a commit that merely mentions Claude or
# Anthropic in prose is fine. Human Co-authored-by trailers are fine.
PAT='^[[:space:]]*Co-authored-by:.*(Claude|Anthropic|noreply@anthropic)|Generated with \[?(Claude|Anthropic)'
fail=0
if git log --format='%B' "$BASE_SHA..$HEAD_SHA" | grep -Eiq "$PAT"; then
echo "::error::AI attribution found in a commit message on this branch"
fail=1
fi
if printf '%s' "$PR_BODY" | grep -Eiq "$PAT"; then
echo "::error::AI attribution found in the PR description"
fail=1
fi
[ "$fail" -eq 0 ] && echo "No AI attribution found."
exit $fail
ci-checks:
name: CI Checks
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6
- name: Validate YAML syntax
run: |
sudo apt-get update && sudo apt-get install -y yamllint
find . -name "*.yml" -o -name "*.yaml" | while read file; do
echo "Checking $file"
yamllint -d "{extends: default, rules: {line-length: disable}}" "$file" || true
done
No AI Attribution은 첫날부터 차단합니다 — 검증할 수 있는 오탐지 표면이 없습니다 (트레일러 및 푸터 형식만 일치시키고 산문은 일치시키지 않음). 따라서 아래의 단계적 적용 경로를 거치지 않습니다.
CI 검사 단계적 적용 경로:
continue-on-error: true로 배포 (정보 제공, 비차단)- 3-5개의 실제 PR에서 검사 통과 확인
continue-on-error제거 및 브랜치 보호 필수 검사에 추가- 코드베이스가 성장함에 따라 프로젝트별 검사 (린트, 테스트, 빌드) 추가
어떤 관문도 건너뛸 수 없습니다. 린트가 실패하면 린트를 수정하세요 — 규칙을 비활성화하지 마세요. 테스트가 실패하면 코드를 수정하세요 — 테스트를 건너뛰지 마세요. CI가 실패하면 실패 출력을 에이전트에 전달하고 다시 푸시하기 전에 수정하세요.
3d. .github/workflows/main-branch.yml
name: Release Automation
on:
push:
branches:
- {{DEFAULT_BRANCH}}
permissions:
contents: write
pull-requests: write
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
config-file: .github/release-please-config.json
manifest-file: .release-please-manifest.json
update-major-tag:
name: Update Major Version Tag
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- name: Update major version tag
env:
TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
run: |
MAJOR_TAG=$(echo "$TAG_NAME" | sed 's/^\(v[0-9]*\).*/\1/')
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa "$MAJOR_TAG" -m "Update $MAJOR_TAG tag to $TAG_NAME"
git push origin "$MAJOR_TAG" --force
3e. .github/dependabot.yml
에코시스템당 하나의 updates 항목을 생성합니다:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
commit-message 구성은 Dependabot PR이 자동으로 Conventional Commits를 따르도록 보장합니다 (예: chore(deps): bump actions/checkout from v5 to v6).
3f. .github/pull_request_template.md
## 변경 사항
[변경 사항을 자세히 설명 - 이것이 변경 로그에 포함됩니다]
## 이유
[변경에 대한 맥락과 동기]
## 체크리스트
- [ ] PR 제목이 Conventional Commits 형식을 따릅니다 (`type: description`)
- [ ] 설명이 무엇과 왜를 설명합니다 (단순히 무엇만 설명하지 않음)
- [ ] 코드, 로그 또는 버전 관리에 비밀이 없습니다
- [ ] 테스트가 로컬에서 통과합니다
"이유" 섹션은 중요합니다 — 변경된 내용만 설명하는 PR 설명은 diff와 중복됩니다. 이유는 향후 git-blame에 유용한 맥락을 제공합니다.
3g. .gitignore (없는 경우)
.gitignore 파일이 있는지 확인합니다. 없으면 프로젝트에 적합한 파일을 만드세요:
# 의존성
node_modules/
# 빌드 출력
сdist/
build/
.next/
# 환경
.env
.env.local
*.pem
# IDE
.idea/
# OS
.DS_Store
Thumbs.db
감지된 스택에 맞게 조정하세요. node_modules/, .env 또는 빌드 아티팩트를 절대 커밋하지 마세요.
3h. docs/DEVELOPMENT.md
다음을 포함하는 개발 가이드를 생성합니다:
- 브랜치 워크플로 — 트렁크 기반 개발: 짧은 수명의 기능 브랜치 (1-3일), 신속하게 병합. 브랜치 이름:
feature/<desc>,fix/<desc>,chore/<desc>. - 커밋 규율 — 브랜치 내에서 자유롭게 커밋 (모든 형식). PR 제목만 중요합니다 — 스쿼시 병합 커밋 메시지가 되어 main에 나타나고 변경 로그에 표시됩니다.
- PR 제목 형식 — 유형 표:
feat,fix,docs,chore,refactor,test,ci. 획기적인 변경:feat!:또는 본문의BREAKING CHANGE:. - 변경 크기 — PR당 ~100줄 목표. 단일 논리적 변경의 경우 ~300줄 허용. ~1000줄 이상은 분할해야 합니다. 리팩터링과 기능 작업을 분리하세요 — 서로 다른 변경입니다.
- PR 설명 — 첫 번째 줄: 짧고, 명령형이며, 독립적. 본문: 무엇을 변경하고 왜인지. 안티패턴: "Fix bug", "Update file", "Phase 1".
- 자동화된 검사 — PR 제목 검증 (필수, 병합 차단), 자동 레이블링 (정보 제공), CI 검사 (초기에는 비차단, 이후 필수로 전환).
- 릴리스 프로세스 — release-please가 릴리스 PR 생성 → 병합 → GitHub 릴리스 + 태그 + 변경 로그. 버전 진행: 1.0 미만 (
feat→ 마이너,fix→ 패치) vs 1.0 이상 (feat→ 마이너,fix→ 패치,BREAKING CHANGE→ 메이저). - 문제 해결 — 일반적인 문제: PR 제목 검증 실패 (제목 편집), CI 검사 실패, 병합 차단 (필수 상태 확인).
제목에 {{PROJECT_NAME}}을 사용합니다.
4단계: 브랜치 보호 권장 사항
파일 생성 후, 다음을 수동 단계로 제시합니다 (자동 적용되지 않음):
{{DEFAULT_BRANCH}}에 대한 권장 GitHub 브랜치 보호 설정:
✓ 병합 전에 풀 리퀘스트 필요
✓ 상태 검사 필요: "Validate PR Title"
✓ 브랜치가 최신 상태여야 함 필요
✓ 스쿼시 병합만 허용
✓ 선형 기록 필요
스쿼시 병합을 사용하는 이유: 브랜치 기록 (WIP 커밋, 수정, 실험)은 PR에 보존됩니다. Main은 깨끗하고 의미 있으며 선형적인 로그를 얻고, 모든 커밋은 Conventional Commit입니다. 이것이 release-please와 git log --oneline을 유용하게 만드는 이유입니다.
5단계: 워크플로 권한 활성화
Release-Please는 GitHub Actions를 통해 풀 리퀘스트를 생성할 수 있는 권한이 필요합니다. 이는 새 저장소에서 기본적으로 비활성화되어 있으며 Release Automation 워크플로가 다음과 함께 실패하게 만듭니다:
GitHub Actions is not permitted to create or approve pull requests.
사용자에게 이 설정을 활성화하도록 안내하세요:
GitHub 레포지토리 → 설정 → 작업 → 일반 → 워크플로 권한:
✓ 읽기 및 쓰기 권한 (release-please가 변경 로그 커밋을 푸시하는 데 필요)
✓ GitHub Actions가 풀 리퀘스트를 생성 및 승인하도록 허용 (릴리스 PR에 필요)
이 단계는 {{DEFAULT_BRANCH}}에 대한 첫 푸시 전에 완료되어야 합니다. 그렇지 않으면 활성화될 때까지 모든 푸시에서 release-please가 실패합니다.
6단계: 확인
# YAML 구문 검증
yamllint .github/workflows/*.yml .github/dependabot.yml
# JSON 검증
python3 -c "import json; json.load(open('.github/release-please-config.json'))"
python3 -c "import json; json.load(open('.release-please-manifest.json'))"
설계 원칙
이 설정의 기본 설계 원칙:
- Shift Left — PR 시점에 문제를 조기에 발견. 커밋 메시지가 main에 도달하면 변경 로그에 영구적으로 남습니다.
- 작은 배치 — 더 작은 PR = 더 빠른 검토 = 더 적은 병합 충돌 = 더 안전한 릴리스.
- 점진적 전환 — CI 검사는
continue-on-error: true로 시작하여 실제 워크플로를 방해하지 않고 검증합니다. - 관문은 관문 — 검사를 건너뛰지 마세요. 수정하지 않고 우회하지 마세요. CI 실패는 학습 기회입니다.
- 사람이 먼저 — 모든 PR 검사가 통과했더라도 사람의 검토가 필요합니다. 자동화는 게이트를 보조하지만 대체하지 않습니다.