GitHub Package Registry を npm で使う

GitHub Package Registry を npm で使うためのノウハウをここにまとめます。

## Github Package Registry?

名前の通り Github でソフトウェアパッケージ（ライブラリ）をホスティングできるサービスです。 npm とか Ruby Gem とか Maven とかのプライベートなサーバーを用意できるサービスと考えると分かりやすいかもしれません。

公式のドキュメントは以下です。合わせてご覧ください。

https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages

## しくみ

Github Package Registry に配置したパッケージを利用したいプロジェクトのルート( package.json があるディレクトリ)に次のような1行の **.npmrc** ファイルを作成しておきます。

```
registry=https://npm.pkg.github.com/OWNER
```

( `OWNER` には github のアカウント名やチーム名が入ります。先頭に `@` は不要です)

そうすると、`npm install`等々のコマンドが npmjs.com ではなく、 GitHub Package Registry にアクセスするようになります。 GitHub Package Registry は、自分のところに無いパッケージは npmjs.com から取得してくれるので、いつもの `npm` と同じ感覚で使いつつ、プライベートなパッケージも入れることができるようになります。

## 下準備

「いつもの `npm` と同じ感覚で使える」と書きましたが、初回のみ若干の下準備が必要です。というのは npm.pkg.github.com にアクセスするにはログインが必要だからです。ログインは、 Github のユーザー名+パスワードではなく、 **個人アクセストークン** でおこないます。 `npm install` `npm ci` を実行するユーザーは全員、以下の手順で `~/.npmrc` に個人アクセストークンを設定しておく必要があります。

### 1. 個人アクセストークンを作成する

[個人アクセストークンを使用する - GitHub Docs](https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) の「トークンの作成」に従い、個人アクセストークンを作成します。

誤操作で変なバージョンを公開しちゃったりするのを防止するためにも、権限（スコープ）はなるべく少なくしたほうが良いでしょう。 `repo` と `read:packages` だけで十分です。トークンの権限は後から修正することもできます。

### 2. `~/.npmrc` にログイン情報を記入する

```bash
echo "//npm.pkg.github.com/:_authToken=TOKEN" >> ~/.npmrc
```

`TOKEN` には個人アクセストークンを入れます。もちろん、テキストエディタで `~/.npmrc` を修正してもOKです。

公式ドキュメントでは `~/.npmrc` を修正する方法と、 `npm login` コマンドを使用する方法が並列で紹介されていますが、どちらでも同じ効果となります。 `npm login` コマンドは同じように `~/.npmrc` を修正してくれるだけです。

### 3. ログインの確認

先ほどの .npmrc のあるディレクトリ（またはその子ディレクトリ）で `npm whoami` を実行して、あなたのユーザー名が表示されることを確認してみてください。

以上で下準備は完了です。今度こそ、いつもの `npm` と同じ感覚で使えます。

## パッケージを登録する

以降はパッケージを登録・管理する人向けの解説となります。

### 1. 権限の追加

パッケージの管理を担当する人は、 `write:packages` の権限も必要となります。 Github の Developer settings -> Personal access tokens のページから権限を追加するか、パッケージ登録用のトークンを別に作成してください。

### 2. package.json の修正

パッケージを公開する上で、いくつか package.json の修正が必要になります。

- パッケージ名は「`@`(オーナー名を小文字にしたもの)`/`(レポジトリ名)」とする必要があります(`@nuxtjs/vuetify` みたいな形式です。 scoped NPM package というやつらしい)。 package.json の `name` を修正しましょう。
- 必須ではありませんが、 package.json に以下を追加しておきましょう。「誤って本家 npmjs に公開してしまった」みたいな問題を避けることができます。

```json
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/"
  }
```

- package.json の `repository` が間違っていないことを確認しましょう。 Github Package Registry は `repository` または `name` から対象レポジトリが判定されるため、誤っているとエラーとなります。

補足：公式では、いわゆる monorepo で一つのレポジトリから複数のパッケージを公開したい場合の方法も解説されています。

https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#publishing-multiple-packages-to-the-same-repository

その他、本家 npmjs で公開する際の一般的な注意点が適用されます。 `main` または `bin` があるかとか、 `version` は更新したかとか、 `"private": true` になってないかとかです。その辺は適宜ググって qiita にでも聞いてください。

### 3. npm publish

あとは本家に公開するときと同じく `npm publish` するだけです。

```bash
npm publish
```

## misc

### パッケージの public/private

※ ここは公式ドキュメントに記述が全然無いので、挙動からの推測となります。

> You can host software packages privately or publicly and use them as dependencies in your projects.

と書いているわりにパッケージの public/private についての説明が一切ないので調べました。結論から言うと、 **親であるレポジトリの public/private に依存して見えるか否かが変わる** ようです。

まず、本家が参考情報としてしかレポジトリ情報を持っていないのとは異なり、 Github Package Regisry では「Github レポジトリがパッケージを持っている」という関係性になっているようです。そのため、 Github レポジトリではないソースからパッケージを作ることはできません。この「どのレポジトリから作られたパッケージか？」というのは、前述のように package.json の `repository` および `name` から判定されています。

GraphQL API で見ても `RegistryPackage` 型は public/private のフラグを持っていないので、親であるレポジトリのステータスに依存すると考えて良さそうです。

すなわち、 .npmrc が以下のようだったなら、、

```
registry=https://npm.pkg.github.com/OWNER
```

- OWNER の public レポジトリに属するパッケージは見える
- OWNER の private レポジトリのうち権限を付与されているものに属するパッケージは見える
- 本家 npmjs で公開されているパッケージは、全部見える

ということになります。ぐだぐだ書きましたが **「レポジトリが見えるならそのパッケージも見える」** ということで、レポジトリと親子関係を持っていることさえ理解すれば割とシンプルです。

補足になりますが、

- OWNER 以外が publish したパッケージは、 public レポジトリであっても **見えない**

という点は注意しないといけないかも知れません。複数のOWNERのパッケージを使う方法についても公式に書いてあります. -> [Installing packages from other organizations
](https://help.github.com/en/github/managing-packages-with-github-package-registry/configuring-npm-for-use-with-github-package-registry#installing-packages-from-other-organizations)

### CodeBuild, etc

CI/CD サービスの中ででも `~/.npmrc` ファイルを作成することで Github Package Registry にアクセスできるようになります。

```bash
echo "//npm.pkg.github.com/:_authToken=TOKEN" >> ~/.npmrc
npm ci # or, npm install
```

### Github Actions

Github Actions では、あらかじめ Github Package Registry にアクセスする権限を持ったトークンが `secrets.GITHUB_TOKEN` にセットされています。それを [actions/setup-node](https://github.com/actions/setup-node) と組み合わせて使います。

```yaml
      - uses: actions/setup-node@v1
        with:
          registry-url: https://npm.pkg.github.com/
          scope: '@your-github-username'
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
```

`NODE_AUTH_TOKEN` という環境変数が出てきますが、これは Github Actions 独自の仕組みです。 actions/setup-node で Node.js をセットアップすると、この環境変数を参照する `~/.npmrc` が自動的に生成されるようです。

### パッケージを削除する

<ins>現在はGraphQLを使わなくとも、Web上からも削除できるようになりました。（2022/07/25 追記）</ins>

- [About GitHub Package Registry - GitHub Help](https://help.github.com/en/github/managing-packages-with-github-package-registry/about-github-package-registry#deleting-a-package)
- [Schema Previews \| GitHub Developer Guide](https://developer.github.com/v4/previews/#access-to-package-version-deletion)

要約すると、 **public なレポジトリのパッケージは削除できない。 private なレポジトリのパッケージは現在のところ GraphQL API からのみ削除できる。** ということになります。少し試してみましたが、 public なレポジトリを一時的に private にして削除するのはアリのようです。 Github Pacage Registry を使う理由はプライベートなレポジトリが欲しいからという人が多いだろういし、あんまり気にしないでいいかもしれません。

試したときに使ったクエリをメモしておきます。

1. レポジトリを private に変更する
2. GraphQL APIでpackageVersionIdを調べる

```graphql
query {
  repository(owner: OWNER, name: PACKAGE_NAME){
    id
    name
    registryPackages(first: 10, publicOnly: false) {
      nodes {
        id
        name
        versions(first: 10) {
          nodes {
            id
            version
          }
        }
      }
    }
  }
}  
```

3. GraphQL APIの `deletePackageVersion` ミューテーションで削除する（パッケージのバージョンが複数ある場合にはこの操作を繰り返す）

```graphql
mutation {
  deletePackageVersion(input: {
    packageVersionId: PACKAGE_VERSION_ID
  }) {
    clientMutationId
    success
  }
}
```

GraphQL APIから削除できると言っても `RegistryPackageVersion.deleted` というフラグがGraphQLスキーマに追加されているように、一覧から見えなくなるだけでgithub内部にデータは残り続けるようです。そのため、削除したものと同じバージョン番号での `npm publish` はエラーになります。