はじめに
GitHub Packages は非常に便利で、npm パッケージを配布する際の最初の選択肢の1つではないでしょうか。
一方で、npm には GitHub Packagesを経由せずとも、GitHub にホストされた repository の URL を直接指定してインストールする機能もあります。
それぞれ下記のように指定します。
{
"name": "from-github-package",
"dependencies": {
"some-package": "^0.0.1"
}
}
{
"name": "from-github-repository",
"dependencies": {
"some-package": "github:<org-name>/<repository-name>"
}
}
一般に前者の方が優れています。
- CDN から配信されるため高速
- バージョンを固定できるため冪等性が高い
後者の欠点はいくつかありますが、最大の問題は repository から直接インストールするため、ビルド済みの成果物を Git 管理する必要が出てくる点だと思います。
prepare
を使えば成果物を Git 管理から外すことも可能ですが、インストール時にビルドが必要になるなど運用が煩雑になります。
また成果物がビルド環境に依存するため、必ずしも冪等とは言えなくなります。
Private なパッケージをインストールする代表的な方法として、PAT(Classic)を使う方法と GitHub App を使う方法があります。
後者を使いたい場面が多いのですが、1つ制約がありました。
というのも私が確認したところ、GitHub App 経由では GitHub Packages からのインストールができないようです。
ここで、公式ドキュメントには次のように書かれています。
GitHub Packages では、personal access token (classic) を使用した認証のみがサポートされています。
つまり、fine-grained PAT では GitHub Packages からインストールできないということです。
また、GitHub App の認証と fine-grained PAT の認証は同等の権限モデルを持つため、同じ制約に直面します。
The permissions available to fine-grained personal access tokens are the same permissions available to GitHub Apps, and repository targeting works the same too.
以上から、GitHub App 単体では GitHub Packages の認証には向かない、という結論を導きました。
本記事の目的
上で触れた 2 つの方法(PAT(Classic)/GitHub App)それぞれで Private パッケージをインストールする方法を示します。
最後に、GitHub Actions の外で Private repository をインストールする特殊ケースの対応例も紹介します。
PAT(Classic)を用いる方法
この方法では、GitHub Packages 経由でパッケージをインストールできます。package.json は例えば次のようになります。
{
"name": "from-github-package",
"dependencies": {
"some-package": "^0.0.1"
}
}
インストール実行ルートに .npmrc
を置き、権限を付与した PAT (Classic) を使う方法がよく使われます。必要な権限の例は次の通りです。
- repo
- read:packages
.npmrc
の例:
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
@org-name:registry=https://npm.pkg.github.com
GitHub Actions での設定例は次の通りです。
- name: Setup Node.js
uses: actions/setup-node@v4
with:
registry-url: https://npm.pkg.github.com/
scope: "@org-name"
- name: Install dependencies
run: npm install
env:
NODE_AUTH_TOKEN: ${{ secrets.PAT }}
行っていることはローカルと同じで、actions/setup-node@v4
が自動的に.npmrc
を生成し、NODE_AUTH_TOKEN
環境変数を参照します。
PAT(Classic)が利用可能である場合、この方法が最もシンプルで確実です。
GitHub App を用いる方法
こちらは repository を直接 clone してインストールする方式になります。
package.json は次のように指定します。
{
"name": "from-github-repository",
"dependencies": {
"some-package": "github:<org-name>/<repository-name>"
}
}
単なる repository の clone なので、GitHub App で発行したトークンを使って Git の URL を置換する方法が使えます。
組織にアプリをインストールする際は、対象 repository に対して「コンテンツ(repository)へのアクセス許可」を与えてください。
ワークフローの主要な流れは次のようになります。
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_SECRET }}
owner: ${{ github.repository_owner }}
repositories: |
some-package
- name: Replace GitHub URL with Token
run: |
git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install dependencies
run: |
npm install
persist-credentials: false
は必須です。デフォルトでは actions/checkout
が GITHUB_TOKEN
を .git/config
に設定するため、これが git config --global
の設定より優先され、権限不足エラーになります。
以上が GitHub App を用いた一般的な流れです。
GitHub Actions 外で Private repository をインストールする例
課題
ここからは付録です。
というのも、Firebase Functions の自動デプロイで遭遇した問題についてです。
Firebase の自動デプロイ時、 npm install
は Cloud Build 上で実行されます。つまり、GitHub Actions 上で npm install
を実行しても、Cloud Build の実行環境には反映されません。したがって、デプロイに .npmrc
を含めるなどの対応が必要になることがあります。
PAT (Classic) が利用可能であれば、上記の .npmrc
を含める方法が最も簡単です。
どうしても GitHub App を使いたい場合、GitHub Package の利用は諦めるしかありません。
ですが、Replace GitHub URL with Token
で実行した URL の書き換えは Cloud Build には引き継がれません。
さて、どうすればいいのでしょうか。
解法
Cloud Build に URL 置換の設定が引き継がれない都合上、インストール対象パッケージの URL を一時的に package.json に直接書き換えるしかありません。
- name: Temporarily update package.json
run: |
npm pkg set dependencies.@org-name/some-package="git+https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/org-name/some-package.git"
この方法で、GitHub App のトークンが有効な間は該当 URL からインストールできます。
まとめ
Private パッケージ をインストールする場合、現時点では PAT (Classic) を使った GitHub Packages を利用することが最も確実です。複雑なビルドプロセスを持つ TypeScript パッケージなどではなおさらです。
この記事の解法はあくまで応急処置的なものであり、根本的な解決策ではありません。
特にFirebase Functions のバンドルの中に、有効期限があるとはいえ、token が含まれた package.json が残るのは好ましくありません。
また 1 例に過ぎないため、他にも良い方法があるかもしれません。
GitHub App を使う場合は、将来的に GitHub がこの制約を解消することを期待しています。
何か誤りや改善点があれば、ご教示いただけると幸いです。