ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

Github Actionsのアクション作ってみる(Typescript編)

f:id:rasukarusan:20210122225003p:plain:w500

Typescriptで独自アクション作る

以前GithubActionsを使ってHomebrewのリリースを自動化し、一部のstepを独自アクションとして切り出した。

www.rasukarusan.com www.rasukarusan.com

前回のDockerで書き出したアクションを、Typescriptで書いてみる。

対象のstep

steps:
  # 1つ前のtagからの差分を取得
  - name: Get commit summary
    id: get_commit_summary
    run: |
      PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
      COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
      COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
      echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY

1つ前のtagからの差分を取得する、というstep。リリースノートに記載するためのもの。

f:id:rasukarusan:20210122223959p:plain

1つ前のtagからの差分コミットを一覧出力

作り方

基本的に公式のJavascriptでの独自アクションの作り方と、Typescriptで書かれたテンプレートのリポジトリを見ながらやればOK。

docs.github.com github.com

完成形

github.com

~/Documents/github/gitblamer/.github/actions/commit-summary
$ tree
.
├── action.yml
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

諸々不要なものを削った最小構成。npmでインストールするものは下記。

npm init -y
npm install @actions/core
npm install @actions/exec
npm install --save-dev @vercel/ncc
npm install --save-dev @types/node
npm install --save-dev typescript
  • @actions/core: 引数受け取ったりecho ::set-output name=みたいなことをするため。
  • @actions/exec: git等のコマンドを実行するため
  • @vercel/ncc: Node.jsモジュールコンパイル君
  • @types/node: typescript使うため
  • typescript: typescript使うため

action.yml

name: 'Get Commit Summary'
description: 'Get commits from previrous tag to new tag'
inputs:
  ref:
    description: 'new tag'
    required: true
    default: 'please set ${{ github.ref }}'
outputs:
  summary:
    description: 'commit summary'
runs:
  using: 'node12'
  main: 'dist/index.js'

Dockerで作ったときとほぼ一緒。違うところはrunsの箇所。

runs:
-   using: 'docker'
-   image: 'Dockerfile'
-   args:
-     - ${{ inputs.ref }}
+   using: 'node12'
+   main: 'dist/index.js'

package.json

{
  "name": "commit-summary",
  "version": "1.0.0",
  "main": "dist/index.js",
  "description": "",
  "scripts": {
    "build": "ncc build src/index.ts -o dist --license licenses.txt"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@actions/core": "^1.2.6",
    "@actions/exec": "^1.0.4"
  },
  "devDependencies": {
    "@types/node": "^14.14.22",
    "@vercel/ncc": "^0.27.0",
    "typescript": "^4.1.3"
  }
}

tsconfig

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  }
}

公式だともうちょっと色々書いてあったけど、必要最小限にした。

src/index.ts

import * as core from '@actions/core'
import * as exec from '@actions/exec'
import { ExecOptions } from '@actions/exec'

const execute = async (command: string): Promise<string> => {
  let output = ''
  const options: ExecOptions = {}
  options.listeners = {
    stdout: (data: Buffer) => {
      output += data.toString()
    },
    stderr: (data: Buffer) => {
      console.error(data)
    }
  }
  await exec.exec(command, null, options)
  return output

}

const main = async () => {
  try {
    const newTag = core.getInput('ref')
    const preTag = await execute('/bin/bash -c "git tag --sort=-creatordate | sed -n 2p"')
    const summary = await execute(`git log --oneline --pretty=tformat:"%h %s" ${preTag.trim()}..${newTag}`)
    core.setOutput("summary", summary)
  } catch (error) {
    core.setFailed(error.message)
  }
}
main()

アクションの肝。基本的にrunsで実行していたコマンドを、exec.exec()で実行していけばいい。
が、exec.exec()const result = await exec.exec('ls')のようにしても標準出力の内容が受け取れない。返ってくるのは01などの終了コードのみ。標準出力を受け取りたい場合、exec.exec()の第三引数のoptionsに、コールバックを受け取るlistenersを登録するなど、ごにょごにょしないといけない。

// ごにょごにょして標準出力を返すようにする
const execute = async (command: string): Promise<string> => {
  let output = ''
  const options: ExecOptions = {}
  options.listeners = {
    stdout: (data: Buffer) => {
      output += data.toString()
    },
    stderr: (data: Buffer) => {
      console.error(data)
    }
  }
  await exec.exec(command, null, options)
  return output
}

また、exec.exec()はパイプの実行に対応していない。下記の書き方空文字が返ってきてしまう。

const preTag = await execute('git tag --sort=-creatordate | sed -n 2p')

パイプで実行したい場合、/bin/bash -cで実行する必要がある。

const preTag = await execute('/bin/bash -c "git tag --sort=-creatordate | sed -n 2p"')

もしくはせっかくtsで書いているので、パイプ以降でやりたい処理をtsで書いてもいい。今回の場合やりたいことは「上から二行目を取得する」なのでpreTag.split('\n')[1]みたいな感じでもいい。

ビルドしてworkflows/release.ymlを更新したら終了

npm run build

ビルド後のディレクトリ構造

~/Documents/github/gitblamer/.github/actions/commit-summary
$ tree
.
├── action.yml
├── dist
│   ├── index.js
│   └── licenses.txt
├── package-lock.json
├── package.json
├── src
│   └── index.ts
└── tsconfig.json

2 directories, 7 files

workflows/release.yml

       # 1つ前のtagからの差分を取得
       - name: Get commit summary
         id: get_commit_summary
-        run: |
-          PREVIOUS_TAG=$(git tag --sort=-creatordate | sed -n 2p)
-          COMMIT_SUMMARY="$(git log --oneline --pretty=tformat:"%h %s" $PREVIOUS_TAG..${{ github.ref }})"
-          COMMIT_SUMMARY="${COMMIT_SUMMARY//$'\n'/'%0A'}"
-          echo ::set-output name=COMMIT_SUMMARY::$COMMIT_SUMMARY
+        uses: ./.github/actions/commit-summary
+        with:
+          ref: ${{ github.ref }}

すっきり。

f:id:rasukarusan:20210122223756p:plain

tagをpushして動作確認

終わり

Docker、Typescriptの両方でアクションを作成することができた。今回みたいなちょっとしたスクリプトレベルだったらTypescriptじゃなくていい。
もうちょっとヘビーな処理とかチーム開発だったら、絶対Typescript選びたくなるんでしょうね。