Skip to content

Merge: main -> env/prod (#234 獲得ログの遡り作成を本番規模で完了できるようにする) #72

Merge: main -> env/prod (#234 獲得ログの遡り作成を本番規模で完了できるようにする)

Merge: main -> env/prod (#234 獲得ログの遡り作成を本番規模で完了できるようにする) #72

Workflow file for this run

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