Github Actionsとは
リポジトリ内で処理を自動化してくれる機能になります。
CI/CDに使うことが多いですが、使い方次第では無限大の可能性があります。
また、自分自身で作るだけでなく世界中のエンジニアが公開しているGithub Actionsを使用して自分のプロジェクトで自動実行することも可能です。
エンジニアは基本面倒くさがり屋です。
だからこそGithub Actionsで徐々に自動化して楽なコーディングライフにして欲しいです。
自動化する処理
今回はあると便利な下記2つをGithub Actionsで自動化していこうと思います。
- GithubActions処理状況をSlack通知
- プルリク作成時にテスト
詳細な処理やコードは次で書いていきます。
実装
実装内容的には簡単ですので是非導入を考えて見てください。
今回はGithubActionsの他にactions/toolkit
を使用していきます。
こちらはGithubActionsの処理をTypescriptで書くことが出来る便利なライブラリになります。
ディレクトリ構成
実装していく2つの機能とも同じディレクトリを使用していきます。
.
├── .github
│ └── actions
│ └── tests
├── slackNotion
│ ├── actions
│ │ └── yml
│ │ └── slack
│ │ └── Github Actionsのメタデータが入っているディレクトリ
│ └── src
│ ├── slack
│ │ └── Slack通知処理に共通処理を管理するディレクトリ
│ └── types
│ └── 型を管理するディレクトリ
├── src
│ └── Nodeで作成する簡単なプロジェクト
└── tests
└── テストコードを管理
Github Actionsのメタデータとは
Github Actionsで使用する共通処理を分けてファイル管理出来ます。
GithubActions処理状況をSlack通知
名前の通り、GithubActionsの処理が「処理開始したか」「正常終了したか」「異常終了したか」「キャンセルしたか」の情報をSlackに通知してくれる処理になります。
通知するにはSlackAPIが必要になります。
workflowについて
一連の流れとして
- 開始通知
- 行いたい処理(今回はテスト処理)
- 終了通知
-
2. 行いたい処理(今回はテスト処理)
で成功時は正常終了通知 -
2. 行いたい処理(今回はテスト処理)
で失敗時は異常終了通知 -
2. 行いたい処理(今回はテスト処理)
でキャンセル時はキャンセル通知
-
という形に処理されます。
終了通知は3つのうちどれか一つが通知されます。
workflow図は下記になります。
コード
メインとなるymlを書いていきます。
前述したworkflowに沿って書かれています。
name: CI with Notify
on:
push:
branches:
- main
jobs:
notify_test_start:
runs-on: ubuntu-latest
outputs:
slack-thread-ts: ${{ steps.notify_test_start.outputs.slack-thread-ts }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Install && Build
uses: ./slackNotion/actions/yml/build
- name: Cache node modules
id: cache-slack-notify-node-modules
uses: actions/cache@v4
with:
path: ./slackNotion/node_modules
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-modules
- name: Cache dist
id: cache-slack-notify-dist
uses: actions/cache@v4
with:
path: ./slackNotion/dist
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-dist
- name: Send Slack notification (test start)
id: notify_test_start
uses: ./slackNotion/actions/yml/slack
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
slack-channel: ${{ secrets.SLACK_CHANNEL }}
status: "started"
run-id: ${{ github.run_id }}
job-name: ${{ github.job }}
repository: ${{ github.repository }}
ref: ${{ github.ref }}
event-name: ${{ github.event_name }}
workflow: ${{ github.workflow }}
test:
runs-on: ubuntu-latest
needs: notify_test_start
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Initialize & Test
uses: ./.github/workflows/actions/tests
notify_test_success:
runs-on: ubuntu-latest
needs: [test, notify_test_start]
if: success()
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Restore node modules
id: cache-slack-notify-node-modules
uses: actions/cache@v4
with:
path: ./slackNotion/node_modules
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-modules
- name: Restore dist
id: cache-slack-notify-dist
uses: actions/cache@v4
with:
path: ./slackNotion/dist
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-dist
- name: Send Slack notification (test success)
id: notify_test_success
uses: ./slackNotion/actions/yml/slack
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
slack-channel: ${{ secrets.SLACK_CHANNEL }}
status: "succeeded"
run-id: ${{ github.run_id }}
job-name: ${{ github.job }}
repository: ${{ github.repository }}
ref: ${{ github.ref }}
event-name: ${{ github.event_name }}
workflow: ${{ github.workflow }}
slack-thread-ts: ${{ needs.notify_test_start.outputs.slack-thread-ts }}
- name: Check output
run: echo ${{ needs.notify_test_start.outputs.slack-thread-ts }}
notify_test_failure:
runs-on: ubuntu-latest
needs: [test, notify_test_start]
if: failure()
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Restore node modules
id: cache-slack-notify-node-modules
uses: actions/cache@v4
with:
path: ./slackNotion/node_modules
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-modules
- name: Restore dist
id: cache-slack-notify-dist
uses: actions/cache@v4
with:
path: ./dist
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-dist
- name: Send Slack notification (test failure)
id: notify_test_failure
uses: ./slackNotion/actions/yml/slack
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
slack-channel: ${{ secrets.SLACK_CHANNEL }}
status: "failed"
run-id: ${{ github.run_id }}
job-name: ${{ github.job }}
repository: ${{ github.repository }}
ref: ${{ github.ref }}
event-name: ${{ github.event_name }}
workflow: ${{ github.workflow }}
slack-thread-ts: ${{ needs.notify_test_start.outputs.slack-thread-ts }}
notify_test_cancelled:
runs-on: ubuntu-latest
needs: [test, notify_test_start]
if: cancelled()
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Restore node modules
id: cache-slack-notify-node-modules
uses: actions/cache@v4
with:
path: ./slackNotion/node_modules
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-modules
- name: Restore dist
id: cache-slack-notify-dist
uses: actions/cache@v4
with:
path: ./dist
key: ${{ runner.os }}-${{ hashFiles('./slackNotion/package-lock.json') }}-dist
- name: Send Slack notification (test cancelled)
id: notify_test_cancelled
uses: ./slackNotion/actions/yml/slack
with:
slack-token: ${{ secrets.SLACK_TOKEN }}
slack-channel: ${{ secrets.SLACK_CHANNEL }}
status: "cancelled"
run-id: ${{ github.run_id }}
job-name: ${{ github.job }}
repository: ${{ github.repository }}
ref: ${{ github.ref }}
event-name: ${{ github.event_name }}
workflow: ${{ github.workflow }}
slack-thread-ts: ${{ needs.notify_test_start.outputs.slack-thread-ts }}
Slack通知処理を整えていきます。
始めにTypescriptを下記の役割で書いていきます。
-
slackNotion/src/slack.ts
- Slack通知を行うメインファイル
-
slackNotion/src/types/slack.ts
- Slack処理に使用する型の管理
-
slackNotion/src/slack/jobStatus.ts
- Slack通知に使用する値の管理
-
slackNotion/src/slack/baseAttachment.ts
- Slack通知のベースファイル
import * as core from "@actions/core";
import axios from "axios";
import { baseAttachment } from "./slack/baseAttachment";
import { JOB_STATUS } from "./slack/jobStatus";
import {
SendOrUpdateSlackNotification,
SlackApi,
SlackAttachment,
SlackPayload,
} from "./types/slack";
const SLACK_API = {
POST: "https://slack.com/api/chat.postMessage",
UPDATE: "https://slack.com/api/chat.update",
};
const slackApi: SlackApi = async ({ url, payload, token }) => {
return await axios.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json; charset=utf-8",
},
});
};
const sendOrUpdateSlackNotification: SendOrUpdateSlackNotification = async (
token,
channel,
attachments,
threadTs
) => {
try {
const slackPayload: SlackPayload = {
channel,
attachments,
token,
};
if (threadTs) slackPayload.ts = threadTs;
const response = await slackApi({
url: threadTs ? SLACK_API.UPDATE : SLACK_API.POST,
payload: slackPayload,
token,
});
if (response.data.ok) {
core.setOutput("slack-thread-ts", response.data.ts);
core.info("Message sent successfully");
return response.data.ts;
} else {
core.setFailed(`Slack API error: ${response.data.error}`);
}
} catch (error) {
core.setFailed(
`Failed to send Slack notification: ${(error as Error).message}`
);
}
};
async function run() {
try {
const slackToken = core.getInput("slack-token");
const slackChannel = core.getInput("slack-channel");
const status = core.getInput("status") as JOB_STATUS;
const runId = core.getInput("run-id");
const jobName = core.getInput("job-name");
const repository = core.getInput("repository");
const ref = core.getInput("ref");
const eventName = core.getInput("event-name");
const workflow = core.getInput("workflow");
const targetThreadTs = core.getInput("slack-thread-ts");
const attachment = baseAttachment(
status,
repository,
ref,
eventName,
workflow,
runId
);
const attachments: SlackAttachment[] = [
{
pretext: `Job ${jobName} with run ID ${runId} has ${status}.`,
color: attachment.color,
fields: attachment.fields,
},
];
const threadTs = await sendOrUpdateSlackNotification(
slackToken,
slackChannel,
attachments,
targetThreadTs
);
if (threadTs && status === JOB_STATUS.START)
core.setOutput("slack-thread-ts", threadTs);
} catch (error) {
core.setFailed(`Action failed with error: ${(error as Error).message}`);
}
}
run();
import { BaseAttachmentFields } from "../slack/baseAttachment";
export type SlackAttachment = {
pretext: string;
color: string;
fields: BaseAttachmentFields[];
};
type SlackApiProp = {
url: string;
payload: {
channel: string;
attachments: SlackAttachment[];
ts?: string;
token: string;
};
token: string;
};
type SlackApiReturn = {
data: {
ok: boolean;
error: string;
ts: string;
};
};
export type SlackApi = ({
url,
payload,
}: SlackApiProp) => Promise<SlackApiReturn>;
export type SendOrUpdateSlackNotification = (
token: string,
channel: string,
attachments: SlackAttachment[],
threadTs?: string
) => Promise<string | undefined>;
export type SlackPayload = {
channel: string;
token: string;
attachments: SlackAttachment[];
ts?: string;
};
export const JOB_STATUS = {
START: "started",
SUCCESS: "succeeded",
FAILURE: "failed",
CANCELLED: "cancelled",
} as const;
export type JOB_STATUS = (typeof JOB_STATUS)[keyof typeof JOB_STATUS];
export const jobStatus: Record<JOB_STATUS, { color: string }> = {
[JOB_STATUS.START]: {
color: "#3ea8ff",
},
[JOB_STATUS.SUCCESS]: {
color: "#2cbe4e",
},
[JOB_STATUS.FAILURE]: {
color: "#cb2431",
},
[JOB_STATUS.CANCELLED]: {
color: "#ffc107",
},
};
import { JOB_STATUS, jobStatus } from "./jobStatus";
export type BaseAttachmentFields = {
title: string;
value: string;
short: boolean;
};
export type BaseAttachment = {
color: string;
fields: BaseAttachmentFields[];
};
export const baseAttachment = (
status: JOB_STATUS,
repository: string,
ref: string,
eventName: string,
workflow: string,
runId: string
): BaseAttachment => {
const jobUrl = `https://github.com/${repository}/actions/runs/${runId}`;
return {
color: jobStatus[status].color,
fields: [
{ title: "Repository", value: repository, short: true },
{ title: "Ref", value: ref, short: true },
{ title: "Event Name", value: eventName, short: true },
{ title: "Workflow", value: `<${jobUrl}|${workflow}>`, short: true },
],
};
};
書けたらメタデータファイルも書いていきます
-
slackNotion/actions/yml/build/action.yml
- 前述したTypescriptを実行するyml
-
inputs
で.github/slackNotify.yml
から値を取得可能
name: "Slack Notification Action"
description: "Sends a notification to Slack using a token and outputs the message thread timestamp"
inputs:
slack-token:
description: "The Slack OAuth token"
required: true
slack-channel:
description: "The Slack channel ID"
required: true
status:
description: "The Status of the workflow"
required: true
run-id:
description: "The GitHub Actions run ID"
required: true
job-name:
description: "The GitHub Actions job name"
required: true
repository:
description: "The GitHub Repository name"
required: true
ref:
description: "The GitHub Ref"
required: true
event-name:
description: "The GitHub event name"
required: true
workflow:
description: "The GitHub Workflow name"
required: true
outputs:
slack-thread-ts:
description: "The Slack message thread timestamp"
runs:
using: "node20"
main: "../../../dist/slack.js"
実行時実行ディレクトリですがメタデータymlが入っているディレクトリになるため処理するファイルが実行されるようにパスを書いていきます。
実行
実行してあげると下記のようにslackに通知されます。
最初は青色で通知、処理が完了したら緑色で通知してくれるので目で見てもわかりやすいようになっています。
プルリク作成時にテスト
こちらはとてもシンプルなのでコードのみ記述していきます。
コード
テストを流すnode.js環境を簡単に実装していきます。
import app from "./app";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
import express, { Application } from "express";
const app: Application = express();
app.get("/", (req, res) => {
res.send("Hello World!");
});
export default app;
コードが書けたらjestでテストコードを実装していきます。
import request from "supertest";
import app from "../src/app";
describe("GET /", () => {
it("should return Hello World", async () => {
const res = await request(app).get("/");
expect(res.status).toBe(200);
expect(res.text).toBe("Hello World!");
});
});
コードが書けたらgithubActionsを整えていきます。
name: Run Jest
on: pull_request
jobs:
job-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Initialize & Tests
uses: ./.github/workflows/actions/tests
runs:
using: "Composite"
steps:
- name: Install dependencies
run: npm ci
shell: bash
- name: Run tests
run: npm test
shell: bash
実行
実際にプルリクエストを作成するとテストが走り、成功したらチェックマークが表示されます。
今回は設定していませんが、GitHubのブランチ保護ルールを設定してあげればGithubActions上のテストが失敗した場合はMerge pull Request
を禁止する制御を行えます。
感想
GithubActionsを実装しとくことで開発を楽に進めるのを再確認することが出来ました。
また、Typescriptを使用することでymlを使った実装するよりも簡単に実装することが出来ました。
今回はymlとTypescriptの組み合わせで実装していきましたが、Typescriptに機能を持たせてymlではTypescriptの実行のみすることも出来そうだったので今後も学んでいきます。