この記事では、新しくなった GitHub Actions における、JavaScript アクション作成方法について解説します。
GitHub Actions とはなんぞやという人は、先にこちらの過去記事をどうぞ。
目次
- アクションについて
- アクションの保存場所
- アクションのバージョニング
- 簡単な例 (Hello, World)
- メタデータ
- JavaScript アクションの使用する Node.js バージョン
- toolkit
- node_modules の管理
- README.md
- 公式テンプレート
- 少し実践的な例
- その他
- まとめ
アクションについて
前回の記事で新しくなった GitHub Actions の使い方について簡単に解説しました。この記事執筆時点では GitHub Actions はまだベータなので、最新の情報を得るためには公式ドキュメントをご参照ください。
前の記事にも書いたとおり、GitHub Actions では「アクション」という単位で実行可能なタスクを作成し、コミュニティに公開することもできます。
アクションには以下の 2 種類が存在し、それぞれ利用可能な VM 環境が異なります。
種類 | 利用可能な VM 環境 |
---|---|
Docker コンテナ | Linux |
JavaScript | Linux, MacOS, Windows |
この記事では、主に JavaScript によるアクションの作成方法について解説します。
JavaScript アクションは、GitHub が提供するすべての VM 環境から直接実行できます。Docker イメージのビルドやコンテナの起動コストが発生しない分、実行が高速です。
アクションの保存場所
公開するアクションの場合、アクションのためのリポジトリを作成して、他のコードとは別に管理することが推奨されています。
公開しない場合はどこに保存してもよく、単独のリポジトリからのみ利用する場合は、.github
ディレクトリ下に保存することが推奨のようです。(.github/actions/action-a
, .github/actions/action-b
のように)
アクションのバージョニング
アクションを利用するときは、以下の 3 つの方法で参照できます。
参照方法 | 例 |
---|---|
commit SHA | actions/setup-node@74bc508 |
タグ | actions/setup-node@v1.0 |
ブランチ | actions/setup-node@master |
タグ作成時は、セマンティックバージョニングに従って作成することが推奨されています。
一方で、アクションを利用する側としては commit SHA 方式で参照するのが一番安全です。タグやブランチは上書き可能なためです。GitHub 公式のアクションならまだしも、信頼性の低いサードパーティーのアクションを利用するときは、安全性を確認した時点での commit SHA を指定して利用するのが無難でしょう。
簡単な例 (Hello, World)
実際に簡単なアクションを作成してみます。
GitHub Actions が有効になったユーザーか organization 下に public なリポジトリを作成し、以下のような action.yml
ファイルを作成します。
name: "Hello World" runs: using: "node12" main: "index.js"
詳細は後述しますが、この action.yml
はアクションのメタデータを定義しています。name
でアクションの名前を、runs
の using
で JavaScript アクションであることを、main
でアクション実行時に読み込まれる JavaScript ファイルを定義しています。
そして、次のような index.js
ファイルを作成します。
console.log("Hello, World!");
これは、main
で定義した JavaScript ファイルで、アクション実行時にこのファイルが GitHub Actions の VM 上で実行されます。
動作確認のために、同じリポジトリ内の .github/workflows/hello.yml
を以下のように作成します。
on: [push] jobs: hello-world: runs-on: ubuntu-latest name: Greeting steps: - name: Checkout uses: actions/checkout@v1 - name: Hello uses: ./
同じリポジトリ内であれば、actions/checkout
を実行した後に ./
のように相対パスでアクションを呼び出すことが可能です。
これらのファイルをすべて push したら、Actions タブを開いてアクションが実行されることを確認します。
今度は、別リポジトリからこのアクションを実行できることを確認します。そのために、まずはアクションを作成したリポジトリで v1
タグを作成して push します。
次に、新しいリポジトリを作成し、.github/workflows/hello.yml
を以下のように作成します。
on: [push] jobs: hello-world: runs-on: ubuntu-latest name: Greeting steps: - name: Hello uses: miyajan/javascript-action-hello@v1
今度はアクションの実行のためにはチェックアウトは必要なく、アクション実行時に対象のリポジトリの中身がダウンロードされます。
このファイルを push したら、Actions タブを開いてアクションが実行されることを確認します。
"Set up job" の段階で作成したアクションがダウンロードされていることがわかります。
以上で、"Hello, World!" と表示するだけの簡単なアクションが作成&公開でき、他のリポジトリからも利用可能になりました。
メタデータ
アクションは YAML 形式で書かれたメタデータが必要です。詳細は公式ドキュメントを参照してもらうとして、簡単に説明すると以下のような情報を定義できます。
name
: アクション名author
: アクションの作成者description
: アクションの説明inputs
: アクション利用時にwith
で指定するパラメータoutputs
: このアクション以降のステップで利用するために出力されるパラメータruns
: アクションが実行するコマンドについての情報branding
: Marketplace 公開時のアイコン情報
JavaScript アクションの使用する Node.js バージョン
現時点だと、action.yml
の main
の using
に指定できるのは、node12
だけです。
仮にアクションを呼び出す前に actions/setup-node で環境の Node.js のバージョンを変更していても、アクションの実行には Node.js の 12 系が使用されます。アクションの実行に使用される Node.js はある程度は環境と切り離されてると考えていいようです。
なので、JavaScript アクション開発時は、ローカルには Node.js 12 系を用意しましょう。
toolkit
アクションを作りやすくするために、公式で toolkit というリポジトリで複数のパッケージが公開されています。
詳細については各パッケージの README.md を見てもらうのが一番いいですが、簡単に説明すると以下のような機能を持つパッケージが存在します。
- @actions/core: inputs の読み込み、outputs の設定、環境変数の出力、シークレットの設定、PATH の操作、終了ステータスの変更、ログ、状態管理など
- @actions/exec: コマンドライン実行
- @actions/io: ファイルシステム操作
- @actions/tool-cache: ダウンロード、解凍、キャッシュ(ビルド間のキャッシュではなく、そのビルド内のみで使えるキャッシュ)など
- @actions/github: context の操作、Ocktokit クライアント
node_modules の管理
アクションは、実行時に指定したリポジトリのソースコードをまるごとダウンロードして利用します。なので、node_modules
下の依存ライブラリもリポジトリに含める必要があります。
しかし、node_modules
を master
に含めてしまうと開発がしづらくなってしまうので、以下のようなリリースブランチとタグのみに node_modules
を含める運用が推奨されています。
master
では.gitignore
にnode_modules
を含め、master
にnode_modules
をコミットしない- メジャーバージョンごとにリリースブランチを作成し、リリースタイミングで
master
から変更を取り込む- このときに、
.gitignore
からnode_modules
を削除して、リリースブランチにnode_modules
を push - すでに
node_modules
が存在するときは、一度全削除して作り直してから push
- このときに、
- リリースブランチが安定したら、タグを作成して push
詳細は、以下のページをご参照ください。
node_modules
をリポジトリに含めたくないという場合は、ncc
というツールを使うとすべての依存関係を 1 つの JS ファイルにまとめることができるので、そちらが使えるようです。
プラットフォーム特有のコンパイルが必要な依存が node_modules
下に含まれてるときにどうなるのかは、まだ試していないのでよくわかっていないです。要探求です。
README.md
public なアクションを保存するリポジトリの README.md
には次のような情報を含めることが推奨されています。
- アクションがなにをするかについての詳細な説明
- inputs/outputs についての説明
- アクションが使用する秘密情報についての説明
- アクションが使用する環境変数についての説明
- アクションのワークフロー内での使用例
公式が提供しているアクションのサンプルリポジトリを見るとわかりやすいと思います。
公式テンプレート
公式で JavaScript アクションを作成時のためのテンプレートとなるリポジトリが用意されています。
.gitignore
、ESLint による lint、Jest によるユニットテスト、それらを実行する GitHub Actions ワークフロー、公開やバージョニングについて書かれた README.md
といったものが含まれています。
このリポジトリの "Use this template" ボタンからリポジトリを複製できます。新規アクション作成時は、とりあえずこのテンプレートから作り始めて、自分の作りたいアクションに合わせて修正していくのがよさそうです。
ただ、テンプレートからだと修正が必要な部分も多いので、慣れてきたら自分用のテンプレートを作成しておくのがよさそうにも思います。
少し実践的な例
ここまでの情報を使って、簡単な例よりもう少し実践的なアクションを作成してみます。
今度は、pull_request
イベント時にプルリクエストにコメントを書き込むアクションを作成します。
まず、上記で説明したテンプレートから新たにリポジトリを作成します。
そして、action.yml
を以下のように修正します。
name: "PR Comment" description: "Comment on PR" runs: using: "node12" main: "index.js" inputs: message: description: "Message to comment" required: true outputs: commentUrl: description: "Comment URL"
inputs
の message
でプルリクエストに書き込むコメントの内容を指定し、書き込んだコメントへの URL が outputs
の commentUrl
に保存される想定です。
テンプレートに含まれてない、アクションに必要なパッケージを npm install
します。
$ npm install --save @actions/github $ npm install --save-dev nock
package.json
がテンプレートのままだと実態と合わないので、アクションに合わせて修正します。記事上では省略します。
次に、index.js
を修正します。
const run = require('./run'); run();
テンプレートだと index.js
内に若干のロジックが書かれていますが、自分は完全にコード実行部分のみにしました。というのも、コード実行部分とロジックが同じファイルに同居してしまうとテストできなくなってしまうためです。
そして、ロジック部分を run.js
に分離しました。
const core = require('@actions/core'); const github = require('@actions/github'); async function run() { try { // pull_request exists on payload when a pull_request event is triggered // Do nothing when pull_request does not exist on payload const pr = github.context.payload.pull_request; if (!pr) { console.log('github.context.payload.pull_request not exist'); return; } // Retrieve GITHUB_TOKEN from environment variable // Do nothing when GITHUB_TOKEN does not exist const token = process.env['GITHUB_TOKEN']; if (!token) { console.log('GITHUB_TOKEN not exist'); return; } // Get input const message = core.getInput('message'); console.log(`message: ${message}`); // Create octokit client const octokit = new github.GitHub(token); // GITHUB_REPOSITORY is GitHub Action's built-in environment variable // https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables const repoWithOwner = process.env['GITHUB_REPOSITORY']; const [owner, repo] = repoWithOwner.split('/'); // Create a comment on PR // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment const response = await octokit.issues.createComment({ owner, repo, issue_number: pr.number, body: message, }); console.log(`created comment URL: ${response.data.html_url}`); core.setOutput('commentUrl', response.data.html_url); } catch (error) { core.setFailed(error.message); } } module.exports = run;
そして、テンプレートに存在した index.test.js
を run.test.js
にして、以下のようにしました。(Jest に慣れてないので、これが一般的な書き方なのかは怪しいです)
const core = require('@actions/core'); const github = require('@actions/github'); const nock = require('nock'); nock.disableNetConnect(); const run = require('./run'); beforeEach(() => { jest.resetModules(); github.context.payload = { action: 'opened', pull_request: { number: 1, }, }; }); describe('run', () => { it('comments on PR', async () => { process.env['INPUT_MESSAGE'] = 'Test Comment'; process.env['GITHUB_REPOSITORY'] = 'testorg/testrepo'; process.env['GITHUB_TOKEN'] = 'test-github-token'; nock('https://api.github.com') .post('/repos/testorg/testrepo/issues/1/comments', body => body.body === 'Test Comment') .reply(200, {html_url: 'https://github.com/testorg/testrepo/issues/1#issuecomment-1'}); const setOutputMock = jest.spyOn(core, 'setOutput'); await run(); expect(setOutputMock).toHaveBeenCalledWith( 'commentUrl', 'https://github.com/testorg/testrepo/issues/1#issuecomment-1'); }); });
inputs
のパラメータは、環境変数の INPUT_<パラメータ名の大文字>
で実は指定可能です。なので、ユニットテストでも process.env
でモックできます。環境変数ももちろんモック可能です。
nock を使えば GitHub API とのやりとりも含めて GitHub Actions の一連の流れをテストすることが可能です。しかし、実際にこのアクションを作成するとしてこのテストを書くかと言われると、たぶんもう少しテストしやすい形にコードを整理すると思いますが、あくまでサンプルとして読んでください。
続いて、.github/workflows/test.yml
も修正します。
name: "test-local" on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - run: npm ci - run: npm test - uses: ./ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: message: PR comment test id: prComment - run: echo "Comment URL - ${{ steps.prComment.outputs.commentUrl }}" if: steps.prComment.outputs.commentUrl
GITHUB_TOKEN
は組み込みで存在する秘密情報に存在するので、それを環境変数に渡すだけで使えます。
if
は文字列が空のときは false
の扱いになるので、特定の outputs
が存在するときだけステップを実行するには上記のように if
に outputs
の変数をそのまま指定すれば大丈夫です。
README.md
もアクションに合わせて修正します。LICENSE
も "GitHub Actions" の部分だけ修正が必要です。この記事内では省略します。
テンプレートに含まれてる wait.js
はいらなくなるので消します。
もろもろ修正が終わったら、ブランチを作成して push します。
$ git checkout -b first-commit $ git add . $ git commit -m "first commit" $ git push origin first-commit
push
イベントでトリガーされるワークフローでは、payload
に pull_request
が存在しないからアクション内部でなにもしないで終了します。最後の URL 出力ステップもスキップされるので以下のようになります。
プルリクエストを作成すると、pull_request
イベントでワークフローがトリガーされ、アクションによってプルリクエストにコメントが作成され、最後の URL 出力ステップも実行されます。
そして、プルリクエスト上でコメントを確認できます。
GITHUB_TOKEN
に組み込みの秘密情報を渡しているため github-actions
bot によってコメントが作成されます。自分がコメントしてるように表示させたいのであれば、自分のアクセストークンを GITHUB_TOKEN
環境変数に渡せば大丈夫です。
プルリクエストを master
にマージし、公開するためにリリースブランチを作成します。まずは、.gitignore
から node_modules
をコメントアウトします。
# comment this out distribution branches # node_modules/
リリースブランチを作成して、production の依存のみ残し、.gitignore
と node_modules
を git add
して、push します。
$ git checkout -b releases/v1 $ rm -rf node_modules $ npm install --production $ git add node_modules .gitignore $ git commit -m node_modules $ git push origin releases/v1
リリースブランチができたら、そのブランチからアクションを実行してみて問題ないかを確認します。適当なリポジトリの GitHub Actions で以下のような感じに実行してみます。
uses: miyajan/javascript-action-pr-comment@releases/v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: message: Nice PR!👍
無事動いてることを確認できたら、タグを作成します。
$ git tag v1 $ git push origin v1
これでアクションがタグ指定で実行できるようになりました。
uses: miyajan/javascript-action-pr-comment@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: message: Nice PR!👍
その他
GitHub 公式のアクション
actions
org 下に GitHub 公式のアクションが複数存在します。(アクション以外の toolkit
とかもありますが)
サードパーティーのアクション
GitHub Marketplace の Actions 一覧から、サードパーティーのアクションを検索できます。(公式のアクションも含まれます)
まとめ
GitHub Actions における JavaScript アクションのつくりかたについて、サンプルを示しつつ解説しました。TypeScript 編とか Docker コンテナアクション編とかはまた別記事でやります。(時間があれば…)
基本的には公式ドキュメントを読みつつ、テンプレートや既存のアクションの実装を参考にしていけばどんどんアクションを作っていけそうという感想です。
NPM エコシステムに乗っかることによって、けっこう高機能なアクションでも簡単に作れそうなのがいいですね。あと、GitHub API との親和性が高いのもやはり強いです。
アクションの拡張性が高そうなので、GitHub Actions 組み込みで機能が足りないところは、どんどんアクションを作ってしまえばいいという感じになるのですかね。よいアクションが出てくるのを楽しみにしつつ、自分でもなにかしら作って探求していきたいです。
2020/02/27 追記
『GitHub Actions 実践入門』の電子書籍版を BOOTH で販売開始しました。より詳細な GitHub Actions の解説や最新情報が書かれています。