GitHubのAdmin権限に関する課題
組織のGitHubリポジトリを運用していると、Admin権限の管理に頭を悩ませることがあります。
Admin権限が多すぎる問題
セキュリティの観点から、最小権限の原則に従ってAdmin権限を持つユーザーはなるべく少なくしたいものです。Admin権限があれば、リポジトリの削除やブランチ保護ルールの変更など、影響の大きい操作が可能になってしまいます。
オンコール担当者には権限が必要
一方で、オンコール担当者は障害対応時に強い権限が必要になる場合があります。例えば:
- 緊急時のホットフィックスをマージする
- ブランチ保護を一時的に解除する
- 本番環境へのデプロイを実行する
これらの操作を迅速に行うために、オンコール担当者にはAdmin権限が必要なケースがあります。
手動での権限管理は大変
しかし、オンコール担当者は毎週(または毎日)交代します。そのたびに手動でAdmin権限を付け替えるのは:
- 作業が面倒
- 付け替え忘れのリスク
- 権限が残ったままになる可能性
といった問題があります。
解決策
PagerDutyとGitHubを連携させる
ZOZOではオンコール担当者の管理にPagerDutyを使用しています。PagerDutyにはスケジュール機能があり、誰がいつオンコール担当なのかが明確に管理されています。
そこで思いついたのが、PagerDutyからオンコール担当者の情報を取得して、GitHubのチームに自動反映させるという方法です。
具体的には:
- PagerDuty APIで現在のオンコール担当者を取得
- 担当者をGitHubの特定チーム(Admin権限を持つチーム)に追加
- オンコールではなくなった人をチームから削除
- これをGitHub Actionsで定期実行
これにより、オンコール担当者だけがAdmin権限を持つチームに所属している状態を自動的に維持できます。
作ってみた
Claude Codeで実装
今回は一切人間がコードを書かずに、すべてClaude Codeに書かせることを目標にしました。
Claude Codeに出した指示は以下の通りです:
PagerDutyのオンコール担当者をGitHub Organizationの特定チームに自動同期するNode.jsスクリプトを作成してください。
## 背景・目的
オンコール担当者が変わるたびに手動でGitHubチームのメンバーを更新するのは手間がかかります。
PagerDutyのスケジュールを正として、GitHubチームのメンバーを自動的に同期させたいです。
## 機能要件
### 1. PagerDutyからオンコール担当者を取得
- PagerDuty REST APIを使用
- 複数のスケジュールIDを指定可能(配列で設定)
- 現在のオンコール担当者のメールアドレスを取得
- 複数スケジュールから取得した担当者の重複は排除
### 2. GitHubチームとの同期
- 取得した担当者をGitHubチームに追加
- オンコールではなくなった担当者をチームから削除
### 3. ユーザーマッピング
- PagerDutyのメールアドレスとGitHubユーザー名を紐付け
- それぞれのユーザーのマッピングは設定ファイルで管理
- マッピングに存在しないユーザーがオンコールにいた場合はエラーメッセージを出力して異常終了
### 4. 保護ユーザー機能
- 削除対象から除外するユーザーを指定可能
- 用途: PAT所有者やbotアカウントなど常にチームに残しておきたいユーザー
- 設定ファイルで配列として管理
### 5. Dry Runモード
- 設定で true/false を切り替え可能
- trueの場合: 追加・削除の対象をログ出力するのみで実際のAPI呼び出しは行わない
- falseの場合: 実際に追加・削除を実行
### 6. エラーハンドリング
- API呼び出しエラー時は詳細をログ出力して異常終了
### 7. ログ出力
- 各処理が進むごとにログを出力する
- エラー発生時のログにはエラー解決に役立つ情報を追加する
## 設定要件
### 環境変数(.env)で管理するもの
- PAGERDUTY_API_KEY: PagerDuty REST APIキー
- GITHUB_TOKEN: GitHub Personal Access Token
### YAML設定ファイル(config.yaml)で管理するもの
- pagerduty.scheduleIds: スケジュールIDの配列
- github.org: Organization名
- github.teamSlug: チームのスラッグ
- dryRun: Dry Runモードのon/off
- protectedUsers: 保護ユーザーの配列
- mapping: メールアドレスとGitHubユーザー名のマッピング配列
## デプロイ要件
### GitHub Actions
- .github/workflows/sync.yml を作成
- 毎時0分(UTC)に自動実行(cron: '0 * * * *')
- 手動実行も可能(workflow_dispatch)
- 必要なシークレット: PAGERDUTY_API_KEY, GITHUB_API_KEY
## ドキュメント要件
### README.md
以下の内容を含める:
1. プロジェクト概要
2. 機能一覧
3. セットアップ手順
- PagerDuty APIキーの作成方法(詳細に)
- スケジュールIDの確認方法
- GitHub PATの作成方法(詳細に)
4. config.yamlの設定例
5. ローカル実行方法
6. GitHub Actionsの設定方法(シークレット設定)
7. よくあるエラーと対処法
8. ファイル構成
実際の開発プロセス
実際には、上記のような完璧な指示を最初から出したわけではありません。ステップバイステップで開発を進め、途中で何度か微修正を繰り返しました。例えば:
- GITHUB_API_KEYという環境変数名はGitHub Actionsで利用できなかったので、別の名前に修正
- エラー発生時の
process.exit(1)が抜けていて、GitHub Actionsでエラーが検出されない問題を修正
しかし、最初の指示でほぼ形にしてくれたので、人間がコードを書くことは一切ありませんでした。
生成されたファイル構成
.
├── .github/workflows/sync.yml # GitHub Actionsワークフロー
├── config.yaml # 設定ファイル
├── index.js # メインスクリプト(約120行)
├── package.json
└── README.md # セットアップ手順を含むドキュメント
ソースコード解説
生成されたソースコードを解説します。
設定ファイル(config.yaml)
まず、設定ファイルを見てみましょう。APIキー以外のすべての設定をこのファイルで管理しています。
# PagerDuty - GitHub Team Sync 設定ファイル
# PagerDuty設定
pagerduty:
scheduleIds:
- PXXXXXX
- PXXXXXX
# GitHub設定
github:
org: <GitHub Organization>
teamSlug: <GitHub Team>
# 実行設定
dryRun: false # trueならログ出力のみ、falseなら実行
# 保護ユーザー(削除対象から除外されるユーザー)
# PAT所有者など、常にチームに残す必要があるユーザーを指定
protectedUsers:
- <botユーザー>
# PagerDuty Email → GitHub ユーザー名のマッピング
mapping:
- pagerduty: <PagerDutyに登録しているメールアドレス>
github: <GitHubのユーザー名>
- pagerduty: <PagerDutyに登録しているメールアドレス>
github: <GitHubのユーザー名>
設定は以下の5つのセクションに分かれています:
| セクション | 説明 |
|---|---|
pagerduty.scheduleIds |
取得対象のPagerDutyスケジュールID(複数指定可) |
github.org / github.teamSlug
|
同期先のGitHub OrganizationとチームのSlug |
dryRun |
trueにすると実際の変更は行わずログ出力のみ |
protectedUsers |
削除対象から除外するユーザー(botアカウントなど) |
mapping |
PagerDutyのメールアドレスとGitHubユーザー名の対応 |
メインスクリプト(index.js)
次に、メインスクリプトを部分ごとに解説します。
初期化部分
require('dotenv').config();
const { Octokit } = require('@octokit/rest');
const fs = require('fs');
const yaml = require('js-yaml');
// --- 環境変数の読み込み(APIキーのみ) ---
const { PAGERDUTY_API_KEY, GITHUB_TOKEN } = process.env;
// --- 設定ファイルの読み込み ---
const config = yaml.load(fs.readFileSync('config.yaml', 'utf8'));
const pagerdutyScheduleIds = config.pagerduty.scheduleIds || [];
const githubOrg = config.github.org;
const githubTeamSlug = config.github.teamSlug;
const isDryRun = config.dryRun;
const protectedUsers = config.protectedUsers || [];
// マッピング配列をオブジェクトに変換
const mappingList = config.mapping || [];
const mapping = {};
for (const item of mappingList) {
mapping[item.pagerduty] = item.github;
}
// --- GitHubクライアント初期化 ---
const octokit = new Octokit({ auth: GITHUB_TOKEN });
ここでは以下の処理を行っています:
-
dotenvで
.envファイルから環境変数を読み込み -
js-yamlで
config.yamlを読み込み、各設定値を変数に展開 - マッピング配列を
{ "email": "github-username" }形式のオブジェクトに変換(後の検索を効率化) - @octokit/restでGitHub APIクライアントを初期化
APIキー(秘密情報)は環境変数で、それ以外の設定はYAMLファイルで管理するという設計にしています。
PagerDutyからオンコール担当者を取得
// 1. PagerDutyから現在の担当者を取得
console.log(`📡 PagerDutyから情報を取得中... (スケジュール数: ${pagerdutyScheduleIds.length})`);
const scheduleParams = pagerdutyScheduleIds.map(id => `schedule_ids[]=${id}`).join('&');
const pdUrl = `https://api.pagerduty.com/oncalls?${scheduleParams}&include[]=users`;
const pdRes = await fetch(pdUrl, {
headers: {
'Authorization': `Token token=${PAGERDUTY_API_KEY}`,
'Accept': 'application/vnd.pagerduty+json;version=2'
}
});
if (!pdRes.ok) throw new Error(`PD API Error: ${pdRes.statusText}`);
const pdData = await pdRes.json();
// メールアドレスを抽出(重複排除)
const emails = [...new Set(pdData.oncalls.map(oc => oc.user.email))];
console.log(`📞 現在のPD担当 (${emails.length}名): ${emails.join(', ')}`);
PagerDuty REST API v2の/oncallsエンドポイントを呼び出しています。ポイントは:
- 複数のスケジュールIDを
schedule_ids[]=ID1&schedule_ids[]=ID2形式でクエリパラメータに展開 -
include[]=usersでユーザー情報も含めて取得 - Node.js 18+標準の
fetchを使用(外部ライブラリ不要) -
Setを使って重複するメールアドレスを排除
マッピング検証
// 3. GitHubユーザー名に変換
const unmappedEmails = emails.filter(email => !mapping[email]);
if (unmappedEmails.length > 0) {
console.error(`❌ マッピングが未設定のユーザーがいます: ${unmappedEmails.join(', ')}`);
console.error('config.yaml の mapping に追加してください。');
process.exit(1);
}
const targetUsers = emails.map(email => mapping[email]);
console.log(`🎯 GitHub同期対象 (${targetUsers.length}名): ${targetUsers.join(', ')}`);
マッピングに存在しないユーザーがいた場合は、エラーメッセージを出力してprocess.exit(1)で異常終了します。これにより:
- 設定漏れに気づける
- GitHub Actionsでワークフローが失敗として記録される
差分計算
// 4. GitHubチームの現在のメンバーを取得
console.log('getting current GitHub team members...');
const currentMembersData = await octokit.rest.teams.listMembersInOrg({
org: githubOrg,
team_slug: githubTeamSlug,
});
const currentMembers = currentMembersData.data.map(m => m.login);
// 5. 差分計算と適用
// 追加が必要なユーザー
const toAdd = targetUsers.filter(u => !currentMembers.includes(u));
// 削除が必要なユーザー (現在のメンバーだが、ターゲットに含まれていない人)
// ただし、保護ユーザーは削除対象から除外
const toRemove = currentMembers.filter(u => !targetUsers.includes(u) && !protectedUsers.includes(u));
if (toAdd.length === 0 && toRemove.length === 0) {
console.log('✅ 変更はありません。同期完了。');
return;
}
GitHubチームの現在のメンバーを取得し、差分を計算しています。削除対象の計算ではprotectedUsersを除外しているのがポイントです。
メンバーの追加・削除
// --- 追加処理 ---
for (const user of toAdd) {
if (isDryRun) {
console.log(`[DRY RUN] ➕ 追加予定: ${user}`);
} else {
console.log(`➕ メンバーを追加中: ${user}`);
await octokit.rest.teams.addOrUpdateMembershipForUserInOrg({
org: githubOrg,
team_slug: githubTeamSlug,
username: user,
role: 'member'
});
}
}
// --- 削除処理 ---
for (const user of toRemove) {
if (isDryRun) {
console.log(`[DRY RUN] ➖ 削除予定: ${user}`);
} else {
console.log(`➖ メンバーを削除中: ${user}`);
await octokit.rest.teams.removeMembershipForUserInOrg({
org: githubOrg,
team_slug: githubTeamSlug,
username: user
});
}
}
console.log('✨ 処理が完了しました');
Dry Runモードの場合はログ出力のみ、そうでなければ実際にGitHub APIを呼び出してメンバーを追加・削除します。
エラーハンドリング
} catch (error) {
console.error('❌ エラーが発生しました:', error);
process.exit(1);
}
}
syncOnCall();
エラーが発生した場合はprocess.exit(1)で異常終了します。これがないと、GitHub Actionsでエラーが検出されず、ワークフローが成功扱いになってしまいます。
GitHub Actionsワークフロー(.github/workflows/sync.yml)
name: Sync PagerDuty OnCall to GitHub Team
on:
schedule:
# 毎時0分に実行 (UTC)
- cron: '0 * * * *'
workflow_dispatch:
# 手動実行も可能
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run sync script
env:
PAGERDUTY_API_KEY: ${{ secrets.PAGERDUTY_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
run: node index.js
ワークフローのポイント:
| 設定 | 説明 |
|---|---|
schedule.cron |
毎時0分(UTC)に自動実行 |
workflow_dispatch |
GitHub UIからの手動実行を有効化 |
cache: 'npm' |
依存関係をキャッシュして実行時間を短縮 |
secrets.GH_PAT |
自動生成のGITHUB_TOKENではなく、PATを使用(チーム管理権限のため) |
動作している様子
ローカル実行
ローカルで実行すると、以下のようなログが出力されます:
🚀 同期を開始します (Dry Run: false)
🛡️ 保護ユーザー: data-sre-bot-auth
📡 PagerDutyから情報を取得中... (スケジュール数: 2)
📞 現在のPD担当 (2名): user1@example.com, user2@example.com
🎯 GitHub同期対象 (2名): github-user1, github-user2
getting current GitHub team members...
➕ メンバーを追加中: github-user1
➖ メンバーを削除中: old-user
✨ 処理が完了しました
GitHub Actionsでの定期実行
GitHub Actionsでは、毎時0分に自動実行されます。Actionsタブを見ると、定期的にワークフローが実行されていることが確認できます。
また、workflow_dispatchを設定しているので、Actionsタブから手動で実行することも可能です。オンコール担当者が急遽変更になった場合など、すぐに反映させたいときに便利です。
エラー時の動作
マッピングが未設定のユーザーがオンコールに入った場合、以下のようにエラーを出力して異常終了します:
❌ マッピングが未設定のユーザーがいます: newuser@example.com
config.yaml の mapping に追加してください。
GitHub Actionsではこのエラーが検出され、ワークフローが失敗として記録されます。これにより、設定漏れに気づくことができます。
この記事もClaude Codeで書いた
実はこのブログ記事自体も、Claude Codeに書かせました。
与えた指示は、各章の構成と、それぞれの章で何を説明したいのかを箇条書きにしたものです:
# はじめに
・ここは人間が書くので空欄にして下さい
# GitHubのAdmin権限に関する課題
・Adminが多すぎるのは最小権限の原則に反するからなるべく減らしたい
・一方でオンコール担当者はもしものときに強い権限を持っていないといけない場合がある
・担当者は毎週変わっているから、毎週Adminを入れ替えるのは面倒
# 解決策
・ZOZOではPagerDutyをオンコール担当者の管理に使っている
・PagerDutyからオンコール担当者の情報を取得して、それをGitHubのチームに反映させれば良いのではと思いつく
...(以下省略)
このように、構成と要点だけ人間が考えれば、文章化はClaude Codeがやってくれます。