2022/11/14(月)GitHub ActionsでSlackに投稿

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

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

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

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アプリ画面から取得する方法

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

2022/11/11(金)Bashで文字列配列を扱う

更新日:
投稿日:

配列をループして処理したい時に使える技。複数行ある場合も想定

一行文字列の配列

スペースで区切った文字列を()で括っておくとfor inとして使える

取り出した要素は${foo}で拾えます。配列全体は${bar[@]}で取れる。以下のようにダブルクォートで囲っていると機能しなくなるので注意
("AAA BBB CCC")

#!/bin/bash

textList=(YWFhYWEK LWFhYQogICAtIOOBguOBguOBguOBgg== LWFhYQogICAtIOOBguOBguOBguOBgg==)

for text in "${textList[@]}"
do
  echo ${text}
done

出力結果

YWFhYWEK
LWFhYQogICAtIOOBguOBguOBguOBgg==
LWFhYQogICAtIOOBguOBguOBguOBgg==

複数行文字列の配列

Base64辺りでエンコードしておくと扱いが楽

#!/bin/bash

textList=(YWFhYWEK LWFhYQogICAtIOOBguOBguOBguOBgg== LWFhYQogICAtIOOBguOBguOBguOBgg==)

for text in "${textList[@]}"
do
  pure=$(echo ${text} | base64 -d)
  echo "$pure"
done

出力結果

aaaaa
-aaa
   - ああああ
-aaa
   - ああああ

lsの結果を配列にする

#!/bin/bash

# .と..は除外
# 最後にxargsを入れておくと改行が飛ぶ
results=($(ls -1a | grep -vP "^\.+$" | xargs))
for item in "${results[@]}"
do
    echo "$item";
done

スペース区切りの文字を配列にする

IFS変数に区切り文字を設定することで実現する。IFSとはInternal Field Separatorのこと

#!/bin/bash
IFS=$'\n'

text=$(cat <<'EOF'
aa bb
a b c
EOF
)
textList=($(echo "$text"))

for text in "${textList[@]}"
do
  echo "${text}"
done

出力結果

aa bb
a b c

2022/11/11(金)GitHub ActionsでJavaScriptカスタムアクションを作る時にハマった部分

更新日:
投稿日:

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);

2022/11/08(火)TypeScriptでprocess.exit()ラッパーの後をデッドロジック扱いにする

更新日:
投稿日:

TSでprocess.exit()をラップしたカスタムexitを作った時に静的解析のフローが壊れたので、その対策。
因みにこの制御フローのことをControl Flow Analysisと呼ぶらしい。TS系の文書ではCFAと略されていることが多い模様。

問題事例

通常のprocess.exit()例の7-8行目のように次の行から先がデッドコードになって欲しいが、上手くいかないので、これを上手くいくようにする。

カスタムexitのコード例 通常のprocess.exit()例
カスタムexitのコード例
静的解析のエディタ表示例

確認環境

Env Ver
TypeScript 4.8.4

サンプルコード

オブジェクトでラップして、カスタムexit関数の戻り値をneverで指定してやると上手くいくようになる。
因みにこれTypeScriptの仕様らしく、上手くやる方法はあんまりなさそう。
実はアロー関数ではなくfunctionを使えば解決したりするが、それはなんか嫌なので…

example.ts

type ErrorPayload = {
  message: string;
  code: number;
};

type ExampleExit = {
  exit: (err: ErrorPayload) => never;
};

const exit = (err: ErrorPayload) => {
  process.exit(err.code);
};

export const Example: ExampleExit = { exit };

implements.ts

import { Example } from './example';

Example.exit({ message: 'exit', code: 1 });
console.log(1);

関連記事

2022/10/21(金)WSL2へのUbuntu 22.04セットアップメモ

更新日:
投稿日:

環境

Env Ver
Windows 11 Pro Build 22621.674
Ubuntu 22.04.01

ベースのセットアップ

  1. wsl --update
  2. Microsoft StoreからUbuntu 22.04.01 LTSをインストール
    1. GUIのウィザードがコケたらやり直す
  3. /etc/wsl.confに起動設定を追加
    1. リファレンス:learn.microsoft.com
[boot]
systemd=true

[user]
default=<ユーザー名>
  1. wsl --shutdown
  2. Ubuntuのコンソールを開きデフォルトユーザーが設定通りなのを確認
  3. アップデート
sudo apt update
sudo apt upgrade

開発環境のセットアップ

  1. 以下を流す
sudo apt install -y \
    zsh \
    ssh \
    net-tools \
    git \
    unzip \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
# Docker公式のGPG鍵を追加
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# リポジトリをパッケージマネージャーに登録
echo 
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# docker本体のインストール
sudo apt install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-compose-plugin
# 一般ユーザーで docker を利用可能にする
sudo usermod -aG docker $USER
# Node.js
curl https://get.volta.sh | bash
# zsh切り替え
chsh -s $(which zsh)
  1. ターミナル再起動
# zsh設定取り込み
git clone https://github.com/Lycolia/my-zsh-conf.git
rm -Rf my-zsh-conf/.git/
mv my-zsh-conf/.* .
rm -Rf my-zsh-conf/
# install Node.js
volta install node@lts
  1. ターミナル再起動してzsh設定の反映を確認