GitHub App まわりでいろいろ躓いた話
PAT(Personal Access Token)に比べて、権限を細かく設定できるのが嬉しい。
また、GitHub App から発行されるトークンは短命なのでよりセキュアに運用できる。
実行環境
- GitHub: GitHub Enterprise Server
- Runner: Self hosted runner
- Ubuntu 22.04
- actions-runner-linux-x64-2.296.3
GitHub App を作成する。
- org 設定ページから GitHub App を作成することで、org の権限で動作させる
- 権限は Contents の read だけ設定する
- Client secret と Private key をそれぞれ作成する
Actions の設定で secret に秘密鍵を設定し、action で GitHub App から token を作成する。
GitHub App から token を生成する action で有名そうなものは次の通り。
-
tibdex/github-app-token
- GitHub の公式ドキュメントでちらほら登場するので、個人的には GitHub のお墨付きのある action と思っている
-
cybozu/octoken-action
- Cybozu が OSS で公開している
-
getsentry/action-github-app-token
- Sentry が OSS で公開している
今回は cybozu/octoken-action を使ってみることにする。次のように job を書く。
- uses: cybozu/octoken-action@v1
id: create-iat
with:
github_app_id: 55
github_app_private_key: ${{ secrets.GH_APP_READ_ALL_PRIVATE_KEY }}
これで実行すると、次のエラーが発生する。
Error: secretOrPrivateKey must be an asymmetric key when using RS256
tibdex/github-app-token の Issue で言及されていた。
どうも Ubuntu のバージョンというか同梱される OpenSSL のバージョン関係で発生している模様。
Ubuntu 20.04 → OK
Ubuntu 22.04 → NG
ちなみに私の環境だと OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) だった。
その後のコメントで、action に次のように環境変数を渡すことで解決するという話があった。
- uses: cybozu/octoken-action@v1
id: create-iat
with:
github_app_id: ${{ secrets.GH_APP_READ_ALL_ID }}
github_app_private_key: ${{ secrets.GH_APP_READ_ALL_PRIVATE_KEY }}
env:
OPENSSL_CONF: /dev/null
確かにこれを入れると正しく token が取得できるようになった。
ただこれでなんで上手くいくのか、この記述が不要になるには action 側の修正が必要なのか、ちゃんと調査する。
次に actions/checkout で、submodule 込みのリポジトリを clone する。
- actions/checkout@v3
- git 2.34.1
Fetching the repository は成功する。
Fetching submodules で次のエラーが発生する。
Error fatal: repository 'https://***/defaultcf/obsidian-vault.git/' not found
Error: fatal: clone of 'git@***:defaultcf/obsidian-vault.git' into submodule path '/home/defaultcf/actions-runner/_work/github-actions-playground/github-actions-playground/docs' failed
.gitsubmodules は ssh プロトコルの URL を書いているのに、actions/checkout では HTTPS になってる...
actions の README をよく読むと、ssh-key のインプットが無い場合は HTTPS に変わるらしい。
submodule をプライベートリポジトリにしている場合、ssh-key を渡さないと無理っぽい。
GitHub App で Contents の読み取り権限を付けて 、 メインとサブのリポジトリ共にアプリがインストールされている なら、GitHub App から生成した token を使って clone ができた。
workflow 内で commit しようとして躓いた話
ここまで来たら、あとは secrets.GITHUB_TOKEN (GitHub App で生成した token ではなく、ワークフローを走らせるごとに自動で生成されるトークン 参考)を使って commit して push すればいい、と考えていた。
しかし、permissions で contents: write を与えていてもプッシュができない。403 エラーが返ってくる。
fatal: unable to access 'https://xxx': The requested URL returned error: 403
どうも checkout した時の token で push しようとするらしい。試しに GitHub App で Contents を read から write に変えたところ push に成功するようになった。
では、checkout には GitHub App で生成した token で、push には secrets.GITHUB_TOKEN を使うにはどうしたら良いか。
remote の URL に token を含めるよう自分で設定することで、指定した token を使って push できそう。
jobs:
deploy:
runs-on: self-hosted
permissions:
contents: write # コミットのため
steps:
- uses: cybozu/octoken-action@v1
id: create-iat
with:
github_app_id: ${{ secrets.GH_APP_READ_ALL_ID }}
github_app_private_key: ${{ secrets.GH_APP_READ_ALL_PRIVATE_KEY }}
env:
OPENSSL_CONF: /dev/null
- uses: actions/checkout@v3
with:
submodules: recursive
token: ${{ steps.create-iat.outputs.token }}
- name: Update docs
run: |
echo "Access Token: ${{secrets.GITHUB_TOKEN}}"
git submodule update --remote docs
git add docs
echo "::group::git status"
git status
echo "::endgroup::"
# 変更があればコミットする
if [ `git diff --staged --name-only | wc -l` -gt 0 ]; then
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git remote get-url origin
git config user.name github-actions
git config user.email github-actions@github.com
git commit -m "chore: Sync [skip ci]"
git push origin main
fi
しかしこれでは同じく 403 エラーとなった。
どうしよう。
一旦の解決策としては、次のようになった。
jobs:
deploy:
runs-on: self-hosted
permissions:
contents: write # コミットのため
steps:
- uses: cybozu/octoken-action@v1
id: create-iat
with:
github_app_id: ${{ secrets.GH_APP_READ_ALL_ID }}
github_app_private_key: ${{ secrets.GH_APP_READ_ALL_PRIVATE_KEY }}
env:
OPENSSL_CONF: /dev/null
- uses: actions/checkout@v3
with:
submodules: recursive
token: ${{ steps.create-iat.outputs.token }}
persist-credentials: false # コレ重要
- name: Update docs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APP_TOKEN: ${{ steps.create-iat.outputs.token }}
run: |
# 重要な2行
git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}
git submodule set-url docs https://github-actions:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY_2}
git submodule update --remote docs
git add docs
echo "::group::git status"
git status
echo "::endgroup::"
# 変更があればコミットする
if [ `git diff --staged --name-only | wc -l` -gt 0 ]; then
git config user.name github-actions
git config user.email github-actions@github.com
git commit -m "chore: Sync [skip ci]"
git push origin main -vvv
fi
重要なのは2点。
- actions/checkout で
persist-credentials: faseを付ける- これは actions/checkout が .git/config に認証情報を書き込んでいるためで、これにより読み取り権限のみの認証情報が書き込まれてしまっている
- おそらく remote の URL 上にトークンが書き込まれているわけではなさそう、後で確認する
- なので opt-out する
-
git remote set-urlとgit submodule set-urlでトークン付きの URL に変更する- それぞれ権限のあるトークン付きの URL に変更する
これでヨシ。
actions/checkout で persist-credentials: true とした時の .git/config を眺めてみる。
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/***
fetch = +refs/heads/*:refs/remotes/origin/*
[gc]
auto = 0
[http "https://github.com/"]
extraheader = AUTHORIZATION: basic ***
[branch "main"]
remote = origin
merge = refs/heads/main
[submodule "docs"]
active = true
url = git@github.com:***
extraheader で認証情報が付いてることが分かった。
この中身というのが、username:token の文字列を base64 にしたものらしい。
やはりこの設定内容だと、clone 時の token である read 権限のみの GitHub App の token が入っている。
しかし write 権限ありの GITHUB_TOKEN にしたところで、それは submodule の read/write 権限が無いわけで...
やっぱり persist-credentials: false で認証情報を保存させず、set-url で付けるのが楽なような気がする。