Run pnpm i to install dependencies.
| command | description |
|---|---|
pnpm i |
Install dependencies |
pnpm pos (dev|build|preview|typecheck) |
Run commands in services/pos |
pnpm mobile (dev|build|start|typecheck) |
Run commands in services/mobile |
pnpm common (typecheck|test:(unit|db)) |
Run commands in modules/common |
.github/workflows にある workflow。*-ci は検査のみ、api-build は Artifact
Registry に成果物を置く、*-deploy-* はデプロイする。
| workflow | 対象 | 何をするか |
|---|---|---|
pos-ci / mobile-ci / common-ci / api-ci |
各パッケージ | typecheck / lint / unit test |
api-build |
api |
イメージをビルドして Artifact Registry へ push し、Cloud Run へデプロイ |
pos-deploy-workers |
services/pos |
ビルドして Cloudflare Workers へデプロイ |
mobile-deploy-workers |
services/mobile |
同上 |
pos-deploy-merge / pos-deploy-pull-request |
services/pos |
Firebase Hosting へデプロイ(Workers と並行稼働中) |
pr-cleanup |
— | PR を閉じたときに Artifact Registry の pr-<番号> タグを外す |
POS と mobile はどちらも ssr: false の SPA。Worker のスクリプトは持たず、
build/client を静的アセットとして配信するだけの構成にしている
(services/*/wrangler.jsonc)。アセットに無いパスは index.html を返す
(not_found_handling: single-page-application)。
main への push と手動実行では本番へ wrangler deploy する。PR では
wrangler versions upload に切り替え、本番のトラフィックは向けずに
プレビュー URL 付きのバージョンだけ作る。
PR ではプレビュー URL をコメントで貼る。2回目以降は新しいコメントを足さず、 同じコメントを書き換える(本文に埋めた目印で自分のコメントを探している)。 POS と mobile は目印が別なので、それぞれ1件ずつ独立して更新される。
PR のビルドでは、その PR のリビジョンの URL をビルド前に確定させて
VITE_API_BASE_URL に焼き込む。
backend は PR ごとに --no-traffic --tag=pr-<番号> でデプロイされ、
https://pr-<番号>---<service>-<hash>.<region>.run.app という専用 URL を持つ。
まず実物を status.traffic から引き、まだ backend がデプロイされていなければ
サービス URL のホスト名に pr-<番号>--- を足して組み立てる(形式は同じなので、
あとで backend が出れば有効になる)。これで順序制御なしにビルドを先に走らせられる。
VITE_* はビルド時に焼き込まれるので、この確定はビルドより前に置いてある。
引けなかった場合は落とさず、VITE_API_BASE_URL_PREVIEW →
VITE_API_BASE_URL の順にフォールバックする。サービス未作成・GCP 障害・
権限不足のいずれでも、Cloudflare へのデプロイ自体は止めない。
初回だけ順番に注意。 versions upload は対象の Worker が既に存在している
ことが前提なので、まだ無いと失敗する。いちばん最初は main へのマージか手動実行を
先に通すこと。
それができない場合のために、Variables に WORKERS_AUTO_DEPLOY_IF_NOT_EXIST = true
を置くと、Worker が存在しないときに限り PR でも wrangler deploy に
フォールバックして Worker を作る。存在するかどうかは wrangler versions list で
先に確認しており、判定できなかった場合(認証エラーなど)は存在する前提で
versions upload を走らせる(誤って本番へ倒さないため)。
この変数が有効な間は、PR から本番の Worker が作られる。 立ち上げが済んだら 変数を消すこと。
| Worker 名 | 対象 |
|---|---|
cafeore-pos |
services/pos |
cafeore-mobile |
services/mobile |
ローカルからは pnpm pos deploy / pnpm mobile deploy で同じことができる。
api-build が api/Dockerfile からイメージを作り、Artifact Registry へ push する。
main への push と手動実行では、続けて Cloud Run サービス cafeore-pos-git を
そのイメージで更新する。PR では本番に触らず、プレビュー用の別サービス
(既定 cafeore-pos-preview)へデプロイし、その URL を PR にコメントする。
本番と分けているのは、同じサービスにリビジョンタグを足すとトラフィック設定が
「常に最新リビジョン」から「特定リビジョンへの固定」に変わり、main のデプロイが
自動で切り替わらなくなるため。プレビュー用サービスの側ではリビジョンタグを使う
(PR ごとに URL と DATABASE_URL が分かれ、同時に複数の PR が開いても潰し合わない)。
PR を閉じると pr-cleanup がタグを外す。
プレビュー用サービスはアクセスが無ければゼロまで縮むので、PR を放置しても 費用は増えない。
| 変数 | ローカル | プレビュー | 本番 |
|---|---|---|---|
DATABASE_URL |
api/.env |
CI が Neon の接続文字列を渡す | Secret Manager の supabase-database-url |
RUN_MIGRATIONS |
true |
CI が true を渡す |
false |
FRONTEND_ORIGINS |
未設定(localhost を許可) |
* |
Workers の URL をカンマ区切り |
PORT |
8080 |
Cloud Run が渡す | Cloud Run が渡す |
プレビューと本番の値は infra リポジトリの gcp/cloud_run_preview.tf と
gcp/cloud_run.tf にある。DATABASE_URL が未設定だと initDB が log.Fatal する。
本番で AutoMigrate を走らせてはいけない。 本番のスキーマは手で作られており、
無条件に走らせると失敗する。listen は initDB の後なので、コンテナが PORT を
開けられず Cloud Run のデプロイごと落ちる。RUN_MIGRATIONS はそのためのガード。
逆にプレビューとローカルは空の DB を使うので、走らせないとテーブルができない。
FRONTEND_ORIGINS から漏れた origin はブラウザから API を叩けない。
フロントのデプロイ先を増やしたら infra 側にも足すこと。
NEON_PROJECT_ID が設定されていれば、PR ごとに Neon のブランチ
preview/pr-<番号> を 0.25〜1 CU で作り、その接続文字列を
プレビュー用 Cloud Run の DATABASE_URL に渡す。Cloud Run の環境変数は
リビジョン単位なので、PR ごとに違う DB を指せる。
ブランチを作った直後に CREATE EXTENSION IF NOT EXISTS "uuid-ossp" を流す。
モデルが default:uuid_generate_v4() を使っているので、拡張の無い空の DB では
AutoMigrate の最初の CREATE TABLE が 42883 で落ち、initDB がエラーを返して
コンテナが起動できない。ローカルの compose では
api/init/00_enable_extension.sql が同じことをしているが、あれは Postgres の
初期化ディレクトリにマウントしているだけなので Neon には効かない。
プレビューは空の DB を使うので、deploy のときに RUN_MIGRATIONS=true も一緒に
渡している(下の環境変数を参照)。
Neon の親ブランチに一度手で同じ SQL を流しておくと、CoW クローンが最初から 拡張を持つのでこのステップは保険になる。
ブランチは copy-on-write なので作成は即時。アイドル 5 分でゼロに縮む。
PR を閉じると pr-cleanup が compute ごと消す。
ブランチ名の規則は api-build.yml の NEON_BRANCH_PREFIX と
pr-cleanup.yml の同名変数で揃えること。 ずれると閉じても消えず溜まる。
接続文字列は ::add-mask:: でログから伏せている。gcloud に渡すときは
区切り文字を ^@^ にしている(接続文字列に = と & が含まれ、既定の
カンマ区切りだと値が途中で切れるため)。
NEON_PROJECT_ID が未設定なら Neon まわりは丸ごとスキップする。その場合
DATABASE_URL が渡らないので、プレビューのコンテナは起動時に落ちる
(initDB が log.Fatal するため)。
イメージはタグではなくダイジェストで指定している。タグは後から別のイメージへ 付け替わりうるが、ダイジェストは今ビルドしたものを必ず指すため。
デプロイ先のサービスは既存の Cloud Build トリガー(infra の cloud_build.tf)も
更新している。あちらの発火条件は disable-auth ブランチへの push なので普段は
ぶつからないが、disable-auth に push するとそちらのビルドで上書きされる。
移行が済んだら Cloud Build トリガーを止めること。
GCP への認証は Workload Identity Federation で、サービスアカウントキーは使わない。
そのため job に permissions: id-token: write が要る(消すと認証が落ちる)。
GCP 側の構成は infra リポジトリ の
gcp/github_actions.tf にある。
fork からの PR には secrets も OIDC トークンも渡らないので、 デプロイ系の job は fork PR ではスキップしている。
信頼の境界は「このリポジトリへの write 権限」。 write を持つ人はデプロイできる、 という前提で運用する。逆に言えば、write を持たない人はデプロイできない。
fork からの PR は二重に止まる。
- GitHub 自体が、fork からの
pull_requestに secrets を渡さずid-token: writeも 与えない。CLOUDFLARE_API_TOKENは空になり、WIF のトークンも発行されない - デプロイ系 job の
ifがhead.repo.full_name == github.repositoryを見ていて、 fork の PR では job ごとスキップされる
デプロイ系の workflow は pull_request_target を使っていない(全て pull_request)。
そのため fork の PR のコードがこのリポジトリの権限で走ることはない。
一方、write 権限を持つ人は制限されない。 同じリポジトリのブランチから PR を出せば
上の条件を通り、pull_request は PR 側の workflow 定義で走るので、workflow を書き換えれば
secrets も取り出せる。main への直 push もそのまま本番デプロイになる。これは想定どおりで、
権限の配り方でコントロールする。
そのため次の運用を守ること。
- 外部の人には write 権限を渡さない。コントリビュートは fork からの PR に統一する
- Settings → Actions → General の 「Fork pull request workflows from outside collaborators」を Require approval for all external contributors にする (fork の PR はデプロイできないが、runner の使用自体を承認制にする)
mainにブランチ保護をかけ、直 push を禁止して PR 経由に統一する
Dependabot の PR はデプロイ系 job から除外している(github.actor != 'dependabot[bot]')。
Dependabot が起点の実行には Actions の secrets が渡らず、権限も read-only なので、
除外しないと npm 更新のたびに落ちるため。依存更新の妥当性は *-ci の typecheck で見る。
Artifact Registry … pr-cleanup がその PR の pr-<番号> タグを外す。
タグが外れたイメージは infra 側のクリーンアップポリシーが7日後に消す。
タグだけを外して本体を消さないのは、fast-forward マージなどで PR の head と
main の tip が同じコミットになったとき、同じダイジェストを latest が
指している可能性があるため。
そのため api-build は PR のイメージに pr-<番号> しか付けない
(sha タグも付けると、タグを外してもイメージが TAGGED のまま残り回収されない)。
workflow が落ちた PR や閉じられないまま放置された PR 用に、30日経った pr- タグを
消す保険のポリシーも入れてある。
Cloudflare Workers … 片付けていない。wrangler に versions delete が無く、
versions upload で作ったバージョンを個別に消す手段が今のところ無いため
(wrangler preview delete は private beta)。閉じた PR のプレビュー URL も
残り続ける。公開したままにしたくない場合は wrangler.jsonc の preview_urls を
false にして、プレビュー URL の配信自体を止めること。
| キー | 種別 | 使う workflow |
|---|---|---|
WORKERS_CLOUDFLARE_API_TOKEN |
Secrets | pos-deploy-workers / mobile-deploy-workers |
WORKERS_CLOUDFLARE_ACCOUNT_ID |
Variables | 同上(アカウント ID は秘密情報ではない) |
WORKERS_AUTO_DEPLOY_IF_NOT_EXIST |
Variables | 同上(任意。true のときだけ上記のフォールバックが働く) |
WEBHOOK_URL |
Secrets | pos-deploy-workers(既存の pos-deploy-* と共用) |
VITE_API_BASE_URL |
Variables | pos-deploy-workers / mobile-deploy-workers |
VITE_API_BASE_URL_PREVIEW |
Variables | 任意。PR で backend の URL を引けなかったときのフォールバック |
NEON_API_KEY |
Secrets | PR ごとの Neon ブランチ作成・削除。project-scoped キー推奨 |
NEON_PROJECT_ID |
Variables | 同上。未設定なら Neon 連携ごとスキップ |
NEON_PREVIEW_CU |
Variables | 任意。既定 0.25-1 |
VITE_SOHOSAI_VOTE_URL |
Variables | mobile-deploy-workers |
VITE_* は静的ファイルに焼き込まれるのでブラウザから読める。未設定だと
空文字が焼き込まれる。
GCP 側(api-build)は Terraform を既定値のまま apply していれば追加設定は不要。
値を変えたときだけ GCP_WORKLOAD_IDENTITY_PROVIDER / GCP_SERVICE_ACCOUNT /
GCP_PROJECT_ID / GCP_REGION / GCP_AR_CONTAINER_REPOSITORY / GCP_CLOUD_RUN_SERVICE /
GCP_CLOUD_RUN_PREVIEW_SERVICE を Variables で上書きする。