← Blog一覧へ

private ComposerパッケージのCI認証をSSH鍵からGitHub Appに移した

#tech#GitHub Actions#Composer

うちのCIには、しばらく前から放置している既知の問題がありました。人間が作ったPRではCIが通るのに、Dependabotが作ったPRだけが落ちるのです。

Error: The ssh-private-key argument is empty. Maybe the secret has not been
configured, or you are using a wrong secret name in your workflow file.

原因は分かっていました。Dependabotが起動したワークフローではDependabot secretsのみが参照可能で、Actions secretsは読めません。

the only secrets available to the workflow are Dependabot secrets. GitHub Actions secrets are not available. — GitHub Docs「Troubleshooting Dependabot on GitHub Actions」

GitHubのシークレットは Actions / Dependabot / Codespaces でストアが完全に独立しています。設定画面のタブが分かれているのは見た目の話ではなく、本当に別の保管庫なんですね。うちのSSH鍵はActions secrets側にしか登録されていなかったので、Dependabot PRでは空文字に解決されていた、というわけです。

前提として、構成はこうです。アプリケーション側のリポジトリが、社内のprivateリポジトリで管理しているComposerパッケージに依存しており、CIでは webfactory/ssh-agent にdeploy keyを渡して composer install していました。

止血だけなら、Dependabot secretsに同じSSH鍵をコピーすれば済みます。これは認証方式の問題ではなく保管場所の問題なので、SSH鍵のままでもトークン方式でも、Dependabot PRで使うなら両方のストアに登録が必要になる。それが分かっていたからこそ、雑に鍵を複製して終わりにしたくなくて手を付けずにいました。今回ようやく余裕ができたので、この機会に認証方式ごと見直して根本解決することにしました。

認証方式の選択

まず、そもそもの認証方式を比較してみました。

資格情報 スコープ 寿命 所有者
Deploy key(現状) 1リポジトリ / read-only 無期限 リポジトリ
Classic PAT repo アクセス可能な全リポジトリにread/write 任意 個人
Fine-grained PAT リポジトリ+権限を限定可 最大366日 個人
GitHub App リポジトリ+権限を限定可 1時間(自動失効) 組織

比較して見えてきたポイントが2つあります。

GitHub Appなら組織所有で、installation tokenは1時間で自動失効します。というわけでGitHub Appを採用しました。

GitHub Appの設定

やることは少ないです。

ワークフローの書き換え

ssh-agentのステップを、actions/create-github-app-token によるトークン発行に置き換えます。

-      - name: Set up SSH agent
-        uses: webfactory/ssh-agent@v0.10.0
+      - name: Generate token for private Composer packages
+        uses: actions/create-github-app-token@v2
+        id: composer-token
         with:
-          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
+          app-id: ${{ secrets.APP_ID }}
+          private-key: ${{ secrets.APP_PRIVATE_KEY }}
+          owner: my-org
+          repositories: my-private-package

発行したトークンは COMPOSER_AUTH 環境変数で渡します。

       - name: Composer install
         run: composer validate --no-check-publish && composer install ...
+        env:
+          COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ steps.composer-token.outputs.token }}"}}'

composer.json のリポジトリURLもSSHからHTTPSへ。

-      "url": "git@github.com:my-org/my-private-package.git"
+      "url": "https://github.com/my-org/my-private-package.git"

そしてシークレット(App IDと秘密鍵)は、冒頭の話のとおり ActionsとDependabotの両方に登録します。

途中で判明した落とし穴

ここからが本題かもしれません。移行作業そのものより、途中で踏んだ罠のほうが学びが多かったです。

1. Organization secretsはGitHub Freeではprivateリポジトリから読めない

App IDと秘密鍵を組織シークレットに一度だけ登録して全リポジトリで共有——としたかったのですが、できませんでした。

Organization-level secrets and variables are not accessible by private repositories for GitHub Free.

Selected repositories の選択UI自体は表示されるのに、privateリポジトリは候補に出てきません。Freeプランではリポジトリ個別のRepository secretsを使うしかない。つまり消費側リポジトリごとに、Actions / Dependabotの2箇所へ登録することになります。地味につらい。

2. composer.jsonのURLを変えるとcomposer.lockのcontent-hashがずれる

repositories セクションを変更すると composer validate がexit 2になります。composer update <package> --no-install でlockを更新すれば解消。

面白いのは、config セクション(後述の preferred-install など)はcontent-hashの計算対象外なこと。そちらの変更ではlock更新は不要です。

3. GitHub Appトークンでは preferred-install: source が使えない

これが一番の罠でした。CIはsuccessなのに、ログをよく見るとこうなっていたんです。

Failed to download my-org/my-private-package from source
remote: Invalid username or token. Password authentication is not supported for Git operations.
Now trying to download from dist      ← フォールバックで救われていた

composerは自社パッケージを source(git clone)で取得する設定にしていました。composerはgithub-oauthトークンをユーザー名の位置に置いたURLで git clone しますが、GitHub Appのinstallation tokenは x-access-token:<token>@github.com 形式のユーザー名を要求するため、この認証は通りません。PATなら通るのに、installation tokenだけ落ちる。

一方 distapi.github.com のzipballをAuthorizationヘッダで取得するため問題なく動きます。だからフォールバックで「たまたま」成功していたわけです。

-    "preferred-install": {
-      "my-org/*": "source",
-      "*": "dist"
-    },
+    "preferred-install": "dist",

dist に統一して解消。副産物としてinstall時間も2m30sから1m22sに短縮されました。

「privateリポジトリはdistで取得できない」という前提は、認証トークンがあれば成り立ちません。昔の名残でsource指定が残っているプロジェクト、意外と多いのではないでしょうか。

4. Dependabot PRのRe-runでは直らない

ワークフローを修正してbaseにマージしたあと、失敗していたDependabot PRを「Re-run」しても直りません。pull_request イベントのRe-runはPR作成時点のmerge commitのワークフローファイルで走るからです。

PRに @dependabot rebase とコメントしてブランチを作り直してもらう必要があります。これを知らないと「直したはずなのに落ち続ける」で無限に混乱します。

まとめ

放置していた既知の問題でしたが、腰を据えて取り組んだら、蓋を開ければシークレット管理と資格情報の設計の話でした。同じ構成(private Composerパッケージ+deploy key)のプロジェクトは他にもあるので、順次同じ移行を進めていきます。