gh-extension-precompile を利用したGitHub CLI 拡張のリリースの仕組み

May 05, 2025

GitHub CLI(ghコマンド)の拡張を最近作ってみて、ドキュメント通りに作成しリリースできたものの、バイナリがどこにどのようにアップロードされて、ユーザーにインストールする際には具体的にどのような挙動になるのかを理解したかったので調べた。

この記事では、gh-extension-precompile がどのような処理を行い、バイナリがどこにどのようにアップロードされ、ユーザーがインストールする際にどのような挙動になるかを紹介する。

GitHub CLI拡張

https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions
https://docs.github.com/en/github-cli/github-cli/using-github-cli-extensions

GitHub CLI拡張は、GitHub CLI(ghコマンド)の機能を拡張するためのカスタムコマンドを作成できる機能。 gh extension install <EXTENSION-NAME> でインストールし、gh EXTENSION-NAME で呼び出すことで、標準機能にない独自の処理を呼び出すことができる。

拡張機能を作る場合、方法として以下の2種類がある。

  1. インタプリタ型拡張: シェルスクリプトなどの実行可能ファイルを直接配置する。gh extension create <EXTENSION-NAME> で作成。
  2. プリコンパイル型拡張: コンパイルされたバイナリをリリースに添付する。gh extension create --precompiled=<go or other> <EXTENSION-NAME> で作成。

インタプリタ型拡張の場合には、ユーザーの環境依存の問題を減らすため、bashスクリプトが推奨されている。 プリコンパイル型拡張では、複数のOS・アーキテクチャ向けにバイナリをコンパイルする必要がある。これを手動で行うのを楽にするために、gh-extension-precompile アクションが用意されている。

gh-extension-precompile の使い方

https://github.com/cli/gh-extension-precompile

gh-extension-precompile は前述の通り、複数OS・アーキテクチャ向けのバイナリを一括で作成してassetsへアップロードしてくれるactions。

Goで書かれた拡張機能のケース

gh extension create --precompiled=go EXTENSION-NAME で作成すると、Goベースのプロジェクトが自動で作成される。
この時、gh-extension-precompile を利用した、リリース用のワークフローも自動で作成される。

name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cli/gh-extension-precompile@v2
        with:
          go_version_file: go.mod

これにより、タグを作成してプッシュするだけでリリースが自動化される。

git tag v1.0.0
git push origin v1.0.0

タグをプッシュすると自動的に以下が実行される。

  • 複数のプラットフォーム向けにGoコードのクロスコンパイルを行う
  • GitHubリリースを作成する
  • コンパイルされたバイナリをリリースアセットとしてアップロードする

その他のコンパイル言語で書かれた拡張機能

Go以外の言語で拡張機能を実装する場合は、自前のビルドスクリプトを用意する必要がある。

- uses: cli/gh-extension-precompile@v2
  with:
    build_script_override: "script/build.sh"

gh-extension-precompile がやっていること

gh-extension-precompile アクションの主な処理は、action.yml とその中で実行される build_and_release.sh シェルスクリプトで実装されている。下記のような動きとなる。

  1. ビルド:

  2. チェックサム生成・署名(optional):

  3. リリース作成:

  4. アーティファクト証明(optional):

チェックサムと署名の有効化

チェックサムファイルを一緒に公開することで、ファイルの改竄を防止できる。ユーザーはダウンロードしたファイルからチェックサムを計算し、公開されている値と比較することで、正しいバイナリであることを確認できる。

- id: import_gpg
  uses: crazy-max/ghaction-import-gpg@v6
  with:
    gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
    passphrase: ${{ secrets.GPG_PASSPHRASE }}
- uses: cli/gh-extension-precompile@v2
  with:
    gpg_fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}

内部的には、checksums.txt を出力して assets へ追加している他、gpg --output checksums.txt.sig --detach-sign checksums.txt で出力された、checksum.sig.txt もassetsへ追加される。

これにより、チェックサム自体が開発者によって正式に署名されたものであることを確認した上で、チェックサムを利用してバイナリが改竄されていないことを確認できる。

アーティファクト証明の有効化

generate_attestations: true を指定することで、actions/attest-build-provenance を利用して、アーティファクト証明を有効化できる。

https://github.com/actions/attest-build-provenance

permissions:
  contents: write
  id-token: write
  attestations: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cli/gh-extension-precompile@v2
        with:
          generate_attestations: true

actions/attest-build-provenance は、GitHub ActionsのOIDCトークンから短期間の証明書を作成し、それを利用して改竄不可能な暗号署名を作成、証明書をGitHubのattestation apiへアップロードする。

これは、<OWNER>/<REPOSITORY>/attestations で確認できる。
例: https://github.com/ntsk/gh-issue-bulk-create/attestations

ダウンロードしたバイナリが、この正しいRepositoryでビルドされたかを確認するには、gh attestation verify コマンドを利用すると良い。

❯ gh attestation verify ~/Downloads/darwin-amd64 -R ntsk/gh-issue-bulk-create

Loaded digest sha256:5931396a28acd7b7a9541f9935aca49b76ef9e810353919b356406a9371b9ea9 for file:///Users/ntsk/Downloads/darwin-amd64
Loaded 1 attestation from GitHub API

The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/ntsk
- Source Repository URI must match:......... https://github.com/ntsk/gh-issue-bulk-create
- Subject Alternative Name must match regex: (?i)^https://github.com/ntsk/gh-issue-bulk-create/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com

✓ Verification succeeded!

The following 1 attestation matched the policy criteria

- Attestation #1
  - Build repo:..... ntsk/gh-issue-bulk-create
  - Build workflow:. .github/workflows/release.yml@refs/tags/v1.1.0
  - Signer repo:.... ntsk/gh-issue-bulk-create
  - Signer workflow: .github/workflows/release.yml@refs/tags/v1.1.0

gh extension install はどこからバイナリをダウンロードするのか

ここまででリリースする方法は分かったが、そもそも、gh extension install は具体的にどこからバイナリをダウンロードするのか。

cli/cli リポジトリの ghコマンド本体の gh extension install のコマンドについて確認する。

https://github.com/cli/cli/blob/a2fcb9b2df3b89418491cc7d4bd733d9c53134b0/pkg/cmd/extension/manager.go#L248-L275

isBinExtension 関数でインタプリタ型かプリコンパイル(バイナリ)型かを特定、この時、GitHub releaseをチェックしてassetsがあればプリコンパイル型だと判定している。

https://github.com/cli/cli/blob/a2fcb9b2df3b89418491cc7d4bd733d9c53134b0/pkg/cmd/extension/manager.go#L743-L765

インタプリタ型であれば、リポジトリ自体をcloneして利用し、プリコンパイル型であれば、installBin 関数にて、fetchLatestReleasefetchReleaseFromTag を利用して最新バージョンをチェックした上で、プラットフォームに対応したバイナリを探し、ダウンロードするという動きになっているようだった。

まとめ

ghコマンドは、実際のユーザー相当の処理を実行できるためかなり権限が強い。

手順通りに作成すれば、extension自体は簡単に作れてしまうが、企業などで利用する場合、攻撃者が用意した悪意のある拡張などをインストールしてしまうと、private repositoryの情報が抜かれたりとリスクが伴う。

このため、どのようにダウンロード・インストールされ、どのように攻撃を回避・防止できるのか確認しておくと良いと考える。