# Deploy backend (Cloud Run) and frontend (Firebase Hosting) on push. # # Auth: WIF (provider + service account email) or legacy JSON key — see docs/CI_GITHUB.md # Note: `secrets` cannot be used in job-level `if:`; we gate in the first step instead. name: Deploy on: push: branches: [main, master] workflow_dispatch: concurrency: group: deploy-${{ github.ref_name }} cancel-in-progress: true env: GCP_PROJECT: rapidmk GCP_REGION: us-central1 AR_REPO: us-central1-docker.pkg.dev/rapidmk/cepheus/api CLOUD_RUN_SERVICE: cepheus-api FIREBASE_PROJECT: rapid-eec43 permissions: contents: read id-token: write jobs: quality-gate: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip cache-dependency-path: backend/requirements-ci.txt - name: Install backend dependencies run: | python -m pip install --upgrade pip pip install -r backend/requirements-ci.txt - name: Backend tests env: CEPHEUS_CLOUD: "1" CEPHEUS_API_KEY: test-key CEPHEUS_AUTH_DEV_MODE: "1" CEPHEUS_CI_STUB_VISION: "1" run: python -m pytest backend/tests -q - uses: actions/setup-node@v4 with: node-version: "20" cache: npm cache-dependency-path: cepheus/package-lock.json - name: Frontend quality checks run: | cd cepheus npm ci npm run lint npm run test npm run build - name: Start API for launch gate env: CEPHEUS_CLOUD: "1" CEPHEUS_API_KEY: test-key CEPHEUS_AUTH_DEV_MODE: "1" CEPHEUS_CI_STUB_VISION: "1" run: | cd backend && uvicorn main:app --host 127.0.0.1 --port 8765 & sleep 5 curl -sf http://127.0.0.1:8765/health/live - name: Launch gate (API smoke) env: CEPHEUS_API_URL: http://127.0.0.1:8765 CEPHEUS_API_KEY: test-key run: node cepheus/scripts/launch-gate.mjs deploy-backend: needs: quality-gate runs-on: ubuntu-latest steps: - name: Check backend credentials id: creds env: WIFP: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} WIFSA: ${{ secrets.GCP_WIF_SERVICE_ACCOUNT }} KEY: ${{ secrets.GCP_SA_KEY }} run: | if [ -n "$WIFP" ] && [ -n "$WIFSA" ]; then echo "run=true" >> "$GITHUB_OUTPUT" echo "auth=wif" >> "$GITHUB_OUTPUT" elif [ -n "$KEY" ]; then echo "run=true" >> "$GITHUB_OUTPUT" echo "auth=key" >> "$GITHUB_OUTPUT" else echo "run=false" >> "$GITHUB_OUTPUT" echo "skip backend deploy: set GCP WIF or GCP_SA_KEY (see docs/CI_GITHUB.md)" >> "$GITHUB_STEP_SUMMARY" fi - uses: actions/checkout@v4 if: steps.creds.outputs.run == 'true' - name: Authenticate to Google Cloud (Workload Identity Federation) if: steps.creds.outputs.run == 'true' && steps.creds.outputs.auth == 'wif' uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_WIF_SERVICE_ACCOUNT }} - name: Authenticate to Google Cloud (JSON key, legacy) if: steps.creds.outputs.run == 'true' && steps.creds.outputs.auth == 'key' uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - uses: google-github-actions/setup-gcloud@v2 if: steps.creds.outputs.run == 'true' with: project_id: ${{ env.GCP_PROJECT }} - name: Build and push image if: steps.creds.outputs.run == 'true' run: | set -eux gcloud config set project "$GCP_PROJECT" gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet TAG="${GITHUB_SHA:0:12}" echo "IMAGE_TAG=$TAG" >> "$GITHUB_ENV" docker build -f backend/Dockerfile.hf -t "${AR_REPO}:${TAG}" -t "${AR_REPO}:latest" ./backend docker push "${AR_REPO}:${TAG}" docker push "${AR_REPO}:latest" - name: Deploy Cloud Run if: steps.creds.outputs.run == 'true' env: CEPHEUS_API_KEY: ${{ secrets.CEPHEUS_API_KEY }} CEPHEUS_JWT_SECRET: ${{ secrets.CEPHEUS_JWT_SECRET }} CEPHEUS_AUTH_USERS: ${{ secrets.CEPHEUS_AUTH_USERS }} CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }} run: | if [ -z "$CEPHEUS_API_KEY" ] || [ -z "$CEPHEUS_JWT_SECRET" ]; then echo "skip Cloud Run deploy: set CEPHEUS_API_KEY and CEPHEUS_JWT_SECRET in GitHub secrets" >> "$GITHUB_STEP_SUMMARY" exit 0 fi gcloud run deploy "$CLOUD_RUN_SERVICE" \ --image "${AR_REPO}:${IMAGE_TAG}" \ --region "$GCP_REGION" \ --project "$GCP_PROJECT" \ --allow-unauthenticated \ --set-env-vars "^@^CEPHEUS_CLOUD=1@CEPHEUS_PRODUCTION=1@CEPHEUS_AUTH_DEV_MODE=0@CORS_ORIGINS=${CORS_ORIGINS}@CEPHEUS_API_KEY=${CEPHEUS_API_KEY}@CEPHEUS_JWT_SECRET=${CEPHEUS_JWT_SECRET}@CEPHEUS_AUTH_USERS=${CEPHEUS_AUTH_USERS}" \ --memory 2Gi \ --cpu 2 \ --timeout 3600 \ --max-instances 5 deploy-frontend: needs: quality-gate runs-on: ubuntu-latest steps: - name: Check frontend credentials id: creds env: WIFP: ${{ secrets.FIREBASE_WORKLOAD_IDENTITY_PROVIDER }} WIFSA: ${{ secrets.FIREBASE_WIF_SERVICE_ACCOUNT }} KEY: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} run: | if [ -n "$WIFP" ] && [ -n "$WIFSA" ]; then echo "run=true" >> "$GITHUB_OUTPUT" echo "auth=wif" >> "$GITHUB_OUTPUT" elif [ -n "$KEY" ]; then echo "run=true" >> "$GITHUB_OUTPUT" echo "auth=key" >> "$GITHUB_OUTPUT" else echo "run=false" >> "$GITHUB_OUTPUT" echo "skip hosting deploy: set Firebase WIF or FIREBASE_SERVICE_ACCOUNT (see docs/CI_GITHUB.md)" >> "$GITHUB_STEP_SUMMARY" fi - uses: actions/checkout@v4 if: steps.creds.outputs.run == 'true' - name: Authenticate for Firebase (Workload Identity Federation) if: steps.creds.outputs.run == 'true' && steps.creds.outputs.auth == 'wif' uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.FIREBASE_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.FIREBASE_WIF_SERVICE_ACCOUNT }} - name: Authenticate for Firebase (JSON key, legacy) if: steps.creds.outputs.run == 'true' && steps.creds.outputs.auth == 'key' uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} - uses: actions/setup-node@v4 if: steps.creds.outputs.run == 'true' with: node-version: "20" cache: npm cache-dependency-path: cepheus/package-lock.json - name: Install and build if: steps.creds.outputs.run == 'true' run: | set -eux cd cepheus npm ci npm run build - name: Deploy to Firebase Hosting if: steps.creds.outputs.run == 'true' run: npx -y firebase-tools@latest deploy --only hosting --project "$FIREBASE_PROJECT" --non-interactive