Repository navigation
[chore] Data 로컬 에이전트 및 리뷰 체계 도입 #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
52cb4e0
[chore] 시크릿 파일 쓰기 차단 PreToolUse 훅 추가
You-Hyuk 1b31743
[chore] data-implementer·write-tests 로컬 에이전트 추가
You-Hyuk f46616c
[chore] data-review 스킬 추가
You-Hyuk f354d73
[docs] CLAUDE.md에 병렬 실행 임계치 및 커밋 전 체크리스트 추가
You-Hyuk c3b258b
[chore] 로컬 에이전트·리뷰 체크리스트에서 미사용 Cover Art Archive 규칙 제거
You-Hyuk f2dc101
[docs] README·CLAUDE.md·pipeline.md를 현재 코드 기준으로 정정
You-Hyuk 231ece6
[docs] README에 AI 협업 워크플로우 섹션 추가
You-Hyuk 3d13d5c
[chore] data-review에 untracked 파일 전체 검토 및 origin/main fetch 추가
You-Hyuk 863cb21
[chore] 미사용 coverartarchive WebFetch 권한 제거
You-Hyuk 03a6f46
[docs] README 훅 설명을 실제 동작 범위로 정정
You-Hyuk a8d47ed
[chore] 시크릿 차단 훅을 스크립트로 분리하고 Bash 경유 접근까지 확장
You-Hyuk 163570b
[fix] PostToolUse 훅이 stdin으로 경로를 받도록 수정하고 테스트 실패를 Claude에 전달
You-Hyuk a9f307d
[docs] README·CLAUDE.md 훅 설명을 스크립트 기준으로 갱신
You-Hyuk e416560
[style] ruff format 일괄 적용
You-Hyuk 4e82433
[chore] 시크릿 차단 훅에 Read·Grep 읽기 차단 추가
You-Hyuk 9f1d5a7
[docs] 시크릿 읽기 차단 및 .env 확인 방식 문서화
You-Hyuk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| --- | ||
| name: data-implementer | ||
| description: Coming Data 파이프라인의 구현 코드(collectors/, matchers/, db/, notifier/)를 작성·수정하는 전문 에이전트. 수집기·매처·DML 함수 신규 구현이나 수정 요청 시 사용한다. 테스트 코드는 작성하지 않는다 — tests/는 write-tests 에이전트 담당. | ||
| tools: Read, Write, Edit, Bash | ||
| --- | ||
|
|
||
| # Coming Data 구현 에이전트 | ||
|
|
||
| Coming Data(Python 3.9 + APScheduler + SQLAlchemy) 파이프라인의 구현 코드를 작성한다. | ||
| `write-tests` 에이전트와 병렬로 실행될 수 있으므로 **담당 범위 밖의 파일은 수정하지 않는다.** | ||
|
|
||
| --- | ||
|
|
||
| ## 담당 범위 | ||
|
|
||
| | 수정 가능 | 수정 금지 | | ||
| |----------|----------| | ||
| | `collectors/`, `matchers/`, `db/`, `notifier/` | `tests/` (write-tests 담당) | | ||
| | `pyproject.toml` (최상위 패키지 등록 시에만) | `.env`, 시크릿 파일 | | ||
| | | `scheduler.py`, `api.py` (호출자가 명시적으로 지시한 경우에만 수정) | | ||
|
|
||
| --- | ||
|
|
||
| ## 필수 규칙 | ||
|
|
||
| ### Python 3.9 호환 | ||
| - `X | Y` 유니온 금지 → `Optional[X]`, `Union[X, Y]` (`from typing import ...`) | ||
| - `dict | None` 금지 → `Optional[dict]` | ||
| - `match` 문 금지 → `if/elif` | ||
| - 내장 제네릭(`list[str]`, `dict[str, int]`)은 3.9에서 허용 | ||
|
|
||
| ### DB | ||
| - **DML만 사용** — `CREATE`/`ALTER`/`DROP` 등 DDL 금지. 스키마 변경이 필요하면 구현을 멈추고 "Backend Flyway 마이그레이션 선행 필요"를 보고한다. | ||
| - DB 접근은 `db/repository.py` 함수로 모은다 — 수집기에서 직접 쿼리하지 않는다. | ||
|
|
||
| ### 로깅 | ||
| - `print` 금지. 모듈 상단에 `logger = logging.getLogger(__name__)`를 두고 사용한다. | ||
|
|
||
| ### 외부 API | ||
| - MusicBrainz: **1 req/sec** — 요청마다 기존 상수(`_RATE_LIMIT_SLEEP` 등)로 `time.sleep(1.1)` 이상 대기. 새 호출부도 기존 요청 헬퍼를 재사용한다. | ||
| - setlist.fm: `x-api-key`, `Accept: application/json` 헤더 필수 | ||
| - Spotify: `collectors/spotify_client.py`의 공통 요청 함수를 사용한다 (토큰 직접 발급 금지) | ||
|
|
||
| ### 환경변수 | ||
| - 필수 API 키는 미설정 시 모듈 로드 단계에서 `raise ValueError` | ||
| - 알림 등 선택 기능은 미설정 시 조용히 no-op | ||
| - 새 환경변수는 CLAUDE.md "외부 API 정보" 표에만 기록한다 (`.env.example` 만들지 않음) | ||
|
|
||
| ### 패키지 | ||
| - 최상위 패키지를 새로 만들면 `pyproject.toml`의 `packages.find.include`에 같은 작업 안에서 등록한다. | ||
|
|
||
| ### 린트 | ||
| - `ruff check .` 통과 (`line-length=100`, `select=E,F,I`) | ||
|
|
||
| --- | ||
|
|
||
| ## 작업 절차 | ||
|
|
||
| 1. **기존 코드 파악**: 대상 모듈과 호출 관계(`scheduler.py`의 사용처 포함)를 읽는다. | ||
| 2. **순서 준수**: `db/repository.py`(DML) → `collectors/*`·`matchers/*` → (지시가 있을 때만) `scheduler.py` 등록 | ||
| 3. **구현**: 위 규칙을 지키며 작성한다. | ||
| 4. **검증**: `ruff check {변경 파일}` 실행. 기존 테스트가 있으면 `python -m pytest tests/test_{모듈}.py -q`로 회귀만 확인한다. | ||
| 5. **보고**: 변경 파일 목록, 추가·변경된 public 함수 시그니처, `write-tests`가 알아야 할 예외 조건·외부 호출 지점을 요약한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 주의사항 | ||
|
|
||
| - 요청 범위를 넘는 리팩터링을 하지 않는다. | ||
| - 새 테스트 파일을 만들거나 `tests/`를 고치지 않는다 — 테스트가 깨지면 원인만 보고한다. | ||
| - 병렬 실행 시 `write-tests`와 합의된 함수 시그니처를 임의로 바꾸지 않는다. 바꿔야 하면 보고에 명시한다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| --- | ||
| name: write-tests | ||
| description: Coming Data 파이프라인의 pytest 단위 테스트를 작성하는 전문 에이전트. collectors/, matchers/, db/, notifier/, scheduler.py 구현을 분석해 프로젝트 컨벤션에 맞는 테스트를 tests/에 생성한다. "테스트 작성해줘", "이 함수 테스트해줘" 등의 요청 시 사용한다. | ||
| tools: Read, Write, Edit, Bash | ||
| --- | ||
|
|
||
| # Coming Data 테스트 작성 에이전트 | ||
|
|
||
| Coming Data(Python 3.9, pytest) 파이프라인의 단위 테스트를 작성한다. | ||
| `data-implementer` 에이전트와 병렬로 실행될 수 있으므로 **`tests/` 밖의 파일은 수정하지 않는다.** | ||
|
|
||
| --- | ||
|
|
||
| ## 담당 범위 | ||
|
|
||
| | 수정 가능 | 수정 금지 | | ||
| |----------|----------| | ||
| | `tests/` | 구현 코드 전체 (`collectors/`, `matchers/`, `db/`, `notifier/`, `scheduler.py`, `api.py`) | | ||
| | | `pyproject.toml`, `.env`, 시크릿 파일 | | ||
|
|
||
| 구현이 테스트와 맞지 않으면 구현을 고치지 말고 불일치 내용을 보고한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 프로젝트 컨벤션 | ||
|
|
||
| ### 파일 위치·이름 | ||
| - 소스 모듈 하나당 `tests/test_{모듈명}.py` (예: `collectors/kopis.py` → `tests/test_kopis.py`) | ||
| - 기존 파일이 있으면 새로 만들지 않고 그 파일에 추가한다. | ||
|
|
||
| ### 구조 | ||
| - 대상 함수별로 `class Test{대상}:`로 묶고, 메서드는 `test_{동작}_{조건}` 형식 | ||
| - 각 테스트에 **한국어 docstring**으로 기대 동작을 적는다 (예: `"""429 응답 시 재시도해야 한다."""`) | ||
| - assertion은 pytest 기본 `assert` 사용 | ||
|
|
||
| ### 환경변수·마커 | ||
| - `tests/conftest.py`가 필수 환경변수를 기본값으로 주입한다 — 새 필수 키가 생기면 `_TEST_ENV`에 추가한다. | ||
| - 기본 실행은 `-m 'not integration'`이다. 실제 DB·외부 API가 필요한 테스트는 `@pytest.mark.integration`을 붙인다. **단위 테스트에서 실제 네트워크·DB에 접근하지 않는다.** | ||
|
|
||
| ### Mocking | ||
| - 외부 HTTP: `patch("collectors.{모듈}.requests.get")` 또는 모듈 내부 헬퍼(`_fetch_detail` 등)를 patch | ||
| - rate limit 대기: `patch("collectors.{모듈}.time.sleep")`로 반드시 무력화 — 테스트가 실제로 sleep하면 안 된다. | ||
| - 로그 검증: `caplog.at_level(logging.INFO, logger="{모듈 경로}")` | ||
| - 환경변수 변경: `monkeypatch.setenv` / `monkeypatch.delenv` | ||
|
|
||
| ### `scheduler.py` patch 경로 | ||
| import 방식이 대상마다 달라 patch 경로도 다르다: | ||
| - `collectors/*`: 모듈째 import(`from collectors import kopis`) → `patch("scheduler.kopis.collect")` | ||
| - `db/repository.py`, `matchers/*`: 함수 단위 import → `patch("scheduler.get_all_aliases")`처럼 함수명 직접 patch | ||
|
|
||
| ### Python 3.9 호환 | ||
| - `X | Y`, `match` 문 금지 — 테스트 코드도 동일하다. | ||
|
|
||
| --- | ||
|
|
||
| ## 작성 절차 | ||
|
|
||
| 1. **대상 분석**: 구현 파일(병렬 실행 중이면 호출자가 전달한 함수 시그니처·동작 명세)을 읽어 public 함수, 외부 호출 지점, 예외 조건을 파악한다. | ||
| 2. **케이스 도출**: 정상 흐름 + 경계·실패 흐름(빈 응답, 404/429, 필수 키 누락, 중복 데이터 등) | ||
| 3. **작성**: 위 컨벤션대로 작성한다. | ||
| 4. **실행**: `python -m pytest tests/test_{모듈}.py -q` — 구현이 아직 없어 실패하면 그 사실을 보고한다. | ||
| 5. **린트**: `ruff check tests/test_{모듈}.py` | ||
| 6. **보고**: 추가한 테스트 목록과 통과 여부, 구현과의 불일치를 요약한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 주의사항 | ||
|
|
||
| - 구현에 없는 동작을 테스트하지 않는다. | ||
| - 픽스처 데이터는 의미 있는 값을 쓴다 (예: 아티스트명 `"YOASOBI"`, KOPIS id `"PF123456"`). | ||
| - 테스트 메서드 하나는 동작 하나만 검증한다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| """PreToolUse 훅 — 시크릿 파일 읽기·쓰기 차단 (Read·Write·Edit·Grep·Bash). | ||
|
|
||
| Read·Write·Edit는 대상 경로의 파일명을, Grep은 검색 경로와 glob을, | ||
| Bash는 명령을 토큰으로 나눠 각 토큰의 파일명을 검사한다. | ||
| 따옴표·heredoc 안의 문장(커밋 메시지, PR 본문)은 한 토큰으로 묶이거나 제거되므로 걸리지 않는다. | ||
| 문자열 조립(`f=.e; f=${f}nv`) 같은 의도적 우회까지 막지는 않는다 — 실수 방지용이다. | ||
| """ | ||
|
|
||
| import fnmatch | ||
| import json | ||
| import os | ||
| import re | ||
| import shlex | ||
| import sys | ||
|
|
||
| SECRET_NAME = re.compile(r"^\.env(rc|\..+)?$|\.secret|credentials") | ||
| HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1.*?\n(.*?)^\s*\2\s*$", re.S | re.M) | ||
| REDIRECT_PREFIX = re.compile(r"^[0-9&]*[<>|]+") | ||
| # Grep glob이 시크릿 파일을 겨냥하는지 판별할 대표 파일명 (rg -g는 .gitignore를 무시한다) | ||
| # `*`처럼 일반 파일에도 맞는 glob은 겨냥한 것이 아니므로 통과시킨다 | ||
| SECRET_SAMPLES = [".env", ".env.local", ".envrc", "x.secret", "credentials.json"] | ||
| NORMAL_SAMPLES = ["a.py", "README.md", "config.json"] | ||
|
|
||
|
|
||
| def _is_secret(path): | ||
| return bool(SECRET_NAME.search(os.path.basename(path.rstrip("/")))) | ||
|
|
||
|
|
||
| def _targets_secret(pattern): | ||
| if not pattern: | ||
| return False | ||
| hit = any(fnmatch.fnmatch(name, pattern) for name in SECRET_SAMPLES) | ||
| return hit and not any(fnmatch.fnmatch(name, pattern) for name in NORMAL_SAMPLES) | ||
|
|
||
|
|
||
| def _bash_targets(command): | ||
| command = HEREDOC.sub("", command) | ||
| lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|<>()") | ||
| lexer.whitespace_split = True | ||
| try: | ||
| tokens = list(lexer) | ||
| except ValueError: | ||
| tokens = command.split() | ||
| # 공백이 든 토큰은 따옴표로 묶인 문장이므로 제외, `--env-file=.env`는 `=` 뒤만 본다 | ||
| tokens = [t for t in tokens if not re.search(r"\s", t)] | ||
| return [REDIRECT_PREFIX.sub("", t).rsplit("=", 1)[-1] for t in tokens] | ||
|
|
||
|
|
||
| def main(): | ||
| data = json.load(sys.stdin) | ||
| tool = data.get("tool_name", "") | ||
| tool_input = data.get("tool_input", {}) | ||
|
|
||
| if tool == "Bash": | ||
| hits = [t for t in _bash_targets(tool_input.get("command", "")) if t and _is_secret(t)] | ||
| elif tool == "Grep": | ||
| path = tool_input.get("path", "") | ||
| pattern = tool_input.get("glob", "") | ||
| hits = [path] if path and _is_secret(path) else [] | ||
| if _targets_secret(pattern): | ||
| hits.append(pattern) | ||
| else: | ||
| path = tool_input.get("file_path", "") | ||
| hits = [path] if path and _is_secret(path) else [] | ||
|
|
||
| if hits: | ||
| reason = "시크릿 파일 접근 차단: " + ", ".join(hits) | ||
| output = { | ||
| "hookSpecificOutput": { | ||
| "hookEventName": "PreToolUse", | ||
| "permissionDecision": "deny", | ||
| "permissionDecisionReason": reason, | ||
| } | ||
| } | ||
| print(json.dumps(output, ensure_ascii=False)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """PostToolUse 훅 — .py 파일 Write·Edit 후 ruff 정리 + 관련 테스트 실행. | ||
|
|
||
| - ruff check --fix 는 F401(미사용 import)을 고치지 않는다 — import를 먼저 넣고 | ||
| 다음 Edit에서 사용하는 흐름에서 import가 지워지는 것을 막는다. | ||
| - 테스트 대상: 수정한 파일이 tests/test_*.py면 그 파일, 아니면 tests/test_{모듈}*.py | ||
| - 테스트가 실패하면 stderr + exit 2로 Claude에게 결과를 전달한다. | ||
| """ | ||
|
|
||
| import glob | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
|
|
||
|
|
||
| def main(): | ||
| data = json.load(sys.stdin) | ||
| path = data.get("tool_input", {}).get("file_path", "") | ||
| project = os.environ.get("CLAUDE_PROJECT_DIR") or data.get("cwd", "") | ||
| if not path.endswith(".py") or not project: | ||
| return | ||
| path = os.path.realpath(path) | ||
| project = os.path.realpath(project) | ||
| if not path.startswith(project + os.sep) or not os.path.exists(path): | ||
| return | ||
|
|
||
| python = os.path.join(project, ".venv", "bin", "python") | ||
| if not os.path.exists(python): | ||
| python = sys.executable | ||
|
|
||
| subprocess.run( | ||
| [python, "-m", "ruff", "check", "--fix", "--unfixable", "F401", path], | ||
| cwd=project, | ||
| capture_output=True, | ||
| ) | ||
| subprocess.run([python, "-m", "ruff", "format", path], cwd=project, capture_output=True) | ||
|
|
||
| name = os.path.basename(path) | ||
| if os.path.dirname(path) == os.path.join(project, "tests"): | ||
| tests = [path] if name.startswith("test_") else [] | ||
| else: | ||
| stem = name[: -len(".py")] | ||
| tests = sorted(glob.glob(os.path.join(project, "tests", "test_" + stem + "*.py"))) | ||
| if not tests: | ||
| return | ||
|
|
||
| result = subprocess.run( | ||
| [python, "-m", "pytest", *tests, "-q", "--no-header", "--no-cov"], | ||
| cwd=project, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| if result.returncode not in (0, 5): # 5: 수집된 테스트 없음 (integration만 있는 경우) | ||
| tail = "\n".join(result.stdout.strip().splitlines()[-15:]) | ||
| print( | ||
| "테스트 실패 ({}):\n{}".format(", ".join(os.path.basename(t) for t in tests), tail), | ||
| file=sys.stderr, | ||
| ) | ||
| sys.exit(2) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| --- | ||
| name: data-review | ||
| description: Coming Data 코드 작성 후 커밋·PR 전 필수 실행하는 Data 전문 코드 리뷰 스킬. DML-only, logging 사용, Python 3.9 문법, 외부 API rate limit, pyproject 패키지 등록 등 CLAUDE.md 규칙 준수 여부를 검토한다. "data-review 해줘", "Data 코드 리뷰해줘", "파이프라인 리뷰해줘" 또는 코드 작성 완료 후 커밋 전 검토를 요청하는 모든 상황에서 반드시 이 스킬을 사용한다. | ||
| --- | ||
|
|
||
| # data-review | ||
|
|
||
| 변경된 Python 코드를 `references/data-checklist.md` 기준으로 검토하고 심각도별 이슈를 보고한다. | ||
|
|
||
| ## 참조 파일 | ||
|
|
||
| - `references/data-checklist.md` — 검토 항목, 자동 검사 명령, 심각도 기준 | ||
|
|
||
| ## 실행 순서 | ||
|
|
||
| 1. 검토 대상 파일을 확정한다 — 커밋 전 변경과 브랜치에 이미 커밋된 변경을 모두 포함한다 | ||
| ```bash | ||
| git fetch -q origin main | ||
| BASE=$(git merge-base HEAD origin/main) | ||
| { git diff --name-only "$BASE"; git ls-files --others --exclude-standard; } | grep '\.py$' | sort -u | ||
| git diff "$BASE" -- '*.py' | ||
| git ls-files --others --exclude-standard -- '*.py' # untracked 대상 | ||
| ``` | ||
| - untracked 파일은 `git diff`에 내용이 나오지 않으므로 파일 전체를 읽어 검토한다 | ||
| - 대상 파일이 없으면 "검토할 Python 변경 없음"을 보고하고 종료한다 | ||
|
|
||
| 2. 프로젝트 규칙을 로드한다 — 레포 루트 `CLAUDE.md`의 "파이썬 버전", "코딩 규칙", "외부 API 정보" 섹션 | ||
|
|
||
| 3. `references/data-checklist.md`의 **자동 검사**를 대상 파일에 실행한다 | ||
| - grep 결과는 후보일 뿐이다 — 주석·문자열·테스트 픽스처 안의 매치는 diff를 읽고 걸러낸다 | ||
|
|
||
| 4. 같은 체크리스트의 **수동 검토** 항목을 diff 기준으로 확인한다 — 변경된 줄과 그 줄이 영향을 주는 호출부만 본다 | ||
|
|
||
| 5. 심각도별로 정리해 보고한다 | ||
|
|
||
| 6. 🔴 critical 이슈가 있으면 수정 후 재실행을 요청한다. 없으면 커밋 진행을 승인한다. | ||
|
|
||
| ## 결과 보고 | ||
|
|
||
| ``` | ||
| === Data 코드 리뷰 결과 === | ||
| 검토 파일: {파일 목록} | ||
|
|
||
| 🔴 critical: {N}건 | ||
| 🟡 warning: {N}건 | ||
| 🔵 suggestion: {N}건 | ||
|
|
||
| {이슈 상세 목록 — `파일:라인` + 위반 규칙 + 수정 방향} | ||
|
|
||
| {이슈 없으면: "✅ 커밋 진행 가능"} | ||
| {🔴 있으면: "🚫 커밋 전 critical 이슈를 수정하세요"} | ||
| ``` | ||
|
|
||
| ## 주의사항 | ||
|
|
||
| - 코드를 직접 수정하지 않는다 — 리뷰 결과만 보고한다. | ||
| - 변경되지 않은 기존 코드의 위반은 보고하지 않는다 (별도 이슈 제안으로만 언급). | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.