Skip to content

Merge: main -> env/dev (#238 expバーが常に満タンだった本当の原因を直す) #166

Merge: main -> env/dev (#238 expバーが常に満タンだった本当の原因を直す)

Merge: main -> env/dev (#238 expバーが常に満タンだった本当の原因を直す) #166

Workflow file for this run

on:
push:
branches:
- env/dev
env:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
GCP_KEY_PATH: "/tmp/key.json"
jobs:
build:
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-
- name: nuxt setup
run: |
echo "${{ secrets.DEV_ENV }}" > .env
# ランキング取得のエッジキャッシュ用オリジン(公開URLなので Secret では
# なく Variable で管理する)。環境ごとにオリジンが異なるため
# ステージングは DEV_RANKING_BASE_URL を使う。未設定なら空行となり、
# フロントは process.env.RANKING_BASE_URL || process.env.API_URL の
# フォールバックで従来どおり関数直叩きになるため無害。
echo "RANKING_BASE_URL=${{ vars.DEV_RANKING_BASE_URL }}" >> .env
working-directory: ./web
- name: nuxt generate
run: |
yarn
yarn generate
working-directory: ./web
- name: copy files
run: |
rm -rf app/dist
cp -r web/dist app
working-directory: ./
- name: firebase setup
run: |
npm install -g firebase-tools
npm install
working-directory: ./app/functions
# deploy setup
- name: gcp_key file
run: |
set -ex
echo "${{ secrets.DEV_GCLOUD_SERVICE_KEY }}" | base64 -d > ${{ env.GCP_KEY_PATH }}
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.DEV_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-dev'
- name: firebase deploy
run: |
firebase deploy --project d-shrine-dev --force
working-directory: ./app
- 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の要件)。
# ubuntu-22.04ランナーのデフォルトJavaはこれを満たさないため明示的にセットアップする。
- name: setup java (for Firestore emulator)
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
# sanpai/ranking/register/statusCacheBackfill等のFirestore統合テストは
# FIRESTORE_EMULATOR_HOST未設定時は自動スキップされる設計(README参照)。
# CIでも実際に検証されるよう、Firestoreエミュレータを起動した状態で
# go test を実行する(firebase emulators:exec が終了時に自動でエミュレータを
# 落とすため、後続ステップに影響しない)。
- name: go test (functions-go, with Firestore emulator)
run: |
firebase emulators:exec --project d-shrine-dev --only firestore \
'cd functions-go && go test ./...'
working-directory: ./app
# OGP画像生成(userOGPGo)のQC。サンプルOGPをPNG/WebPでレンダリングして
# アーティファクト化し、生成物を目視確認できるようにする(描画の破綻を
# デプロイ前に検知する目的も兼ねる)。
- 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
# ここから Go版 Cloud Run functions のデプロイ。
# Gen2(Cloud Run)関数のデプロイは内部でCloud Buildのコンテナビルドを伴い
# 1本あたり数分かかる。関数間に依存が無いため、以前のように1本ずつ直列で
# deployすると全体で15〜30分規模になっていた。そこで前提処理(サービス有効化・
# Pub/Subトピック作成・認証情報取得・ロケーション解決)を先に済ませてから、
# 全関数のdeployをバックグラウンドで同時に起動して一括waitする
# (体感で最も遅い1本分の時間に短縮)。
# HTTP関数とスケジュール関数(Pub/Subトリガー)の両方に必要なGCPサービスを一括有効化。
- 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-dev
# 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-dev \
|| gcloud artifacts repositories create gcf-artifacts \
--location=us-central1 --repository-format=docker --project=d-shrine-dev
# Cloud Functions(Gen2)の --trigger-topic は Gen1と異なりPub/Subトピックを
# 自動作成しない(存在しないトピックを指定すると
# `Validation failed for trigger ...: Resource not found` で即失敗する)。
# そのため各スケジュール関数用トピックを事前に作成しておく
# (既に存在すれば describe が成功するのでスキップする、冪等な実装)。
- 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-dev \
|| gcloud pubsub topics create "$TOPIC" --project=d-shrine-dev
done
# GitHub OAuth Appのclient_id/client_secretは、Node版が使う
# `firebase functions:config:get` (既にGCP側に設定済みの値)からそのまま取得し、
# 新規のGitHub Secretsは増やさない。ログに値が出ないよう ::add-mask:: で
# マスクしてから $GITHUB_ENV に渡す(sanpaiGoで使用)。
- name: fetch GitHub OAuth credentials for sanpaiGo
run: |
set -eo pipefail
CONFIG_JSON=$(firebase functions:config:get github --project d-shrine-dev)
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 から読む値と同じもので、
# sanpaiGoのGitHub資格情報と同様に既存のFirebase Functions Configから取得する
# (ogpRewriteGoで使用)。
- name: fetch base_url for ogpRewriteGo
run: |
set -eo pipefail
CONFIG_JSON=$(firebase functions:config:get func --project d-shrine-dev)
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アプリと
# 同一でなければならない(App Engineが存在するプロジェクトでは、それ以外の
# リージョンを指定すると `Location must equal <app-engine-location>` エラーになる)。
# `gcloud app describe` が返す locationId はレガシーなApp Engine表記
# (例: "us-central")の場合があり、Cloud Scheduler/Cloud Functions等の
# 通常のCloudリージョン表記(例: "us-central1")とは異なることがあるため、
# 既知の対応表で変換する。
- name: resolve App Engine location for Cloud Scheduler
run: |
set -eo pipefail
RAW_LOCATION=$(gcloud app describe --project=d-shrine-dev --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する。
# 依存関係(トピック/認証情報/ロケーション)は上の先行ステップで解決済み。
# timeout: HTTP関数は30s、スケジュール関数は全ユーザースキャン等で長くなり得るため
# 安全側に300sを明示。ログは関数ごとにファイルへ退避し、最後に順番に出力する
# (並行実行でログが混ざるのを防ぐ)。1本でも失敗したらステップを失敗させる。
- 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-dev/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-dev \
--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=60"
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=60,KUDA_BASE_URL=https://kuda.kojiran.workers.dev,KUDA_API_KEY=${{ secrets.DEV_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-dev"
# userOGPGoは画像合成(base同梱)+WebPエンコード+アバター取得+GCSアップロードを
# 行うため、他HTTP関数より余裕を持たせる(512Mi/60s)。
deploy userOGPGo UserOGPGo \
--trigger-http --allow-unauthenticated --memory=512Mi --timeout=60s \
--set-env-vars="STORAGE_BUCKET_NAME=d-shrine-dev.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-dev.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ジョブを作成/更新する(冪等)。
# ジョブはトピックにメッセージを流すだけなので関数デプロイ完了後に一括で行う。
# update が存在しないジョブに対して失敗したら create にフォールバックする。
- 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-dev --location="$APP_ENGINE_LOCATION" \
--schedule="$schedule" \
--topic="$topic" \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub "$job" \
--project=d-shrine-dev --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