Open9

GitHub App まわりでいろいろ躓いた話

defaultcfdefaultcf

PAT(Personal Access Token)に比べて、権限を細かく設定できるのが嬉しい。
また、GitHub App から発行されるトークンは短命なのでよりセキュアに運用できる。

defaultcfdefaultcf

実行環境

  • 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 をそれぞれ作成する
defaultcfdefaultcf

Actions の設定で secret に秘密鍵を設定し、action で GitHub App から token を作成する。

GitHub App から token を生成する action で有名そうなものは次の通り。

今回は 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 }}
defaultcfdefaultcf

これで実行すると、次のエラーが発生する。

Error: secretOrPrivateKey must be an asymmetric key when using RS256

tibdex/github-app-token の Issue で言及されていた。
https://github.com/tibdex/github-app-token/issues/54#issuecomment-1410471261

どうも 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 側の修正が必要なのか、ちゃんと調査する。

defaultcfdefaultcf

次に 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 に変わるらしい。
https://github.com/actions/checkout

defaultcfdefaultcf

submodule をプライベートリポジトリにしている場合、ssh-key を渡さないと無理っぽい

https://github.com/actions/checkout/issues/287

GitHub App で Contents の読み取り権限を付けてメインとサブのリポジトリ共にアプリがインストールされている なら、GitHub App から生成した token を使って clone ができた。

defaultcfdefaultcf

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 できそう。
https://stackoverflow.com/a/58393457

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 エラーとなった。
どうしよう。

defaultcfdefaultcf

一旦の解決策としては、次のようになった。

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-urlgit submodule set-url でトークン付きの URL に変更する
    • それぞれ権限のあるトークン付きの URL に変更する

これでヨシ。

defaultcfdefaultcf

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 にしたものらしい。
https://medium.com/@szpytfire/authenticating-with-github-via-a-personal-access-token-7c639a979eb3

やはりこの設定内容だと、clone 時の token である read 権限のみの GitHub App の token が入っている。
しかし write 権限ありの GITHUB_TOKEN にしたところで、それは submodule の read/write 権限が無いわけで...
やっぱり persist-credentials: false で認証情報を保存させず、set-url で付けるのが楽なような気がする。