From 0161eb04b5d22407ec861f24a9b757cd38fa0ef6 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:15:01 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20Issue=C2=B7PR=20=EB=B3=B8=EB=AC=B8?= =?UTF-8?q?=EA=B3=BC=20=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EA=B7=9C=EC=B9=99=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/skills/commit/SKILL.md | 38 +++------ .agent/skills/issue/SKILL.md | 59 ++++--------- .agent/skills/pull-request/SKILL.md | 107 ++++++----------------- .claude/skills/commit/SKILL.md | 38 +++------ .claude/skills/issue/SKILL.md | 59 ++++--------- .claude/skills/pull-request/SKILL.md | 107 ++++++----------------- .codex/skills/commit/SKILL.md | 38 +++------ .codex/skills/issue/SKILL.md | 59 ++++--------- .codex/skills/pull-request/SKILL.md | 107 ++++++----------------- .github/ISSUE_TEMPLATE/01-feat.yml | 32 +++++-- .github/ISSUE_TEMPLATE/02-fix.yml | 32 +++++-- .github/ISSUE_TEMPLATE/03-chore.yml | 32 +++++-- .github/ISSUE_TEMPLATE/04-refactor.yml | 32 +++++-- .github/ISSUE_TEMPLATE/config.yml | 1 + .github/pull_request_template.md | 20 +++-- AGENTS.md | 15 ++-- CLAUDE.md | 15 ++-- docs/collaboration.md | 114 +++++++++++++++++++++++++ 18 files changed, 416 insertions(+), 489 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 docs/collaboration.md diff --git a/.agent/skills/commit/SKILL.md b/.agent/skills/commit/SKILL.md index 973fb6c..5023636 100644 --- a/.agent/skills/commit/SKILL.md +++ b/.agent/skills/commit/SKILL.md @@ -1,33 +1,23 @@ -# 커밋 작성 +--- +name: commit +description: Framework 저장소의 목적별 커밋 작성 시 한국어 제목과 변경 종류에 맞는 타입 적용. +--- -커밋은 한 가지 목적의 변경만 담고, 제목은 한국어로 작성해요. +# Commit 작성 -## 형식 +[공통 작성 규칙](../../../docs/collaboration.md)의 커밋 타입을 따른다. -```text -: <변경 내용> -``` - -`type`은 `feat`, `fix`, `chore`, `refactor` 중 하나이며, 이슈 및 브랜치의 유형과 맞춰요. - -예시: - -```text -feat: 다운로드 버튼과 진행 상태 추가 -fix: 로그인 후 잘못된 경로 이동 수정 -chore: 테스트 의존성 업데이트 -refactor: 인증 토큰 검증 로직 분리 -``` - -## 확인 후 커밋 - -변경 파일과 테스트 결과를 확인한 뒤 커밋해요. +- 형식은 `: <한국어 변경 내용>` +- 타입은 `feat/fix/refactor/hotfix/docs/style/remove/test/chore/comment` +- 한 커밋에 한 가지 목적. 개별 커밋 타입은 Issue·브랜치 타입과 달라도 됨 +- 기능 브랜치에서도 오류 수정은 `fix`, 문서는 `docs`, 테스트는 `test`로 구분 +- 코드 식별자와 전문 용어는 실제 표기 유지 +- 커밋 전 변경 파일·검증 범위·`git diff --check` 확인 +- 관련 없는 파일·비밀 정보·생성물 제외 ```bash git status --short git diff --check git add <변경한-파일> -git commit -m "feat: 다운로드 버튼과 진행 상태 추가" +git commit -m "fix: 얼굴 등록 카메라 세션 충돌 수정" ``` - -관련 없는 파일, 비밀 정보, 생성물은 커밋하지 않아요. diff --git a/.agent/skills/issue/SKILL.md b/.agent/skills/issue/SKILL.md index 630dce5..c26d44c 100644 --- a/.agent/skills/issue/SKILL.md +++ b/.agent/skills/issue/SKILL.md @@ -1,51 +1,22 @@ -# 이슈 생성 +--- +name: issue +description: Framework 저장소의 Issue 생성 및 작업 체크리스트 갱신 시 공통 제목과 개조식 본문 적용. +--- -작업을 시작하기 전에 GitHub 이슈를 만들어요. 이슈는 작업을 추적하는 기준점이에요. +# Issue 작성 -## 작업 유형 +[공통 작성 규칙](../../../docs/collaboration.md)의 Issue 양식을 읽는다. -아래 네 유형 중 하나를 선택해요. - -| 유형 | 사용할 때 | -| --- | --- | -| `feat` | 새 기능을 개발할 때 | -| `fix` | 버그를 수정할 때 | -| `chore` | 기타 자잘한 수정이나 설정 변경을 할 때 | -| `refactor` | 동작은 유지하며 코드를 정리할 때 | - -## 제목 - -제목은 한국어로, 아래 형식으로 작성해요. - -```text -: <작업 내용> -``` - -예시: - -```text -feat: GitHub 릴리스 다운로드 기능 추가 -fix: 로그인 후 화면 전환 오류 수정 -chore: 의존성 버전 정리 -refactor: 인증 서비스 구조 개선 -``` - -## 담당자 - -기본 담당자는 이슈를 만든 본인이에요. 다른 사람이 실제 작업을 맡는다면 그 사람을 담당자로 지정해요. - -## GitHub 웹에서 만들기 - -이슈를 만들 때 작업 유형에 맞는 양식을 선택하고, 제목과 담당자를 지정해요. - -## GitHub CLI에서 만들기 +- 작업 전 Issue 생성, 실제 작업자 지정 +- 제목은 `feat/fix/chore/refactor: <한국어 작업 내용>` +- 본문은 `작업 내용 / 작업 체크리스트 / 완료 기준`, 필요한 경우 마지막에 `참고` 추가 +- 한 항목에 한 가지 내용, 명사형 종결 사용. 진행 항목은 `- [ ]`로 작성하고 수행 후 체크 +- 완료 기준은 확인 가능한 결과로 작성 +- CLI 본문은 UTF-8 Markdown 파일과 `--body-file` 사용. 빈 본문과 문자열 `\n` 사용 금지 ```bash -gh issue create \ - --title "feat: GitHub 릴리스 다운로드 기능 추가" \ - --label "feat" \ - --assignee "@me" \ - --body "" +gh issue create --title "fix: 카메라 세션 충돌 수정" \ + --label "fix" --assignee "@me" --body-file issue.md ``` -생성된 이슈 번호를 이후 브랜치와 PR에 사용해요. +생성한 번호를 브랜치와 PR에 사용한다. 작업 진행에 따라 기존 Issue 체크 상태를 갱신한다. diff --git a/.agent/skills/pull-request/SKILL.md b/.agent/skills/pull-request/SKILL.md index cc3c142..11fd5f6 100644 --- a/.agent/skills/pull-request/SKILL.md +++ b/.agent/skills/pull-request/SKILL.md @@ -1,95 +1,42 @@ -# Pull Request 생성 +--- +name: pull-request +description: Framework 저장소의 Draft PR 생성, 검증 결과와 체크리스트 갱신, merge commit 작성 시 공통 규칙 적용. +--- -작업을 공유할 준비가 되면 PR을 초안(Draft)으로 먼저 만들어요. 처음부터 Open PR로 만들지 않아요. +# PR 작성 -## 제목 +[공통 작성 규칙](../../../docs/collaboration.md)과 `.github/pull_request_template.md`를 읽는다. -브랜치의 작업 유형은 콜론으로 구분하고, 나머지 슬러그와 이슈 번호는 브랜치와 같게 사용해요. +## 생성과 갱신 -```text -: /# -``` - -예시: - -```text -fix: preflight-dim/#67 -``` - -## 생성 - -변경 사항과 테스트를 확인하고 원격 브랜치를 푸시한 뒤 Draft PR을 만들어요. - -PR 본문은 아래 틀을 유지해요. - -```markdown -## 변경 내용 - -- 핵심 변경 사항 -- 주요 동작 또는 영향 범위 - -## 확인 방법 - -- 빌드·테스트 결과 -- 주요 검증 항목 - -Closes # -``` - -본문은 구현 과정을 길게 설명하지 않고 핵심 결론만 간결한 개조식으로 작성해요. - -- 비슷한 변경은 한 항목으로 묶어요. -- 세부 구현보다 사용자가 체감하는 동작과 중요한 기술적 영향만 남겨요. -- 테스트 개수, 성공 여부, 핵심 검증 범위만 남겨요. -- signaling 변경 없음, 물리 기기 미검증처럼 중요한 제약·미수행 항목은 유지해요. -- 이미 본문에서 드러나는 내용을 반복하지 않아요. +- 제목은 `: /#`, head 브랜치와 일치 +- 구현 중 Draft 생성 허용. 검증 전에는 결과에 `검증 대기`, 체크리스트에는 미완료 항목 기재 +- 본문은 `변경 내용 / 검증 결과 / 머지 체크리스트 / 관련 이슈` 순서 고정 +- 명사형 종결의 최상위 개조식 사용. 변경 내용·검증 결과는 각각 1~3개 항목 +- 핵심 동작·영향·실제 결과만 기록. 구현 과정·작업 순서·시도한 방법·중첩 목록·홍보 문구 생략 +- 중요한 미검증 범위와 API 계약·배포 조건 유지. 상세 설명은 Issue 또는 문서에 기록 +- 체크리스트는 해당 변경의 필수 검증과 리뷰 확인으로 구성. 미수행 항목 체크 금지 +- 실제 검증 후 결과·체크 상태 갱신, Ready for review 전환, 리뷰 반영 후 머지 +- 이슈 전체 완료는 `Closes #번호`, 부분 완료·참고는 `Refs #번호` +- 본문은 파일과 `--body-file`로 전달. 문자열 `\n` 사용 금지 ```bash -git status --short git push -u origin HEAD -gh pr create \ - --draft \ - --title "feat: github-release-download/#654" \ - --body "## 변경 내용\n\n- GitHub 릴리스 다운로드 기능 추가\n\n## 확인 방법\n\n- 관련 테스트 통과\n\nCloses #654" -``` - -## Open PR 전환 - -자가 확인이 끝난 뒤에만 Ready for review로 전환해요. - -```bash +gh pr create --draft --title "fix: camera-session/#123" --body-file pr.md +gh pr edit --body-file pr.md gh pr ready ``` ## 병합 -병합 커밋 제목은 GitHub 기본 제목을 사용하지 않고 아래 형식으로 작성해요. - -```text -merge: /# -``` - -브랜치가 다음과 같다면: - -```text -chore/readme-update/#390 -``` - -기본 제목: - -```text -Merge pull request #392 from team-framework/chore/readme-update/#390 -``` - -대신 다음 제목을 사용해요. - -```text -merge: chore/readme-update/#390 -``` - -CLI로 병합할 때도 병합 커밋 제목을 직접 지정해요. +사용자 요청 범위와 저장소 승인 조건을 확인한다. 공통 규칙의 필수 검사·체크리스트·리뷰 확인 후 merge commit으로 병합한다. ```bash -gh pr merge --merge \ - --subject "merge: chore/readme-update/#390" +head_branch=$(gh pr view --json headRefName --jq .headRefName) +head_sha=$(gh pr view --json headRefOid --jq .headRefOid) +gh pr checks --required +gh pr merge --merge --match-head-commit "$head_sha" \ + --subject "merge: $head_branch" --body "" ``` + +병합 후 실제 커밋의 제목·빈 본문·두 부모를 확인한다. 자동 동기화·배포 묶음 PR의 예외는 공통 규칙을 따른다. diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md index 973fb6c..5023636 100644 --- a/.claude/skills/commit/SKILL.md +++ b/.claude/skills/commit/SKILL.md @@ -1,33 +1,23 @@ -# 커밋 작성 +--- +name: commit +description: Framework 저장소의 목적별 커밋 작성 시 한국어 제목과 변경 종류에 맞는 타입 적용. +--- -커밋은 한 가지 목적의 변경만 담고, 제목은 한국어로 작성해요. +# Commit 작성 -## 형식 +[공통 작성 규칙](../../../docs/collaboration.md)의 커밋 타입을 따른다. -```text -: <변경 내용> -``` - -`type`은 `feat`, `fix`, `chore`, `refactor` 중 하나이며, 이슈 및 브랜치의 유형과 맞춰요. - -예시: - -```text -feat: 다운로드 버튼과 진행 상태 추가 -fix: 로그인 후 잘못된 경로 이동 수정 -chore: 테스트 의존성 업데이트 -refactor: 인증 토큰 검증 로직 분리 -``` - -## 확인 후 커밋 - -변경 파일과 테스트 결과를 확인한 뒤 커밋해요. +- 형식은 `: <한국어 변경 내용>` +- 타입은 `feat/fix/refactor/hotfix/docs/style/remove/test/chore/comment` +- 한 커밋에 한 가지 목적. 개별 커밋 타입은 Issue·브랜치 타입과 달라도 됨 +- 기능 브랜치에서도 오류 수정은 `fix`, 문서는 `docs`, 테스트는 `test`로 구분 +- 코드 식별자와 전문 용어는 실제 표기 유지 +- 커밋 전 변경 파일·검증 범위·`git diff --check` 확인 +- 관련 없는 파일·비밀 정보·생성물 제외 ```bash git status --short git diff --check git add <변경한-파일> -git commit -m "feat: 다운로드 버튼과 진행 상태 추가" +git commit -m "fix: 얼굴 등록 카메라 세션 충돌 수정" ``` - -관련 없는 파일, 비밀 정보, 생성물은 커밋하지 않아요. diff --git a/.claude/skills/issue/SKILL.md b/.claude/skills/issue/SKILL.md index 630dce5..c26d44c 100644 --- a/.claude/skills/issue/SKILL.md +++ b/.claude/skills/issue/SKILL.md @@ -1,51 +1,22 @@ -# 이슈 생성 +--- +name: issue +description: Framework 저장소의 Issue 생성 및 작업 체크리스트 갱신 시 공통 제목과 개조식 본문 적용. +--- -작업을 시작하기 전에 GitHub 이슈를 만들어요. 이슈는 작업을 추적하는 기준점이에요. +# Issue 작성 -## 작업 유형 +[공통 작성 규칙](../../../docs/collaboration.md)의 Issue 양식을 읽는다. -아래 네 유형 중 하나를 선택해요. - -| 유형 | 사용할 때 | -| --- | --- | -| `feat` | 새 기능을 개발할 때 | -| `fix` | 버그를 수정할 때 | -| `chore` | 기타 자잘한 수정이나 설정 변경을 할 때 | -| `refactor` | 동작은 유지하며 코드를 정리할 때 | - -## 제목 - -제목은 한국어로, 아래 형식으로 작성해요. - -```text -: <작업 내용> -``` - -예시: - -```text -feat: GitHub 릴리스 다운로드 기능 추가 -fix: 로그인 후 화면 전환 오류 수정 -chore: 의존성 버전 정리 -refactor: 인증 서비스 구조 개선 -``` - -## 담당자 - -기본 담당자는 이슈를 만든 본인이에요. 다른 사람이 실제 작업을 맡는다면 그 사람을 담당자로 지정해요. - -## GitHub 웹에서 만들기 - -이슈를 만들 때 작업 유형에 맞는 양식을 선택하고, 제목과 담당자를 지정해요. - -## GitHub CLI에서 만들기 +- 작업 전 Issue 생성, 실제 작업자 지정 +- 제목은 `feat/fix/chore/refactor: <한국어 작업 내용>` +- 본문은 `작업 내용 / 작업 체크리스트 / 완료 기준`, 필요한 경우 마지막에 `참고` 추가 +- 한 항목에 한 가지 내용, 명사형 종결 사용. 진행 항목은 `- [ ]`로 작성하고 수행 후 체크 +- 완료 기준은 확인 가능한 결과로 작성 +- CLI 본문은 UTF-8 Markdown 파일과 `--body-file` 사용. 빈 본문과 문자열 `\n` 사용 금지 ```bash -gh issue create \ - --title "feat: GitHub 릴리스 다운로드 기능 추가" \ - --label "feat" \ - --assignee "@me" \ - --body "" +gh issue create --title "fix: 카메라 세션 충돌 수정" \ + --label "fix" --assignee "@me" --body-file issue.md ``` -생성된 이슈 번호를 이후 브랜치와 PR에 사용해요. +생성한 번호를 브랜치와 PR에 사용한다. 작업 진행에 따라 기존 Issue 체크 상태를 갱신한다. diff --git a/.claude/skills/pull-request/SKILL.md b/.claude/skills/pull-request/SKILL.md index cc3c142..11fd5f6 100644 --- a/.claude/skills/pull-request/SKILL.md +++ b/.claude/skills/pull-request/SKILL.md @@ -1,95 +1,42 @@ -# Pull Request 생성 +--- +name: pull-request +description: Framework 저장소의 Draft PR 생성, 검증 결과와 체크리스트 갱신, merge commit 작성 시 공통 규칙 적용. +--- -작업을 공유할 준비가 되면 PR을 초안(Draft)으로 먼저 만들어요. 처음부터 Open PR로 만들지 않아요. +# PR 작성 -## 제목 +[공통 작성 규칙](../../../docs/collaboration.md)과 `.github/pull_request_template.md`를 읽는다. -브랜치의 작업 유형은 콜론으로 구분하고, 나머지 슬러그와 이슈 번호는 브랜치와 같게 사용해요. +## 생성과 갱신 -```text -: /# -``` - -예시: - -```text -fix: preflight-dim/#67 -``` - -## 생성 - -변경 사항과 테스트를 확인하고 원격 브랜치를 푸시한 뒤 Draft PR을 만들어요. - -PR 본문은 아래 틀을 유지해요. - -```markdown -## 변경 내용 - -- 핵심 변경 사항 -- 주요 동작 또는 영향 범위 - -## 확인 방법 - -- 빌드·테스트 결과 -- 주요 검증 항목 - -Closes # -``` - -본문은 구현 과정을 길게 설명하지 않고 핵심 결론만 간결한 개조식으로 작성해요. - -- 비슷한 변경은 한 항목으로 묶어요. -- 세부 구현보다 사용자가 체감하는 동작과 중요한 기술적 영향만 남겨요. -- 테스트 개수, 성공 여부, 핵심 검증 범위만 남겨요. -- signaling 변경 없음, 물리 기기 미검증처럼 중요한 제약·미수행 항목은 유지해요. -- 이미 본문에서 드러나는 내용을 반복하지 않아요. +- 제목은 `: /#`, head 브랜치와 일치 +- 구현 중 Draft 생성 허용. 검증 전에는 결과에 `검증 대기`, 체크리스트에는 미완료 항목 기재 +- 본문은 `변경 내용 / 검증 결과 / 머지 체크리스트 / 관련 이슈` 순서 고정 +- 명사형 종결의 최상위 개조식 사용. 변경 내용·검증 결과는 각각 1~3개 항목 +- 핵심 동작·영향·실제 결과만 기록. 구현 과정·작업 순서·시도한 방법·중첩 목록·홍보 문구 생략 +- 중요한 미검증 범위와 API 계약·배포 조건 유지. 상세 설명은 Issue 또는 문서에 기록 +- 체크리스트는 해당 변경의 필수 검증과 리뷰 확인으로 구성. 미수행 항목 체크 금지 +- 실제 검증 후 결과·체크 상태 갱신, Ready for review 전환, 리뷰 반영 후 머지 +- 이슈 전체 완료는 `Closes #번호`, 부분 완료·참고는 `Refs #번호` +- 본문은 파일과 `--body-file`로 전달. 문자열 `\n` 사용 금지 ```bash -git status --short git push -u origin HEAD -gh pr create \ - --draft \ - --title "feat: github-release-download/#654" \ - --body "## 변경 내용\n\n- GitHub 릴리스 다운로드 기능 추가\n\n## 확인 방법\n\n- 관련 테스트 통과\n\nCloses #654" -``` - -## Open PR 전환 - -자가 확인이 끝난 뒤에만 Ready for review로 전환해요. - -```bash +gh pr create --draft --title "fix: camera-session/#123" --body-file pr.md +gh pr edit --body-file pr.md gh pr ready ``` ## 병합 -병합 커밋 제목은 GitHub 기본 제목을 사용하지 않고 아래 형식으로 작성해요. - -```text -merge: /# -``` - -브랜치가 다음과 같다면: - -```text -chore/readme-update/#390 -``` - -기본 제목: - -```text -Merge pull request #392 from team-framework/chore/readme-update/#390 -``` - -대신 다음 제목을 사용해요. - -```text -merge: chore/readme-update/#390 -``` - -CLI로 병합할 때도 병합 커밋 제목을 직접 지정해요. +사용자 요청 범위와 저장소 승인 조건을 확인한다. 공통 규칙의 필수 검사·체크리스트·리뷰 확인 후 merge commit으로 병합한다. ```bash -gh pr merge --merge \ - --subject "merge: chore/readme-update/#390" +head_branch=$(gh pr view --json headRefName --jq .headRefName) +head_sha=$(gh pr view --json headRefOid --jq .headRefOid) +gh pr checks --required +gh pr merge --merge --match-head-commit "$head_sha" \ + --subject "merge: $head_branch" --body "" ``` + +병합 후 실제 커밋의 제목·빈 본문·두 부모를 확인한다. 자동 동기화·배포 묶음 PR의 예외는 공통 규칙을 따른다. diff --git a/.codex/skills/commit/SKILL.md b/.codex/skills/commit/SKILL.md index 973fb6c..5023636 100644 --- a/.codex/skills/commit/SKILL.md +++ b/.codex/skills/commit/SKILL.md @@ -1,33 +1,23 @@ -# 커밋 작성 +--- +name: commit +description: Framework 저장소의 목적별 커밋 작성 시 한국어 제목과 변경 종류에 맞는 타입 적용. +--- -커밋은 한 가지 목적의 변경만 담고, 제목은 한국어로 작성해요. +# Commit 작성 -## 형식 +[공통 작성 규칙](../../../docs/collaboration.md)의 커밋 타입을 따른다. -```text -: <변경 내용> -``` - -`type`은 `feat`, `fix`, `chore`, `refactor` 중 하나이며, 이슈 및 브랜치의 유형과 맞춰요. - -예시: - -```text -feat: 다운로드 버튼과 진행 상태 추가 -fix: 로그인 후 잘못된 경로 이동 수정 -chore: 테스트 의존성 업데이트 -refactor: 인증 토큰 검증 로직 분리 -``` - -## 확인 후 커밋 - -변경 파일과 테스트 결과를 확인한 뒤 커밋해요. +- 형식은 `: <한국어 변경 내용>` +- 타입은 `feat/fix/refactor/hotfix/docs/style/remove/test/chore/comment` +- 한 커밋에 한 가지 목적. 개별 커밋 타입은 Issue·브랜치 타입과 달라도 됨 +- 기능 브랜치에서도 오류 수정은 `fix`, 문서는 `docs`, 테스트는 `test`로 구분 +- 코드 식별자와 전문 용어는 실제 표기 유지 +- 커밋 전 변경 파일·검증 범위·`git diff --check` 확인 +- 관련 없는 파일·비밀 정보·생성물 제외 ```bash git status --short git diff --check git add <변경한-파일> -git commit -m "feat: 다운로드 버튼과 진행 상태 추가" +git commit -m "fix: 얼굴 등록 카메라 세션 충돌 수정" ``` - -관련 없는 파일, 비밀 정보, 생성물은 커밋하지 않아요. diff --git a/.codex/skills/issue/SKILL.md b/.codex/skills/issue/SKILL.md index 630dce5..c26d44c 100644 --- a/.codex/skills/issue/SKILL.md +++ b/.codex/skills/issue/SKILL.md @@ -1,51 +1,22 @@ -# 이슈 생성 +--- +name: issue +description: Framework 저장소의 Issue 생성 및 작업 체크리스트 갱신 시 공통 제목과 개조식 본문 적용. +--- -작업을 시작하기 전에 GitHub 이슈를 만들어요. 이슈는 작업을 추적하는 기준점이에요. +# Issue 작성 -## 작업 유형 +[공통 작성 규칙](../../../docs/collaboration.md)의 Issue 양식을 읽는다. -아래 네 유형 중 하나를 선택해요. - -| 유형 | 사용할 때 | -| --- | --- | -| `feat` | 새 기능을 개발할 때 | -| `fix` | 버그를 수정할 때 | -| `chore` | 기타 자잘한 수정이나 설정 변경을 할 때 | -| `refactor` | 동작은 유지하며 코드를 정리할 때 | - -## 제목 - -제목은 한국어로, 아래 형식으로 작성해요. - -```text -: <작업 내용> -``` - -예시: - -```text -feat: GitHub 릴리스 다운로드 기능 추가 -fix: 로그인 후 화면 전환 오류 수정 -chore: 의존성 버전 정리 -refactor: 인증 서비스 구조 개선 -``` - -## 담당자 - -기본 담당자는 이슈를 만든 본인이에요. 다른 사람이 실제 작업을 맡는다면 그 사람을 담당자로 지정해요. - -## GitHub 웹에서 만들기 - -이슈를 만들 때 작업 유형에 맞는 양식을 선택하고, 제목과 담당자를 지정해요. - -## GitHub CLI에서 만들기 +- 작업 전 Issue 생성, 실제 작업자 지정 +- 제목은 `feat/fix/chore/refactor: <한국어 작업 내용>` +- 본문은 `작업 내용 / 작업 체크리스트 / 완료 기준`, 필요한 경우 마지막에 `참고` 추가 +- 한 항목에 한 가지 내용, 명사형 종결 사용. 진행 항목은 `- [ ]`로 작성하고 수행 후 체크 +- 완료 기준은 확인 가능한 결과로 작성 +- CLI 본문은 UTF-8 Markdown 파일과 `--body-file` 사용. 빈 본문과 문자열 `\n` 사용 금지 ```bash -gh issue create \ - --title "feat: GitHub 릴리스 다운로드 기능 추가" \ - --label "feat" \ - --assignee "@me" \ - --body "" +gh issue create --title "fix: 카메라 세션 충돌 수정" \ + --label "fix" --assignee "@me" --body-file issue.md ``` -생성된 이슈 번호를 이후 브랜치와 PR에 사용해요. +생성한 번호를 브랜치와 PR에 사용한다. 작업 진행에 따라 기존 Issue 체크 상태를 갱신한다. diff --git a/.codex/skills/pull-request/SKILL.md b/.codex/skills/pull-request/SKILL.md index cc3c142..11fd5f6 100644 --- a/.codex/skills/pull-request/SKILL.md +++ b/.codex/skills/pull-request/SKILL.md @@ -1,95 +1,42 @@ -# Pull Request 생성 +--- +name: pull-request +description: Framework 저장소의 Draft PR 생성, 검증 결과와 체크리스트 갱신, merge commit 작성 시 공통 규칙 적용. +--- -작업을 공유할 준비가 되면 PR을 초안(Draft)으로 먼저 만들어요. 처음부터 Open PR로 만들지 않아요. +# PR 작성 -## 제목 +[공통 작성 규칙](../../../docs/collaboration.md)과 `.github/pull_request_template.md`를 읽는다. -브랜치의 작업 유형은 콜론으로 구분하고, 나머지 슬러그와 이슈 번호는 브랜치와 같게 사용해요. +## 생성과 갱신 -```text -: /# -``` - -예시: - -```text -fix: preflight-dim/#67 -``` - -## 생성 - -변경 사항과 테스트를 확인하고 원격 브랜치를 푸시한 뒤 Draft PR을 만들어요. - -PR 본문은 아래 틀을 유지해요. - -```markdown -## 변경 내용 - -- 핵심 변경 사항 -- 주요 동작 또는 영향 범위 - -## 확인 방법 - -- 빌드·테스트 결과 -- 주요 검증 항목 - -Closes # -``` - -본문은 구현 과정을 길게 설명하지 않고 핵심 결론만 간결한 개조식으로 작성해요. - -- 비슷한 변경은 한 항목으로 묶어요. -- 세부 구현보다 사용자가 체감하는 동작과 중요한 기술적 영향만 남겨요. -- 테스트 개수, 성공 여부, 핵심 검증 범위만 남겨요. -- signaling 변경 없음, 물리 기기 미검증처럼 중요한 제약·미수행 항목은 유지해요. -- 이미 본문에서 드러나는 내용을 반복하지 않아요. +- 제목은 `: /#`, head 브랜치와 일치 +- 구현 중 Draft 생성 허용. 검증 전에는 결과에 `검증 대기`, 체크리스트에는 미완료 항목 기재 +- 본문은 `변경 내용 / 검증 결과 / 머지 체크리스트 / 관련 이슈` 순서 고정 +- 명사형 종결의 최상위 개조식 사용. 변경 내용·검증 결과는 각각 1~3개 항목 +- 핵심 동작·영향·실제 결과만 기록. 구현 과정·작업 순서·시도한 방법·중첩 목록·홍보 문구 생략 +- 중요한 미검증 범위와 API 계약·배포 조건 유지. 상세 설명은 Issue 또는 문서에 기록 +- 체크리스트는 해당 변경의 필수 검증과 리뷰 확인으로 구성. 미수행 항목 체크 금지 +- 실제 검증 후 결과·체크 상태 갱신, Ready for review 전환, 리뷰 반영 후 머지 +- 이슈 전체 완료는 `Closes #번호`, 부분 완료·참고는 `Refs #번호` +- 본문은 파일과 `--body-file`로 전달. 문자열 `\n` 사용 금지 ```bash -git status --short git push -u origin HEAD -gh pr create \ - --draft \ - --title "feat: github-release-download/#654" \ - --body "## 변경 내용\n\n- GitHub 릴리스 다운로드 기능 추가\n\n## 확인 방법\n\n- 관련 테스트 통과\n\nCloses #654" -``` - -## Open PR 전환 - -자가 확인이 끝난 뒤에만 Ready for review로 전환해요. - -```bash +gh pr create --draft --title "fix: camera-session/#123" --body-file pr.md +gh pr edit --body-file pr.md gh pr ready ``` ## 병합 -병합 커밋 제목은 GitHub 기본 제목을 사용하지 않고 아래 형식으로 작성해요. - -```text -merge: /# -``` - -브랜치가 다음과 같다면: - -```text -chore/readme-update/#390 -``` - -기본 제목: - -```text -Merge pull request #392 from team-framework/chore/readme-update/#390 -``` - -대신 다음 제목을 사용해요. - -```text -merge: chore/readme-update/#390 -``` - -CLI로 병합할 때도 병합 커밋 제목을 직접 지정해요. +사용자 요청 범위와 저장소 승인 조건을 확인한다. 공통 규칙의 필수 검사·체크리스트·리뷰 확인 후 merge commit으로 병합한다. ```bash -gh pr merge --merge \ - --subject "merge: chore/readme-update/#390" +head_branch=$(gh pr view --json headRefName --jq .headRefName) +head_sha=$(gh pr view --json headRefOid --jq .headRefOid) +gh pr checks --required +gh pr merge --merge --match-head-commit "$head_sha" \ + --subject "merge: $head_branch" --body "" ``` + +병합 후 실제 커밋의 제목·빈 본문·두 부모를 확인한다. 자동 동기화·배포 묶음 PR의 예외는 공통 규칙을 따른다. diff --git a/.github/ISSUE_TEMPLATE/01-feat.yml b/.github/ISSUE_TEMPLATE/01-feat.yml index 79ccb3c..c96803f 100644 --- a/.github/ISSUE_TEMPLATE/01-feat.yml +++ b/.github/ISSUE_TEMPLATE/01-feat.yml @@ -1,5 +1,5 @@ name: 기능 개발 (feat) -description: 새 기능 개발 작업을 등록해요. +description: 새 기능 개발 작업 등록 title: "feat: " labels: ["feat"] @@ -7,12 +7,30 @@ body: - type: markdown attributes: value: | - 담당자를 꼭 지정해 주세요. + 담당자 지정. 한 항목에 한 가지 내용, 명사형 종결의 개조식 작성. - type: textarea - id: details + id: work attributes: - label: 추가 설명 - description: 필요한 내용이 있으면 적어 주세요. - placeholder: 선택 사항이에요. + label: 작업 내용 + description: 해결할 문제 또는 추가할 기능 + placeholder: "- 카메라 중복 세션 생성 차단" validations: - required: false + required: true + - type: textarea + id: checklist + attributes: + label: 작업 체크리스트 + description: 실제 작업·검증 항목 작성, 수행 후 체크 + value: | + - [ ] 구현 항목 + - [ ] 변경 범위에 맞는 검증 + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: 완료 기준 + description: 완료 여부를 확인할 수 있는 동작 또는 결과 + placeholder: "- 반복 진입 시 카메라 정상 동작" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02-fix.yml b/.github/ISSUE_TEMPLATE/02-fix.yml index 8571b51..770be20 100644 --- a/.github/ISSUE_TEMPLATE/02-fix.yml +++ b/.github/ISSUE_TEMPLATE/02-fix.yml @@ -1,5 +1,5 @@ name: 버그 수정 (fix) -description: 버그 수정 작업을 등록해요. +description: 버그 수정 작업 등록 title: "fix: " labels: ["fix"] @@ -7,12 +7,30 @@ body: - type: markdown attributes: value: | - 담당자를 꼭 지정해 주세요. + 담당자 지정. 한 항목에 한 가지 내용, 명사형 종결의 개조식 작성. - type: textarea - id: details + id: work attributes: - label: 추가 설명 - description: 필요한 내용이 있으면 적어 주세요. - placeholder: 선택 사항이에요. + label: 작업 내용 + description: 해결할 문제 또는 추가할 기능 + placeholder: "- 카메라 중복 세션 생성 차단" validations: - required: false + required: true + - type: textarea + id: checklist + attributes: + label: 작업 체크리스트 + description: 실제 작업·검증 항목 작성, 수행 후 체크 + value: | + - [ ] 구현 항목 + - [ ] 변경 범위에 맞는 검증 + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: 완료 기준 + description: 완료 여부를 확인할 수 있는 동작 또는 결과 + placeholder: "- 반복 진입 시 카메라 정상 동작" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/03-chore.yml b/.github/ISSUE_TEMPLATE/03-chore.yml index 4f1bafd..5e3ecf5 100644 --- a/.github/ISSUE_TEMPLATE/03-chore.yml +++ b/.github/ISSUE_TEMPLATE/03-chore.yml @@ -1,5 +1,5 @@ name: 기타 작업 (chore) -description: 기능 개발·버그 수정·리팩터링에 해당하지 않는 자잘한 수정 사항을 등록해요. +description: 설정·의존성·운영 작업 등록 title: "chore: " labels: ["chore"] @@ -7,12 +7,30 @@ body: - type: markdown attributes: value: | - 담당자를 꼭 지정해 주세요. + 담당자 지정. 한 항목에 한 가지 내용, 명사형 종결의 개조식 작성. - type: textarea - id: details + id: work attributes: - label: 추가 설명 - description: 필요한 내용이 있으면 적어 주세요. - placeholder: 선택 사항이에요. + label: 작업 내용 + description: 해결할 문제 또는 추가할 기능 + placeholder: "- 카메라 중복 세션 생성 차단" validations: - required: false + required: true + - type: textarea + id: checklist + attributes: + label: 작업 체크리스트 + description: 실제 작업·검증 항목 작성, 수행 후 체크 + value: | + - [ ] 구현 항목 + - [ ] 변경 범위에 맞는 검증 + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: 완료 기준 + description: 완료 여부를 확인할 수 있는 동작 또는 결과 + placeholder: "- 반복 진입 시 카메라 정상 동작" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/04-refactor.yml b/.github/ISSUE_TEMPLATE/04-refactor.yml index 0035a15..269e519 100644 --- a/.github/ISSUE_TEMPLATE/04-refactor.yml +++ b/.github/ISSUE_TEMPLATE/04-refactor.yml @@ -1,5 +1,5 @@ name: 리팩터링 (refactor) -description: 구조 개선 작업을 등록해요. +description: 동작을 유지하는 구조 개선 작업 등록 title: "refactor: " labels: ["refactor"] @@ -7,12 +7,30 @@ body: - type: markdown attributes: value: | - 담당자를 꼭 지정해 주세요. + 담당자 지정. 한 항목에 한 가지 내용, 명사형 종결의 개조식 작성. - type: textarea - id: details + id: work attributes: - label: 추가 설명 - description: 필요한 내용이 있으면 적어 주세요. - placeholder: 선택 사항이에요. + label: 작업 내용 + description: 해결할 문제 또는 추가할 기능 + placeholder: "- 카메라 중복 세션 생성 차단" validations: - required: false + required: true + - type: textarea + id: checklist + attributes: + label: 작업 체크리스트 + description: 실제 작업·검증 항목 작성, 수행 후 체크 + value: | + - [ ] 구현 항목 + - [ ] 변경 범위에 맞는 검증 + validations: + required: true + - type: textarea + id: acceptance + attributes: + label: 완료 기준 + description: 완료 여부를 확인할 수 있는 동작 또는 결과 + placeholder: "- 반복 진입 시 카메라 정상 동작" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1df96ff..b7a7989 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,21 @@ ## 변경 내용 - + +- 핵심 변경 사항 -## 확인 방법 +## 검증 결과 - + +- 검증 대기 -## 해결한 이슈 +## 머지 체크리스트 -Closes # + +- [ ] 변경 범위 확인 +- [ ] 변경 범위에 맞는 검증 완료 +- [ ] 검증 결과 및 미검증 범위 기재 +- [ ] 리뷰 반영 및 미해결 의견 확인 + +## 관련 이슈 + +- Closes # diff --git a/AGENTS.md b/AGENTS.md index 45587d5..1cadf20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,12 @@ # Framework 협업 규칙 -작업을 시작하기 전에 작업 종류와 사용하는 에이전트에 맞는 `.codex`, `.claude`, `.agent`의 `skills/{issue,branch,commit,pull-request}/SKILL.md`를 읽는다. +작업 종류와 사용하는 에이전트에 맞는 `.codex`, `.claude`, `.agent`의 `skills/{issue,branch,commit,pull-request}/SKILL.md`를 읽는다. Issue·Commit·PR 작성 시 `docs/collaboration.md`와 해당 템플릿을 따른다. -- 이슈는 작업 유형과 한국어 작업 내용을 분명히 작성한다. -- 브랜치는 연결된 이슈를 만든 뒤 `//#` 형식으로 만든다. -- 커밋은 하나의 목적만 담고 `: <한국어 변경 내용>` 형식을 사용한다. -- PR은 `: /#` 형식으로 만들고 처음에는 Draft로 유지한다. -- 다른 사람이 만든 변경과 저장소 고유 규칙을 보존한다. +- Issue는 한국어 제목과 작업 체크리스트, 완료 기준을 작성한다. +- 브랜치는 `//#` 형식을 사용한다. +- 커밋은 변경 목적별로 분리하고 `: <한국어 변경 내용>` 형식을 사용한다. 개별 커밋 타입은 브랜치와 달라도 된다. +- PR은 구현 중 Draft로 생성할 수 있다. 본문은 변경 내용·검증 결과·머지 체크리스트·관련 이슈의 개조식으로 작성한다. +- PR에 구현 과정과 서술형 문단을 적지 않는다. 변경 내용과 검증 결과는 각각 1~3개 항목으로 제한한다. +- 미검증 항목은 체크하지 않는다. 실제 검증 후 결과와 체크 상태를 갱신하고 필수 항목 완료 후 머지한다. +- merge commit만 사용하고 제목은 `merge: `, 본문은 빈 값으로 지정한다. +- 기존 Issue·PR 기록, 다른 사람의 변경, 저장소 고유 규칙을 보존한다. diff --git a/CLAUDE.md b/CLAUDE.md index 5bba6b3..1cadf20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,12 @@ # Framework 협업 규칙 -작업을 시작하기 전에 작업 종류에 맞는 `.claude/skills/{issue,branch,commit,pull-request}/SKILL.md`를 읽는다. +작업 종류와 사용하는 에이전트에 맞는 `.codex`, `.claude`, `.agent`의 `skills/{issue,branch,commit,pull-request}/SKILL.md`를 읽는다. Issue·Commit·PR 작성 시 `docs/collaboration.md`와 해당 템플릿을 따른다. -- 이슈는 작업 유형과 한국어 작업 내용을 분명히 작성한다. -- 브랜치는 연결된 이슈를 만든 뒤 `//#` 형식으로 만든다. -- 커밋은 하나의 목적만 담고 `: <한국어 변경 내용>` 형식을 사용한다. -- PR은 `: /#` 형식으로 만들고 처음에는 Draft로 유지한다. -- 다른 사람이 만든 변경과 저장소 고유 규칙을 보존한다. +- Issue는 한국어 제목과 작업 체크리스트, 완료 기준을 작성한다. +- 브랜치는 `//#` 형식을 사용한다. +- 커밋은 변경 목적별로 분리하고 `: <한국어 변경 내용>` 형식을 사용한다. 개별 커밋 타입은 브랜치와 달라도 된다. +- PR은 구현 중 Draft로 생성할 수 있다. 본문은 변경 내용·검증 결과·머지 체크리스트·관련 이슈의 개조식으로 작성한다. +- PR에 구현 과정과 서술형 문단을 적지 않는다. 변경 내용과 검증 결과는 각각 1~3개 항목으로 제한한다. +- 미검증 항목은 체크하지 않는다. 실제 검증 후 결과와 체크 상태를 갱신하고 필수 항목 완료 후 머지한다. +- merge commit만 사용하고 제목은 `merge: `, 본문은 빈 값으로 지정한다. +- 기존 Issue·PR 기록, 다른 사람의 변경, 저장소 고유 규칙을 보존한다. diff --git a/docs/collaboration.md b/docs/collaboration.md new file mode 100644 index 0000000..bd13a51 --- /dev/null +++ b/docs/collaboration.md @@ -0,0 +1,114 @@ +# Issue·Commit·PR 작성 규칙 + +## 제목과 브랜치 + +| 항목 | 형식 | +| --- | --- | +| Issue | `: <한국어 작업 내용>` | +| 브랜치 | `//#` | +| Commit | `: <한국어 변경 내용>` | +| PR | `: /#` | +| 머지 커밋 | `merge: ` | + +Issue·브랜치·PR 타입은 `feat/fix/chore/refactor` 사용. 개별 커밋은 변경 목적에 따라 `feat/fix/refactor/hotfix/docs/style/remove/test/chore/comment` 사용. 기능 브랜치의 수정·문서·테스트 커밋은 각각 `fix/docs/test` 허용. + +## 본문 문체 + +- 한 항목에 한 가지 내용, 최상위 `-` 개조식 사용 +- 항목 끝은 `추가`, `수정`, `제거`, `통과`, `확인`, `대기` 등 명사형 사용 +- `했습니다`, `합니다`, `했어요` 등 문장형 종결과 서술형 문단 사용 금지 +- PR의 변경 내용·검증 결과는 각각 1~3개 항목 +- PR에 구현 과정·작업 순서·시도한 방법 기록 금지 +- 상세 근거·로그·성능 표는 Issue 또는 문서에 기록 +- API 계약 영향·배포 선행 조건·중요한 미검증 범위는 핵심 항목으로 유지 +- 중첩 목록·추가 PR 섹션·생성 도구 홍보 문구·em dash 사용 금지 + +## Issue + +```markdown +## 작업 내용 + +- 해결할 문제 또는 추가할 기능 + +## 작업 체크리스트 + +- [ ] 구현 항목 +- [ ] 변경 범위에 맞는 검증 + +## 완료 기준 + +- 완료 여부를 확인할 수 있는 동작 또는 결과 +``` + +필요한 경우 마지막에 `## 참고`와 링크 목록 추가. 빈 참고 섹션 생략. 작업 체크리스트는 진행에 따라 갱신하며, 미완료 항목 유지 허용. 완료 기준은 작업 순서 대신 검증 가능한 결과로 작성. 기본 담당자는 실제 작업자 지정. + +## PR + +```markdown +## 변경 내용 + +- 카메라 중복 세션 생성 차단 + +## 검증 결과 + +- 카메라 세션 테스트 18개 통과 +- 실제 기기 검증 대기 + +## 머지 체크리스트 + +- [x] 변경 범위 확인 +- [x] 관련 테스트 완료 +- [ ] 실제 기기 반복 진입 확인 +- [ ] 리뷰 반영 및 미해결 의견 확인 + +## 관련 이슈 + +- Closes #395 +``` + +- 구현 중 Draft 생성 허용. 검증 전에는 `검증 대기`와 미완료 체크박스 기재 +- 체크리스트는 해당 변경의 머지에 필요한 항목만 작성. 검증 항목과 리뷰 확인 항목 포함 +- `변경 범위 확인`, `리뷰 반영 및 미해결 의견 확인`은 고정 필수 항목. 해당 변경의 검증 항목을 하나 이상 추가 +- 수행하지 않은 검증 체크 금지. 빌드·실기기 검증이 미대상이면 결과에 이유 기재 +- 검증 결과는 실행 명령·대상·통과 개수·핵심 결과로 요약 +- 검증 후 결과·체크 상태 갱신, Ready for review 전환, 리뷰 반영 후 머지 +- 리뷰는 요청한 리뷰 또는 자가 검토의 실제 확인 결과로 체크. 체크만으로 승인 리뷰를 대체하지 않음 +- `Closes #번호`는 이슈 전체 완료, `Refs #번호`는 일부 완료 또는 참고에 사용 +- 중요 미검증 범위를 후속 Issue로 분리한 경우 `검증 결과`에 범위와 번호 기재. 필수 항목을 삭제해 검사만 통과시키지 않음 + +웹 편집 또는 `gh pr edit --body-file <파일>`로 체크 상태 갱신. 관련 이슈 키워드는 bare line 또는 `-` 항목 허용. + +## 자동 동기화·배포 묶음 예외 + +- 하네스 Bot의 `harness-sync/framework-agent` 브랜치와 `chore: framework-agent-harness-sync` 제목 유지 +- 자동 동기화도 공통 PR 본문·미완료 검증·머지 체크리스트 사용. 연결 이슈가 없으면 `- 관련 이슈 없음: 자동 하네스 동기화` 기재 +- 배포 묶음은 `chore/batch-deploy-MMDD-N` 브랜치와 변환한 PR 제목 사용 +- 배포 묶음의 관련 이슈는 `Refs`로 기재. 기능 PR이 완료한 이슈를 중복 종료하지 않음 + +## 자동 검사와 적용 시점 + +- `Collaboration / PR format`: 제목·브랜치·섹션 순서·개조식·문장형 종결·항목 수·이슈 연결 검사 +- `Collaboration / merge readiness`: Draft의 미완료 체크 허용, Ready PR의 미완료 체크 차단 +- `Collaboration / policy tests`: 검사기의 정상·거부·Draft·Ready 전환 회귀 검증 +- `Collaboration / Issue format`: 새 Issue의 본문 검사, 오류 시 `needs-format` 표시, 수정 후 표시 제거 +- CI는 내용의 사실성·검증 충분성·과정 서술의 의미를 판정하지 않음. 작성자와 리뷰어가 확인 +- `.github/collaboration-policy.json`의 기존 번호 이하 기록은 새 양식 소급 적용 제외 +- 적용 기준 파일은 저장소별 관리. 하네스 동기화 대상에서 제외하여 재동기화 시 기준 유지 + +## 병합 + +merge commit 사용, squash·rebase 비활성화. 병합 제목은 PR head 브랜치 전체 이름, 본문은 빈 값 지정. + +```bash +head_branch=$(gh pr view --json headRefName --jq .headRefName) +head_sha=$(gh pr view --json headRefOid --jq .headRefOid) +gh pr checks --required +gh pr merge --merge --match-head-commit "$head_sha" \ + --subject "merge: $head_branch" --body "" +``` + +필수 검사와 리뷰 확인 후 병합. GitHub 웹에서도 같은 제목·빈 본문 사용. 병합 후 실제 커밋의 제목·본문·두 부모 확인. 이 명령 예시는 사용자 승인·저장소 권한·요청 범위를 확장하지 않음. + +## 규칙 원본 + +공통 문서·템플릿·검사기·작성 스킬 원본은 `team-framework/framework-agent-harness-sync`. MCP는 자동 동기화 제외를 유지하고 같은 변경을 별도 PR로 적용. 저장소 고유 지침과 관련 없는 작업 보존. From 11de442b820448c94d09b4912eb285c25ef4905e Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:15:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EB=B3=B8=EB=AC=B8=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EA=B3=BC=20=EB=A8=B8=EC=A7=80=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B2=80=EC=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/collaboration-policy.json | 5 + .github/scripts/collaboration-policy.mjs | 147 ++++++++++++++++++ .github/scripts/collaboration-policy.test.mjs | 120 ++++++++++++++ .github/workflows/collaboration-policy.yml | 107 +++++++++++++ 4 files changed, 379 insertions(+) create mode 100644 .github/collaboration-policy.json create mode 100644 .github/scripts/collaboration-policy.mjs create mode 100644 .github/scripts/collaboration-policy.test.mjs create mode 100644 .github/workflows/collaboration-policy.yml diff --git a/.github/collaboration-policy.json b/.github/collaboration-policy.json new file mode 100644 index 0000000..1a0e78a --- /dev/null +++ b/.github/collaboration-policy.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "legacyPullRequestMaxNumber": 32, + "legacyIssueMaxNumber": 32 +} diff --git a/.github/scripts/collaboration-policy.mjs b/.github/scripts/collaboration-policy.mjs new file mode 100644 index 0000000..1cd378f --- /dev/null +++ b/.github/scripts/collaboration-policy.mjs @@ -0,0 +1,147 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; + +const ISSUE_HEADINGS = ['작업 내용', '작업 체크리스트', '완료 기준']; +const PR_HEADINGS = ['변경 내용', '검증 결과', '머지 체크리스트', '관련 이슈']; +const ISSUE_TITLE = /^(feat|fix|chore|refactor): \S.*$/u; +const BRANCH = /^(feat|fix|chore|refactor)\/[a-z0-9]+(?:-[a-z0-9]+)*\/#([1-9]\d*)$/u; +const POLITE_ENDING = /(?:했습니다|하였습니다|되었습니다|합니다|됩니다|입니다|습니다|했어요|해요|이에요|예요|있어요|없어요|할게요)[.!。]*$/u; +const PLACEHOLDER = /^(?:핵심 변경 사항|실행한 검증과 결과|검증 결과 입력|작업 내용 입력|완료 조건 입력|관련 이슈 입력|TODO|TBD|내용 입력|\.{3})$/iu; + +function error(errors, message) { errors.push(message); } + +function parseBody(body, headings, optionalHeadings = [], allowFormHeadings = false) { + const errors = []; + if (typeof body !== 'string' || !body.trim()) return { errors: ['본문 필수'], sections: new Map() }; + if (body.includes('\\n')) error(errors, '문자열 \\n 대신 실제 줄바꿈 사용'); + if (body.includes('—')) error(errors, 'em dash 사용 금지'); + const clean = body.replace(//gu, '').replace(/\r\n?/gu, '\n'); + const sections = new Map(); + const found = []; + let current; + for (const line of clean.split('\n')) { + if (!line.trim()) continue; + const heading = (allowFormHeadings ? /^#{2,3} (.+)$/u : /^## (.+)$/u).exec(line); + if (heading) { + current = heading[1].trim(); + if (sections.has(current)) error(errors, `중복 섹션: ${current}`); + sections.set(current, []); + found.push(current); + continue; + } + if (!current) { error(errors, '섹션 밖 내용 금지'); continue; } + sections.get(current).push(line); + } + const expected = [...headings, ...optionalHeadings.filter(name => sections.has(name))]; + if (JSON.stringify(found) !== JSON.stringify(expected)) error(errors, `섹션 순서: ${expected.join(' / ')}`); + for (const name of headings) if (!sections.get(name)?.length) error(errors, `필수 내용: ${name}`); + return { errors, sections }; +} + +function bullets(lines = [], name, errors, { max = Infinity, checklist = false, allowLinks = false } = {}) { + if (lines.length > max) error(errors, `${name}: 최대 ${max}개 항목`); + const items = []; + for (const line of lines) { + const match = checklist ? /^- \[([ xX])\] (\S.*)$/u.exec(line) : /^- (\S.*)$/u.exec(line); + if (!match || (!checklist && /^- \[[ xX]\] /u.test(line))) { + error(errors, `${name}: ${checklist ? '- [ ] 또는 - [x]' : '-'} 개조식 항목만 허용`); + continue; + } + const text = checklist ? match[2] : match[1]; + if (PLACEHOLDER.test(text.trim())) error(errors, `${name}: 예시 문구 대신 실제 내용 작성`); + const prose = text.replace(/`[^`]*`/gu, '').replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1').trim(); + if (POLITE_ENDING.test(prose)) error(errors, `${name}: 명사형 종결 사용`); + if (!allowLinks && /^https?:\/\//u.test(text)) error(errors, `${name}: 링크 대신 핵심 내용 작성`); + items.push({ text, checked: checklist && match[1].toLowerCase() === 'x' }); + } + return items; +} + +export function validateIssue(issue, { legacyIssueMaxNumber = 0 } = {}) { + if (issue.number > 0 && issue.number <= legacyIssueMaxNumber) return { errors: [], legacy: true }; + const { errors, sections } = parseBody(issue.body, ISSUE_HEADINGS, ['참고'], true); + if (!ISSUE_TITLE.test(issue.title ?? '') || !/[가-힣]/u.test(issue.title ?? '')) error(errors, 'Issue 제목: : <한국어 작업 내용>'); + bullets(sections.get('작업 내용'), '작업 내용', errors); + bullets(sections.get('작업 체크리스트'), '작업 체크리스트', errors, { checklist: true }); + bullets(sections.get('완료 기준'), '완료 기준', errors); + if (sections.has('참고')) { + if (!sections.get('참고').length) error(errors, '빈 참고 섹션 생략'); + bullets(sections.get('참고'), '참고', errors, { allowLinks: true }); + } + return { errors }; +} + +function pullRequestKind(pr, errors) { + const branch = pr.head?.ref ?? ''; + const actor = pr.user?.login ?? ''; + if (branch === 'harness-sync/framework-agent' && /^framework-harness-sync(?:\[bot\])?$/u.test(actor)) { + if (pr.title !== 'chore: framework-agent-harness-sync') error(errors, '자동 동기화 PR 제목 불일치'); + return { kind: 'sync' }; + } + if (/^chore\/batch-deploy-\d{4}-\d+$/u.test(branch)) { + if (pr.title !== branch.replace('/', ': ')) error(errors, '배포 묶음 PR 제목과 브랜치 불일치'); + return { kind: 'deploy' }; + } + const match = BRANCH.exec(branch); + if (!match) error(errors, '브랜치: //#'); + if (pr.title !== branch.replace('/', ': ')) error(errors, 'PR 제목과 head 브랜치 불일치'); + return { kind: 'normal', issueNumber: match?.[2] }; +} + +export function validatePullRequest(pr, { legacyPullRequestMaxNumber = 0 } = {}) { + if (pr.number > 0 && pr.number <= legacyPullRequestMaxNumber) return { errors: [], legacy: true }; + const { errors, sections } = parseBody(pr.body, PR_HEADINGS); + const kind = pullRequestKind(pr, errors); + bullets(sections.get('변경 내용'), '변경 내용', errors, { max: 3 }); + bullets(sections.get('검증 결과'), '검증 결과', errors, { max: 3 }); + const checklist = bullets(sections.get('머지 체크리스트'), '머지 체크리스트', errors, { checklist: true }); + for (const required of ['변경 범위 확인', '리뷰 반영 및 미해결 의견 확인']) { + if (!checklist.some(item => item.text === required)) error(errors, `필수 체크 항목: ${required}`); + } + if (checklist.length < 3) error(errors, '머지 체크리스트: 공통 확인 2개와 변경에 맞는 검증 항목 필요'); + const references = []; + for (const line of sections.get('관련 이슈') ?? []) { + const reference = /^(?:- )?(Closes|Fixes|Resolves|Refs) (?:(?:[\w.-]+)\/(?:[\w.-]+))?#([1-9]\d*)$/iu.exec(line); + if (reference) { references.push(reference); continue; } + if (kind.kind === 'sync' && line === '- 관련 이슈 없음: 자동 하네스 동기화') continue; + error(errors, '관련 이슈: Closes #번호 또는 Refs #번호'); + } + if (kind.kind === 'normal' && kind.issueNumber && !references.some(ref => ref[2] === kind.issueNumber && /^(?:- )?(Closes|Fixes|Resolves|Refs) #/iu.test(ref[0]))) { + error(errors, '브랜치의 이슈 번호를 관련 이슈에 기재'); + } + if (kind.kind === 'deploy' && !references.length) error(errors, '배포 묶음의 관련 이슈 기재'); + if (kind.kind === 'deploy' && references.some(ref => ref[1].toLowerCase() !== 'refs')) error(errors, '배포 묶음은 Refs 사용'); + return { errors }; +} + +export function validateReadiness(pr, config = {}) { + const result = validatePullRequest(pr, config); + if (result.legacy || pr.draft) return result; + const { sections } = parseBody(pr.body, PR_HEADINGS); + const items = bullets(sections.get('머지 체크리스트'), '머지 체크리스트', [] , { checklist: true }); + for (const item of items) if (!item.checked) error(result.errors, `머지 전 완료 필요: ${item.text}`); + return result; +} + +export function validateEvent(payload, mode, config = {}) { + if (mode === 'issue') return validateIssue(payload.issue ?? payload, config); + if (mode === 'pr-format') return validatePullRequest(payload.pull_request ?? payload, config); + if (mode === 'pr-readiness') return validateReadiness(payload.pull_request ?? payload, config); + throw new Error(`지원하지 않는 검사: ${mode}`); +} + +async function main() { + const [mode, eventPath = process.env.GITHUB_EVENT_PATH, configPath = '.github/collaboration-policy.json'] = process.argv.slice(2); + if (!eventPath) throw new Error('검사할 JSON 파일 경로 필요'); + let config = {}; + try { config = JSON.parse(await readFile(configPath, 'utf8')); } + catch (cause) { if (cause.code !== 'ENOENT') throw cause; } + const payload = JSON.parse(await readFile(eventPath, 'utf8')); + const result = validateEvent(payload, mode, config); + const report = result.legacy ? '기존 기록: 새 양식 소급 적용 제외' : result.errors.length ? result.errors.join('\n') : '검사 통과'; + if (process.env.GITHUB_STEP_SUMMARY) await writeFile(process.env.GITHUB_STEP_SUMMARY, `## ${mode}\n\n${report.split('\n').map(line => `- ${line}`).join('\n')}\n`, { flag: 'a' }); + console.log(report); + process.exitCode = result.errors.length ? 1 : 0; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main(); diff --git a/.github/scripts/collaboration-policy.test.mjs b/.github/scripts/collaboration-policy.test.mjs new file mode 100644 index 0000000..f888be8 --- /dev/null +++ b/.github/scripts/collaboration-policy.test.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; +import { validateIssue, validatePullRequest, validateReadiness, validateEvent } from './collaboration-policy.mjs'; + +const body = `## 변경 내용 + +- 카메라 중복 세션 생성 차단 + +## 검증 결과 + +- 회귀 테스트 18개 통과 +- 실기기 검증 대기 + +## 머지 체크리스트 + +- [x] 변경 범위 확인 +- [x] 회귀 테스트 완료 +- [ ] 실기기 반복 진입 확인 +- [x] 리뷰 반영 및 미해결 의견 확인 + +## 관련 이슈 + +- Closes #123 +`; +const pr = { number: 200, title: 'fix: camera-session/#123', body, head: { ref: 'fix/camera-session/#123' }, user: { login: 'developer' }, draft: true }; +const issue = { number: 123, title: 'fix: 카메라 중복 세션 수정', body: `## 작업 내용 + +- 카메라 세션 충돌 수정 + +## 작업 체크리스트 + +- [x] 충돌 조건 확인 +- [ ] 수정 검증 + +## 완료 기준 + +- 반복 진입 시 카메라 정상 동작 +` }; + +test('Draft PR은 미검증 항목을 보존한 채 형식 검사 통과', () => { + assert.deepEqual(validatePullRequest(pr).errors, []); + assert.deepEqual(validateReadiness(pr).errors, []); +}); +test('Ready PR의 미완료 필수 항목은 머지 차단', () => { + assert.deepEqual(validatePullRequest({ ...pr, draft: false }).errors, []); + assert.match(validateReadiness({ ...pr, draft: false }).errors.join('\n'), /실기기 반복 진입 확인/u); +}); +test('체크 후 같은 PR의 머지 검사 통과', () => { + assert.deepEqual(validateReadiness({ ...pr, draft: false, body: body.replace('- [ ]', '- [x]') }).errors, []); +}); +test('필수 공통 확인 항목 삭제로 머지 검사를 우회하지 못함', () => { + assert.ok(validatePullRequest({ ...pr, body: body.replace('- [x] 리뷰 반영 및 미해결 의견 확인\n', '') }).errors.length); + assert.ok(validatePullRequest({ ...pr, body: body.replace('- [x] 회귀 테스트 완료\n- [ ] 실기기 반복 진입 확인\n', '') }).errors.length); +}); +test('미완료 항목이 있는 Issue는 진행 추적 허용', () => assert.deepEqual(validateIssue(issue).errors, [])); +test('GitHub 웹 Issue Form의 H3 섹션도 같은 양식으로 검사', () => assert.deepEqual(validateIssue({ ...issue, body: issue.body.replaceAll('## ', '### ') }).errors, [])); +test('Issue의 완료 기준 누락 거부', () => assert.ok(validateIssue({ ...issue, body: issue.body.split('## 완료 기준')[0] }).errors.length)); +test('본문 서술형 문단 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('- 카메라 중복 세션 생성 차단', '카메라 중복 세션 생성 차단') }).errors.length)); +test('명사형 종결과 다른 말투 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('생성 차단', '생성을 차단했습니다.') }).errors.length)); +test('코드 안의 종결 표현은 문체 검사에서 제외', () => assert.deepEqual(validatePullRequest({ ...pr, body: body.replace('생성 차단', '생성 차단: `했습니다`') }).errors, [])); +test('구현 과정 섹션 추가 거부', () => assert.ok(validatePullRequest({ ...pr, body: body + '\n## 구현 과정\n\n- 함수 추가\n' }).errors.length)); +test('변경 항목 4개 이상 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('- 카메라 중복 세션 생성 차단', '- 카메라 세션 정리\n- 카메라 권한 확인\n- 카메라 입력 교체\n- 카메라 오류 안내') }).errors.length)); +test('검증 결과 항목 4개 이상 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('- 실기기 검증 대기', '- Debug 빌드 통과\n- Release 빌드 통과\n- 실기기 검증 대기') }).errors.length)); +test('문자열 줄바꿈·빈 체크리스트·템플릿 문구 거부', () => { + assert.ok(validatePullRequest({ ...pr, body: body.replaceAll('\n', '\\n') }).errors.length); + assert.ok(validatePullRequest({ ...pr, body: body.replace('- [x] 회귀 테스트 완료\n- [ ] 실기기 반복 진입 확인', '') }).errors.length); + assert.ok(validatePullRequest({ ...pr, body: body.replace('카메라 중복 세션 생성 차단', '핵심 변경 사항') }).errors.length); +}); +test('중복·순서 변경 섹션 거부', () => { + assert.ok(validatePullRequest({ ...pr, body: body + '\n## 변경 내용\n\n- 중복\n' }).errors.length); + assert.ok(validatePullRequest({ ...pr, body: body.replace('## 변경 내용', '## 검증 결과').replace('## 검증 결과\n\n- 회귀', '## 변경 내용\n\n- 회귀') }).errors.length); +}); +test('제목·브랜치·연결 이슈 번호 불일치 거부', () => { + assert.ok(validatePullRequest({ ...pr, title: 'fix: 다른 제목' }).errors.length); + assert.ok(validatePullRequest({ ...pr, body: body.replace('#123', '#124') }).errors.length); +}); +test('부분 완료 PR의 Refs 허용', () => assert.deepEqual(validatePullRequest({ ...pr, body: body.replace('Closes', 'Refs') }).errors, [])); +test('다른 저장소 이슈만으로 로컬 브랜치 이슈를 대체하지 못함', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('Closes #123', 'Refs other/repo#123') }).errors.length)); +test('자동 동기화 예외는 지정 Bot과 고정 브랜치에만 허용', () => { + const sync = { ...pr, title: 'chore: framework-agent-harness-sync', head: { ref: 'harness-sync/framework-agent' }, user: { login: 'framework-harness-sync[bot]' }, body: body.replace('- Closes #123', '- 관련 이슈 없음: 자동 하네스 동기화') }; + assert.deepEqual(validatePullRequest(sync).errors, []); + assert.ok(validatePullRequest({ ...sync, user: { login: 'developer' } }).errors.length); +}); +test('배포 묶음 PR은 연결 이슈를 Refs로 유지', () => { + const deploy = { ...pr, title: 'chore: batch-deploy-1001-1', head: { ref: 'chore/batch-deploy-1001-1' }, body: body.replace('Closes', 'Refs') }; + assert.deepEqual(validatePullRequest(deploy).errors, []); + assert.ok(validatePullRequest({ ...deploy, body }).errors.length); +}); +test('기존 기록만 소급 적용 제외', () => { + const legacy = { ...pr, number: 10, body: '기존 본문', draft: false }; + assert.equal(validateReadiness(legacy, { legacyPullRequestMaxNumber: 10 }).legacy, true); + assert.ok(validatePullRequest({ ...legacy, number: 11 }, { legacyPullRequestMaxNumber: 10 }).errors.length); + assert.equal(validateIssue({ ...issue, number: 10, body: '' }, { legacyIssueMaxNumber: 10 }).legacy, true); +}); +test('GitHub 이벤트 payload와 직접 JSON 검사 결과 일치', () => { + assert.deepEqual(validateEvent({ pull_request: pr }, 'pr-format'), validatePullRequest(pr)); + assert.deepEqual(validateEvent({ issue }, 'issue'), validateIssue(issue)); +}); +test('Issue 선택 참고 항목 허용, 빈 참고와 영어 제목 거부', () => { + assert.deepEqual(validateIssue({ ...issue, body: issue.body + '\n## 참고\n\n- https://github.com/team-framework/innolive-client/issues/123\n' }).errors, []); + assert.ok(validateIssue({ ...issue, body: issue.body + '\n## 참고\n' }).errors.length); + assert.ok(validateIssue({ ...issue, title: 'fix: camera error' }).errors.length); +}); +test('검사 CLI의 종료 코드와 Actions 요약으로 실패·복구 확인', async () => { + const dir = await mkdtemp(join(tmpdir(), 'collaboration-policy-')); + const event = join(dir, 'event.json'); + const config = join(dir, 'config.json'); + const summary = join(dir, 'summary.md'); + await writeFile(config, '{}'); + await writeFile(event, JSON.stringify({ pull_request: { ...pr, draft: false } })); + const run = () => spawnSync(process.execPath, [new URL('./collaboration-policy.mjs', import.meta.url).pathname, 'pr-readiness', event, config], { env: { ...process.env, GITHUB_STEP_SUMMARY: summary }, encoding: 'utf8' }); + assert.equal(run().status, 1); + assert.match(await readFile(summary, 'utf8'), /머지 전 완료 필요/u); + await writeFile(event, JSON.stringify({ pull_request: { ...pr, draft: false, body: body.replace('- [ ]', '- [x]') } })); + assert.equal(run().status, 0); +}); diff --git a/.github/workflows/collaboration-policy.yml b/.github/workflows/collaboration-policy.yml new file mode 100644 index 0000000..6361450 --- /dev/null +++ b/.github/workflows/collaboration-policy.yml @@ -0,0 +1,107 @@ +name: Collaboration Policy + +on: + pull_request: + types: [opened, edited, synchronize, reopened, ready_for_review, converted_to_draft] + issues: + types: [opened, edited, reopened] + push: + branches: [main] + paths: + - .github/scripts/collaboration-policy* + - .github/workflows/collaboration-policy.yml + - sync/** + - tests/** + - package.json + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: collaboration-policy-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} + cancel-in-progress: true + +jobs: + policy-tests: + if: github.event_name != 'issues' + name: Collaboration / policy tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '24' + - run: node --test .github/scripts/collaboration-policy.test.mjs + - if: github.repository == 'team-framework/framework-agent-harness-sync' + run: npm ci + - if: github.repository == 'team-framework/framework-agent-harness-sync' + run: npm test + - if: github.repository == 'team-framework/framework-agent-harness-sync' + run: npm run typecheck + + pr-format: + if: github.event_name == 'pull_request' + name: Collaboration / PR format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '24' + - run: node .github/scripts/collaboration-policy.mjs pr-format + + merge-readiness: + if: github.event_name == 'pull_request' + name: Collaboration / merge readiness + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '24' + - run: node .github/scripts/collaboration-policy.mjs pr-readiness + + issue-format: + if: github.event_name == 'issues' + name: Collaboration / Issue format + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '24' + - id: validate + continue-on-error: true + run: node .github/scripts/collaboration-policy.mjs issue + - name: 양식 수정 필요 표시 + uses: actions/github-script@v7 + env: + VALIDATION_OUTCOME: ${{ steps.validate.outcome }} + with: + script: | + const params = { ...context.repo, issue_number: context.issue.number }; + const { data: issue } = await github.rest.issues.get(params); + const hasLabel = issue.labels.some(label => (typeof label === 'string' ? label : label.name) === 'needs-format'); + if (process.env.VALIDATION_OUTCOME === 'failure') { + try { + await github.rest.issues.createLabel({ ...context.repo, name: 'needs-format', color: 'B60205', description: '공통 Issue 본문 양식 수정 필요' }); + } catch (error) { + if (error.status !== 422) throw error; + } + if (!hasLabel) await github.rest.issues.addLabels({ ...params, labels: ['needs-format'] }); + core.setFailed('Issue 본문 양식 수정 필요: 검사 요약 확인'); + } else if (hasLabel) { + await github.rest.issues.removeLabel({ ...params, name: 'needs-format' }); + } From 33960dec3d272c81ec2c0c23ddb9bd18c3280f24 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:15:01 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=ED=95=98=EB=84=A4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20PR=EA=B3=BC=20=ED=98=91=EC=97=85=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=ED=8C=8C=EC=9D=BC=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-webhook.yml | 7 +++++++ .github/workflows/sync-harness.yml | 9 +++++++-- deploy/Dockerfile | 1 + sync/github-app.ts | 3 ++- sync/manifest.ts | 10 +++++++-- sync/pr-body.ts | 27 +++++++++++++++++++++++++ tests/harness-sync-installation.test.ts | 7 ++++++- tests/sync.test.ts | 27 +++++++++++++++++++++++++ 8 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 sync/pr-body.ts diff --git a/.github/workflows/deploy-webhook.yml b/.github/workflows/deploy-webhook.yml index c40fe4b..e2e0e13 100644 --- a/.github/workflows/deploy-webhook.yml +++ b/.github/workflows/deploy-webhook.yml @@ -13,6 +13,11 @@ on: - .agent/** - .codex/** - .claude/** + - .github/ISSUE_TEMPLATE/** + - .github/pull_request_template.md + - .github/scripts/collaboration-policy* + - .github/workflows/collaboration-policy.yml + - docs/collaboration.md - .github/workflows/deploy-webhook.yml workflow_dispatch: @@ -36,6 +41,8 @@ jobs: rsync -a --delete .codex/ "$app_dir/.codex/" rsync -a --delete .claude/ "$app_dir/.claude/" rsync -a --delete .github/ "$app_dir/.github/" + install -d "$app_dir/docs" + install -m 600 docs/collaboration.md "$app_dir/docs/collaboration.md" install -m 600 deploy/Dockerfile "$app_dir/Dockerfile" install -m 600 deploy/compose.yaml "$app_dir/compose.yaml" cd "$app_dir" diff --git a/.github/workflows/sync-harness.yml b/.github/workflows/sync-harness.yml index ccbbd76..5966e68 100644 --- a/.github/workflows/sync-harness.yml +++ b/.github/workflows/sync-harness.yml @@ -12,6 +12,9 @@ on: - .gitattributes - .github/ISSUE_TEMPLATE/** - .github/pull_request_template.md + - .github/scripts/collaboration-policy* + - .github/workflows/collaboration-policy.yml + - docs/collaboration.md - .github/workflows/assign-issue-author.yml - sync/** - .github/workflows/sync-harness.yml @@ -137,7 +140,7 @@ jobs: git -C target push origin --delete "$branch" fi git -C target switch -c "$branch" - sync_paths=(.agent .codex .claude .github .gitattributes AGENTS.md CLAUDE.md) + sync_paths=(.agent .codex .claude .github docs/collaboration.md .gitattributes AGENTS.md CLAUDE.md) for path in "${sync_paths[@]}"; do if git -C target check-ignore -q --no-index -- "$path"; then echo "대상 레포의 .gitignore에 포함되어 동기화에서 제외해요: $path" @@ -151,7 +154,9 @@ jobs: fi git -C target commit -m "chore: 에이전트 협업 규칙 동기화" git -C target push origin "HEAD:$branch" - pr_url=$(gh pr create --repo "$TARGET_REPOSITORY" --base "$BASE_BRANCH" --head "$branch" --draft --title "chore: framework-agent-harness-sync" --body "Framework Agent Harness Sync 변경을 동기화했어요.") + pr_body_file="$RUNNER_TEMP/harness-pr.md" + npx --prefix source tsx source/sync/pr-body.ts > "$pr_body_file" + pr_url=$(gh pr create --repo "$TARGET_REPOSITORY" --base "$BASE_BRANCH" --head "$branch" --draft --title "chore: framework-agent-harness-sync" --body-file "$pr_body_file") npx --prefix source tsx source/sync/write-result.ts --file "$result_file" --repository "$TARGET_REPOSITORY" --status pr_created --detail "$pr_url" - name: 대상 결과 보관 if: always() diff --git a/deploy/Dockerfile b/deploy/Dockerfile index fb22d90..495c40b 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -10,6 +10,7 @@ COPY .codex ./.codex COPY .claude ./.claude COPY .agent ./.agent COPY .github ./.github +COPY docs ./docs COPY AGENTS.md CLAUDE.md .gitattributes ./ RUN chmod -R a+rX /app diff --git a/sync/github-app.ts b/sync/github-app.ts index f751946..52d9b94 100644 --- a/sync/github-app.ts +++ b/sync/github-app.ts @@ -3,6 +3,7 @@ import { lstat, readdir, readFile } from "node:fs/promises"; import { join, relative } from "node:path"; import { selectTargets } from "./discover.ts"; import { mergeManagedInstructions } from "./instructions.ts"; +import { syncPullRequestBody } from "./pr-body.ts"; import { syncBranch, syncCommitMessage, syncItems, syncPullRequestTitle, type SyncItem } from "./manifest.ts"; const API_BASE_URL = "https://api.github.com"; @@ -159,7 +160,7 @@ async function createSyncPullRequest({ client, target, files }) { head: syncBranch, base: target.baseBranch, draft: true, - body: "Framework Agent Harness Sync 변경을 동기화했어요." + body: syncPullRequestBody } }); return { repository: target.repository, status: "pr_created", detail: pullRequest.html_url }; diff --git a/sync/manifest.ts b/sync/manifest.ts index 1fc3b18..868219d 100644 --- a/sync/manifest.ts +++ b/sync/manifest.ts @@ -1,4 +1,4 @@ -const issueTemplates = ["01-feat.yml", "02-fix.yml", "03-chore.yml", "04-refactor.yml"]; +const issueTemplates = ["01-feat.yml", "02-fix.yml", "03-chore.yml", "04-refactor.yml", "config.yml"]; export type SyncItem = { id?: string; @@ -39,7 +39,13 @@ export const syncItems: SyncItem[] = [ id: "pull-request-template", source: ".github/pull_request_template.md", destination: ".github/pull_request_template.md" - } + }, + ...[ + "docs/collaboration.md", + ".github/scripts/collaboration-policy.mjs", + ".github/scripts/collaboration-policy.test.mjs", + ".github/workflows/collaboration-policy.yml" + ].map((path) => ({ source: path, destination: path })) ]; export const syncBranch = "harness-sync/framework-agent"; diff --git a/sync/pr-body.ts b/sync/pr-body.ts new file mode 100644 index 0000000..7e8a87f --- /dev/null +++ b/sync/pr-body.ts @@ -0,0 +1,27 @@ +import { pathToFileURL } from "node:url"; + +export const syncPullRequestBody = `## 변경 내용 + +- Framework 공통 협업 파일 변경 반영 +- 저장소 고유 지침과 관리 대상 밖 파일 유지 + +## 검증 결과 + +- 동기화 변경 파일 생성 완료 +- 대상 저장소 검증·리뷰 대기 + +## 머지 체크리스트 + +- [ ] 변경 범위 확인 +- [ ] 대상 저장소 검증 완료 +- [ ] 검증 결과 및 미검증 범위 기재 +- [ ] 리뷰 반영 및 미해결 의견 확인 + +## 관련 이슈 + +- 관련 이슈 없음: 자동 하네스 동기화 +`; + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.stdout.write(syncPullRequestBody); +} diff --git a/tests/harness-sync-installation.test.ts b/tests/harness-sync-installation.test.ts index 373f570..a64e8ad 100644 --- a/tests/harness-sync-installation.test.ts +++ b/tests/harness-sync-installation.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { generateKeyPairSync } from "node:crypto"; import test from "node:test"; import { createAppJwt, syncHarnessInstallation } from "../sync/github-app.ts"; +import { syncPullRequestBody } from "../sync/pr-body.ts"; function json(body, status = 200) { return { ok: status >= 200 && status < 300, status, text: async () => JSON.stringify(body), json: async () => body }; @@ -59,13 +60,17 @@ test("설치된 위키와 MCP 저장소는 읽거나 쓰지 않고 다른 레포 assert.ok(tree.tree.some((file) => file.path === ".gitattributes")); assert.ok(tree.tree.some((file) => file.path === "AGENTS.md")); assert.ok(tree.tree.some((file) => file.path === "CLAUDE.md")); + assert.ok(tree.tree.some((file) => file.path === "docs/collaboration.md")); + assert.ok(tree.tree.some((file) => file.path === ".github/scripts/collaboration-policy.mjs")); + assert.ok(tree.tree.some((file) => file.path === ".github/workflows/collaboration-policy.yml")); + assert.equal(tree.tree.some((file) => file.path === ".github/collaboration-policy.json"), false); const pullRequest = calls.find((call) => call.path.endsWith("/pulls") && call.options.method === "POST"); assert.deepEqual(JSON.parse(pullRequest.options.body), { title: "chore: framework-agent-harness-sync", head: "harness-sync/framework-agent", base: "main", draft: true, - body: "Framework Agent Harness Sync 변경을 동기화했어요." + body: syncPullRequestBody }); }); diff --git a/tests/sync.test.ts b/tests/sync.test.ts index 5f64b3d..80a09be 100644 --- a/tests/sync.test.ts +++ b/tests/sync.test.ts @@ -6,6 +6,9 @@ import test from "node:test"; import { applyHarnessFiles } from "../sync/apply.ts"; import { selectTargets } from "../sync/discover.ts"; import { renderSyncReport } from "../sync/report.ts"; +import { syncItems } from "../sync/manifest.ts"; +import { syncPullRequestBody } from "../sync/pr-body.ts"; +import { validatePullRequest, validateReadiness } from "../.github/scripts/collaboration-policy.mjs"; test("설치된 활성 레포만 동기화 대상으로 선택해요", () => { const targets = selectTargets([ @@ -126,3 +129,27 @@ test("동기화 결과를 레포별 표로 만들어요", () => { assert.match(report, /Draft PR 생성/); assert.match(report, /기존 동기화 PR 대기/); }); + +test("전체 동기화 후에도 저장소별 기존 기록 기준과 제품 파일 유지", async () => { + const target = await mkdtemp(join(tmpdir(), "collaboration-rollout-")); + await mkdir(join(target, ".github"), { recursive: true }); + const cutoff = '{"legacyPullRequestMaxNumber":42,"legacyIssueMaxNumber":41}\n'; + await writeFile(join(target, ".github", "collaboration-policy.json"), cutoff); + await writeFile(join(target, "product.txt"), "existing product\n"); + await applyHarnessFiles({ sourceRoot: process.cwd(), targetRoot: target }); + await applyHarnessFiles({ sourceRoot: process.cwd(), targetRoot: target }); + assert.equal(await readFile(join(target, ".github", "collaboration-policy.json"), "utf8"), cutoff); + assert.equal(await readFile(join(target, "product.txt"), "utf8"), "existing product\n"); + assert.equal(await readFile(join(target, "docs", "collaboration.md"), "utf8"), await readFile("docs/collaboration.md", "utf8")); + for (const path of [".github/scripts/collaboration-policy.mjs", ".github/scripts/collaboration-policy.test.mjs", ".github/workflows/collaboration-policy.yml"]) { + assert.equal(await readFile(join(target, path), "utf8"), await readFile(path, "utf8")); + } + assert.equal(syncItems.some(item => item.destination === ".github/collaboration-policy.json"), false); +}); + +test("자동 동기화 본문은 Draft 검사 통과, 미검증 상태의 Ready 머지 차단", () => { + const pr = { title: "chore: framework-agent-harness-sync", body: syncPullRequestBody, head: { ref: "harness-sync/framework-agent" }, user: { login: "framework-harness-sync[bot]" }, draft: true }; + assert.deepEqual(validatePullRequest(pr).errors, []); + assert.deepEqual(validateReadiness(pr).errors, []); + assert.ok(validateReadiness({ ...pr, draft: false }).errors.length); +}); From 5df475a3d5b70dfa47401bbf83ff9456cf4e8742 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:23:24 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EC=97=B0=EA=B2=B0=20Issue=20?= =?UTF-8?q?=EB=B3=B8=EB=AC=B8=EC=9D=84=20PR=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/collaboration-policy.mjs | 19 ++++++++++++++++++- .github/scripts/collaboration-policy.test.mjs | 19 ++++++++++++++++++- .github/workflows/collaboration-policy.yml | 5 +++++ docs/collaboration.md | 4 +++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/scripts/collaboration-policy.mjs b/.github/scripts/collaboration-policy.mjs index 1cd378f..1c7ecc4 100644 --- a/.github/scripts/collaboration-policy.mjs +++ b/.github/scripts/collaboration-policy.mjs @@ -123,6 +123,21 @@ export function validateReadiness(pr, config = {}) { return result; } +export async function validateLinkedIssue(pr, config, { repository, token, fetchImpl = fetch }) { + if (pr.number > 0 && pr.number <= (config.legacyPullRequestMaxNumber ?? 0)) return { errors: [], legacy: true }; + const match = BRANCH.exec(pr.head?.ref ?? ''); + if (!match) return { errors: [] }; + if (!/^[\w.-]+\/[\w.-]+$/u.test(repository ?? '') || !token) return { errors: ['연결 Issue 검사 인증·저장소 정보 필요'] }; + const response = await fetchImpl(`https://api.github.com/repos/${repository}/issues/${match[2]}`, { + headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, 'X-GitHub-Api-Version': '2022-11-28' } + }); + if (!response.ok) return { errors: [`연결 Issue #${match[2]} 조회 실패: HTTP ${response.status}`] }; + const issue = await response.json(); + if (issue.pull_request || String(issue.number) !== match[2]) return { errors: ['브랜치 번호는 실제 Issue에 연결 필요'] }; + const result = validateIssue(issue, config); + return { ...result, errors: result.errors.map(message => `Issue #${match[2]}: ${message}`) }; +} + export function validateEvent(payload, mode, config = {}) { if (mode === 'issue') return validateIssue(payload.issue ?? payload, config); if (mode === 'pr-format') return validatePullRequest(payload.pull_request ?? payload, config); @@ -137,7 +152,9 @@ async function main() { try { config = JSON.parse(await readFile(configPath, 'utf8')); } catch (cause) { if (cause.code !== 'ENOENT') throw cause; } const payload = JSON.parse(await readFile(eventPath, 'utf8')); - const result = validateEvent(payload, mode, config); + const result = mode === 'pr-issue' + ? await validateLinkedIssue(payload.pull_request ?? payload, config, { repository: process.env.GITHUB_REPOSITORY, token: process.env.GH_TOKEN }) + : validateEvent(payload, mode, config); const report = result.legacy ? '기존 기록: 새 양식 소급 적용 제외' : result.errors.length ? result.errors.join('\n') : '검사 통과'; if (process.env.GITHUB_STEP_SUMMARY) await writeFile(process.env.GITHUB_STEP_SUMMARY, `## ${mode}\n\n${report.split('\n').map(line => `- ${line}`).join('\n')}\n`, { flag: 'a' }); console.log(report); diff --git a/.github/scripts/collaboration-policy.test.mjs b/.github/scripts/collaboration-policy.test.mjs index f888be8..ab60db6 100644 --- a/.github/scripts/collaboration-policy.test.mjs +++ b/.github/scripts/collaboration-policy.test.mjs @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import test from 'node:test'; -import { validateIssue, validatePullRequest, validateReadiness, validateEvent } from './collaboration-policy.mjs'; +import { validateIssue, validatePullRequest, validateReadiness, validateEvent, validateLinkedIssue } from './collaboration-policy.mjs'; const body = `## 변경 내용 @@ -105,6 +105,23 @@ test('Issue 선택 참고 항목 허용, 빈 참고와 영어 제목 거부', () assert.ok(validateIssue({ ...issue, body: issue.body + '\n## 참고\n' }).errors.length); assert.ok(validateIssue({ ...issue, title: 'fix: camera error' }).errors.length); }); +test('연결 Issue가 잘못된 양식이면 PR 검사 실패, 수정 후 통과', async () => { + const options = value => ({ repository: 'team-framework/innolive-client', token: 'test-token', fetchImpl: async url => { + assert.equal(url, 'https://api.github.com/repos/team-framework/innolive-client/issues/123'); + return { ok: true, json: async () => value }; + } }); + assert.ok((await validateLinkedIssue(pr, {}, options({ ...issue, body: '서술형 본문' }))).errors.length); + assert.deepEqual((await validateLinkedIssue(pr, {}, options(issue))).errors, []); +}); +test('기존 Issue는 소급 양식 검사 제외, PR 번호를 Issue 대신 연결하면 거부', async () => { + const options = value => ({ repository: 'team-framework/innolive-client', token: 'test-token', fetchImpl: async () => ({ ok: true, json: async () => value }) }); + assert.deepEqual((await validateLinkedIssue(pr, { legacyIssueMaxNumber: 123 }, options({ ...issue, body: '기존 본문' }))).errors, []); + assert.ok((await validateLinkedIssue(pr, {}, options({ ...issue, pull_request: {} }))).errors.length); +}); +test('연결 Issue 조회 실패는 검사 통과로 취급하지 않음', async () => { + const result = await validateLinkedIssue(pr, {}, { repository: 'team-framework/innolive-client', token: 'test-token', fetchImpl: async () => ({ ok: false, status: 404 }) }); + assert.ok(result.errors.length); +}); test('검사 CLI의 종료 코드와 Actions 요약으로 실패·복구 확인', async () => { const dir = await mkdtemp(join(tmpdir(), 'collaboration-policy-')); const event = join(dir, 'event.json'); diff --git a/.github/workflows/collaboration-policy.yml b/.github/workflows/collaboration-policy.yml index 6361450..87f718a 100644 --- a/.github/workflows/collaboration-policy.yml +++ b/.github/workflows/collaboration-policy.yml @@ -17,6 +17,7 @@ on: permissions: contents: read + issues: read concurrency: group: collaboration-policy-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} @@ -54,6 +55,10 @@ jobs: with: node-version: '24' - run: node .github/scripts/collaboration-policy.mjs pr-format + - name: 연결 Issue 본문 확인 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/scripts/collaboration-policy.mjs pr-issue merge-readiness: if: github.event_name == 'pull_request' diff --git a/docs/collaboration.md b/docs/collaboration.md index bd13a51..5ce9151 100644 --- a/docs/collaboration.md +++ b/docs/collaboration.md @@ -87,7 +87,7 @@ Issue·브랜치·PR 타입은 `feat/fix/chore/refactor` 사용. 개별 커밋 ## 자동 검사와 적용 시점 -- `Collaboration / PR format`: 제목·브랜치·섹션 순서·개조식·문장형 종결·항목 수·이슈 연결 검사 +- `Collaboration / PR format`: 제목·브랜치·섹션 순서·개조식·문장형 종결·항목 수·이슈 연결 및 연결 Issue 본문 검사 - `Collaboration / merge readiness`: Draft의 미완료 체크 허용, Ready PR의 미완료 체크 차단 - `Collaboration / policy tests`: 검사기의 정상·거부·Draft·Ready 전환 회귀 검증 - `Collaboration / Issue format`: 새 Issue의 본문 검사, 오류 시 `needs-format` 표시, 수정 후 표시 제거 @@ -99,6 +99,8 @@ Issue·브랜치·PR 타입은 `feat/fix/chore/refactor` 사용. 개별 커밋 merge commit 사용, squash·rebase 비활성화. 병합 제목은 PR head 브랜치 전체 이름, 본문은 빈 값 지정. +GitHub 기본 설정은 PR 제목·빈 본문 조합 사용. 웹의 기본 제목은 `merge: `를 지원하지 않으므로 아래 명령 또는 웹 제목 편집으로 직접 지정. + ```bash head_branch=$(gh pr view --json headRefName --jq .headRefName) head_sha=$(gh pr view --json headRefOid --jq .headRefOid) From f584bfe5b2105fcf168c5d54381d191c72b2b8bd Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:30:28 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=ED=95=98=EB=84=A4=EC=8A=A4=20App=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=EC=97=90=20=EB=A7=9E=EC=B6=B0=20workflow=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EA=B2=BD=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/collaboration.md | 4 ++++ sync/manifest.ts | 9 +++++++-- tests/harness-sync-installation.test.ts | 2 +- tests/sync.test.ts | 7 +++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/collaboration.md b/docs/collaboration.md index 5ce9151..9464196 100644 --- a/docs/collaboration.md +++ b/docs/collaboration.md @@ -114,3 +114,7 @@ gh pr merge --merge --match-head-commit "$head_sha" \ ## 규칙 원본 공통 문서·템플릿·검사기·작성 스킬 원본은 `team-framework/framework-agent-harness-sync`. MCP는 자동 동기화 제외를 유지하고 같은 변경을 별도 PR로 적용. 저장소 고유 지침과 관련 없는 작업 보존. + +현재 하네스 App 권한은 Contents·PR 쓰기이며 Workflows 쓰기 권한은 없음. 검사 workflow의 최초 설치와 workflow 자체 변경은 관리자 PR로 별도 적용. 자동 동기화는 문서·템플릿·스킬·검사기 파일을 전파하며 workflow 파일을 작성하지 않음. 새 저장소에는 `manualBootstrapItems`의 workflow와 저장소별 적용 기준·필수 검사 설정을 먼저 적용. + +연결 Issue 양식 수정 후 PR 검사가 실패 상태라면 해당 PR 검사를 재실행하거나 PR 본문을 갱신하여 최신 Issue를 검사. diff --git a/sync/manifest.ts b/sync/manifest.ts index 868219d..69d6542 100644 --- a/sync/manifest.ts +++ b/sync/manifest.ts @@ -43,11 +43,16 @@ export const syncItems: SyncItem[] = [ ...[ "docs/collaboration.md", ".github/scripts/collaboration-policy.mjs", - ".github/scripts/collaboration-policy.test.mjs", - ".github/workflows/collaboration-policy.yml" + ".github/scripts/collaboration-policy.test.mjs" ].map((path) => ({ source: path, destination: path })) ]; +// GitHub App has Contents/PR permissions; workflow bootstrap uses a maintainer PR. +export const manualBootstrapItems: SyncItem[] = [{ + source: ".github/workflows/collaboration-policy.yml", + destination: ".github/workflows/collaboration-policy.yml" +}]; + export const syncBranch = "harness-sync/framework-agent"; export const syncCommitMessage = "chore: 에이전트 협업 규칙 동기화"; export const syncPullRequestTitle = "chore: framework-agent-harness-sync"; diff --git a/tests/harness-sync-installation.test.ts b/tests/harness-sync-installation.test.ts index a64e8ad..b8b4c98 100644 --- a/tests/harness-sync-installation.test.ts +++ b/tests/harness-sync-installation.test.ts @@ -62,7 +62,7 @@ test("설치된 위키와 MCP 저장소는 읽거나 쓰지 않고 다른 레포 assert.ok(tree.tree.some((file) => file.path === "CLAUDE.md")); assert.ok(tree.tree.some((file) => file.path === "docs/collaboration.md")); assert.ok(tree.tree.some((file) => file.path === ".github/scripts/collaboration-policy.mjs")); - assert.ok(tree.tree.some((file) => file.path === ".github/workflows/collaboration-policy.yml")); + assert.equal(tree.tree.some((file) => file.path.startsWith(".github/workflows/")), false); assert.equal(tree.tree.some((file) => file.path === ".github/collaboration-policy.json"), false); const pullRequest = calls.find((call) => call.path.endsWith("/pulls") && call.options.method === "POST"); assert.deepEqual(JSON.parse(pullRequest.options.body), { diff --git a/tests/sync.test.ts b/tests/sync.test.ts index 80a09be..a0d390b 100644 --- a/tests/sync.test.ts +++ b/tests/sync.test.ts @@ -6,7 +6,7 @@ import test from "node:test"; import { applyHarnessFiles } from "../sync/apply.ts"; import { selectTargets } from "../sync/discover.ts"; import { renderSyncReport } from "../sync/report.ts"; -import { syncItems } from "../sync/manifest.ts"; +import { syncItems, manualBootstrapItems } from "../sync/manifest.ts"; import { syncPullRequestBody } from "../sync/pr-body.ts"; import { validatePullRequest, validateReadiness } from "../.github/scripts/collaboration-policy.mjs"; @@ -141,9 +141,12 @@ test("전체 동기화 후에도 저장소별 기존 기록 기준과 제품 파 assert.equal(await readFile(join(target, ".github", "collaboration-policy.json"), "utf8"), cutoff); assert.equal(await readFile(join(target, "product.txt"), "utf8"), "existing product\n"); assert.equal(await readFile(join(target, "docs", "collaboration.md"), "utf8"), await readFile("docs/collaboration.md", "utf8")); - for (const path of [".github/scripts/collaboration-policy.mjs", ".github/scripts/collaboration-policy.test.mjs", ".github/workflows/collaboration-policy.yml"]) { + for (const path of [".github/scripts/collaboration-policy.mjs", ".github/scripts/collaboration-policy.test.mjs"]) { assert.equal(await readFile(join(target, path), "utf8"), await readFile(path, "utf8")); } + await assert.rejects(readFile(join(target, ".github/workflows/collaboration-policy.yml")), { code: "ENOENT" }); + await applyHarnessFiles({ sourceRoot: process.cwd(), targetRoot: target, items: manualBootstrapItems }); + assert.equal(await readFile(join(target, ".github/workflows/collaboration-policy.yml"), "utf8"), await readFile(".github/workflows/collaboration-policy.yml", "utf8")); assert.equal(syncItems.some(item => item.destination === ".github/collaboration-policy.json"), false); }); From bca1ec2603e5724d83d3c7d4022411e71df2fc7e Mon Sep 17 00:00:00 2001 From: chaeyn Date: Thu, 1 Oct 2026 15:34:15 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=EC=84=9C=EC=88=A0=ED=98=95=20?= =?UTF-8?q?=EC=A2=85=EA=B2=B0=20=ED=91=9C=ED=98=84=EC=9D=98=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EA=B2=80=EC=82=AC=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/collaboration-policy.mjs | 4 ++-- .github/scripts/collaboration-policy.test.mjs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/scripts/collaboration-policy.mjs b/.github/scripts/collaboration-policy.mjs index 1c7ecc4..5a8c8c6 100644 --- a/.github/scripts/collaboration-policy.mjs +++ b/.github/scripts/collaboration-policy.mjs @@ -5,7 +5,7 @@ const ISSUE_HEADINGS = ['작업 내용', '작업 체크리스트', '완료 기 const PR_HEADINGS = ['변경 내용', '검증 결과', '머지 체크리스트', '관련 이슈']; const ISSUE_TITLE = /^(feat|fix|chore|refactor): \S.*$/u; const BRANCH = /^(feat|fix|chore|refactor)\/[a-z0-9]+(?:-[a-z0-9]+)*\/#([1-9]\d*)$/u; -const POLITE_ENDING = /(?:했습니다|하였습니다|되었습니다|합니다|됩니다|입니다|습니다|했어요|해요|이에요|예요|있어요|없어요|할게요)[.!。]*$/u; +const SENTENCE_ENDING = /(?:했습니다|하였습니다|되었습니다|합니다|됩니다|입니다|습니다|했어요|해요|이에요|예요|있어요|없어요|할게요|주세요|줘요|한다|했다|하였다|된다|되었다|됐다|있다|없다|이다|였다)[.!。]*$/u; const PLACEHOLDER = /^(?:핵심 변경 사항|실행한 검증과 결과|검증 결과 입력|작업 내용 입력|완료 조건 입력|관련 이슈 입력|TODO|TBD|내용 입력|\.{3})$/iu; function error(errors, message) { errors.push(message); } @@ -50,7 +50,7 @@ function bullets(lines = [], name, errors, { max = Infinity, checklist = false, const text = checklist ? match[2] : match[1]; if (PLACEHOLDER.test(text.trim())) error(errors, `${name}: 예시 문구 대신 실제 내용 작성`); const prose = text.replace(/`[^`]*`/gu, '').replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1').trim(); - if (POLITE_ENDING.test(prose)) error(errors, `${name}: 명사형 종결 사용`); + if (SENTENCE_ENDING.test(prose)) error(errors, `${name}: 명사형 종결 사용`); if (!allowLinks && /^https?:\/\//u.test(text)) error(errors, `${name}: 링크 대신 핵심 내용 작성`); items.push({ text, checked: checklist && match[1].toLowerCase() === 'x' }); } diff --git a/.github/scripts/collaboration-policy.test.mjs b/.github/scripts/collaboration-policy.test.mjs index ab60db6..bb61675 100644 --- a/.github/scripts/collaboration-policy.test.mjs +++ b/.github/scripts/collaboration-policy.test.mjs @@ -60,7 +60,11 @@ test('미완료 항목이 있는 Issue는 진행 추적 허용', () => assert.de test('GitHub 웹 Issue Form의 H3 섹션도 같은 양식으로 검사', () => assert.deepEqual(validateIssue({ ...issue, body: issue.body.replaceAll('## ', '### ') }).errors, [])); test('Issue의 완료 기준 누락 거부', () => assert.ok(validateIssue({ ...issue, body: issue.body.split('## 완료 기준')[0] }).errors.length)); test('본문 서술형 문단 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('- 카메라 중복 세션 생성 차단', '카메라 중복 세션 생성 차단') }).errors.length)); -test('명사형 종결과 다른 말투 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('생성 차단', '생성을 차단했습니다.') }).errors.length)); +test('명사형 종결과 다른 말투 거부', () => { + for (const prose of ['생성을 차단했습니다.', '생성을 차단해요.', '생성을 차단한다.', '생성을 차단했다.', '생성을 차단해주세요.']) { + assert.ok(validatePullRequest({ ...pr, body: body.replace('생성 차단', prose) }).errors.length, prose); + } +}); test('코드 안의 종결 표현은 문체 검사에서 제외', () => assert.deepEqual(validatePullRequest({ ...pr, body: body.replace('생성 차단', '생성 차단: `했습니다`') }).errors, [])); test('구현 과정 섹션 추가 거부', () => assert.ok(validatePullRequest({ ...pr, body: body + '\n## 구현 과정\n\n- 함수 추가\n' }).errors.length)); test('변경 항목 4개 이상 거부', () => assert.ok(validatePullRequest({ ...pr, body: body.replace('- 카메라 중복 세션 생성 차단', '- 카메라 세션 정리\n- 카메라 권한 확인\n- 카메라 입력 교체\n- 카메라 오류 안내') }).errors.length));