🔥

GitHub Packagesを試す

2023/04/09に公開

始めに

共通のコードを使い回すためにnpmパッケージ化が案として上がると思いますが、全世界にpublishするほどのものではない場合があると思います。GitHubにはGitHub Packagesというパッケージを配置する場所が用意されており、npmパッケージも同様にpublishすることができるのでそれを試してみました。

プロジェクト構成

この記事では以下のプロジェクト構成で検証しました。

publishされたパッケージはこちらで参照できます。privateなパッケージは他の人からは見えないと思います。

https://github.com/orgs/wintyo-github-packages/packages

パッケージ作成

まずはinstallするためのパッケージを作成します。

パッケージコードの用意

ディレクトリ構成は重要なところだけピックアップすると以下のようになります。

├── dist (ビルド後の成果物の出力先)
├── src (ビルド前のソースコード)
│   └── index.ts (ビルド時のエントリーポイント)
└── package.json (パッケージ情報)

今回は検証のためなのでシンプルなメソッドだけexportします。

src/index.ts
/**
 * 加算メソッド
 * @param x - 数値
 * @param y - 数値
 * @returns - 足した値
 */
export const add = (x: number, y: number) => {
  return x + y;
};

package.jsonは以下のように書いています。nameimport {} from 'xxx'で使われる名前で、mainがインポート時に参照されるファイルになります。

package.json
{
  "name": "@wintyo-github-packages/utils",
  "version": "0.0.0",
  "description": "utility package",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/*",
    "src/*"
  ],
  "scripts": {
    "build": "tsc"
  },
  "author": "wintyo <wintyo1024@gmail.com>",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^4.9.5"
  }
}

tsconfig.jsonは以下のようにしています。自動生成したものから必要そうな項目を指定しているのでもしかしたら不要なものとかがいくつか入っているかもしれないですが、そこはご了承ください。

tsconfig.json
{
  "compilerOptions": {
    /* Language and Environment */
    "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,

    /* Modules */
    "module": "commonjs" /* Specify what module code is generated. */,
    "rootDir": "./src" /* Specify the root folder within your source files. */,
    "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */,

    /* Emit */
    "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
    "sourceMap": true /* Create source map files for emitted JavaScript files. */,
    "outDir": "./dist" /* Specify an output folder for all emitted files. */,

    /* Interop Constraints */
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,

    /* Type Checking */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
    "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,

    /* Completeness */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}

GitHub PackagesにpublishするCIを作成

次にCI上でGitHub PackagesにpublishするCIを書きます。以下のCIではタグが振られた時にpublishするものとなります。
CI設定で重要な点は以下の2点です。

  • GITHUB_TOKENのpermissionsの設定
    • このままだとpublishするための権限がないので、permisssons.contentspermissions.packagesに適切な権限を設定します
  • setup-noderegistry-urlscopeを設定する
    • npm publish先はデフォルトだとnpmレジストリになるため、GitHub Packagesに変更します
    • scopeはGitHubのOrganization名または個人アカウント名になります
GitHub PackagesにpublishするGitHub Actions
name: Publish npm package

on:
  push:
    tags:
      - 'v*.*.*'

permissions:
  contents: read
  packages: write

jobs:
  publish:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: '.node-version'
          registry-url: https://npm.pkg.github.com
          scope: '@wintyo-github-packages'

      - name: Install
        run: yarn install

      - name: Build
        run: yarn build

      - name: Publish npm package
        run: yarn publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish先のURLが違うだけで基本的にはnpmレジストリにpublishするのと同じなので、一度publishしたバージョンは上書きできないので、追加でpublishする場合はpackage.jsonversionを繰り上げる必要があることに注意してください。
ただGitHub Packagesの場合は手動で該当のバージョンを削除することができるので、一度削除すればpublishし直すことはできると思います(未検証)。

publishに成功するとGitHubリポジトリページの右側のPackagesに表示されます。

https://github.com/wintyo-github-packages/utils

publishしたパッケージをprivateからpublicにする

GitHub Packagesはデフォルトはprivateなので、それをpublicに変更します。

Organization設定からpublicパッケージの設定をできるようにする

そもそもpublicパッケージに切り替えることもOrganizationだと最初はできないので、できるように設定します。
Organizationの設定ページのPackagesのところでpublicに変更できるようにチェックを入れます。

publishしたパッケージの設定画面に入ってpublicに切り替える

先ほどpublishされたことを確認した場所からパッケージの詳細に飛びます。

パッケージの詳細ページの右下にある「Package Settings」をクリックします。

そのページにある「Change Visibility」からpublishに切り替えます。

privateパッケージも用意する

上記の作業をもう一度行って別なパッケージを作成し、パッケージの閲覧権限だけprivateのままにします。

作成したパッケージを使用する

手元で使用する

GitHub Packagesをinstallする場合は一手間が必要です。npm installまたはyarn installする際に参照するレジストリを切り替える必要がありますが、GitHub Packagesの場合は必ずPATを入れる必要があります(publicなパッケージであっても)。この辺の設定を.npmrcに書くと以下のようになります。

.npmrc
//npm.pkg.github.com/:_authToken={PATを入れる}
@wintyo-github-packages:registry=https://npm.pkg.github.com

PATの作成方法や.npmrcの設定の意味はこちらをご参照ください。
https://qiita.com/marumaru0113/items/21b600c21caf5d9b9775#認証に必要なもの

これを設定することで後はいつも通りyarn add @wintyo-github-packages/utilsとするだけでinstallされます。

.npmrcを生成するスクリプトを用意

上記の方法で動きますが、PATが含まれているためこのままコミットすることができません。PATは.envに登録しておいて、それをもとに.npmrcが生成された方が運用しやすいと思うのでそのスクリプトを作成します。

まずは雛形を用意します。

./scripts/.npmrc.template
//npm.pkg.github.com/:_authToken=${GITHUB_PKG_TOKEN}
@wintyo-github-packages:registry=https://npm.pkg.github.com

shellスクリプトからテンプレートエンジンっぽく動かすにはenvsubstコマンドを使うと良く、以下のようにすることで${GITHUB_PKG_TOKEN}が差し変わります。

$ export GITHUB_PKG_TOKEN=ghp_XXX
$ cat ./scripts/.npmrc.template | envsubst

envsubstはこちらの記事を参考にしました。

https://kakakakakku.hatenablog.com/entry/2019/06/03/001716

これを使って.npmrcファイルを生成するスクリプトを書きます。

./scripts/generate-npmrc.sh
if [ -z $GITHUB_PKG_TOKEN ]; then
  echo "GITHUB_PKG_TOKENの環境変数が設定されていません"
  exit -1
fi

export GITHUB_PKG_TOKEN=$GITHUB_PKG_TOKEN
cat ./scripts/.npmrc.template | envsubst > .npmrc

最後にsetup.shを用意して、これを呼び出すだけで動くようにします。

./setup.sh
if [ -f .env ]; then
  source .env
fi

source ./scripts/generate-npmrc.sh

GitHub Actionsで使用する

GitHub Actions上ではPATをsecretsに登録して、それを環境変数に入れてsetup.shを実行してからyarn installすることで動きます。secretsの登録の際はGITHUB_*は予約語になっていて使えなかったのでNPM_GITHUB_PKG_TOKENという名前にしています。

GitHub PackagesをGitHub Actions上でinstallする
name: Trial

on: push

jobs:
  install-github-packages:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: '.node-version'

      - name: Setup npmrc
        run: ./setup.sh
        env:
          GITHUB_PKG_TOKEN: ${{ secrets.NPM_GITHUB_PKG_TOKEN }}

      - name: Install
        run: yarn install

      - name: Run
        run: yarn start

余談

ちなみにpublicパッケージだけであればトークンはなんでも良さそうで、GITHUB_TOKENを使ってもinstallできました。

その他

package.jsonのversionを起点にCIを書く

GitHub Packagesへのpublishでタグが振られた場合にCIを動かす書き方をしましたが、この場合packages.jsonのversionが上がっていなかった場合publishに失敗してしまいます。実際のところ、gitのタグというよりpackage.jsonのversionが変わった時に動くと良さそうです。
残念ながらその要望に完全にマッチするやり方は存在しませんが、publish可能ならpublishして、かつpackage.jsonに入っているversionでタグを振っておくCIを書くことができます。

package.jsonのversionを起点にpublishしたりタグを振ったりする
 name: Publish npm package
 
 on:
   push:
+    branches:
+      - main
     tags:
-      - 'v*.*.*'
+      - '!*'

 permissions:
-  contents: read
+  contents: write
   packages: write

 jobs:
   publish:
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout
         uses: actions/checkout@v3

       - name: Setup Node.js
         uses: actions/setup-node@v3
         with:
           node-version-file: '.node-version'
           registry-url: https://npm.pkg.github.com
           scope: '@wintyo-github-packages'

       - name: Install
         run: yarn install

       - name: Build
         run: yarn build

+      # publish可能かのステータスを見る(0が正常。1がエラー)
+      - id: publishable
+        name: Check Publishable
+        run: "\
+          npx can-npm-publish --verbose \
+          && echo status=0 >> $GITHUB_OUTPUT \
+          || echo status=1 >> $GITHUB_OUTPUT"
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+      # publish可能(まだpublishされていないバージョン)ならpublishする
       - name: Publish npm package
+        if: ${{ steps.publishable.outputs.status == 0 }}
         run: yarn publish
         env:
           NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+      # package.jsonのバージョンを取得する
+      - id: package-version
+        name: Register Package Version
+        run: node -p -e '`PACKAGE_VERSION=${require("./package.json").version}`' >> $GITHUB_OUTPUT

+      # tagが振られてなかったら振る
+      - name: Add Tag
+        uses: pkgdeps/git-tag-action@v2
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          github_repo: ${{ github.repository }}
+          version: ${{ steps.package-version.outputs.PACKAGE_VERSION }}
+          git_commit_sha: ${{ github.sha }}
+          git_tag_prefix: 'v'

以下の記事を参考にしましたが、いくつか変更があります。

  • publish可能かを別stepに分ける
    • 参考記事ではfallbackを使って1つにまとめて書いてますが、その場合、yarn publishで失敗した場合にCIが落ちなくなってしまいます。なので先にpublish可能かのステータスをGITHUB_OUTPUTに出力して、publishステップ自体をスキップできるようにしました。
  • tagを振るGitHub Actionsのバージョンを2系にする

https://tech.plaid.co.jp/npm-private-registry-to-github-packages-registry

以上のやり方でpackage.jsonversionを上げるだけでpublishしたりタグを振ってくれたりしますが、CI設定の複雑さが大分上がってしまいました。そもそもバージョン上げる際はnpm version minorとかしたら勝手にpackage.jsonversionを上げるのとタグ付けを一緒にやってくれてそちらで運用する場合はわざわざここまでしなくても良いのかなと感じました。

終わりに

以上がGitHub Packagesでパッケージをpublishして使用する方法でした。publish自体は簡単にできますが、使うときにPATを生成して.npmrcの設定をしないといけないのがなかなかネックだなと感じました。SSHで認証できたら今すぐにでも使っていきたいですが、まだ実装の目処は立っていなそうです。

https://github.com/orgs/community/discussions/16240

別な手段としてGitHubのリポジトリを直接参照してinstallするやり方も試しており、そちらの方が使い勝手が良さそうな印象でしたので興味がある方はこちらもご参照いただけると幸いです。

https://zenn.dev/wintyo/articles/7989edb7e1d282

Discussion