はじめに
こんにちは、iOSアプリエンジニアの羽柴です。本記事では、LINEの膨大なソースコードを扱う上で欠かせない「コードオーナー」の設定率を向上させるため、GitHub Actionsとして実装したチェッカーツールについてご紹介します。
プロジェクトの背景
LINEは、2011年のリリース以来急成長を遂げて来たアプリです。その巨大なコードベースにおいて、特に古くからある機能などに、どのチームがオーナーシップを持っているのかわからないことがあります。これを解決するのがコードオーナーです。ファイルにコードオーナーが設定されていれば、そのコードに修正を加える際に迅速な議論や開発をすることができます。したがって、LINEプロジェクトにおけるコードオーナーの設定率向上を目指す"CODEOWNER Improvements"というプロジェクトが発足しました。このプロジェクトの取り組みの一つが、今回紹介する"Codemap Checker"です。
LINEのコードオーナー管理
LINEアプリでは、codemap.yml
というファイルでコードオーナーを管理しています。ファイルは次のような形式をしています:
codemap.yml
- owners:
- "@hashiba-satsuki"
- "@LINE-Client/official-account-dev-ios"
path_patterns:
- "PATH/TO/FILE/OfficialAccountViewController.swift"
- "ANOTHER/PATH/TO/FILE/OfficialAccount/"
// ...その他メタ情報
owners
: オーナーシップを持つGitHubアカウントやユーザーグループ。横断的にオーナーシップを持つこともあるため、配列の形をとっています。path_patterns
: 該当のファイルを示すパス。ディレクトリ単位での指定も可能です。
GitHub標準のCODEOWNERファイルではなくcodemap.yml
にコードオーナーを書いている理由は以下の通りです:
- YAMLフォーマットで記述しやすい
- オーナー以外のメタ情報も同時に管理できる
- その他社内ツールとのインテグレーションのため
なお、レビュワーの指定やGitHub上での確認を 可能にするため、CODEOWNERファイルはcodemap.yml
から自動的に生成しています。
このcodemap.yml
は各開発者が適宜更新しなければならず、更新を忘れてしまうという課題がありました。
既存のCodemap Checker
この更新漏れを防ぐために、Codemap Checkerという社内ツールが存在しています。これはGitHub webhookを使ったもので、プルリクエストで変更されたファイルのうちオーナーが設定されていないものをコメントしてくれるbotです。このチェッカーには次のような問題点がありました:
- 結果が見づらい: 既存のチェッカーでは、オーナーの存在しないファイル名がただ羅列されるだけになっています。また、5つまでしかファイル名が表示されず、あとは省略される仕様でした。
- 設定漏れ以外の警告がない: コードオーナーはディレクトリ単位でも指定できる一方で、ファイル追加やリネームの際に意図せず異なるコードオーナーが設定されてしまうケースもありました。現状のチェッカーでは、これに気付くことができません。
- メンテナンス性の低さ: GitHub webhookを用いているためデバッグが煩雑であったり、プルリクエストのその他チェッカーと複合的に実装されているために変更が加えづらかったりして、メンテナンスが難しくなっていました。
新しいCodemap Checker
こうした背景から、GitHub Actionsを用いて新しいCodemap Checkerを実装する取り組みが開始されました。
なぜGitHub Actionsを選んだのか
GitHub Webhookを利用するには、外部サーバーを用意する必要があり、これに伴うサーバーの管理やメンテナンスコストが発生します。さらに、イベントの受信ロジックを記述したり、エラーハンドリングを考慮したりする必要があるため、実装がより複雑になります。
一方で、GitHub ActionsはGitHubプラットフォームに統合されており、YAMLファイルを使って簡単に設定できます。また、アクションの実行状態は自動的に追跡され、UIで確認することもできます。こうした環境により、開発者はアクション自体の実装に集中することができます。
これらの理由から、私たちはGitHub Actionsを選択しました。
Codemap Checkerの要件
プルリクエストの作成時/更新時に、そのプルリクエスト上で変更されたファイルを取得し、それらに対してcodemap.yml
でコードオーナーが設定されているかを確認します。設定されていない場合にはエラーを出し、更新が必要な可能性がある場合には警告を出します。「更新が必要な可能性があるファイル」とは、「該当のプルリクエストにて追加もしくはリネームされたが、コードオーナーが変更されていないファイル」と定義しました。
実行結果はGitHub ActionsのJob Summariesに表示します。プルリクエストへのコメントも検討しましたが、現在GitHubからの通知が社内のメールストレージを圧迫しているという問題が発生しているため、Job Summariesに出力するのみとしました。
実装の概要
使用言語には、LINEアプリ向けツールチェーンの言語統一のためPythonを採用しました。Codemap Checkerは以下の図のような構成になっています:
主要なクラスは以下の通りです:
GitHubClient
: GitHub APIとやりとりするためのクライアント。CodemapProvider
:codemap.yml
を読み込み、解析するプロバイダー。CodeOwnerChecker
: 変更されたファイルに対してコードオーナーの問題を検出するチェッカー。CommentBuilder
: チェック結果をGitHub Actionsのサマリーとして出力するためのビルダー。
また、以下が使用した外部モジュールです:
- PyGithub: GitHub REST APIにアクセスできるPythonライブラリ。このモジュールを使用することで、リポジトリやプルリクエストの情報を簡単に取得できます。
- PyYAML: YAMLファイルをPythonのデータ構造に変換するためのライブラリ。
codemap.yml
の解析に使用しています。 - pathspec: ファイルパスのパターンマッチングを行うためのライブラリ。
codemap.yml
で指定されたパスパターンに基づいて、ファイルのコードオーナーを解決する際に使用します。
Tips: ChatGPTの活用
GitHub Actionsの開発もPythonの利用も今回がほぼ初めてですが、ChatGPTを使うことで効率的に開発を進めることができました。ChatGPTに尋ねた質問には以下のようなものです:
- GitHub ActionsをDockerコンテナとして提供する場合、ディレクトリ構成はどのようにすればいいですか?
- Pythonにおける単体テストの記述方法を教えてください。
- Swiftのクロージャのように、Pythonで遅延評価をするには?
実装の詳細
1. 環境変数の取得
GitHub Actionsから提供される環境変数を取得します。これには、リポジトリ名、プルリクエスト番号、トークンなどが含まれます。
main.py
token = os.getenv("GITHUB_TOKEN")
repo_name = os.getenv("GITHUB_REPOSITORY")
pull_number = int(os.getenv("GITHUB_PR_NUMBER"))
ref = os.getenv("GITHUB_REF")
base_ref = os.getenv("GITHUB_BASE_REF")
token
とpull_number
はデフォルトの引数にないので、アクションの入力として受け取っています。後に記載するaction.yml
の定義を参照してください。
2. GitHubから情報を取得
GitHubClient
クラスを使用して、プルリクエストで変更されたファイルとcodemap.yml
の内容を取得します。ブランチ間でのコードオーナーの差分を考慮するため、ベースブランチのcodemap.yml
も同様に取得します。
main.py
github = GitHubClient(config.HOSTNAME, token, repo_name, pull_number)
changed_files = github.get_changed_files()
codemap_provider = CodemapProvider(
raw_codemap=github.get_content(config.CODEMAP_FILE_PATH, ref),
raw_base_codemap=github.get_content(config.CODEMAP_FILE_PATH, base_ref)
)
3. codemap.yml
の解析
CodemapProvider
内で、StringのYAMLデータを辞書型の配列に変換します。これは、PyYAMLのsafe_load
関数を用いて行われます。
codemap_provider.py
class CodemapProvider:
def build_codemap(self) -> list[dict]:
return yaml.safe_load(self.raw_codemap)
変換された配列は以下のような形になります:
[
[
"owners": ["@xxx", "@yyy"],
"path_patterns": ["PATH/TO/SOURCE/CODE.txt", "ANOTHER/PATH/"],
],
// ...
]
4. コードオーナー情報の解決
続いてCodeOwnerResolver
でコードオーナー情報を解決します。CodemapProvider
から得られるpath_patterns
は文字列のため、そのままではパターンマッチが煩雑になります。これを解決するために、CodeOwnerResolver
内でpath_patterns
からPathSpec
型を生成します:
code_owner_resolver.py
@dataclass
class PathSpecCodemapEntry:
owners: list[str]
path_spec: PathSpec
class CodeOwnerResolver:
@staticmethod
def compile_path_spec_codemap(codemap: list[dict]) -> list[PathSpecCodemapEntry]:
path_spec_codemap = []
for entry in codemap:
if "owners" not in entry or "path_patterns" not in entry:
continue
path_spec = PathSpec.from_lines(patterns.GitWildMatchPattern, entry["path_patterns"])
path_spec_codemap.append(PathSpecCodemapEntry(entry["owners"], path_spec))
return path_spec_codemap
コードオーナーを解決するロジックは以下の通りです:
code_owner_resolver.py
class CodeOwnerResolver:
@staticmethod
def resolve(
files: list[ChangedFile],
path_spec_codemap: list[PathSpecCodemapEntry],
should_ignore: Callable[[ChangedFile], bool]
) -> list[CodeOwnerInfo]:
code_owner_infos = []
for file in files:
if should_ignore(file):
continue
owners = []
for entry in path_spec_codemap:
if entry.path_spec.match_file(file.filename):
owners = entry.owners
break
code_owner_infos.append(CodeOwnerInfo(file=file, owners=owners))
return code_owner_infos
should_ignore
には、コードオーナーのチェックをスキップする条件を指定することができます。このCodemap Checkerでは、以下のような条件を設けています:
- ファイルのステータスがREMOVEDの場合
- ファイルの拡張子が画像や動画などリソースを表すものの場合
最後に、該当のプルリクエストでcodemap.yml
の変更があった場合は、変更前のcodemap.yml
上でのコードオーナーを取得し、現在のものと異なっている場合にCodeOwnerInfo
のis_updated
をtrue
にします。
5. 結果の生成・出力
CodeOwnerResolver
が作ったCodeOwnerInfo
の配列を元に、CodeOwnerChecker
内でCodeOwnerIssues
型としてチェックの結果を生成します。CodeOwnerIssues
が持つプロパティは次の2つです:
no_code_owners
: 変更されたファイルの うち、コードオーナーが存在しないファイルのリストoutdated_code_owners
: 変更されたファイルのうち、追加・リネームされたもので、かつそれらのコードオーナーが該当プルリクエスト上で変更されていないファイルのリスト
code_owner_checker.py
return CodeOwnerIssues(
no_code_owners=[info for info in code_owner_infos if not info.owners],
outdated_code_owners=[
info for info in code_owner_infos
if info.file.status in {FileStatus.ADDED, FileStatus.RENAMED} and info.owners and not info.is_updated
]
)
CodeOwnerIssues
を元にCommentBuilder
でMarkdown形式のコメントを生成し、GitHub Actionsのワークフローコマンドを用いて結果を出力します。コードオーナーがないファイルがある場合はアクションの実行結果を失敗とし、それ以外の場合は成功とします。また、同じ内容をGitHub ActionsのJob Summariesにも書き込みます。
main.py
summary_body = CommentBuilder.build_summary_body(code_owner_issues)
with open(os.getenv("GITHUB_STEP_SUMMARY"), "a") as summary_file:
summary_file.write(summary_body)
if code_owner_issues.no_code_owners:
print(f"::error:: {summary_body}")
// アクションの実行結果を失敗にする
sys.exit(1)
elif code_owner_issues.outdated_code_owners:
print(f"::warning:: {summary_body}")
else:
print(f"::notice:: {summary_body}")
Dockerコンテナアクションとして提供
Codemap CheckerはDockerコンテナアクションとして提供されています。依存関係の管理が容易であると同時に、どの環境でも一貫した動作を保証します。
Dockerfile
FROM python:3.9-alpine
WORKDIR /action/workspace
COPY requirements.txt *.py /action/workspace/
RUN pip install --no-cache-dir -r requirements.txt
ENTRYPOINT ["/action/workspace/main.py"]
また、action.yml
は以下のように定義されています:
action.yml
name: 'Check codemap'
description: 'Check if codemap is set for changed files in a PR'
inputs:
GITHUB_TOKEN:
description: "GitHub token used to fetch changed files from Github's API."
required: false
default: ${{ github.token }}
GITHUB_PR_NUMBER:
description: "GitHub PR number where action was executed."
required: false
default: ${{ github.event.number }}
runs:
using: 'docker'
image: 'Dockerfile'
env:
GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }}
GITHUB_PR_NUMBER: ${{ inputs.GITHUB_PR_NUMBER }}
テスト
pytestを使用し、CodeOwnerResolver
およびCodeOwnerChecker
のテストを記述しています。ビルドおよびテストの実行も、GitHub Actionsを用いて設定しています。
利用方法
Codemap Checkerは以下のようなYAMLファイルを.github/workflows
配下に作成することで利用できます:
check-codemap.yml
name: Check Codemap
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
check-codemap:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: LINE-Client/codemap-checker/check-changed-files@v1
今後の展望
GitHub Actionsのステータスには成功・失敗しかなく、成功として出力される警告は確実に見逃されると予想しています。プルリクエストにコメントを残せないか担当チームに掛け合うとともに、それ以外の方法も模索していきます。また、無効なユーザ(退職など)がコードオーナーに設定されている場合もあり、これを検知できるようにしたいと考えています。
おわりに
Codemap Checkerは現在LINEのiOSレポジトリに導入済みで、しばらく様子を見た後にそのままAndroidレポジトリにも適用される予定です。プルリクエストのステータスにエラーを表示することでcodemap.yml
の更新忘れを防ぎ、コードオーナー設定率の維持および向上に寄与することを期待しています。