GitHub Actions (beta) が使えるようになったので調査した

GitHub Actions Open Beta に申し込んで1ヶ月以上経ち,ようやく使えるようになったみたいなので実際にどう使うのか調査してみたメモ. Beta 版は GitHub のプライベートリポジトリにしか使えないため,公開リポジトリに使うにはもう少し待つ必要がありそう.

GitHub Actions とは

GitHub Universe 2018 で発表されたときにメディアが記事にしているので,そちらを読んでください.

公式ドキュメント

hello world 的なのは下記のリンクから action をつくるチュートリアルと workflow をつくるチュートリアルをやれば良いです.

全体的な流れは,

  1. Dockerfileentrypoint.sh をつくって欲しいアクションを定義する(プリセットのアクションしか使わない場合はこのステップは不要)
  2. GitHub の Actions タブに行き,workflow をビジュアルエディタで定義する.ワークフローの起点と,そこからのアクションの連なりをグラフエディタでつくれる.直接 .github/ 以下を書いても良いけど,こっちのほうがチェックもしてくれるし良さそう
  3. 実際に起点となるイベントを起こしてみる(例えば git push
  4. 再び Actions タブに行くと,各ワークフローがどう走って結果がどうだったか(ログなど)が確認できる
  5. ワークフローを編集したい時は .github/ 以下の .workflow ファイルの Edit ボタンをクリックすると再びビジュアルエディタが立ち上がる

参考リンク:

基本

各アクションはアクションを走らせる Docker コンテナのための Dockerfile と処理のエントリポイントになるシェルスクリプト entrypoint.sh の組み合わせで定義できます.

Dockerfile のラベルでメタ情報(description とか action name とか)を記述します.実際の処理は entrypoint.sh(もしくは entrypoint.sh から呼ばれるスクリプトなど)でシェルスクリプトで定義します.依存しているツール(例えば JSON を扱うなら jq とか)は Dockerfile をビルドするときにコンテナに apt install などでインストールしておきます.

コンテナ実行時の情報はスクリプトの引数か,環境変数で与えられます.環境変数はアクションをワークフローエディタで編集する時に指定でき,スクリプト側からそれらが参照できます.またスクリプトの引数もアクションの編集で指定でき,entrypoint.sh の引数としてアクション側に渡ってきます.秘密の情報 (secrets) はリポジトリページの settings から secrets タブを選択してキー・バリューで秘密の情報を入力しておき,アクションの設定でどのキーを使うかを指定しておくと,Docker コンテナから環境変数としてそれらが見える?ようです(env コマンドの出力はフィルタされているのか確認できなかった)

アクションの起点は GitHub の公式ドキュメントに乗っている一覧のイベント から選べます.例えば on: "push" と指定するとコミットをプッシュするたびにワークフローが走ります.

どうやってアクションを書くのか

注:特に明記しない限り,実動作ベースでの調査をしたので,今後動作が変わったり,勘違いしている箇所があるかもしれません

公式のアクション

actions organization に各アクションごとにリポジトリが置かれているので,それを参考にできます.

アクションが実行される環境

pwd で確認すると,アクションのエントリポイントとなる entrypoint.sh/github/workspace というディレクトリ内で実行されています.

このディレクトリについては下記の公式ドキュメントに詳細がありました:

developer.github.com

どうやら対象のリポジトリのルートディレクトリになっているらしいです.試しに ls -la してみると

total 24
drwxr-xr-x 6 root root 4096 Dec  9 13:54 .
drwxr-xr-x 5 root root 4096 Dec  9 13:55 ..
drwxr-xr-x 8 root root 4096 Dec  9 13:54 .git
drwxr-xr-x 2 root root 4096 Dec  9 13:54 .github
drwxr-xr-x 2 root root 4096 Dec  9 13:54 action-a
drwxr-xr-x 2 root root 4096 Dec  9 13:54 action-b

.git ディレクトリが置かれており,リポジトリのルートにいると分かります.

すでにリポジトリはクローンされた状態で実行されるので,自前で対象のリポジトリをクローンしてくる必要は無さそうです.

アクション内で参照できる環境変数

どうやら $GITHUB_* という環境変数に情報が入っているようです.一覧は公式ドキュメントにあり,

developer.github.com

例えば,on: "push"リポジトリへの push を行った際の環境変数は下記です:

GITHUB_EVENT_PATH=/github/workflow/event.json
GITHUB_WORKFLOW=hello
GITHUB_ACTION=Hello World
GITHUB_REPOSITORY=rhysd/hello-github-actions
GITHUB_WORKSPACE=/github/workspace
GITHUB_SHA=52875f0b1ed9882770c0cfddbcfe95607e4b2986
GITHUB_ACTOR=rhysd
GITHUB_REF=refs/heads/master
GITHUB_EVENT_NAME=push

これでどのワークフローやアクションとして自身が実行されているかを知ることができます.

アクション内で参照できるフックイベントの情報

ちなみに $GITHUB_EVENT_PATH/github/workflow/event.json には起点になったイベントの情報が JSON で入っています.各イベントごとに入っている情報はGitHub の公式ドキュメントで知ることができます.詳細なイベントフックの情報が欲しい場合はこっちを見たほうが良さそうです.

例えば on: "push" での event.json の中身は下記です:

{
  "after": "c776e7146a031950fa579791f71448336a07880c",
  "base_ref": null,
  "before": "52875f0b1ed9882770c0cfddbcfe95607e4b2986",
  "commits": [
    {
      "added": [],
      "author": {
        "email": "my-email@example.com",
        "name": "rhysd",
        "username": "rhysd"
      },
      "committer": {
        "email": "my-email@example.com",
        "name": "rhysd",
        "username": "rhysd"
      },
      "distinct": true,
      "id": "c776e7146a031950fa579791f71448336a07880c",
      "message": "check event.json",
      "modified": [
        "action-a/entrypoint.sh"
      ],
      "removed": [],
      "timestamp": "2018-12-09T21:39:47+09:00",
      "tree_id": "7d4c96c54a304cd3af1bdbac684099bbbeb1dcc9",
      "url": "https://github.com/rhysd/hello-github-actions/commit/c776e7146a031950fa579791f71448336a07880c"
    }
  ],
  "compare": "https://github.com/rhysd/hello-github-actions/compare/52875f0b1ed9...c776e7146a03",
  "created": false,
  "deleted": false,
  "forced": false,
  "head_commit": {
    "added": [],
    "author": {
      "email": "my-email@example.com",
      "name": "rhysd",
      "username": "rhysd"
    },
    "committer": {
      "email": "my-email@example.com",
      "name": "rhysd",
      "username": "rhysd"
    },
    "distinct": true,
    "id": "c776e7146a031950fa579791f71448336a07880c",
    "message": "check event.json",
    "modified": [
      "action-a/entrypoint.sh"
    ],
    "removed": [],
    "timestamp": "2018-12-09T21:39:47+09:00",
    "tree_id": "7d4c96c54a304cd3af1bdbac684099bbbeb1dcc9",
    "url": "https://github.com/rhysd/hello-github-actions/commit/c776e7146a031950fa579791f71448336a07880c"
  },
  "pusher": {
    "email": "rhysd@users.noreply.github.com",
    "name": "rhysd"
  },
  "ref": "refs/heads/master",
  "repository": {
    "archive_url": "https://api.github.com/repos/rhysd/hello-github-actions/{archive_format}{/ref}",
    "archived": false,
    "assignees_url": "https://api.github.com/repos/rhysd/hello-github-actions/assignees{/user}",
    "blobs_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/blobs{/sha}",
    "branches_url": "https://api.github.com/repos/rhysd/hello-github-actions/branches{/branch}",
    "clone_url": "https://github.com/rhysd/hello-github-actions.git",
    "collaborators_url": "https://api.github.com/repos/rhysd/hello-github-actions/collaborators{/collaborator}",
    "comments_url": "https://api.github.com/repos/rhysd/hello-github-actions/comments{/number}",
    "commits_url": "https://api.github.com/repos/rhysd/hello-github-actions/commits{/sha}",
    "compare_url": "https://api.github.com/repos/rhysd/hello-github-actions/compare/{base}...{head}",
    "contents_url": "https://api.github.com/repos/rhysd/hello-github-actions/contents/{+path}",
    "contributors_url": "https://api.github.com/repos/rhysd/hello-github-actions/contributors",
    "created_at": 1544355055,
    "default_branch": "master",
    "deployments_url": "https://api.github.com/repos/rhysd/hello-github-actions/deployments",
    "description": null,
    "downloads_url": "https://api.github.com/repos/rhysd/hello-github-actions/downloads",
    "events_url": "https://api.github.com/repos/rhysd/hello-github-actions/events",
    "fork": false,
    "forks": 0,
    "forks_count": 0,
    "forks_url": "https://api.github.com/repos/rhysd/hello-github-actions/forks",
    "full_name": "rhysd/hello-github-actions",
    "git_commits_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/commits{/sha}",
    "git_refs_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/refs{/sha}",
    "git_tags_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/tags{/sha}",
    "git_url": "git://github.com/rhysd/hello-github-actions.git",
    "has_downloads": true,
    "has_issues": true,
    "has_pages": false,
    "has_projects": true,
    "has_wiki": true,
    "homepage": null,
    "hooks_url": "https://api.github.com/repos/rhysd/hello-github-actions/hooks",
    "html_url": "https://github.com/rhysd/hello-github-actions",
    "id": 161032314,
    "issue_comment_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues/comments{/number}",
    "issue_events_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues/events{/number}",
    "issues_url": "https://api.github.com/repos/rhysd/hello-github-actions/issues{/number}",
    "keys_url": "https://api.github.com/repos/rhysd/hello-github-actions/keys{/key_id}",
    "labels_url": "https://api.github.com/repos/rhysd/hello-github-actions/labels{/name}",
    "language": "Dockerfile",
    "languages_url": "https://api.github.com/repos/rhysd/hello-github-actions/languages",
    "license": null,
    "master_branch": "master",
    "merges_url": "https://api.github.com/repos/rhysd/hello-github-actions/merges",
    "milestones_url": "https://api.github.com/repos/rhysd/hello-github-actions/milestones{/number}",
    "mirror_url": null,
    "name": "hello-github-actions",
    "node_id": "MDEwOlJlcG9zaXRvcnkxNjEwMzIzMTQ=",
    "notifications_url": "https://api.github.com/repos/rhysd/hello-github-actions/notifications{?since,all,participating}",
    "open_issues": 0,
    "open_issues_count": 0,
    "owner": {
      "avatar_url": "https://avatars3.githubusercontent.com/u/823277?v=4",
      "email": "rhysd@users.noreply.github.com",
      "events_url": "https://api.github.com/users/rhysd/events{/privacy}",
      "followers_url": "https://api.github.com/users/rhysd/followers",
      "following_url": "https://api.github.com/users/rhysd/following{/other_user}",
      "gists_url": "https://api.github.com/users/rhysd/gists{/gist_id}",
      "gravatar_id": "",
      "html_url": "https://github.com/rhysd",
      "id": 823277,
      "login": "rhysd",
      "name": "rhysd",
      "node_id": "MDQ6VXNlcjgyMzI3Nw==",
      "organizations_url": "https://api.github.com/users/rhysd/orgs",
      "received_events_url": "https://api.github.com/users/rhysd/received_events",
      "repos_url": "https://api.github.com/users/rhysd/repos",
      "site_admin": false,
      "starred_url": "https://api.github.com/users/rhysd/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/rhysd/subscriptions",
      "type": "User",
      "url": "https://api.github.com/users/rhysd"
    },
    "private": true,
    "pulls_url": "https://api.github.com/repos/rhysd/hello-github-actions/pulls{/number}",
    "pushed_at": 1544359187,
    "releases_url": "https://api.github.com/repos/rhysd/hello-github-actions/releases{/id}",
    "size": 3,
    "ssh_url": "git@github.com:rhysd/hello-github-actions.git",
    "stargazers": 0,
    "stargazers_count": 0,
    "stargazers_url": "https://api.github.com/repos/rhysd/hello-github-actions/stargazers",
    "statuses_url": "https://api.github.com/repos/rhysd/hello-github-actions/statuses/{sha}",
    "subscribers_url": "https://api.github.com/repos/rhysd/hello-github-actions/subscribers",
    "subscription_url": "https://api.github.com/repos/rhysd/hello-github-actions/subscription",
    "svn_url": "https://github.com/rhysd/hello-github-actions",
    "tags_url": "https://api.github.com/repos/rhysd/hello-github-actions/tags",
    "teams_url": "https://api.github.com/repos/rhysd/hello-github-actions/teams",
    "trees_url": "https://api.github.com/repos/rhysd/hello-github-actions/git/trees{/sha}",
    "updated_at": "2018-12-09T12:34:31Z",
    "url": "https://github.com/rhysd/hello-github-actions",
    "watchers": 0,
    "watchers_count": 0
  },
  "sender": {
    "avatar_url": "https://avatars3.githubusercontent.com/u/823277?v=4",
    "events_url": "https://api.github.com/users/rhysd/events{/privacy}",
    "followers_url": "https://api.github.com/users/rhysd/followers",
    "following_url": "https://api.github.com/users/rhysd/following{/other_user}",
    "gists_url": "https://api.github.com/users/rhysd/gists{/gist_id}",
    "gravatar_id": "",
    "html_url": "https://github.com/rhysd",
    "id": 823277,
    "login": "rhysd",
    "node_id": "MDQ6VXNlcjgyMzI3Nw==",
    "organizations_url": "https://api.github.com/users/rhysd/orgs",
    "received_events_url": "https://api.github.com/users/rhysd/received_events",
    "repos_url": "https://api.github.com/users/rhysd/repos",
    "site_admin": false,
    "starred_url": "https://api.github.com/users/rhysd/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/rhysd/subscriptions",
    "type": "User",
    "url": "https://api.github.com/users/rhysd"
  }
}

アクション間で情報を受け渡しする

各アクションで毎回リポジトリが clone されるわけではなく,共通のワークスペースが使われます.なので前段のアクションで作成したファイルは後段のアクションでもアクセスすることができます.これによって,あるアクションでビルドした生成物を使って,後段で linter やテスト,デプロイを走らせるといったことができそうです.

サードパーティのアクションが後段にいる場合にはアクセストークンなどの秘密情報をファイルに保存しないように気をつける必要があります.試した限りでは環境変数は後段のアクションに受け継がれないので,前述の secrets 機能で環境変数に置いておくのが良さそうです.

Custom GitHub Action をつくる

アクションは別リポジトリや Docker コンテナに切り出して再利用することができます.actions organization に置かれている公式のアクション集が参考になります.

アクション1つのみを公開するとき

つくるリポジトリに対して公開するアクションが1つのときはリポジトリのルートにそのまま Dockerfileentrypoint.sh を置きます

your-awesome-action/
├── Dockerfile
└── entrypoint.sh

使う側のリポジトリ.github/*.workflow には usesowner/repo@ref を指定すると使えるようになります.ref はブランチ名か commit SHA1 の初め7桁を指定します(タグ名でも良い?).uses は他にも docker:// で始めて直接 Docker コンテナを指定することもできるようです.

action "Awesome Action" {
  uses = "your-name/your-awesome-action@master"
}

workflow "hello" {
  on = "push"
  resolves = ["Awesome Action"]
}

例: https://github.com/actions/npm

アクションを複数公開する時

1つのリポジトリで複数のアクションを公開したい時は,Dockerfileentrypoint.sh をサブディレクトリに置きます.

your-awesome-action/
├── action-a
│   ├── Dockerfile
│   └── entrypoint.sh
└── action-b
    ├── Dockerfile
    └── entrypoint.sh

使う側のリポジトリ.github/*.workflow には usesowner/repo/subdir@ref を指定すると使えるようになります(ref はアクション1つのみの場合と同じ).

action "Awesome Action A" {
  uses = "your-name/your-awesome-action/action-a@master"
}

workflow "hello" {
  on = "push"
  resolves = ["Awesome Action A"]
}

例: https://github.com/actions/bin

感想

ざっと見た感じ,GitHub Action は下記の場合に便利そうです

  • 開発のワークフローを自動化したいとき
    • 自動でラベルを貼り替える
    • 自動で issue を close する
    • パッケージングしてデプロイしたりライブラリをリリースしたり
  • ちょっとした CI を回したい
    • Docker で実行すれば十分な(WindowsmacOS でのテストが要らない)場合
    • linter ぐらいはかけておきたい

ただし下記の点は気をつけておいたほうが良さそうです

  • アクションへの入力のインターフェースが貧弱
    • 現状,前段のアクションで行ったビルド結果を利用したりする必要があるが,それらの前提条件を記述・チェックするような仕組みはない(各アクションで走るスクリプト内で自前でチェックする必要がある)
    • 動的に情報を与える口はスクリプトの引数と環境変数しかないので,あまり凝った入力をさせられない(せいぜい JSONシリアライズして渡すなど).アクションをできるだけ小さく保つことで入力のインターフェースを複雑にしないように気をつけたほうが良さそうに感じました