この記事はtraP 2025 夏のブログリレーの 16 日目の記事です。
ikura-hamu です。traP では主に SysAd 班でサーバーアプリケーションを書いています。Go が好きです。
この記事では、GitHub Pages を 使って、Go のモジュールを自分が所持しているドメインで配布する方法を書きます。つまり、
go run {自分のドメイン}/some/cmd@latest
みたいなことができる、ということです。かっこいいですね。
Go のモジュール
Go のモジュールとは、ざっくり言うとパッケージの集合です。Go は 1 つのディレクトリに原則 1 つのパッケージが存在しますが、これをまとめたものがモジュールであり、go.mod ファイルによってその識別子や依存関係が管理されています。
https://go.dev/ref/mod#introduction
よくあるモジュールパス
モジュールの識別子(パス)は go.mod の module
という項目に書かれています。大体のサードパーティのモジュールは GitHub のリポジトリの URL から https://
を取り除いたやつになっています。例えば github.com/jmoiron/sqlx
や github.com/spf13/cobra
です。パッケージのパスはこのモジュールのパスにディレクトリ名をつなげたものになります。
この仕組みは別に GitHub に依存しているわけではありません。Bitbucket のような他の Git ホスティングサービスでもよいですし、なんなら Git である必要すらありません。公式ドキュメントによると、Git、Subversion、Mercurial、Bazaar、Fossil が対応しており、モジュールパスに VCS qualifier(.git
など) が含まれていた場合は自動で認識してそれを使ってくれるようです。
また、GitHub などのよくある VCS のホスティングサービスでは、VCS qualifier がなくてもパッケージパスからいい感じに依存を解決してくれます。
https://pkg.go.dev/cmd/go#hdr-Remote_import_paths
実際には Module Proxy というサーバーが存在してそこにモジュールがキャッシュされているので、キャッシュが存在しないときのみ、VCS へのリクエストが飛ぶことになります。
VCS 以外の URL を使ったモジュールパス
多くのモジュールが github.com
を使っている一方で、それ以外のモジュールもちょくちょくあります。たとえば準標準パッケージの golang.org/x
以下のモジュールや gorm.io/gorm
、 rsc.io/omap
などです。これらのモジュールパスには VCS qualifier も含まれていません。このようなときは、go
コマンドはモジュールパス(もしくはパッケージパス)に対して、https://example.org/pkg/foo?go-get=1
のような HTTP リクエストを送り、そのレスポンスの HTML の go-import
という名前の meta タグを調べます。
https://go.dev/ref/mod#vcs-find
たとえば、https://golang.org/x/mod?go-get=1
のようなリクエストを送ると以下のような HTML が返ってきます。
<!DOCTYPE html>
<html lang="en">
<title>The Go Programming Language</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="golang.org/x/mod git https://go.googlesource.com/mod">
<meta http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/mod">
</head>
<body>
<a href="https://pkg.go.dev/golang.org/x/mod">Redirecting to documentation...</a>
</body>
</html>
<meta name="go-import" content="golang.org/x/mod git https://go.googlesource.com/mod">
という要素があるのが分かると思います。これは、リクエストされたモジュール(もしくはパッケージ)のモジュールパスは golang.org/x/mod
で、VCS には Git を使っており、リポジトリのルートは https://go.googlesource.com/mod
にある、という意味になります。go
コマンドはこれを読んでリポジトリにソースコードを取りに行きます。この HTML ファイルによって、GitHub で開発を行いつつモジュールを好きなドメインで配布できるようになります。
これを実現するためのツールとして、rsc.io/go-import-redirector
というモジュールがあります。これは 動的な HTTP サーバーで、リクエストのURIに対して HTML を組み立てて返す、という動作をします。
作ったもの
rsc.io/go-import-redirector
は動的なアプリなため、それを動かすサーバーが必要です。Go Module Proxy もあって直接リクエストが来ることは少ないので、Google Cloud の Cloud Run の無料枠の範囲に収まりそうではありますが、内容の変化しない HTML を配信するだけでそのようなサーバーを動かすのはもったいない気がするので、HTML を組み立てて GitHub Pages で HTML を配信する GitHub Actions の Action を作りました。
https://github.com/ikura-hamu/go-import-pages
ざっくりとした使い方
詳しい使い方はドキュメントの方を確認してほしいのですが、ざっくりとした説明をします。
現時点では、1 つのドメインから複数のモジュールを配布することを考えています。そのような場合は、Go のソースコードを管理するリポジトリとは別に、HTML ファイルを管理するリポジトリを用意する必要があります。それぞれのリポジトリでこの Action を使った GitHub Actions のワークフローを書き、ソースコードが更新されたら HTML も必要に応じて更新するようにします。
例として https://github.com/ikura-hamu/card (Go のソースコード) と https://github.com/ikura-hamu/go.ikura-hamu.work (HTML)というリポジトリを作ったので参考にしてください。go.ikura-hamu.work/card
からモジュールが配信されるような設定になっています。以下のコマンドを実行することで、ターミナル上で動く名刺アプリケーションが起動します。
go run go.ikura-hamu.work/card@latest
仕組み
この Action (群)の処理の流れを説明します。
1. モジュールの情報を取得する
Go のモジュールを管理するリポジトリで、モジュール名とモジュールに含まれるパッケージ名を調べます。これには go list
コマンドを使うことができ、モジュール名は go list -m
、パッケージ名は go list ./...
で取得できます。
2. モジュールの情報を通知する
1 で得た情報を元に HTML を生成しますが、Go のリポジトリと HTML のリポジトリは別なため、GitHub Actions の Workflow を呼ぶには何かしらの方法で通知する必要があります。GitHub Actions では repository_dispatch
というイベントを使うことができます。
このイベントを使うことで、任意のペイロードを持ったメッセージを送って Workflow から別リポジトリの Workflow を起動できます。しかし、このイベントを発行するためには、受け取り側のリポジトリへの contents:write
のスコープを持った Personal Access Token や GitHub App Token が必要になります。このスコープはかなり強力なものになっており、トークンが漏れてしまったときのことを考えると心配です。
そこで、今回の実装では Issue comment を使った通知を行っています。Go のリポジトリの Workflow から HTML のリポジトリの指定された Issue にコメントを書きこみます。HTML のリポジトリでは、Issue のコメントが作成されたときに起動する Workflow を用意し、その中でソースコードの書き換えを行っています。この場合は、HTML のリポジトリへの issues:write
のスコープを持ったトークンが必要になります。トークンが必要なのは変わりないですが、contents:write
に比べて漏洩したときの影響が小さくすむと考えられます。
この実装のアイディアは、csm-actions/securefix-action を参考にしました。こちらは GitHub Actions の Artifact と Issue label を使った、リポジトリ間で Workflow を呼び出す、よりセキュリティが強い方法になっていますが、GitHub App が2つ必要になるなど手間がかかると考えたため、容易に実装できる Issue comment を使った方法を採用しました。
3. HTML を更新する
HTML のリポジトリでは、受け取ったモジュールの情報をもとに、HTMLファイルを生成し、差分が生じたらコミットしたり PR を作ったりします。
GitHub Pages へのデプロイは自分で書く必要があります。ですが、HTML を生成するだけなので、自分のドメインを設定した他のホスティングサービスからも配信できるということになります。
おわり
自分の好きなドメインでモジュールを配布できるようになって嬉しいですが、自分が持っているドメインが長い (ikura-hamu.work
)ので、どう頑張っても rsc.io
とか gorm.io
とか go.uber.org
みたいにかっこよくならないことに気づいてしまいました。短いドメインを持っている方はぜひ検討してみてください。今後も開発を続けていこうと思います。
夏のブログリレー明日の担当は @mutv625 さんです。
参考にした記事
