cd ~/gh-pm-lab source pm-lab/env.sh
1통제 목표에서 규칙으로
통제 목표 하나에 규칙 하나 또는 설정 하나가 대응한다. 아래 표의 규칙 유형과 설정 이름은 규칙 REST API(GET /repos/{owner}/{repo}/rulesets/{ruleset_id})의 rules[].type과 parameters에 나오는 이름이다.
| 통제 목표 | 규칙 유형 | 설정 | 막는 일 |
|---|---|---|---|
| 승인 없이 병합하지 않는다 | pull_request | required_approving_review_count | PR 없이 푸시하거나 승인 수가 모자란 PR을 병합하는 일 |
| 새 커밋이 올라오면 이전 승인을 무효로 한다 | pull_request | dismiss_stale_reviews_on_push | 승인 뒤에 바꾼 코드가 다시 검토되지 않고 들어가는 일 |
| 마지막 푸시는 푸시하지 않은 사람이 승인한다 | pull_request | require_last_push_approval | 리뷰어가 직접 커밋을 더하고 자기 승인으로 병합하는 일 |
| 코드 소유자가 승인한다 | pull_request | require_code_owner_review | B1의 기준선 파일이 소유자 승인 없이 바뀌는 일 |
| 리뷰 대화를 모두 해결한 뒤 병합한다 | pull_request | required_review_thread_resolution | 리뷰 지적이 답 없이 남은 채 병합되는 일 |
| 필수 검사를 통과한 뒤 병합한다 | required_status_checks | required_status_checks, strict_required_status_checks_policy | 테스트가 실패했거나 최신 main 기준으로 검사하지 않은 코드의 병합 |
| 강제 푸시로 이력을 고쳐 쓰지 않는다 | non_fast_forward | - | 병합된 커밋을 지우거나 바꾸는 일 |
| 기본 브랜치를 지우지 않는다 | deletion | - | main 자체의 삭제 |
| 이력을 선형으로 유지한다 | required_linear_history | - | 병합 커밋으로 이력이 갈라지는 일 |
| 서명된 커밋만 받는다 | required_signatures | - | 작성자를 확인할 수 없는 커밋 |
| 병합 큐를 거쳐 병합한다 | merge_queue | - | 각자는 통과했지만 함께 합치면 실패하는 PR들이 연달아 들어가는 일 |
| 배포 환경에서 성공한 뒤 병합한다 | required_deployments | required_deployment_environments | 스테이징 배포가 실패한 변경의 병합 |
모두 예방 통제다. 규칙이 없을 때 같은 목표를 지키려면 실습 26처럼 기록을 사후에 검사하는 탐지 통제를 둔다.
2규칙 세트와 브랜치 보호
GitHub에는 같은 목적의 기능이 두 가지 있다. 오래된 브랜치 보호 규칙(branch protection rules)과 2023년에 정식 출시된 규칙 세트다. 새로 설정한다면 규칙 세트를 쓴다.
| 항목 | 브랜치 보호 규칙 | 규칙 세트 |
|---|---|---|
| 여러 규칙이 같은 브랜치에 걸릴 때 | 가장 구체적인 패턴의 규칙 하나만 적용된다 | 모두 겹쳐 적용되고, 같은 규칙은 가장 엄격한 값이 쓰인다 |
| 상태 | 있거나 없다 | 적용(active), 평가(evaluate), 꺼짐(disabled). 평가는 GitHub Enterprise에서만 쓸 수 있다 |
| 누가 볼 수 있는가 | 저장소 관리자 | 저장소에 읽기 권한이 있는 사람 모두. 우회 목록(bypass_actors)은 규칙 세트를 고칠 수 있는 사람에게만 보인다 |
| 예외 | 관리자에게 적용할지 여부 하나 | 우회 목록에 역할, 팀, 앱을 넣고 방식을 고른다 |
| 조직 단위 | 없다 | 조직 규칙 세트로 여러 저장소에 한 번에 건다 (Team, Enterprise) |
| gh 명령 | gh api .../branches/{branch}/protection | gh ruleset list, view, check와 gh api .../rulesets |
읽기 권한만으로 규칙 세트를 볼 수 있다는 점은 통제 점검에서 쓸모가 있다. PM이나 감사인이 관리자 권한을 받지 않고도 규칙이 있다는 증적을 직접 확인할 수 있다.
3규칙 세트 만들기
gh ruleset에는 list, view, check만 있고 만드는 명령은 없다. 웹의 Settings → Rules → Rulesets에서 만들거나, REST API(POST /repos/{owner}/{repo}/rulesets)에 JSON 본문을 보낸다. 키트의 본문은 main에 아래 규칙을 건다.
{
"name": "main 보호",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"bypass_actors": [],
"rules": [
{"type": "deletion"},
{"type": "non_fast_forward"},
{"type": "required_linear_history"},
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": true,
"require_last_push_approval": true,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["squash"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": true,
"do_not_enforce_on_create": false,
"required_status_checks": [{"context": "test"}]
}
}
]
}
| 칸 | 값 | 고칠 곳 |
|---|---|---|
conditions.ref_name.include | ~DEFAULT_BRANCH | 기본 브랜치 이름이 바뀌어도 따라간다. 다른 브랜치는 refs/heads/release/*처럼 적는다 |
bypass_actors | 빈 목록 | 아무도 우회할 수 없다. 저장소 관리자도 PR과 승인을 거쳐야 한다. 6단계에서 다시 정한다 |
required_status_checks[].context | test | 저장소 CI의 검사 이름과 같아야 한다. 검사가 아직 없으면 이 규칙을 지우고 만든다. 없는 검사를 필수로 두면 모든 PR이 병합되지 않는다 |
required_approving_review_count | 1 | 혼자 실습하는 저장소에서는 승인할 사람이 없어 자기 PR을 병합할 수 없다. 실습 짝을 협업자로 초대한다 |
gh api --method POST "repos/$PM_REPO/rulesets" \ --input github-pm-starter-kit/templates/ruleset-main.json \ --jq '"\(.id) \(.name) \(.enforcement)"'
개인 계정의 비공개 저장소에서 GitHub Free를 쓰면 이 요청은 실패한다. 실습 저장소를 공개로 바꾸거나(gh repo edit "$PM_REPO" --visibility public --accept-visibility-change-consequences) 웹 화면에서 요금제를 확인한다.
4걸린 규칙 읽기
아래 화면은 Launchpad 저장소에 10월 19일에 만든 규칙 세트 세 개를 읽는다. Launchpad 시나리오의 설정이므로 gh 2.101.0을 가짜 API 서버에 연결해 재현했다. 출력 형식은 gh가 만든 그대로다.
view의 Rules 아래 pull_request 줄에 설정 여섯 개가 이름순으로 나온다. dismiss_stale_reviews_on_push: false, require_last_push_approval: false, required_review_thread_resolution: false 세 개가 꺼져 있다. Bypass List의 RepositoryRole (ID: 5)는 저장소 역할 하나가 PR을 거쳐서(pull_request) 규칙을 우회할 수 있다는 뜻이다. ID가 어느 역할인지는 웹의 규칙 세트 화면에서 이름으로 확인한다. 규칙 세트가 여러 개면 브랜치 하나에 실제로 걸리는 규칙은 check로 본다.
check는 켜진(active) 규칙 세트의 규칙만 보여 준다. 꺼져 있는 4213 “서명된 커밋”의 required_signatures는 목록에 없다. 규칙 세트 목록에서 “있다”고 보이는 규칙과 브랜치에 “걸린” 규칙이 다를 수 있으므로, 증적은 check의 결과로 남긴다.
5PR 하나에서 확인
규칙이 동작하는지는 열린 PR 하나로 확인한다. 10월 30일 Launchpad에서 아직 리뷰를 받지 않은 PR #21을 읽는다.
| 필드 | 값 | 뜻 |
|---|---|---|
reviewDecision | REVIEW_REQUIRED | 필수 리뷰가 아직 모자란다 |
CHANGES_REQUESTED | 변경 요청이 남아 있다 | |
APPROVED | 필수 승인을 채웠다. 리뷰 규칙이 없는 저장소에서는 이 필드가 빈 값이다 (실습 9) | |
mergeStateStatus | BLOCKED | 규칙(승인, 필수 검사 등) 때문에 병합할 수 없다 |
BEHIND | main보다 뒤처져 있다. 최신 코드 기준 검사(strict)를 켰을 때 갱신이 필요하다 | |
UNSTABLE | 병합할 수 있지만 필수가 아닌 검사가 실패했다 | |
CLEAN | 병합할 수 있고 검사도 통과했다 | |
DIRTY, UNKNOWN, HAS_HOOKS | 충돌이 있다, 아직 계산하지 못했다, 서버 측 훅이 있다 |
규칙 세트를 만들기 전에는 reviewDecision이 빈 값이다(실습 9). 실습 저장소에서 PR을 하나 만들어 규칙 세트 전후의 두 값을 비교한다.
gh pr view 번호 -R "$PM_REPO" --json number,reviewDecision,mergeStateStatus gh pr merge 번호 -R "$PM_REPO" --squash
승인 없이 병합을 시도하면 규칙 세트가 거절한다. 거절 메시지를 캡처해 두면 “규칙이 동작한다”는 증적이 된다.
6직무 분리와 우회
직무 분리(segregation of duties)는 한 사람이 변경을 만들고 승인하고 반영하는 일을 혼자 하지 못하게 나누는 원칙이다. 코드 변경에서는 작성자와 승인자를 다르게 두는 4-eyes 원칙(네 개의 눈)으로 나타난다. GitHub는 PR 작성자가 자기 PR을 승인하지 못하게 막지만, 다른 사람이 커밋을 더하고 그 사람이 승인하는 경우는 require_last_push_approval이 켜져 있어야 막는다.
규칙에는 예외가 필요한 순간이 있다. 출시 직전의 장애 수정을 승인할 사람이 없는 경우다. 규칙 세트는 우회 목록에 역할, 팀, GitHub App, 배포 키를 넣고 방식을 정한다.
우회 방식 (bypass_mode) | 동작 | 통제 관점 |
|---|---|---|
pull_request | PR을 통해서만 규칙을 건너뛰어 병합할 수 있다. 브랜치 대상 규칙 세트에만 쓴다 | PR 기록이 남으므로 사후에 찾을 수 있다 |
always | PR 없이 직접 푸시해도 규칙을 건너뛴다 | PR 목록에 나오지 않는 변경이 생긴다 |
exempt | 규칙을 평가하지 않는다 | 우회했다는 감사 기록도 남지 않는다 |
작은 팀은 승인자를 따로 둘 수 없는 순간이 생기므로 우회를 완전히 없애기 어렵다. 이때는 우회를 pull_request 방식의 관리자 역할 하나로 좁히고, 우회해 병합한 PR에는 이유와 사후 리뷰 담당을 적은 예외 기록 댓글을 남기고, 정해진 주기로 그 기록을 확인하는 탐지 통제를 함께 둔다. 예방 통제가 약한 자리를 탐지 통제로 메우는 것을 보완 통제(compensating control)라고 한다.
배포 환경의 승인
배포를 승인한 사람을 남기려면 배포 환경(environment)에 필수 리뷰어를 둔다. 워크플로가 그 환경으로 배포하는 작업에 이르면 리뷰어의 승인을 기다린다. Free, Pro, Team 요금제에서는 공개 저장소에서만 필수 리뷰어를 쓸 수 있다.
REVIEWER_ID=$(gh api users/dev-kim --jq .id)
gh api --method PUT "repos/$PM_REPO/environments/production" --input - <<EOF
{"prevent_self_review": true,
"reviewers": [{"type": "User", "id": $REVIEWER_ID}],
"deployment_branch_policy": {"protected_branches": true, "custom_branch_policies": false}}
EOF
gh api "repos/$PM_REPO/environments/production" --jq '[.name, (.protection_rules[] | .type)] | @tsv'
prevent_self_review는 배포를 시작한 사람이 자기 배포를 승인하지 못하게 한다. protected_branches: true는 보호 규칙이 있는 브랜치에서만 이 환경으로 배포하게 한다. 병합 큐는 조직이 소유한 저장소에서만 쓸 수 있어 개인 계정의 실습 저장소에서는 만들 수 없다.
7접근 권한
규칙은 권한을 가진 사람에게 걸린다. 누가 어떤 권한을 갖는지가 통제의 범위를 정한다. 조직 저장소에는 기본 역할 다섯 개가 있다.
| 역할 | 할 수 있는 일 (요약) | Launchpad에 맞는 사람 |
|---|---|---|
| read | 코드 읽기, 이슈와 PR에 댓글 | 마케팅 담당, 감사인 |
| triage | read에 더해 이슈와 PR의 라벨, 담당자, 닫기를 관리한다. 코드는 쓸 수 없다 | QA (테스트 항목과 이슈만 다룬다) |
| write | 브랜치 푸시, PR 승인, 규칙이 허용하면 병합 | 개발자 |
| maintain | write에 더해 일부 저장소 설정을 바꾼다. 규칙 세트와 협업자 관리 같은 민감한 설정은 할 수 없다 | 개발 리드 |
| admin | 모든 설정, 규칙 세트, 협업자, 우회 권한 | 저장소 소유자 한두 명 |
최소 권한(least privilege)은 일에 필요한 가장 낮은 역할을 주는 원칙이다. 조직은 사람마다 역할을 주기보다 팀(team)에 역할을 주고 사람을 팀에 넣어, 사람이 바뀌어도 권한 표가 그대로 유지되게 한다. 개인 계정 저장소는 역할을 나눌 수 없다. 소유자가 admin이고 협업자는 모두 write와 같은 권한을 받는다.
사람 대신 프로그램이 쓰는 토큰도 접근 권한이다. 실습 18의 스냅숏 워크플로는 classic 토큰을 썼다. classic 토큰은 범위(scope)를 고르면 그 계정이 접근할 수 있는 모든 저장소에 같은 권한이 생긴다. 세분화된 개인 토큰(fine-grained personal access token)은 저장소와 권한 항목을 하나씩 고르고 만료일을 정한다. GitHub App은 설치된 저장소에만 1시간짜리 토큰을 받는다. 오래 쓰는 자동화는 GitHub App이나 세분화된 토큰으로 옮기고, 토큰마다 만든 사람, 용도, 만료일을 기록한다.
# 팀 명단에 없는 협업자를 지운다 (개인 계정 저장소) gh api --method DELETE "repos/$PM_REPO/collaborators/intern-cho" # 조직 저장소에서는 역할을 낮춘다 (permission: pull, triage, push, maintain, admin) gh api --method PUT "repos/ORG/REPO/collaborators/qa-park" -f permission=triage
8통제 매트릭스
통제 매트릭스는 통제 목표마다 설정, 판정, 우회, 증적 명령을 한 줄로 적은 표다. control-matrix.sh는 규칙 세트 상세 JSON과 협업자 JSON으로 이 표의 앞부분을 만든다. 규칙 세트 목록 API는 규칙(rules)을 주지 않으므로 규칙 세트마다 상세를 받아 모은다.
받은 두 파일은 키트의 samples/b-rulesets.json, samples/b-collaborators.json과 같다. 팀 명단은 실습 25의 roles.tsv를 쓴다.
bash github-pm-starter-kit/control-matrix.sh pm-lab/rulesets.json pm-lab/collaborators.json \ --roster github-pm-starter-kit/roles.tsv
# 통제 매트릭스: gildong/launchpad-pm-lab · main - 규칙 세트 3개 (적용 2, 평가만 0, 꺼짐 1), main에 걸리는 것 2개 - 협업자 5명: admin 1, write 4 ## 통제 목표와 설정 | 통제 목표 | 규칙 | 설정 | 판정 | 규칙 세트 | 우회 | 증적 명령 | | - | - | - | - | - | - | - | | 승인 없이 병합하지 않는다 | pull_request | 승인 1명, 병합 방식 squash | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 새 커밋이 올라오면 이전 승인을 무효로 한다 | pull_request | dismiss_stale_reviews_on_push 꺼짐 | **약함** | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 마지막 푸시는 푸시하지 않은 사람이 승인한다 | pull_request | require_last_push_approval 꺼짐 | **약함** | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 코드 소유자가 승인한다 | pull_request | require_code_owner_review 켜짐 | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 리뷰 대화를 모두 해결한 뒤 병합한다 | pull_request | required_review_thread_resolution 꺼짐 | **약함** | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 필수 검사를 통과한 뒤 병합한다 | required_status_checks | 검사 test, 최신 코드 기준 켜짐 | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 강제 푸시로 이력을 고쳐 쓰지 않는다 | non_fast_forward | - | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 기본 브랜치를 지우지 않는다 | deletion | - | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 이력을 선형으로 유지한다 | required_linear_history | - | 적용 | main 보호 (active) | RepositoryRole 5 (pull_request) | `gh ruleset view 4211` | | 서명된 커밋만 받는다 | required_signatures | - | **꺼짐** | 서명된 커밋 (disabled) | 없음 | `gh ruleset view 4213` | | 배포 환경에서 성공한 뒤 병합한다 | required_deployments | - | **없음** | - | - | `gh ruleset check main` | | 병합 큐를 거쳐 병합한다 | merge_queue | - | **없음** | - | - | `gh ruleset check main` | | 릴리스 태그를 지우거나 옮기지 않는다 | tag: deletion, update | refs/tags/v* | 적용 | 릴리스 태그 보호 (active) | 없음 | `gh ruleset view 4212` | 판정: 적용(켜진 규칙 세트에 있고 설정이 켜짐), 약함(규칙은 있으나 설정이 꺼짐), 꺼짐·평가만(규칙 세트가 강제되지 않음), 없음(규칙이 없음). ## 우회 권한 | 규칙 세트 | 대상 | 상태 | 우회 주체 | | - | - | - | - | | 4211 main 보호 | branch ~DEFAULT_BRANCH | active | RepositoryRole 5 (pull_request) | | 4212 릴리스 태그 보호 | tag refs/tags/v* | active | 없음 | | 4213 서명된 커밋 | branch ~DEFAULT_BRANCH | disabled | 없음 | ## 접근 권한 | 아이디 | 역할(role_name) | admin | maintain | push | triage | pull | 명단의 역할 | | - | - | - | - | - | - | - | - | | dev-kim | write | 꺼짐 | 꺼짐 | 켜짐 | 켜짐 | 켜짐 | 개발 | | dev-lee | write | 꺼짐 | 꺼짐 | 켜짐 | 켜짐 | 켜짐 | 개발 | | gildong | admin | 켜짐 | 켜짐 | 켜짐 | 켜짐 | 켜짐 | PM | | intern-cho | write | 꺼짐 | 꺼짐 | 켜짐 | 켜짐 | 켜짐 | **명단에 없음** | | qa-park | write | 꺼짐 | 꺼짐 | 켜짐 | 켜짐 | 켜짐 | QA | ## 확인할 것 - 새 커밋이 올라오면 이전 승인을 무효로 한다: 약함 (pull_request, dismiss_stale_reviews_on_push 꺼짐) - 마지막 푸시는 푸시하지 않은 사람이 승인한다: 약함 (pull_request, require_last_push_approval 꺼짐) - 리뷰 대화를 모두 해결한 뒤 병합한다: 약함 (pull_request, required_review_thread_resolution 꺼짐) - 서명된 커밋만 받는다: 꺼짐 (required_signatures) - 배포 환경에서 성공한 뒤 병합한다: 없음 (required_deployments) - 병합 큐를 거쳐 병합한다: 없음 (merge_queue) - 관리자(admin) 1명: gildong. 규칙 세트를 고치고 지울 수 있다. - 쓰기 권한 이상 5명, 필요한 승인 1명. 작성자를 빼고 승인할 수 있는 사람은 최대 4명이다. - 팀 명단에 없는 협업자: intern-cho. 권한을 회수할지 확인한다.
판정이 “적용”이 아닌 줄과 확인할 것을 하나씩 정리한다.
| 결과 | 원인 | 조치 |
|---|---|---|
dismiss_stale_reviews_on_push, require_last_push_approval 약함 | 10월 19일 규칙 세트를 만들 때 기본값(꺼짐)으로 두었다 | 둘 다 켠다. 승인 뒤에 커밋을 더하면 다시 승인을 받아야 한다 |
required_review_thread_resolution 약함 | 같은 이유 | 켤지 팀과 정한다. 켜면 대화를 해결로 표시하는 습관이 필요하다 |
| 서명된 커밋 꺼짐 | 팀원이 커밋 서명을 설정하기 전이라 꺼 두었다 | 서명 설정을 마친 날 4213을 적용으로 바꾸거나, 계획이 없으면 지운다. 꺼진 규칙 세트는 통제가 있는 것처럼 보이게 한다 |
| 배포 환경, 병합 큐 없음 | Launchpad는 배포 워크플로가 없고, 개인 계정 저장소라 병합 큐를 쓸 수 없다 | 통제 목표에서 빼고, 뺀 이유를 매트릭스에 적는다 |
| intern-cho 명단에 없음 | 인턴 기간이 끝난 뒤에도 협업자로 남았다 | 권한을 회수한다. 회수 날짜와 명령을 기록한다 |
꺼진 설정 두 개는 키트 본문으로 규칙 세트를 갱신해 켠다. PUT은 본문의 규칙으로 규칙 세트 전체를 바꾸므로, 본문에 빠진 규칙은 지워진다.
gh api --method PUT "repos/$PM_REPO/rulesets/4211" \ --input github-pm-starter-kit/templates/ruleset-main.json --jq '.updated_at' gh ruleset view 4211 -R "$PM_REPO"
스크립트의 표에 팀이 두 칸을 더하면 산출물인 통제 매트릭스가 된다. 점검 주기(매주, 분기, 변경할 때)와 담당(1선, 2선)이다. 예를 들어 “승인 없이 병합하지 않는다”는 매 PR마다 GitHub가 막고(예방, 상시), 매주 2선이 우회 병합 PR을 확인하고(탐지, 매주), 분기마다 3선이 gh ruleset check 결과를 증적으로 받는다(검토, 분기).
막히면
| 증상 | 확인과 해결 |
|---|---|
| 규칙 세트 만들기가 HTTP 403 오류로 실패한다 | 개인 계정의 비공개 저장소다. 공개로 바꾸거나 요금제를 확인한다 |
| 규칙 세트 만들기가 HTTP 422 오류로 실패한다 | 본문의 JSON이 틀렸다. jq . github-pm-starter-kit/templates/ruleset-main.json으로 문법을 보고, 규칙 유형과 설정 이름의 철자를 확인한다 |
no rulesets found in ... | 규칙 세트가 없거나 다른 저장소를 보고 있다. echo "$PM_REPO"를 확인한다 |
모든 PR이 BLOCKED이고 승인해도 병합되지 않는다 | 필수 검사 이름이 CI의 검사 이름과 다르다. PR 화면의 Checks 이름과 required_status_checks[].context를 맞춘다 |
| 혼자 실습해서 자기 PR을 병합할 수 없다 | 실습 짝을 협업자로 초대한다(gh api --method PUT "repos/$PM_REPO/collaborators/아이디"). 짝이 없으면 승인 수를 0으로 두고 나머지 규칙만 실습한다 |
control-matrix.sh가 “rules가 없다”고 한다 | 목록 API(.../rulesets)의 결과를 넣었다. 8단계의 명령처럼 규칙 세트마다 상세를 받아 jq -s .로 모은다 |
| 매트릭스의 우회 칸이 “알 수 없음”이다 | 규칙 세트를 고칠 권한이 없는 계정으로 받았다. 우회 목록은 그 권한이 있어야 API 응답에 들어 있다 |
- 통제 목표 다섯 개 이상을 규칙 유형과 설정 이름에 대응시켰다.
- 실습 저장소에 main 규칙 세트를 만들고
gh ruleset check --default로 걸린 규칙을 확인했다. - 규칙 세트 전후로 PR 하나의
reviewDecision과mergeStateStatus를 비교했다. - 우회 방식 세 가지의 차이와, 우회를 남겨 둘 때의 보완 통제를 설명할 수 있다.
- 협업자 목록을 팀 명단과 대조했다.
control-matrix.sh결과에 점검 주기와 담당 칸을 더해 통제 매트릭스를 만들었다.
마치며...
규칙 세트는 승인과 검사를 병합 시점에 강제하는 예방 통제이고, 매트릭스는 그 통제가 지금 어떤 상태인지 보여 주는 증적이다. Launchpad의 main에는 승인 1명과 필수 검사가 걸려 있었지만, 승인 뒤의 커밋을 다시 검토하게 하는 설정 두 개가 꺼져 있었고, 서명 규칙은 꺼진 채 남아 있었고, 팀을 떠난 협업자가 쓰기 권한을 갖고 있었다. 설정이 남긴 결과는 릴리스에 들어간 PR의 기록에 나타난다. B3에서는 M1 릴리스의 PR 8건을 업무, 리뷰, 검사와 연결한 추적 보고로 만들고, 꺼진 설정이 어떤 PR로 이어졌는지 찾는다.