お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
Node.js::その他サービス::GitHub::GitHub Actions

Node.jsのスクリプトを適当に書き次のようなWorkflowsを記述することで実行できる。勿論Dockerイメージを作ってそこから立ち上げることもできるので好みの問題。直に呼べる分、取り回しは楽だと思う

name: hoge
description: hoge
inputs:
  GITHUB_TOKEN:
    description: 'Workflowsを実行するリポジトリのGITHUB_TOKEN'
    required: true
on:
  workflow_call:
runs:
  using: node20
  main: dist/index.js

inputs:outputs:を記述すれば入出力の引数も定義できる。

inputs.GITHUB_TOKENについて

これを書いておくとoctokitを使ってGitHubのAPIを叩けるようになる。普通は何かAPIを叩くはずなので書いておいた方が良い。

以下は一例。

const github = require('@actions/github');
const core = require('@actions/core');

async function run() {
    const token = core.getInput('GITHUB_TOKEN');
    const octokit = github.getOctokit(token);

    const { data: pullRequest } = await octokit.rest.pulls.get({
        owner: 'octokit',
        repo: 'rest.js',
        pull_number: 123,
        mediaType: {
          format: 'diff'
        }
    });

    console.log(pullRequest);
}

参考

投稿日:
サービス::GitHub

久々にGitHub Projectsを見たら以前より格段に機能が増えて便利になっていた。

まず、プロジェクトの作成時に様々な機能からテンプレートを選べるようになっており、大まかな機能は作ってくれるようになっているようだ。

様々なProjectsテンプレートが選べるようになっている

バックログやカンバン、イテレーションボード、ロードマップ(ガントチャート)、自分のタスク一覧など、様々なタブが作れるようになっており以前より格段に便利になったと思う。

以前はイテレーションボードがなく、カンバンをしていると無限にタスクがたまっていく問題があり難儀していた。当時はどうしようもなかったのでToDo, WIP, Doneのバックログタブを作り、巨大なカンバンとして利用していたように思うが、イテレーションで絞り込むのが地味に怠く、難儀した記憶がある。

ただWorkflowがまだイマイチだ。Item added to projectではIssueとPullRequestでワークフローを分けられないので、IssueはToDoとして追加し、PullRequestはWIPとして追加するみたいなのが出来ないし、Auto-add to projectも複数リポジトリを対象にできないので、単一リポジトリからのIssue, PullRequestしか自動追加に対応できず、折角リポジトリを跨いだ管理ができる利点を損ねている。

ただまだ発展途上に見えるので、これからに期待したいところだ。例えばタブをダブルクリックしてリネームしてもリロードすると元に戻るが、オプションから明示的にリネームするとできるなど、バグのような挙動も見られる。この事からまだBetaくらいだと思っていたほうがよさそうだ。Beta外れてるように見えるけど()

参考までに今回適当に作ったやつを貼っておく。

バックログ

これは以前からあった機能で特に進化していないと思う。ごく普通のバックログだ。カラムの値でグループ化して区切れたりして地味に便利な奴だ。

バックログ

カンバン

これも以前からあったやつで特に進化していないと思う。ごく普通のカンバンだ。昔はこれでイテレーション管理しようとして無理で諦めた。

カンバン

イテレーション

以前はなかったが増えてたやつだ。スプリントとかをやるときに便利かもしれない。イテレーションはバックログに列を追加すると、そこから設定可能なようだ。

イテレーション

ロードマップ

いわゆるガントチャートが引けるやつだ。詳しく見てないのでWBS的な使い方ができるのかは謎だが、ないよりマシくらいな機能感は見える。恐らく予実管理には向かないだろう。

ロードマップ

自分のタスク

自分のタスクだけが見れる奴だと思う。バックログでも見れるのでありがたみは薄いが、面倒くさがりや、ライトユーザーには嬉しい機能かもしれない。

自分のタスク

その他

タブをカスタマイズするオプションが豊富にあり、色々出来そうな気配を感じた。

タブのカスタマイズオプション

この記事ではGitHub ActionsのCustom actionをJavaScriptで実装するJavaScript actionをTypeScriptとSWCを使って実装した方法を書いてます。

モチベーション

  • GitHub ActionsのWorkflowsを共通化したい
  • TypeScriptでロジックを書きたい
  • SWCを使いたい(nccはtscを使うので避けたい
  • github-scriptは一定以上のボリュームがあるものには向かない
    • これを使いつつUTを書いたりするとなると結構面倒になると思う

今回作るもの

Custom actionのうちJavaScript actionを作成します。
実装コードはTypeScript、トランスパイラはSWC、バンドラはwebpackを利用します。
バンドラを利用するのは、node_modules/をGitで管理したくないためです。
ビルド成果物であるdist/は実行時に必要なため、Gitで管理します。
(CI上でビルドしてキャッシュさせておくことも出来ると思いますが、今回は扱いません)

確認環境

Env Ver
@actions/core 1.10.0
@actions/github 5.1.1
@swc/cli 0.1.57
@swc/core 1.3.26
swc-loader 0.2.3
typescript 4.9.5
webpack 5.75.0
webpack-cli 5.0.1

サンプルコード

Custom Actions本体

Custom action本体のサンプルコードです。以下に一式があります。
https://github.com/Lycolia/typescript-code-examples/tree/main/swc-ts-custom-actions

ディレクトリ構成

dist/配下を叩くため、ここはGit管理に含めます。バンドルするのでnode_modules/はGit管理から外して問題ありません。

├─dist/
│ └─index.js        # Custom Actionsとして実行するファイル本体
├─node_modules/
├─src/
│ └─index.ts        # TypeScript実装
├─action.yaml       # Custom Actionsの定義
├─package-lock.json
├─package.json
├─swcrc-base.js     # SWCの設定
├─tsconfig.json     # tscの設定
└─webpack.config.js # webpackの設定
swcrc-base.js

SWCの設定例。特にJavaScript actionのための設定はなく、CLI向けのトランスパイルが出来る設定ならおk。
ファイル名は何でも大丈夫ですが、この場では.swcrcにしないことで、直接SWCで利用しないことを判りやすくするために違う名前にしています。

module.exports = {
    module: {
        type: 'commonjs',
    },
    jsc: {
        target: 'es2020',
        parser: {
            syntax: 'typescript',
            tsx: false,
            decorators: false,
            dynamicImport: false,
        },
        baseUrl: '.',
        paths: {
            'src/*': ['src/*'],
        },
    },
};
webpack.config.js

SWCを使って.tsファイルをバンドルするための設定。これがないとimportの解決ができずにコケます。
node_modules/配下をGit管理に含める場合は不要かもしれませんが、それをするのは微妙だと思います。

const path = require('path');
const swcrcBase = require(path.resolve(__dirname, 'swcrc-base'));

module.exports = {
    // エントリポイント
    entry: path.resolve(__dirname, 'src/index.ts'),
    // 出力設定
    output: {
        // クリーンアップ後に出力
        clean: true,
        // 出力ファイル名
        filename: 'index.js',
        // 出力パス
        path: path.resolve(__dirname, 'dist'),
    },
    // 設定必須なので何か指定しておく
    mode: 'production',
    // 指定してないとNode.jsのネイティブAPIが呼べない
    target: ['node'],
    module: {
        // swc-loaderの設定
        rules: [
            {
                test: /\.ts$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'swc-loader',
                    // swcrcの設定
                    options: {
                        ...swcrcBase,
                    },
                },
            },
        ],
    },
    resolve: {
        // import時のファイル拡張子を省略してる場合にパスを解決するための設定
        extensions: ['', '.ts', '.js'],
    },
};
src/index.ts

最低限これだけ確認できれば応用して実装できるだろうという程度のサンプルコード。

@actions/*系の使い方は以下のリンクから確認できます。
actions/toolkit: The GitHub ToolKit for developing GitHub Actions.

import * as core from '@actions/core';
import * as github from '@actions/github';

const githubToken = core.getInput('GITHUB_TOKEN', { required: true });
const octokit = github.getOctokit(githubToken);

console.log('octokit', octokit);
console.log('context', github.context);
core.setOutput('RESULT_MESSAGE', 'test result message');
action.yaml

実装の参考例として引数と出力を定義してます。特に不要な場合は書かなくてもいいです。
Node.jsのバージョンを詳細に指定したい場合は、composite action にすれば可能だとは思いますが、試してない。
composite actionにしてnvmか何かでインストールしてやれば恐らく可能。

構文は以下のページで確認できます。
GitHub Actions のメタデータ構文 - GitHub Docs

name: example
description: custom actions example
inputs:
  GITHUB_TOKEN:
    description: 'Repogitory GITHUB_TOKEN'
    required: true
outputs:
  RESULT_MESSAGE:
    description: 'Result message'
on:
  workflow_call:
runs:
  using: node16
  main: dist/index.js

Custom actionを使う側

Custom actionを使うWorkflowのサンプルコードです。以下にソースがあります。
https://github.com/Lycolia/custom-actions-usage-example

.github/workflows/example.yaml

usesのところにはリポジトリの組織名と、リポジトリ名、action.yamlが配置されているディレクトリまでのパスを書きます。ルートディレクトリにある場合はパスを書かなくてOK
最後に@sha-hashでコミットハッシュかタグを付けてやれば呼べるようになります。
動作確認中はハッシュが頻繁に変わるので、最新のハッシュを取得してきて設定されるようにしておくと便利かもしれません。

name: run example
on:
  workflow_dispatch:
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - name: run custom actions
        id: test
        uses: org-name/repo-name/path/to/file@sha-hash
        with:
          # Custom action側で定義されている引数(input)の設定
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: show custom actions output
        # Custom action側で定義されている出力(output)の取得
        run: echo ${{ steps.test.outputs.RESULT_MESSAGE }}

参考資料

割とハマってだるいので今回はサンプル程度にPRの一覧を取得して個別にSlackに投げるものを作ってみます

PRが2個ある場合、出力イメージはこんな感じ。2投稿に分けて投稿します

GitHub ActionからのSlackへの投稿イメージ

API トークンの入手

まずはSlack APIを叩くためのトークンをゲットします

  1. Create an appからアプリを作成
  2. 左のメニューからFeatures -> OAuth & Permissions
  3. Scopesを設定
    1. 今回はBot Token Scopesをchat:writeとします
  4. 左のメニューからSettings -> Install App to Your Teamでアプリをインストール
  5. トークンが吐き出されるのでメモする

GitHub Actions Workflowsの作成

SECRETの設定

  1. Slack APIトークンをリポジトリのSecretsに突っ込んでおきます
  2. 名前は一旦SLACK_TOKENとします

Workflowsの作成

前提
  • PR一覧の取得には actions/github-script を利用します
    • GitHub内部の情報を抜いたり、JSで処理を組みたいときに重宝します
    • APIリファレンスが読みやすいので、使うのにはあんま苦労しないと思います
  • Slack APIを叩くのにはcurlを利用します
    • actions/github-scriptから叩くのは多分難しいです
ベースの作成

これに肉付けをしていきます

name: Post to slack example
on:
  workflow_dispatch:
jobs:
  post-slack:
    runs-on: ubuntu-latest
    steps:
PR一覧の取得

List pull requestsにある通りに進めていきます

- uses: actions/github-script@v6
  id: set-result
  with:
    result-encoding: string
    script: |
      const { data: respPulls } = await github.rest.pulls.list({
        owner: context.repo.owner,
        repo: context.repo.repo,
      });

      console.log(respPulls);
PR一覧の加工

こんなデータを取る感じで組んでいきます
API仕様は List pull requests を参照

type PullRequest = {
  id: number;
  reviewers: string[];
};

先ほど取得したrespPullsを上記の型付けになるように加工します

const getReviewersName = (requested_reviewers) => {
  return requested_reviewers.map((reviewers) => {
    return reviewers.login;
  });
};

const getPullRequests = (pulls) => {
  return pulls.map((pull) => {
    return {
      id: pull.number,
      reviewers: getReviewersName(pull.requested_reviewers),
    };
  });
};

const pulls = getPullRequests(respPulls);
Slackに投げるメッセージの作成

こんなメッセージをPRの数分組んでいきます

なお実際にSlackでメンションを作る場合はGitHubのスクリーン名とSlackのユーザー IDの突き合わせ処理が別途必要です。やり方は別途後述します

@foo @bar
https://example.com/pulls/1

やっていること

  • 上記のフォーマットでメッセージを作成
  • シェルスクリプトで配列として扱うためにBase64にエンコード
    • 改行コードが混ざっていると扱いづらいので
  • エンコードした文字列をスペース区切り文字列として連結
    • AAA BBB CCC ...みたいな
  • 最後にWorkflowsの戻り値として設定しています
const encodedMessages = pulls.reduce((messages, pull) => {
  const reviewersBuff = pull.reviewers
    .reduce((acc, cur) => {
      return `${acc}${cur} `;
    }, '')
    .replace(/ $/, '');

  const reviewers = reviewersBuff === '' ? 'レビュアー未設定' : reviewersBuff;

  const message = `${reviewers}\\nhttps://example.com/pulls/${pull.id}`;
  const encodedMessage = Buffer.from(message).toString('Base64');

  return `${messages}${encodedMessage} `;
}, '');

return encodedMessages;
curlを利用してSlack APIを叩く

やっていること

  • encodedMessages=(${{steps.set-result.outputs.result}})
    • 前項で作った文字列を配列として取得しています
  • for message in ${encodedMessages[@]}
    • foreach的なやつです
    • 改行コードがこの時点で存在すると上手くいきません
  • decoded_mes=$(echo ${message} | base64 -di)
    • ここでBase64エンコードをデコードします
  • postSlack "$decoded_mes"
    • 別引数にならないように""で固めます
  • curl叩いてるところ
    • -dの中をヒアドキュメントで展開するのが味噌です
    • 単純に文字列として扱うと変数展開が起きてJSONが壊れます
      - run: |
        postSlack() {
            local mes=$1

            curl -sS https://slack.com/api/chat.postMessage \
                -H 'Authorization: Bearer ${{ secrets.SLACK_TOKEN }}' \
                -H 'Content-Type: application/json; charset=UTF-8' \
                -d @- <<EOF
                {
                    token: "${{ secrets.SLACK_TOKEN }}",
                    channel: "#api-test",
                    text: "$mes"
                }
        EOF
        }

        encodedMessages=(${{steps.set-result.outputs.result}})

        for message in ${encodedMessages[@]}
        do
            decoded_mes=$(echo ${message} | base64 -di)
            postSlack "$decoded_mes"
        done
コード全体
name: Post to slack example
on:
  workflow_dispatch:
jobs:
  post-slack:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        id: set-result
        with:
          result-encoding: string
          script: |
            const getReviewersName = (requested_reviewers) => {
              return requested_reviewers.map((reviewers) => {
                return reviewers.login;
              });
            };

            const getPullRequests = (pulls) => {
              return pulls.map((pull) => {
                return {
                  id: pull.number,
                  reviewers: getReviewersName(pull.requested_reviewers),
                }
              });
            }

            const { data: respPulls } = await github.rest.pulls.list({
              owner: context.repo.owner,
              repo: context.repo.repo,
            });

            const pulls = getPullRequests(respPulls);

            const encodedMessages = pulls.reduce((messages, pull) => {
              const reviewersBuff = pull.reviewers.reduce((acc, cur) => {
                return `${acc}${cur} `
              }, '').replace(/ $/, '');

              const reviewers = reviewersBuff === '' ? 'レビュアー未設定' : reviewersBuff;

              const message = `${reviewers}\\nhttps://example.com/pulls/${pull.id}`;
              const encodedMessage = Buffer.from(message).toString('Base64');

              return `${messages}${encodedMessage} `
            }, '');


            return encodedMessages;
      - run: |
          postSlack() {
            local mes=$1

            curl -sS https://slack.com/api/chat.postMessage \
                -H 'Authorization: Bearer ${{ secrets.SLACK_TOKEN }}' \
                -H 'Content-Type: application/json; charset=UTF-8' \
                -d @- <<EOF
                {
                    token: "${{ secrets.SLACK_TOKEN }}",
                    channel: "#api-test",
                    text: "$mes"
                }
          EOF
          }

          encodedMessages=(${{steps.set-result.outputs.result}})

          for message in ${encodedMessages[@]}
          do
              decoded_mes=$(echo ${message} | base64 -di)
              postSlack "$decoded_mes"
          done

Appendix:Slackにメンションを投げる方法

Slackへ実際にメンションを投げるのはユーザーIDを指定する必要があります
参考:Formatting text for app surfaces

"text": "<@U024BE7LH> Hello"のようにすることでメンションを投げられます

ユーザーIDはSlackアプリから相手のプロフィールを開き、そこにあるハンバーガーメニューみたいなやつから取れます。一応取得用のAPIもあります

ユーザーIDをSlackアプリ画面から取得する方法

投稿日:
開発::自動化サービス::GitHub::GitHub Actions

About custom actionsに書いてあることほぼそのままですが、読みづらいのでメモがてら
どうもカスタムアクションからjsを呼び出して使うの、バンドルする必要があるようでかなりだるそうなので実用性は微妙かも…

フォルダ構成

この説明ではtest.yamlをworkflowとし、index.jsを蹴るためのサンプルで説明します

└─.github/
   └─workflows/
       ├─.actions/
       │   ├─action.yaml
       │   └─index.js
       └─test.yaml

サンプルコード

test.yaml

workflow本体です

name: test
on:
  workflow_dispatch:
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./.github/workflows/actions
        with:
          param1: 'xxx'

action.yaml

custom actionです。
ファイル名はaction.ymlないしaction.yamlである必要があります。
これ以外のファイル名の場合、実行時に次のエラーが出ます。

Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '/home/runner/work/ci-test/ci-test/.github/workflows/actions'. Did you forget to run actions/checkout before running your local action?
name: test actions
description: test
runs:
  using: 'node16'
  main: 'index.js'

index.js

console.log(123);