Merge: main -> env/prod (#234 獲得ログの遡り作成を本番規模で完了できるようにする) #72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| on: | |
| push: | |
| branches: | |
| - env/prod | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| COMPOSE_DOCKER_CLI_BUILD: 1 | |
| GCP_KEY_PATH: "/tmp/key.json" | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run actionlint | |
| shell: bash | |
| run: | | |
| set -ex | |
| bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) | |
| ./actionlint -color | |
| - name: setup node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24.18.0" | |
| - name: cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-node- | |
| # firebase-tools は Go関数デプロイの前提処理(functions:config:get)と | |
| # 最後の firebase deploy の両方で使う。Node関数の依存もここで入れる。 | |
| - name: firebase setup | |
| run: | | |
| set -ex | |
| npm install -g firebase-tools | |
| npm install | |
| working-directory: ./app/functions | |
| # deploy setup | |
| - name: gcp_key file | |
| run: | | |
| set -ex | |
| echo "${{ secrets.PROD_GCLOUD_SERVICE_KEY }}" | base64 -d > ${{ env.GCP_KEY_PATH }} | |
| - uses: 'google-github-actions/auth@v2' | |
| with: | |
| credentials_json: '${{ secrets.PROD_GCLOUD_SERVICE_KEY }}' | |
| - name: 'Use gcloud CLI' | |
| run: 'gcloud auth list --filter=status:ACTIVE --format="value(account)"' | |
| - name: 'Set up Cloud SDK' | |
| uses: 'google-github-actions/setup-gcloud@v2' | |
| with: | |
| install_components: '' | |
| project_id: 'd-shrine' | |
| # ==================================================================== | |
| # 1) バックエンド(Go関数)を先にデプロイする。 | |
| # hosting rewrite (/u/* -> ogpRewriteGo) や フロントからのGo関数呼び出しが | |
| # あるため、フロント(firebase deploy)より先にGo関数を出す必要がある。 | |
| # ==================================================================== | |
| - name: setup go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.25" | |
| - name: go build & test (functions-go) | |
| run: | | |
| go build ./... | |
| go vet ./... | |
| working-directory: ./app/functions-go | |
| # Firestoreエミュレータの実行にはJDK 21以上が必要(firebase-toolsの要件)。 | |
| - name: setup java (for Firestore emulator) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "21" | |
| # Firestore統合テストはFIRESTORE_EMULATOR_HOST未設定時は自動スキップされる設計。 | |
| # 本番デプロイ前のゲートとしてエミュレータを起動した状態で go test を実行する。 | |
| - name: go test (functions-go, with Firestore emulator) | |
| run: | | |
| firebase emulators:exec --project d-shrine --only firestore \ | |
| 'cd functions-go && go test ./...' | |
| working-directory: ./app | |
| # OGP画像生成(userOGPGo)のQC。サンプルOGPをレンダリングしてアーティファクト化する。 | |
| - name: generate OGP sample for QC | |
| run: | | |
| go test ./internal/ogpimage/ -run TestWriteQCArtifacts -count=1 -v | |
| working-directory: ./app/functions-go | |
| env: | |
| OGP_QC_OUT: ${{ github.workspace }}/ogp-qc | |
| - name: upload OGP sample artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ogp-sample | |
| path: ${{ github.workspace }}/ogp-qc/ | |
| if-no-files-found: error | |
| - name: enable required GCP services | |
| run: | | |
| gcloud services enable \ | |
| cloudfunctions.googleapis.com \ | |
| run.googleapis.com \ | |
| cloudbuild.googleapis.com \ | |
| artifactregistry.googleapis.com \ | |
| pubsub.googleapis.com \ | |
| cloudscheduler.googleapis.com \ | |
| eventarc.googleapis.com \ | |
| --project=d-shrine | |
| # gen2(Cloud Run)関数のイメージ格納先であるArtifact Registryリポジトリ | |
| # (デフォルト名 gcf-artifacts)を事前に作成しておく(冪等)。 | |
| # gcloudは通常このリポジトリを遅延自動解決するが、複数のdeployを並列実行すると | |
| # その解決が競合し、一部が | |
| # `gcloud crashed (AttributeError): 'NoneType' object has no attribute 'dockerRepository'` | |
| # でクラッシュする(並列時のみ・ランダムな一部で発生する非決定的な競合)。 | |
| # 事前作成し、各deployで --docker-repository を明示指定することで自動解決の競合を無くす。 | |
| - name: ensure Artifact Registry repo for gen2 functions | |
| run: | | |
| gcloud artifacts repositories describe gcf-artifacts \ | |
| --location=us-central1 --project=d-shrine \ | |
| || gcloud artifacts repositories create gcf-artifacts \ | |
| --location=us-central1 --repository-format=docker --project=d-shrine | |
| - name: create Pub/Sub topics for scheduled Go functions | |
| run: | | |
| for TOPIC in ranking-update-go ranking-cache-go status-cache-backfill-go scheduled-ogp-delete-go battle-log-backfill-go ranking-close-retry-go ranking-close-july-go; do | |
| gcloud pubsub topics describe "$TOPIC" --project=d-shrine \ | |
| || gcloud pubsub topics create "$TOPIC" --project=d-shrine | |
| done | |
| # GitHub OAuth Appのclient_id/client_secretは、Node版が使う | |
| # `firebase functions:config:get` から取得する(sanpaiGoで使用)。 | |
| - name: fetch GitHub OAuth credentials for sanpaiGo | |
| run: | | |
| set -eo pipefail | |
| CONFIG_JSON=$(firebase functions:config:get github --project d-shrine) | |
| CLIENT_ID=$(echo "$CONFIG_JSON" | jq -r '.client_id') | |
| CLIENT_SECRET=$(echo "$CONFIG_JSON" | jq -r '.client_secret') | |
| echo "::add-mask::$CLIENT_ID" | |
| echo "::add-mask::$CLIENT_SECRET" | |
| echo "SANPAI_GH_CLIENT_ID=$CLIENT_ID" >> "$GITHUB_ENV" | |
| echo "SANPAI_GH_CLIENT_SECRET=$CLIENT_SECRET" >> "$GITHUB_ENV" | |
| working-directory: ./app | |
| # FUNC_BASE_URLはNode版が functions.config().func.base_url から読む値と同じもの(ogpRewriteGoで使用)。 | |
| - name: fetch base_url for ogpRewriteGo | |
| run: | | |
| set -eo pipefail | |
| CONFIG_JSON=$(firebase functions:config:get func --project d-shrine) | |
| BASE_URL=$(echo "$CONFIG_JSON" | jq -r '.base_url') | |
| echo "OGP_FUNC_BASE_URL=$BASE_URL" >> "$GITHUB_ENV" | |
| working-directory: ./app | |
| # Cloud SchedulerジョブのlocationはApp Engineアプリと同一でなければならない。 | |
| - name: resolve App Engine location for Cloud Scheduler | |
| run: | | |
| set -eo pipefail | |
| RAW_LOCATION=$(gcloud app describe --project=d-shrine --format='value(locationId)') | |
| case "$RAW_LOCATION" in | |
| us-central) LOCATION=us-central1 ;; | |
| europe-west) LOCATION=europe-west1 ;; | |
| *) LOCATION="$RAW_LOCATION" ;; | |
| esac | |
| echo "resolved App Engine location: $RAW_LOCATION -> $LOCATION" | |
| echo "APP_ENGINE_LOCATION=$LOCATION" >> "$GITHUB_ENV" | |
| # 全Go関数のデプロイをバックグラウンドで同時起動し、まとめてwaitする。 | |
| # sanpaiGoのクールダウンは本番=300s(Node版 projectID=='d-shrine' ? 300 : 60 と一致)。 | |
| # userOGPGo/scheduledOgpDeleteGoのバケットは本番の d-shrine.appspot.com。 | |
| - name: deploy all Go Cloud Run functions in parallel | |
| working-directory: ./app/functions-go | |
| run: | | |
| set -uo pipefail | |
| # 事前作成済みのAR docker リポジトリを明示指定し、並列deploy時の自動解決競合を防ぐ。 | |
| DOCKER_REPO="projects/d-shrine/locations/us-central1/repositories/gcf-artifacts" | |
| declare -a PIDS NAMES | |
| deploy() { | |
| local name="$1"; shift | |
| local entry="$1"; shift | |
| echo "start deploy: $name" | |
| # 直前のデプロイrunのGCP側オペレーションが未解放だと 409 | |
| # (unable to queue the operation)で即失敗するため、90秒おいて | |
| # 1回だけリトライする(マージが近接して連続デプロイになった場合の対策)。 | |
| ( for attempt in 1 2; do | |
| if gcloud functions deploy "$name" \ | |
| --project=d-shrine \ | |
| --gen2 \ | |
| --runtime=go125 \ | |
| --region=us-central1 \ | |
| --docker-repository="$DOCKER_REPO" \ | |
| --source=. \ | |
| --entry-point="$entry" \ | |
| "$@"; then | |
| exit 0 | |
| fi | |
| if [ "$attempt" -eq 1 ]; then | |
| echo "deploy $name failed (attempt 1); retrying in 90s..." | |
| sleep 90 | |
| fi | |
| done | |
| exit 1 ) > "/tmp/deploy-$name.log" 2>&1 & | |
| PIDS+=("$!") | |
| NAMES+=("$name") | |
| } | |
| # HTTPトリガー関数 | |
| deploy statusGo StatusGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy sanpaiGo SanpaiGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \ | |
| --set-env-vars="GITHUB_CLIENT_ID=$SANPAI_GH_CLIENT_ID,GITHUB_CLIENT_SECRET=$SANPAI_GH_CLIENT_SECRET,SANPAI_NEXT_TIME_SECONDS=300" | |
| deploy registerGo RegisterGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy rankingGo RankingGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy rankingArchiveGo RankingArchiveGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy sanpaiHistoryGo SanpaiHistoryGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy profileStatsGo ProfileStatsGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| deploy badgeGo BadgeGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s | |
| # GitHub APIのレート制限緩和のため sanpaiGo と同じOAuth資格情報を使う | |
| deploy githubStatsGo GithubStatsGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=60s \ | |
| --set-env-vars="GITHUB_CLIENT_ID=$SANPAI_GH_CLIENT_ID,GITHUB_CLIENT_SECRET=$SANPAI_GH_CLIENT_SECRET" | |
| # ピン留め保存(GitHubでリポジトリを検証するため同じ資格情報を使う) | |
| deploy pinnedReposGo PinnedReposGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \ | |
| --set-env-vars="GITHUB_CLIENT_ID=$SANPAI_GH_CLIENT_ID,GITHUB_CLIENT_SECRET=$SANPAI_GH_CLIENT_SECRET" | |
| deploy omikujiGo OmikujiGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \ | |
| --set-env-vars="OMIKUJI_COOLDOWN_SECONDS=28800,KUDA_BASE_URL=https://kuda.kojiran.workers.dev,KUDA_API_KEY=${{ secrets.PROD_KUDA_API_KEY }}" | |
| deploy ogpRewriteGo OgpRewriteGo \ | |
| --trigger-http --allow-unauthenticated --memory=256Mi --timeout=30s \ | |
| --set-env-vars="FUNC_BASE_URL=$OGP_FUNC_BASE_URL,OGP_PROJECT_ID=d-shrine" | |
| deploy userOGPGo UserOGPGo \ | |
| --trigger-http --allow-unauthenticated --memory=512Mi --timeout=60s \ | |
| --set-env-vars="STORAGE_BUCKET_NAME=d-shrine.appspot.com" | |
| # Pub/Subトリガー(スケジュール)関数 | |
| deploy rankingUpdateGo RankingUpdateGo \ | |
| --trigger-topic=ranking-update-go --memory=256Mi --timeout=300s | |
| deploy rankingCacheGo RankingCacheGo \ | |
| --trigger-topic=ranking-cache-go --memory=256Mi --timeout=300s | |
| # 定時実行は無い。ログ方式への移行時に手で1度だけ叩く(#226) | |
| deploy battleLogBackfillGo BattleLogBackfillGo \ | |
| --trigger-topic=battle-log-backfill-go --memory=512Mi --timeout=540s | |
| # 定時実行は無い。締めを取りこぼしたときに手で叩く(#226) | |
| deploy rankingCloseRetryGo RankingCloseRetryGo \ | |
| --trigger-topic=ranking-close-retry-go --memory=512Mi --timeout=540s | |
| # 一度きり。締め機能より前に過ぎた2026年7月を後から作る(#226) | |
| deploy rankingCloseJulyGo RankingCloseJulyGo \ | |
| --trigger-topic=ranking-close-july-go --memory=512Mi --timeout=540s | |
| deploy statusCacheBackfillGo StatusCacheBackfillGo \ | |
| --trigger-topic=status-cache-backfill-go --memory=512Mi --timeout=300s | |
| deploy scheduledOgpDeleteGo ScheduledOgpDeleteGo \ | |
| --trigger-topic=scheduled-ogp-delete-go --memory=256Mi --timeout=300s \ | |
| --set-env-vars="STORAGE_BUCKET_NAME=d-shrine.appspot.com" | |
| fail=0 | |
| for idx in "${!NAMES[@]}"; do | |
| name="${NAMES[$idx]}" | |
| if wait "${PIDS[$idx]}"; then | |
| status=OK | |
| else | |
| status=FAILED | |
| fail=1 | |
| fi | |
| echo "::group::deploy $status: $name" | |
| cat "/tmp/deploy-$name.log" | |
| echo "::endgroup::" | |
| done | |
| if [ "$fail" -ne 0 ]; then | |
| echo "one or more Go function deployments failed" >&2 | |
| fi | |
| exit "$fail" | |
| # スケジュール関数用のCloud Schedulerジョブを作成/更新する(冪等)。 | |
| - name: create/update Cloud Scheduler jobs for scheduled Go functions | |
| run: | | |
| set -uo pipefail | |
| upsert_job() { | |
| local job="$1" schedule="$2" topic="$3" | |
| gcloud scheduler jobs update pubsub "$job" \ | |
| --project=d-shrine --location="$APP_ENGINE_LOCATION" \ | |
| --schedule="$schedule" \ | |
| --topic="$topic" \ | |
| --message-body="{}" \ | |
| --time-zone=Etc/UTC \ | |
| || gcloud scheduler jobs create pubsub "$job" \ | |
| --project=d-shrine --location="$APP_ENGINE_LOCATION" \ | |
| --schedule="$schedule" \ | |
| --topic="$topic" \ | |
| --message-body="{}" \ | |
| --time-zone=Etc/UTC | |
| } | |
| upsert_job ranking-update-go "0 * * * *" ranking-update-go | |
| upsert_job ranking-cache-go "0 */2 * * *" ranking-cache-go | |
| upsert_job status-cache-backfill-go "*/30 * * * *" status-cache-backfill-go | |
| upsert_job scheduled-ogp-delete-go "0 */1 * * *" scheduled-ogp-delete-go | |
| # ==================================================================== | |
| # 2) Go関数のデプロイ完了後にフロント(+Node関数)をデプロイする。 | |
| # これにより hosting rewrite が指すGo関数が既に存在する状態で公開される。 | |
| # ==================================================================== | |
| - name: nuxt setup | |
| run: | | |
| echo "${{ secrets.PROD_ENV }}" > .env | |
| # ランキング取得のエッジキャッシュ用オリジン(公開URLなので Secret では | |
| # なく Variable で管理する)。環境ごとにオリジンが異なるため本番は | |
| # PROD_RANKING_BASE_URL を使う。未設定なら空行となり、フロントは | |
| # process.env.RANKING_BASE_URL || process.env.API_URL のフォールバックで | |
| # 従来どおり関数直叩きになるため無害。 | |
| echo "RANKING_BASE_URL=${{ vars.PROD_RANKING_BASE_URL }}" >> .env | |
| working-directory: ./web | |
| - name: nuxt generate | |
| run: | | |
| set -ex | |
| yarn | |
| yarn generate | |
| working-directory: ./web | |
| - name: copy files | |
| run: | | |
| set -ex | |
| rm -rf app/dist | |
| cp -r web/dist app | |
| working-directory: ./ | |
| - name: firebase deploy | |
| run: | | |
| firebase deploy --project d-shrine --force | |
| working-directory: ./app |