リリース内容をGitHub ActionsでSlack通知できるようにしてみましたの巻
こんにちは。昨年12月に入社して半月ほど現場研修を経た後、開発チームに参加した新入エンジニアの宮原です。
今回は、入社前から外部サービス(Integromat)を使って全社発信していた リリース内容お知らせチャンネル の自動通知をGitHub Actions化してリニューアルした件について紹介したいと思います。
リリース内容お知らせチャンネルとは?
弊社で開発した機能のアップデートなどGitHubのPullRequest単位でマスタブランチにマージされるとリリース内容を自動発信してくれているSlackチャンネルです。
開発チームとほか部門(CS,マーケ、バイヤー ...etc)との情報共有とディスカッションの場として有効活用しています。
リリース内容の通知はだいたい1日で1~3件くらい、多い日には5件を超える通知があり状況に応じてデプロイも随時[1]行っています。
リニューアル内容
概要
従来のリリース内容の記載ルールはほぼそのまま使用しつつも、文字装飾や複数画像添付ができるようにしたい。
…をコンセプトに開発しました。
Before:従来(Integromat利用)
Integromat(現在はmake)という異なるツールどうしを連携できるノーコードツールサービスを使用していました。
手法としては管理画面で通知連携設定をゴニョゴニョ作り込むスタイルでした。
検索に機能追加した時のSlack
After:現在(GitHub Actions利用)
GitHub ActionsでGitHubから直接Slack通知するスタイルにリフォームしました。
お気に入り機能をリリースした時のSlack
リリース内容の記載方法
PullRequestのテンプレートにリリース内容を記載するタグが入っているので、各々書き込みます。
軽微なリファクタなどサービス面への変化がなく報告不要と判断した場合はこのタグをすべて消してSlackへの通知をOFFにすることもあります。
## `#notify-launch`
<!-- 変更内容 -->
ここに書いた内容がSlackに通知されます。必要のない場合はコメントタグごと消してください。
(Slackの `mrkdwn記法` が使用可能です。)
<!-- /変更内容 -->
<!-- 変更前画像 -->
ここに書いたimgタグ(複数可)がSlackに通知されます。必要のない場合はコメントタグごと消してください。
<!-- /変更前画像 -->
<!-- 変更後画像 -->
ここに書いたimgタグ(複数可)がSlackに通知されます。必要のない場合はコメントタグごと消してください。
<!-- /変更後画像 -->
コメントアウトでリリース内容を記載するタグで定義して囲っています。
今回はゆるく移行できるように従来のフォーマットの形をほぼ残した形にとどめています。
記載項目
PullRequest | Slack |
---|---|
タイトル | 件名に使用します。 |
本文の変更内容 タグ |
テキスト内容を使用します。 |
本文の変更前画像 タグ |
画像とテキストを使用します。 |
本文の変更後画像 タグ |
(同上) |
タイトル
・ほか部門の方が見てわかりやすい件名をつけるように心がけています。
本文の変更内容
タグ
・リニューアル時にSlackのmrkdwn記法で装飾ができるように対応しました。
本文の変更前画像
変更後画像
タグ
・編集画面で貼り付けた画像を使用できます。
・リニューアル時に複数画像を表示できるように対応しました。
※2023年8月8日 追記
この記事で紹介している方法ではパブリックで公開されている画像のみ表示できます。
GitHubの画像は2023年5月ごろ、認証が必須になったため、画像表示ができなくなりました。
一応認証を通す方法があり社内で利用しているものにはすでに適用済みですがこの記事では割愛します。
実装
社内向けのツールなので荒削りではありますが、今回は読者の皆様にも真似しやすいように現状のソースコードを貼り付けましたのでざっくり紹介します。
GitHub ActionsのYAML(メタデータファイル)
この中に書いてあるジョブはGitHub上とローカルデバッグのどちらでも動くように書いています。
name: Slack Notification Launch
on:
pull_request:
branches:
- master
types: [closed]
jobs:
slackNotification:
name: Slack Notification Launch
runs-on: ubuntu-latest
defaults:
run:
working-directory: .github/workflows/notify-launch
if: ${{ github.event.pull_request.merged && contains(github.event.pull_request.body, '<!-- 変更内容 -->') }}
steps:
- uses: actions/checkout@v3
- name: Get PULL_REQUEST.json
if: ${{ !env.ACT }}
run: |
cat <<'EOF' > PULL_REQUEST.json
${{ toJSON(github.event.pull_request) }}
EOF
- uses: actions/setup-node@v3
with:
node-version: 19
cache: 'npm'
cache-dependency-path: .github/workflows/notify-launch/package-lock.json
- name: npm install
run: npm ci
- name: Set Values
id: set-values
run: |
echo "blocks=$(node convert-to-slack-msg.js PULL_REQUEST.json)" >> $GITHUB_OUTPUT
echo "summary=$(node convert-to-label-category.js PULL_REQUEST.json)" >> $GITHUB_OUTPUT
- name: Slack Incoming Webhook
if: ${{ steps.set-values.outputs.blocks != '' }}
uses: tokorom/action-slack-incoming-webhook@main
env:
INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_NOTIFY_LAUNCH_URL }}
with:
text: '${{ steps.set-values.outputs.summary }}'
attachments: |
[
${{ steps.set-values.outputs.blocks }}
]
プルリクのラベルからカテゴライズされた件名を作成するスクリプト
notify-icon
…がついているラベルをピックアップして冒頭の要約(text)を出力している機能です。
const fs = require('fs')
const prJsonFilePath = process.argv[2]
const fileJson = fs.readFileSync(prJsonFilePath, 'utf8')
const pullRequest = JSON.parse(fileJson)
// リリースのカテゴリーを取得
const category = pullRequest.labels.map(label => label.name.match(/^notify-icon(.+)/)?.[1]).filter(Boolean)
if (category.length > 0) {
console.log(category.join(',') + ' のリリースお知らせ')
} else {
// カテゴリーラベルがない場合はこの表記を使用
console.log(':rocket:機能リリースのお知らせ')
}
プルリクの本文からSlack本文
Slackに投稿する件名と本文(attachments)の部分を出力する機能です。
Slack Block Kitのルールにした従って本文を組み立ています。
あとリフォーム前のルールを踏襲して社内のほか部門の方はGitHubのユーザー名を見ても誰なのかピンとこないのでSlackと同じ名前に変換するしくみも入れています。
const marked = require('marked')
const mrkdwn = require('html-to-mrkdwn')
const fs = require('fs')
const prJsonFilePath = process.argv[2]
// テキストから<!-- ** -->...<!-- /** -->タグ中に囲まれた範囲を取得するメソッド
function getTagText (text, tagName) {
const regex = new RegExp(`<\\!-- ${tagName} -->(.*?)<\\!-- /${tagName} -->`, 's')
const textMatches = text.match(regex)
return (textMatches && textMatches[1]) ? textMatches[1].trim() : ''
}
// githubのユーザー名から名前を取得(取得できない場合はユーザー名表示)
function getAuthorName (githubUser) {
const authors = {
rentio: '蓮手尾 太郎'
}
return authors[githubUser] ?? githubUser
}
const fileJson = fs.readFileSync(prJsonFilePath, 'utf8')
const pullRequest = JSON.parse(fileJson)
const msgParams = {
title: pullRequest.title,
author: getAuthorName(pullRequest.user.login),
discription: getTagText(pullRequest.body, '変更内容'),
beforeImages: getTagText(pullRequest.body, '変更前画像'),
afterImages: getTagText(pullRequest.body, '変更後画像')
}
const blockKitItems = { blocks: [] }
// 変更内容
if (msgParams.discription) {
blockKitItems.blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: mrkdwn(marked.parse(msgParams.discription)).text
}
})
}
// 変更前画像
if (msgParams.beforeImages) {
let imgNumber = 0
msgParams.beforeImages.split(/\n/).forEach(line => {
const item = mrkdwn(marked.parse(line))
if (item.image) {
imgNumber++
blockKitItems.blocks.push({
type: 'image',
title: {
type: 'plain_text',
text: `変更前画像-${imgNumber}`
},
image_url: item.image,
alt_text: `変更前画像-${imgNumber}`
})
} else if (item.text) {
blockKitItems.blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: mrkdwn(marked.parse(item.text)).text
}
})
}
})
}
// 変更後画像
if (msgParams.afterImages) {
let imgNumber = 0
msgParams.afterImages.split(/\n/).forEach(line => {
const item = mrkdwn(marked.parse(line))
if (item.image) {
imgNumber++
blockKitItems.blocks.push({
type: 'image',
title: {
type: 'plain_text',
text: `変更後画像-${imgNumber}`
},
image_url: item.image,
alt_text: `変更後画像-${imgNumber}`
})
} else if (item.text) {
blockKitItems.blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: mrkdwn(marked.parse(item.text)).text
}
})
}
})
}
// JSON出力
if (blockKitItems.blocks.length > 0) {
// タイトルと区切りを先頭に挿入
blockKitItems.blocks.unshift(
{
type: 'header',
text: {
type: 'plain_text',
text: msgParams.title
}
},
{
type: 'divider'
}
)
// 担当者を最後に挿入
blockKitItems.blocks.push(
{
type: 'context',
elements: [
{
type: 'plain_text',
text: `担当者: ${msgParams.author}`
}
]
}
)
console.log(JSON.stringify(blockKitItems))
} else {
console.error('** NO MESSAGE **', blockKitItems)
}
{
"dependencies": {
"html-to-mrkdwn": "^3.0.0",
"Markdown-to-html": "^0.0.13"
},
"devDependencies": {
"standard": "^17.0.0"
}
}
※開発時に.github/workflows/notify-launch
直下でnpm install
してpackage-lock.jsonがある状態でコミットしています。
今回のSlack通知GitHub Actions開発よもやま
💬Slack通知のテストは砂場チャンネルでやろう
いつものSlackチャネルは大勢の参加者(現在は3桁ほど)が見ているので、デバッグ中は連続投稿してもかまわないチャンネルを用意していただけることになりました。
今なら自分一人で作れただろうとは思いましたが、当時は現場研修が終わって開発メンバーと合流して間もなかったためありがたかったです。
そのままだとメインブランチにマージしないと動かないため、所々コメントアウトしてコミットを積みプルリクにプッシュして動作確認してました。
[skip ci]
を使おう
💸GitHub Actionsの実行時間節約したい時はGitHub Actionsの実行時間は従量課金で、メインのリポジトリにはCircleCIやほかのActionsも多数あるのでやみくもにプッシュしているとお金がもったいない。
…ということでたどり着いたのが[skip ci]
をコミットメッセージに入れてやめる。という手法でした。
ただし、文字通りCIを全部スキップしてしまうのでローカル環境でデバッグを充実させねば…!と使命感を感じました😅
💻ローカル環境でデバッグにチャレンジ
nektos/act[2]をセットアップしてローカル環境でデバッグしました。
デバッグ実行コマンドはこんな感じです。
act --secret-file debug.secrets -e GITHUB_EVENT.json -W .github/workflows/notify-launch.yml
debug.secrets
を用意する
GitHubのSecretsのデバッグ用に今回はGitHubのSecretsに以下の変数を登録しているので--secret-file
の引数に渡すファイルを作成しておきます。
変数 | 値 |
---|---|
SLACK_WEBHOOK_NOTIFY_LAUNCH_URL |
SlackチャンネルのWebhookURL |
# GitHubのSecretsのデバッグ値
SLACK_WEBHOOK_NOTIFY_LAUNCH_URL=https://hooks.slack.com/services/*****
GITHUB_EVENT.json
を用意する
GitHubのeventのデバッグ用にactコマンドをそのまま実行するとymlで使われているGitHubコンテキスト(github.event.pull_request.body
など)を取得ができずにこの分岐で実行されないので
if: ${{ github.event.pull_request.merged && contains(github.event.pull_request.body, '<!-- 変更内容 -->') }}
-e
の引数に渡すファイルを作成しておきます。
{
"pull_request":{
"merged": true,
"body":"<!-- 変更内容 -->ローカル環境は手動で配置した`PULL_REQUEST.json`を参照します"
}
}
GitHubコンテキストはjsonファイルに書き出しておこう
これは何に使うのかというとname: Set Values
で実行しているシェルスクリプトのローカルデバッグ用に使用します。
使用する変数が少ないので手作りしてもよいですが、GitHub Actionsのymlのname: Get PULL_REQUEST.json
の実行結果のjsonをコピぺして書き出してみると実データと同じ物を用意できます。
このファイルを使うスクリプトに関しては難しいことは別スクリプトにやらせようを参照してみてください。
Markdown記法
をSlackのmkdwn記法
でアウトプットする
💬GitHubの両者は似ているようでルールが微妙に異なりますので、GitHubの本文から抜き出した原文ママ(Markdown+HTML混在)を送るとmrkdwn[3]にしか対応できないSlack上では表示が崩れたりします。
今回はGitHubのPullRequestに書いた内容をSlackで送るため、別スクリプトを噛ませて変換し出力させることにしました。
🧹難しいことは別スクリプトにやらせよう
前述のSlackのmkdwn記法の変換などシェルスクリプトだと書きづらい処理はNode.jsのスクリプトで対応しました。
それぞれのスクリプトに共通する点としては、GitHub Actionsのフローを介さずとも直デバッグできるようにGitHubコンテキストを書き出したファイル(PULL_REQUEST.json)
をコマンド引数で受け取り、各々の処理を実行できるように作っています。
👓CI/CDで使うnpmを見直す。
npmを使うときnpm install
を使っていたのですが、レビュー時にGitHub Actionsにはnpm ci
を使うのが良いのでは?
というコメントがあり、npm install
の方を使う理由がない箇所はnpm ci
に書き換えました。
npm ci
はnpm install
と似ていますが、後者よりも高速で依存関係のクリーンインストールを確実に行ってくれます。
テストプラットフォーム、継続的インテグレーション、デプロイなどの自動化された環境で使用することを意図しているようです。
まとめ
今回はリリース内容お知らせチャンネル通知のGitHub Actionsの紹介でしたが、積極的にツール導入やAWSを活用していたり、TDDにも旺盛でリファクタをして運用負荷軽減しつつ技術的負債と戦っているチームだなぁと感心しました。
採用情報
レンティオではエンジニアを絶賛募集中です!もし興味をお持ちいただけたらこちらもお目通しいただけるとうれしいです。
余談:現場研修とは?
レンティオのエンジニアは入社直後に自社の倉庫拠点でアルバイトさんと同じ目線で現場のお仕事を半月ほど体験する慣習があります。
どんなお仕事をするのかというと、レンタルが終わって返却された商品を清掃したり、これからレンタルされる商品の検品・梱包して発送用のカーゴに入れる業務などをします。
Slackや社内ドキュメントだけでは深い理解が難しいレンタルサブスク現場の雰囲気を体感でき、開発チーム合流後の業務理解に役立っています。
自分は地方在住のため、入社日から一時的にホテル暮らしで現場研修を受けることになりましたが、生まれてからこんなに長く東京に滞在したことがなかったので人生においてもよい経験でした。
今後は、地方ユーザー目線でレンティオのサービスをよりよいものにしたいと思っています。
-
初めてのデプロイ当番の時は1日3回 AWS Copilot でポチポチ対応しました。
デプロイ環境に興味のある方はこの過去記事も見てみてください。
https://zenn.dev/rentio/articles/convox-to-copilot ↩︎ -
GitHub Actions ワークフローをローカル実行するための定番ツール。
実際の環境での挙動を完全にエミュレートできないため賛否両論ありますが、とりあえず実行するにはDocker環境が必要です。 ↩︎ -
mrkdwn(マークアップ)のルールは公式サイト参照。
https://slack.com/intl/ja-jp/help/articles/202288908-メッセージの書式設定 ↩︎
Discussion