以下の内容はhttps://swfz.hatenablog.com/entry/2024/12/30/091722より取得しました。


GitHub ActionsでWorkflowが失敗したときだけSlack通知するカスタムアクションを作った

背景と課題感

やっと重い腰を上げて記事化した(作ったのは1年前の話)

SlackにGitHub ActionsのFailed時のログを表示したい

公式のGitHub AppはWorkflowの結果をSlackと連携して流すことはできるが、すべての実行で通知が行ってしまう(失敗時のみ通知が欲しい)

失敗時ログの内容を知りたい(ログをさっと見れたら当てをつけやすいがログの内容は見えない)

Issueも発行されてはいるが+1ばかりで特に進捗がない

ちょうど何かしらGitHubActionsのカスタムアクションを作ってみたかったところなので良い題材かもということで、自分ならこれが欲しいという掲題のアクションを作ってみた

actions-failed-log-to-slack

使い方

通知部分はworkflow_runイベントで行う

workflow_runについては下記

ワークフローをトリガーするイベント - GitHub Docs

docs.github.com

次のようにSlack通知する部分(カスタムアクションの利用部分)のみ記述したファイルを用意

  • .github/workflows/slack.yml
name: slack notification

on:
  workflow_run:
    workflows:
      - test
      - e2e
    types:
      - completed
jobs:
  main:
    name: main
    runs-on: ubuntu-latest
    steps:
      - uses: swfz/failed-log-to-slack-action@v1.1.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

例として、テスト用のファイルを用意(上記サンプルだとe2eのWorkflowファイルも対象なので用意する)

  • .github/workflows/test.yml
name: test

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4.2.2
      - uses: actions/setup-node@v4.1.0
        with:
          node-version-file: .node-version
          cache: yarn
      - name: test
        run: |
          yarn
          yarn test

testのWorkflow終了後slack notificationワークフローが発火し、Slack通知される

  • 通知イメージ

失敗したときの実際のログの最後の方数行が表示される

作ったときの話

※使う側からしたらこの先は特に読まなくて良さそうです

せっかくなのでカスタムアクションの開発で学んだこととか使ったものの所感などをつらつら書いていく

TypeScriptのテンプレートリポジトリから作成した

actions/typescript-action: Create a TypeScript Action with tests, linting, workflow, publishing, and versioning

github.com

今回はTypeScriptのテンプレートリポジトリから作成した

Linterとかもろもろ設定済みなので最初からある程度開発に集中できる

actions/toolkitの使い方とか開発しながらドキュメント読みつつって感じだろうけど、一通り実装されているサンプルがある状態なので「こういうときはこう書くのね」みたいなのがある程度分かるので参考にしやすかった

特にcorecontextあたりの違いや使い方などはサンプルコードがあったのでやりやすかった

テストもある状態から始められるので意識しながら開発できるのと、例を参考にしながら実際に作成したコードに対してテストコードを書けたので良かった

SuperLinter

Super-Linter · Actions · GitHub Marketplace

github.com

テンプレートリポジトリに含まれていたので使ってみた

GitHubが作っていて、初期の開発スタート時にLinterの設定をしたり試したりがたいへんだよねっていうところから来ているらしい

たしかに、Linterの選定、フォーマッタの選定…考え出すと際限なくいろいろできるのでこの辺に労力使いたくないよねっていうのはある

設定で変えたい部分などは基本的に環境変数経由で差し込む

使って開発していたが、MarkdownのMarkdownLintで何度もCI段階で落とされたのでローカルでも実行できるようにした…

  • linter.yml(一部抜粋)
      - name: Lint Codebase
        id: super-linter
        uses: super-linter/super-linter/slim@v5
        env:
          DEFAULT_BRANCH: main
          FILTER_REGEX_EXCLUDE: dist/**/*
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TYPESCRIPT_DEFAULT_STYLE: prettier
          VALIDATE_ALL_CODEBASE: true
          VALIDATE_JAVASCRIPT_STANDARD: false
          VALIDATE_JSCPD: false
  • 結果のスクショ

イベントの情報とか

カスタムアクションは、Workflow実行時の情報をつかって何かするみたいなことが主な要件だと思うが、実際に実行してどのようなデータ構造でどんな情報が参照できるのかわからなかった

これもソースコード読んだりして把握した

actions/toolkit:github/src/context.ts

下記のように基本的には環境変数にセットしてツール側がそれを参照する形でデータのやりとりを行っている

import {WebhookPayload} from './interfaces'
import {readFileSync, existsSync} from 'fs'
import {EOL} from 'os'

export class Context {
  constructor() {
    this.payload = {}
    if (process.env.GITHUB_EVENT_PATH) {
      if (existsSync(process.env.GITHUB_EVENT_PATH)) {
        this.payload = JSON.parse(
          readFileSync(process.env.GITHUB_EVENT_PATH, {encoding: 'utf8'})
        )
      } else {
        const path = process.env.GITHUB_EVENT_PATH
        process.stdout.write(`GITHUB_EVENT_PATH ${path} does not exist${EOL}`)
      }
    }
    this.eventName = process.env.GITHUB_EVENT_NAME as string
    this.sha = process.env.GITHUB_SHA as string
    this.ref = process.env.GITHUB_REF as string
    this.workflow = process.env.GITHUB_WORKFLOW as string
    this.action = process.env.GITHUB_ACTION as string
    this.actor = process.env.GITHUB_ACTOR as string
    this.job = process.env.GITHUB_JOB as string
    this.runAttempt = parseInt(process.env.GITHUB_RUN_ATTEMPT as string, 10)
    this.runNumber = parseInt(process.env.GITHUB_RUN_NUMBER as string, 10)
    this.runId = parseInt(process.env.GITHUB_RUN_ID as string, 10)
    this.apiUrl = process.env.GITHUB_API_URL ?? `https://api.github.com`
    this.serverUrl = process.env.GITHUB_SERVER_URL ?? `https://github.com`
    this.graphqlUrl =
      process.env.GITHUB_GRAPHQL_URL ?? `https://api.github.com/graphql`
  }

  get repo(): {owner: string; repo: string} {
    if (process.env.GITHUB_REPOSITORY) {
      const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
      return {owner, repo}
    }

    if (this.payload.repository) {
      return {
        owner: this.payload.repository.owner.login,
        repo: this.payload.repository.name
      }
    }

    throw new Error(
      "context.repo requires a GITHUB_REPOSITORY environment variable like 'owner/repo'"
    )
  }
}

GITHUB_EVENT_PATHに基本的なイベントデータが入っているのでそこに実際のデータを用意して差し込むのが開発時は良さそう

この方法だと、テスト時にJestで環境変数を切り替えてテストしなくてはならなくなったのでたいへんだったがこの辺はまた後日記事にしたい

context? github.event?

開発をしていくなかでcontext、とgithub.eventの違いがよくわからなくなってしまったのでそれぞれ出力して比べてみた

  • イベントデータを出力するサンプル
jobs:
  main:
    name: main
    runs-on: ubuntu-20.04
    steps:
      - run: |
          echo '${{ toJson(github) }}'
      - run: |
          cat $GITHUB_EVENT_PATH
      - uses: actions/github-script@v6
        with:
          script: |
            core.info(JSON.stringify(context))

結論は

context.payload = github.event = GITHUB_EVENT_PATHの中身

で、納得した

ログの扱い

失敗時のJobを特定してログの中身を取得したかったがサクッとはいかなかった

最初、octokitを使ってログを出力させたが、すべてのJobのログが一緒になった状態で返ってきてしまったのでこれだと使えないな…という感じだった

調べたらgh run view --failed-log ${run_id}でやりたいことができているのでもはやこの出力をそのまま流したいw

ということでGitHub CLIの下記コードを参考にするため読んだ

https://github.com/cli/cli/blob/trunk/pkg/cmd/run/view/view.go#L534

これ見ると、ログファイル郡はzipになっていてそれをステップごとにみているっぽい

展開後のツリーはこんな感じ

tree tmp
tmp
├── 0_test.txt
├── -2147483648_test.txt
└── test
    ├── 16_Post Run actionscheckout@v4.1.1.txt
    ├── 17_Complete job.txt
    ├── 1_Set up job.txt
    ├── 2_Run actionscheckout@v4.1.1.txt
    ├── 3_Run actionssetup-node@v4.0.0.txt
    ├── 4_Install Dependencies.txt
    ├── 5_Install Playwright Browsers.txt
    ├── 6_Build.txt
    ├── 7_Run Playwright tests.txt
    └── 8_Run actionsupload-artifact@v3.1.3.txt

1 directory, 12 files

通し番号+Step名というファイル名になっている

APIで取れるJobデータの中にステップ数と名前、成否のデータはあるので、ほしいログファイルの中身を取得できるようになった

ログファイル(zip)の出力タイミング

これでログも見れるようになった!と喜んでいたらworkflow_runイベント以外の呼び出しで失敗するようになってしまった…

JobのログをDLするところで失敗していた、よく考えたら現在実行中のWorkflow以下のJobたちのログをWorkflowの最後に取得するってできないよね(zipだし…)

対象WorkflowのJobログのDLができるようにworkflow_runでWorkflowを分割し、対象Workflowが終了するのを待ってログをDL可能にした

という経緯があり本カスタムアクションはworkflow_runイベントを前提とした処理になっている

一応workflow_run以外でも実行できるが出力される情報が少なくなり、ログについては出力されない

Slack通知 Block KitとAttachments

公式的にはBlock kitを使ってねって流れだったがBlock Kitと併用してAttachmentsを使うようにした

Block Kitは、Slackのよう々な場所で利用できる、インタラクティブなUIを構築するためのフレームワーク

Block Kit | Slack

AttachmentsはSlack通知ではお馴染みの緑や赤などメッセージの横に色をつけられるやつ

Attachmentsについてはドキュメント見に行ったら以前はなくなるよって書かれてた気がするがそうは書かれていなかった

この機能は、Slackアプリのメッセージング機能のレガシーな部分です。レイアウトブロックにこだわることをお勧めしますが、それでも添付ファイルを使いたい場合は、注意事項をお読みください。

やはり色をつけられるのは視認性という意味で外せないんだよな…

セカンダリーコンテンツは、優先順位の低いコンテンツ(メッセージの意図を理解するために必ずしも見る必要はないが、おそらくさらなる文脈や追加情報を追加するコンテンツ)をメッセージに添付できる。

Reference: Secondary message attachments | Slack

api.slack.com

  • Block Kitとattachmentsの併用サンプルコード
  return await webhook.send({
    blocks: [block],
    attachments: [
      {
        color: '#FF6600',
        blocks: blocks
      }
    ]
  });

Slack通知、メッセージの折りたたみ

エラー文言出すならできれば折り畳めたほうがよい

5行以上連続した文かつblockkitのトップレベル以外

とのことだったので折り畳めるようなメッセージの組み立て方にした

まとめ

  • GitHub Actionsのカスタムアクションを作った
    • ワークフローの失敗時のみSlack通知を送れる機能
    • 失敗したワークフローのログの最後の方数行のログを表示させる機能
  • GitHub Actionsがどう動いているかの解像度が上がった
    • actions/toolkitの使い方
    • 開発時のデータの作り方
    • ログファイルの置かれ方
  • テンプレートリポジトリ学びが多い
    • 普段使っていないツールが入っているのでInputになる

よかったら使ってみてください




以上の内容はhttps://swfz.hatenablog.com/entry/2024/12/30/091722より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14