本エントリはGMOペパボエンジニア Advent Calendar 2021の25日目のエントリです。メリークリスマス!
はじめまして、技術部技術基盤チームの@k1LoW と申します。最近はYouTube Musicの「おすすめのアーティスト」をふらふらと漂流するのが好きです。その漂流で見つけた Nubiyan Twist がカッコいいです。
GMOペパボではGitHub Enterprise Server (以下、GHES)を利用しており、CI/CD基盤としてGitHub Actionsを活用しています。
本ブログでも様々なGitHub Actions活用事例を紹介しています。
手動で実施していた業務やタスクをGitHub Actionsのワークフローとして構築しなおす
GMOペパボではGitHub Actionsをいわゆる「CI/CDの基盤」として活用するだけでなく、さまざまな業務やタスクの自動化の基盤としても活用しています。
その自動化の範囲は開発の現場だけでなく、開発の現場以外でも適用可能です(その一部ですが、 @ITにて連載させていただいた「GMOペパボに学ぶ「CI/CD」活用術 」でも紹介していますので是非)。
GitHub Actionsはワークフローという単位で処理を構築します。
GitHub Actionsのワークフローは1つもしくは複数のジョブで構成されており、ジョブは1つまたは複数のステップで構成されています。
ステップごとに uses:
セクションで「Action」と呼ばれるGitHub Actions専用のアプリケーションを呼び出したり、 run:
セクションで独自に処理を書いたりして、最終的にワークフローを構築します。
もともと手動で実施していた業務やタスクをGitHub Actionsのワークフローとして構築しなおして、その実行がバシッと決まるのは気持ちが良いものです。
また、GitHub Actionsに業務やタスクの実施を任せることによって本来注力すべきことに注力できるのも気持ちが良いものです。
独自のワークフロー
「手動で実施していた業務やタスク」を既に世の中に存在するActionの組み合わせのみで実現できれば最高です。
また、ローカルでの実行のための既にコード化されている処理( make XXX
や rake XXX
) の実行や、ちょっとした処理ならステップの run:
セクションにそのままコマンドを書けばいいでしょう。
しかし、既存のActionだとあと少し痒い所に手が届かず、「ちょっとした処理」というには run:
セクションのコマンドだと表現が難しい、ということがあります。
今回、上記のような「独自のワークフロー」をGitHub Actions上で構築するために作成した仕組みを2つ紹介したいと思います。
github-script-ruby
1つ目の紹介です。
「GitHub Actions上でGitHubのコンテキストに沿った操作を簡単に書けるAction」として思いつくのは actions/github-script かと思います。
octokit/rest.js が認証情報が設定されインスタンス化された状態で github
に読み込まれていたり、GitHub Actionsの情報も context
からアクセス可能になっていたり、他にも便利なライブラリが読み込まれた状態からすぐに必要な処理を書き出せます。
actions/github-script はとても使いやすいのですが、処理を書くプログラミング言語にはJavaScriptしか選択できません。
GMOペパボはJavaScriptもですがRubyも好きなエンジニアも在籍していますので(他の言語も好きなエンジニアもたくさんいます!)、 「Ruby版 actions/github-script」を用意してみました。
例えば actions/github-script のExamplesにある例ですが、 actions/github-script を使ってIssueにコメントするワークフローを書くと
on:
issues:
types: [opened]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v5
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '👋 Thanks for reporting!'
})
となりますが、k1LoW/github-script-ruby を使って書くと
on:
issues:
types: [opened]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: k1LoW/github-script-ruby@v1
with:
script: |
repo = "#{context.repo.owner}/#{context.repo.repo}"
number = context.issue.number
comment = '👋 Thanks for reporting!'
github.add_comment(repo, number, comment)
となります。書き味はそのままに、Rubyスクリプトです。
Gemの追加
k1LoW/github-script-ruby のステップで使いたいGemをインストールしたい場合は gemfile:
セクションでGemfileを書くことで追加可能です。
以下の例はSlack通知のために slack-ruby-client.gem を追加で事前インストールしている例です。
- name: 'Slack の #general チャンネルにメッセージをポストする'
uses: k1LoW/github-script-ruby@v1
with:
script: |
require 'slack-ruby-client'
Slack.configure do |config|
config.token = ENV['SLACK_API_TOKEN']
end
client = Slack::Web::Client.new
client.chat_postMessage(channel: '#general', text: 'Hello, Slack bot!')
gemfile: |
source 'https://rubygems.org'
gem 'octokit', '~> 4.0'
gem 'slack-ruby-client'
env:
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
Native extentionのためのパッケージの追加
また、例えば mysql2.gem のようなCネイティブ拡張を含んだGemをビルドするために必要なパッケージを pre-command:
セクションを使って、事前にインストールすることも可能です。
- name: 'List users'
uses: k1LoW/github-script-ruby@v1
with:
script: |
require 'mysql2'
client = Mysql2::Client.new(:host => "localhost", :username => "root")
client.query('SELECT * FROM users').each do |row|
puts row['name']
end
pre-command: |
apt-get update
apt-get install -y libmysqld-dev
gemfile: |
source 'https://rubygems.org'
gem 'mysql2'
github-script-ruby の個人的にお気に入りの特徴は、 actions/github-script と同様にワークフローの途中で突然Ruby環境を用意できるというところです。
PHPアプリケーションのCIのワークフローの途中でも、Goアプリケーションのビルドのワークフローの途中でも、すぐにRubyを書いて独自の処理を追加することができます。
これは github-script-ruby が「Docker container action」の仕組みを使って実装されているからなのですが、事前になにもセットアップせずにRubyスクリプトを書けるのは便利です。
社内の利用事例
Slackチャンネルにブログのfeedを取得して通知してくれるbotが生息しているのですが、そのfeedのURLを URI.regexp
を使ってチェックしています。
- name: validate feed.yaml
uses: k1LoW/github-script-ruby@v1
with:
script: |
require 'yaml'
require 'uri'
Dir.glob("path/to/config/*feed.yaml").each { |yaml|
YAML.load_file(yaml).each { |url|
unless URI.regexp.match(url)
raise 'URL is invalid: "%s"' % url
end
}
}
URI.regexp
知りませんでした。便利だ…。
ghdag
2つ目の紹介です。
独自のワークフローの多くのケースに
- GitHubリポジトリの操作(IssueやPull Request)
- Slack通知
があることが感覚としてありました。
ただ、その2つに元々の業務やタスクの独自性があるため、既存のActionでは表現が難しいことがありました。
そこで上記2つに特化して少し抽象化をした仕組みを作りました。
https://github.com/k1LoW/ghdag
ghdagはGitHub Actions上でIssueやPull Requestに対して動く小さなワークフローエンジンです。
まず GitHub Actionsの .github/workflows/*.yml
とは別にghdag専用のワークフローファイルを用意します。
ghdag init
でサンプルワークフローファイルを生成可能です。
$ ghdag init myworkflow
2021-02-23T23:29:48+09:00 [INFO] ghdag version 0.2.3
2021-02-23T23:29:48+09:00 [INFO] Creating myworkflow.yml
Do you generate a workflow YAML file for GitHub Actions? (y/n) [y]: y
2021-02-23T23:29:48+09:00 [INFO] Creating .github/workflows/ghdag_workflow.yml
タスクとアクション
ghdagにはタスクという単位でIssueやPull Requestに対して処理を実行します。実際のGitHubの操作やSlack通知などはアクションという名前で提供されています。
ghdagのサンプルワークフローファイルを見てみます。
# myworkflow.yml
tasks:
-
id: set-question-label
if: 'is_issue && len(labels) == 0 && title endsWith "?"'
do:
labels: [question]
ok:
run: echo 'Set labels'
ng:
run: echo 'failed'
name: Set 'question' label
1つのワークフローファイルには tasks:
に複数のタスクを設定できます。タスクは大きく4つのセクションに分かれてます。
if:
… そのタスクを実行するための条件を定義do:
…if:
セクションの条件が true の場合に発動するアクションを定義ok:
…do:
セクションの実行が成功した場合に発動するアクションを定義ng:
…do:
セクションの実行が失敗した場合に発動するアクションを定義
アクションには以下の8種類のみが用意されています。
run:
… 任意のコマンドを実行しますlabels:
… 対象のIssue/Pull Requestにラベルを設定しますassignees:
… 対象のIssue/Pull RequestにAssigneeを設定しますreviewers:
… 対象のPull Requestにレビュアーを設定しますcomment:
… 対象のIssue/Pull Requestにコメントしますstate:
… 対象のIssue/Pull Requestの状態を変更しますnotify:
… Slack通知を送りますnext:
… 別のタスクを実行します
上記のサンプルワークフローファイルに設定されたタスクは
もし対象がIssueで、かつセットされたラベルが0、かつIssueタイトルの末尾に ?
がついていたら、 question
のラベルをつける。成功したら echo 'Set labels'
、失敗したら echo 'failed'
を実行する
という意味になります。
タスクの実行タイプ
タスクは全てIssueかPull Requestに対して実行します。
また、ghdagは「CloseしたIssueやPull Requestは対象として扱わない」「IssueやPull RequestのOpenからCloseまでのワークフローを管理する」という割り切りをしています。
タスクの実行タイプは2種類あり、それぞれghdagは自動で判定します。
実行タイプ1. 特定のIssueやPull Requestに対して実行
on.issues:
や on.pull_request:
などでghdagが実行された場合、ghdagはそのトリガーを発火させたIssueやPull Requestに対してしかタスクを実行しません。
タスクは対象のIssueやPull Requestに対して tasks:
セクションの上から順に実行されます。
例えば、
Pull Requestに'needs review'ラベルがついたとき、まだレビュアーがいなかったら、ランダムでアサインする
を実現したい場合、以下のようなタスクとGitHub Actionsのワークフローになります。
# myworkflow.yml
---
tasks:
-
id: assign-pull-request
if: 'is_pull_request && len(reviewers) == 0 && "needs review" in labels'
do:
reviewers: [alice bob charlie]
env:
GITHUB_REVIEWERS_SAMPLE: 1
# .github/workflows/ghdag_workflow.yml
name: ghdag workflow
on:
pull_request:
types: [labeled]
jobs:
run-workflow:
name: 'Run workflow for A **single** `opened` issue that triggered the event'
runs-on: ubuntu-latest
container: ghcr.io/k1low/ghdag:latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run ghdag
run: ghdag run myworkflow.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
実行タイプ2. Openしている全てのIssueやPull Requestに対して実行
on.push:
や on.schedule:
など、特定のIssueやPull Requestに紐づかないイベントでghdagが実行された場合、ghdagはOpenしている全てのIssueとPull Requestに対してタスクを実行します。
タスクはそれぞれのIssueやPull Requestに対して tasks:
セクションの上から順に実行されます。
例えば、
Issueが30日間放置されているとき、かつコメントが1件もついていなかったら、Closeする
を実現したい場合、以下のようなタスクとGitHub Actionsのワークフローになります。
# myworkflow.yml
---
tasks:
-
id: close-issues-30days
if: is_issue && hours_elapsed_since_updated > (30 * 24) && number_of_comments == 0
do:
state: close
# .github/workflows/ghdag_workflow.yml
name: ghdag workflow
on:
schedule:
# Run at 00:05 UTC every day.
- cron: 5 0 * * *
jobs:
run-workflow:
name: 'Run workflow for **All** `opened` and `not draft` issues and pull requests'
runs-on: ubuntu-latest
container: ghcr.io/k1low/ghdag:latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Run ghdag
run: ghdag run myworkflow.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ghdag の個人的にお気に入りの特徴は、1つのステップ内で多くのタスクを同時に処理できるところです。タスクを next:
アクションでうまくつなげていくことで小さなワークフローを1ステップ内に作ることも可能です。
GitHub Actionsでも if:
セクションや needs:
セクションを使って同じような処理を書くことができるので好みにはなりますが、選択肢が増えるのは良いことだと思っています。
社内の利用事例
Pull Requestのレビューのステータスをラベルでわかりやすくして運用しているリポジトリで、そのラベルの操作を自動化しています。
---
tasks:
-
id: remove-label-In-Review
name: レビューされてApproveされていたらIn Reviewラベルを外す
if: |
github.event_name == "pull_request_review"
&& github.event.action in ["submitted", "edited"]
&& is_approved
&& is_pull_request
&& "In Review" in labels
do:
labels: ["In Review"]
env:
GHDAG_ACTION_LABELS_BEHAVIOR: remove
確かに、レビュー後に「レビュー中」ラベルは外し忘れがちですよね。
まとめ
今回は「GitHub Actions上で独自のワークフローを構築しやすくする」という少しメタな視点での仕組みの紹介でした。
GitHub Actionsで何かを実現したいときの1つの選択肢となればとても嬉しいです。