GitHubのManage Accessのチーム紐付け as Code

# はじめに

みなさんはGitHub Organizationで管理するリポジトリのManage accessのチームの紐付けは普段どのように設定していますでしょうか？
![スクリーンショット 2020-12-19 22.22.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156024/f1ca9250-88ab-a5c9-a22b-d87b7e11c00a.png)
いつもポチポチ設定するの面倒ではないでしょうか？

```yaml:some_repository.yml
read:
  - team1
  - team2
write:
  - team3
admin:
  - team4
```
こんな感じにリポジトリごとにYamlを用意してコード管理できたら嬉しくないですか？

GitHub APIを扱えるPyGithubを使って考えてみました。
https://github.com/PyGithub/PyGithub

# ディレクトリ構成

構成としてはこのような形です。

```bash
├── .github
│   ├── scripts
│   │   └── duplicate_team_check.py
│   └── workflows
│       └── duplicate_team_check.yml
├── README.md
├── organization_name # GitHub Organization名
│   ├── repo1.yml # リポジトリ名.yml
│   ├── repo2.yml
│   └── repo3.yml
└── main.py
```

ディレクトリ名、ファイル名を使い、Yamlに書かれた設定通りにチームとManage Accessを紐付けていきます。
また、Yaml内のチームの重複を防ぐためのCIも設定しています。

# 各種コード

## main.py

YamlをparseしてPyGithubで用意されている**update_team_repository**を利用して紐付けを行います。
Yaml側になくてGitHub側にあるチームは**remove_from_repos**でサヨナラします。

```python:main.py
import glob
import os
import sys
import threading
import yaml
from github import Github


file_list = glob.glob('./*/*')
g = Github(os.environ['TOKEN'])


class th(threading.Thread):

    def __init__(self, file):
        threading.Thread.__init__(self)
        self._file = file

    def run(self):
        l = self._file.split('/')
        owner_name = l[1]
        repo_name = os.path.splitext(os.path.basename(l[2]))[0]
        obj = self.yaml_load(f)
        org = g.get_organization(owner_name)
        repo = g.get_repo(owner_name + '/' + repo_name)
        self._delete_teams(repo, obj)
        for k, v in obj.items():
            for i in v:
                team = org.get_team_by_slug(i)
                team.update_team_repository(repo, k)
        print(f'Finished setting of {repo_name}')

    def yaml_load(self, path):
        try:
            with open(path) as file:
                obj = yaml.safe_load(file)
        except Exception as e:
            print(e, file=sys.stderr)
            sys.exit(1)
        return obj

    def delete_teams(self, repo, obj):
        teams = repo.get_teams()
        l = []
        for _, v in obj.items():
            l.append(v)
        l = sum(l, [])
        for team in teams:
            if team.name not in l:
                team.remove_from_repos(repo)

for f in file_list:
    t = th(f)
    t.start()
```

## リポジトリ名.yml

各権限がキー、チームが配列となるように書いていきます。

```yaml:リポジトリ名.yml
pull:
  - team2
push:
  - team3
admin:
  - team4
maintain:
  - team5
triage:
  - team1
```

各権限に関しては以下に書かれています。
https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization

## .github/scripts/duplicate_team_check.py

Yaml内ではチームがユニークであってほしいので、それをマージ前に防ぎましょう。

```yaml:こういうのを防ぎたい
pull:
  - team1
push:
  - team1
admin:
  - team1
```

```python:duplicate_team_check.py
import glob
import sys
import yaml

file_list = glob.glob('./*/*')


def yaml_load(path):
    try:
        with open(path) as file:
            obj = yaml.safe_load(file)
    except Exception as e:
        print(e, file=sys.stderr)
        sys.exit(1)
    return obj

for f in file_list:
    l = []
    obj = yaml_load(f)
    for _, v in obj.items():
        l.append(v)
    l = sum(l, [])
    dup = [x for x in set(l) if l.count(x) > 1]
    if dup:
        print(f'{dup} is duplicate in {f}')
        sys.exit(1)
```

## .github/workflows/duplicate_team_check.yml

```yaml:duplicate_team_check.yml
name: Duplicate Team Check

on:
  pull_request:
    types:
      - opened
      - synchronize

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
            fetch-depth: 2
      - name:
        run: python3 .github/scripts/duplicate_team_check.py 
```

# 実行

準備ができたら実行するだけです。

```
$ TOKEN=xxxxxxxxxx python3 main.py # トークンの権限はFull control of private repositoriesが必要
```

こんな感じで紐付けができていることが確認できます。
![スクリーンショット 2020-12-20 14.49.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156024/b7333a98-2a41-a24d-14d3-648a198de51a.png)

# その他

### CODEOWNERSとの連携

CODEOWNERSとの連携を行って、そのリポジトリのManage Accessの設定はそのリポジトリのadminチームのレビューを通さないとマージできない仕組みにすればいい感じかも。

```:.github/CODEOWNERS
organization-name/repo1.yml @organization-name/<repo1のadminを持つチーム>
```

CODEOWNERSについては[こちら](https://qiita.com/FumiyaShibusawa/items/c7a3ff4d0793ca2d281f)の記事が参考になります。

# 注意👺

### 権限設定の実行はCIで行わないこと

マージされたら権限設定も自動で行えればかっこいいと思うのですが、ちょっと危ないです。
GitHub Actionsではトリガーとなる部分が書き換えられたらfeatureブランチでも実行できちゃうので、悪意のある人が任意の権限を設定しちゃうかもしれません。

```yaml:やりたい例
on:
  pull_request:
    types:
      - closed
    branches:
      - master

env:
  TOKEN: ${{ secrets.TOKEN }}
```

↓

```yaml:悪用例
on:
  pull_request:
    types:
      - opened
      - synchronize

env:
  TOKEN: ${{ secrets.TOKEN }}
```

信頼を持てるメンバーであれば良いと思いますが、
基本的には**ローカルでそのOrganizationの責任を持つ人が実行する**のが良いでしょう。

### チームで設定すること

誤って外部コントリビューターをinviteしてしまう危険性があるため、チーム名で設定するのがおすすめです。
（ここはもしかしたらOrganizationの設定で防ぐことも可能かもしれません。）

# まとめ

良いことが多いです。

* ポチポチしなくて良い
* 権限設定のレビューができる⭐
* 時短

ちゃんと調べてないですが、すでにオープンソースであったりしそう…🤔

コードはこちらにあります。
いい感じに改修してくれる方、プルリク待ってます！
https://github.com/hikimochi/gmac