Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f62771e
feat: add Cloud Run deployment workflow and infrastructure
snomiao Oct 30, 2025
0f48f18
fix: convert test files from jest to bun:test
snomiao Oct 30, 2025
87715b8
fix(infra): use "$@" instead of $* in tf.sh for proper argument handling
snomiao Oct 30, 2025
ab06c7c
fix(infra): update container_port to 80 to match deployment config
snomiao Oct 30, 2025
ebc09be
fix(infra): replace Team Dash references with comfy-pr for consistency
snomiao Oct 30, 2025
f30a9e6
docs(infra): update CLAUDE.md examples to reference existing resources
snomiao Oct 30, 2025
76d66d9
fix: use mockImplementation for complex mock values
snomiao Oct 30, 2025
eebd702
fix: cast mock collection methods to any for type flexibility
snomiao Oct 30, 2025
7f31eb4
fix: replace remaining jest references with bun mock API
snomiao Oct 30, 2025
64ec39c
fix: update frontend issue transfer test assertion
snomiao Oct 30, 2025
d6e40a3
fix: correct labels expectation in frontend issue transfer test
snomiao Oct 30, 2025
6eb8804
fix: add missing HTTP mocks in frontend issue transfer tests
snomiao Oct 30, 2025
e0f33b6
fix: skip MongoDB tests when MONGODB_URI not available
snomiao Oct 30, 2025
7ee8a31
fix: lazy initialize GitHub client to avoid build-time failures
snomiao Oct 30, 2025
2af4aea
fix: lazy initialize MongoDB connection to avoid build failures
snomiao Oct 30, 2025
8dd9b64
fix: make db.collection() return lazy proxy for createIndex calls
snomiao Oct 30, 2025
fe91ea1
chore: trigger CI
snomiao Oct 30, 2025
a2a67b8
fix: improve collection proxy to expose methods synchronously
snomiao Oct 30, 2025
e0bfc52
fix: resolve merge conflicts from main branch
snomiao Oct 30, 2025
fb1b9f3
fix: allow Object.assign on collection proxy for custom methods
snomiao Oct 30, 2025
42a721c
fix: prevent MongoDB connection during Next.js build phase
snomiao Oct 30, 2025
33cd609
fix: prevent MongoDB connection during Next.js build phase
snomiao Nov 10, 2025
5f2d253
Merge remote-tracking branch 'origin/main' into sno-cloudrun
snomiao Nov 10, 2025
aee548e
fix: mark dashboard page as dynamic to prevent build-time prerendering
snomiao Nov 10, 2025
b8c9a4c
fix: mark all database-dependent dashboard pages as dynamic
snomiao Nov 10, 2025
4adb706
fix: mark gh-design task page as dynamic
snomiao Nov 10, 2025
90c33f9
fix: mark tasks index page as dynamic
snomiao Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
name: Test, Build & Deploy

on:
push:
branches: [main, dev]
pull_request:
branches: [main]

concurrency:
group: deploy-${{ github.head_ref || github.ref }}
cancel-in-progress: false

env:
PROJECT_ID: dreamboothy-dev
GAR_LOCATION: us-west2
SERVICE: comfy-pr
REGION: us-west2

jobs:
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v5

# Build and test
- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Cache TypeScript incremental build
uses: actions/cache@v3
with:
path: tsconfig.tsbuildinfo
key: ${{ runner.os }}-tsc-${{ hashFiles('**/*.ts', '**/*.tsx', 'tsconfig.json') }}
restore-keys: |
${{ runner.os }}-tsc-

- name: Type check
run: bunx tsc --noEmit

- name: Run tests
run: bun test

- name: Build Next.js app
run: bun run build

# Deploy to CloudRun
- name: Google Auth
id: auth
uses: google-github-actions/auth@v2
with:
credentials_json: "${{ secrets.GCP_SA_KEY }}"

- name: Cache Google Cloud SDK
uses: actions/cache@v3
with:
path: |
~/.config/gcloud
~/google-cloud-sdk
key: ${{ runner.os }}-gcloud-${{ hashFiles('**/deploy.yml') }}
restore-keys: |
${{ runner.os }}-gcloud-

- name: Set up Cloud SDK (gcloud)
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ env.PROJECT_ID }}
skip_install: false
install_components: ""

- name: Configure Docker to use gcloud
run: gcloud auth configure-docker $GAR_LOCATION-docker.pkg.dev --quiet

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and Push Container
run: |-
# Get branch name for tagging
BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's|/|-|g' | sed 's|_|-|g' | tr '[:upper:]' '[:lower:]')

# Build with multiple tags: SHA, branch, and latest
docker buildx build \
--cache-from=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:buildcache \
--cache-from=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:latest \
--cache-to=type=registry,ref=$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:buildcache,mode=max \
--push \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:$GITHUB_SHA" \
-t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:branch-$SANITIZED_BRANCH" \
-t "$GAR_LOCATION-docker.pkg.dev/$PROJECT_ID/$SERVICE/$SERVICE:latest" \
.

- name: Extract branch name
id: extract_branch
run: |
BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}

# For PRs, use the actual branch SHA instead of the merge commit SHA
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
ACTUAL_SHA="${{ github.event.pull_request.head.sha }}"
echo "Using PR head SHA: $ACTUAL_SHA (instead of merge commit: $GITHUB_SHA)"
else
ACTUAL_SHA="$GITHUB_SHA"
fi

# Sanitize branch name for Cloud Run tags
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's|/|-|g' | sed 's|_|-|g' | tr '[:upper:]' '[:lower:]' | sed 's/-*$//')
# If branch starts with a number, prefix with 'br-'
if [[ "$SANITIZED_BRANCH" =~ ^[0-9] ]]; then
SANITIZED_BRANCH="br-${SANITIZED_BRANCH}"
fi

# Create revision suffix: branch-first7chars (max 30 chars for branch, 7 for hash)
BRANCH_SUFFIX=$(echo "$SANITIZED_BRANCH" | cut -c1-30)
COMMIT_SHORT=$(echo "$ACTUAL_SHA" | cut -c1-7)
REVISION_SUFFIX="${BRANCH_SUFFIX}-${COMMIT_SHORT}"

# Truncate tag to fit within Cloud Run's 46 char limit
TAG_FOR_TRAFFIC=$(echo "$SANITIZED_BRANCH" | cut -c1-37)

echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "sanitized_branch=$SANITIZED_BRANCH" >> $GITHUB_OUTPUT
echo "revision_suffix=$REVISION_SUFFIX" >> $GITHUB_OUTPUT
echo "tag_for_traffic=$TAG_FOR_TRAFFIC" >> $GITHUB_OUTPUT
echo "Deploying branch: $BRANCH_NAME (sanitized: $SANITIZED_BRANCH)"
echo "Revision suffix: $REVISION_SUFFIX"
echo "Traffic tag: $TAG_FOR_TRAFFIC"

- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v2
with:
service: ${{ env.SERVICE }}
region: ${{ env.REGION }}
image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{
env.SERVICE }}:${{ github.sha }}
Comment on lines +141 to +142
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The multi-line image reference is split awkwardly. Consider keeping the entire expression on a single line for better readability, or if line length is a concern, split at a more natural boundary.

Suggested change
image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{
env.SERVICE }}:${{ github.sha }}
image: ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{ env.SERVICE }}:${{ github.sha }}

Copilot uses AI. Check for mistakes.
flags: |
${{ steps.extract_branch.outputs.branch == 'main' && '--memory=2Gi --cpu=2' || '--memory=1Gi --cpu=1' }}
--port=80
--allow-unauthenticated
--service-account=comfy-pr-sa@${{ env.PROJECT_ID }}.iam.gserviceaccount.com
--update-labels=branch=${{ steps.extract_branch.outputs.sanitized_branch }},commit=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }},deployed-by=github-actions
--tag=${{ steps.extract_branch.outputs.tag_for_traffic }}
--revision-suffix=${{ steps.extract_branch.outputs.revision_suffix }}
${{ steps.extract_branch.outputs.branch == 'main' && '--min-instances=1' || '--min-instances=0' }}
--max-instances=1
${{ steps.extract_branch.outputs.branch != 'main' && '--no-traffic' || '' }}
env_vars: |
BRANCH_NAME=${{ steps.extract_branch.outputs.branch }}
NODE_ENV=production
GH_TOKEN=${{ secrets.GH_TOKEN }}
SALT=${{ secrets.SALT }}
GIT_USEREMAIL=${{ secrets.GIT_USEREMAIL }}
GIT_USERNAME=${{ secrets.GIT_USERNAME }}
FORK_PREFIX=${{ secrets.FORK_PREFIX }}
FORK_OWNER=${{ secrets.FORK_OWNER }}
MONGODB_URI=${{ secrets.MONGODB_URI }}
SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}
NOTION_TOKEN=${{ secrets.NOTION_TOKEN }}

- name: Generate tagged URL
id: tagged_url
if: steps.extract_branch.outputs.branch != 'main'
run: |
# Generate the correct tagged URL format: https://{tag}---{service}-{hash}.a.run.app
SERVICE_URL="${{ steps.deploy.outputs.url }}"
TAGGED_URL=$(echo "$SERVICE_URL" | sed "s|https://|https://${{ steps.extract_branch.outputs.tag_for_traffic }}---|")
echo "tagged_url=$TAGGED_URL" >> $GITHUB_OUTPUT
echo "Generated tagged URL: $TAGGED_URL"

- name: Comment on PR
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: "<!-- deploy-comment -->"
comment-author: "github-actions[bot]"
edit-mode: replace
body: |
<!-- deploy-comment -->
## 🚀 Deployment Ready

Your PR has been deployed successfully!

**🔗 Cloud Run URL:** ${{ steps.tagged_url.outputs.tagged_url }}

**Branch:** `${{ steps.extract_branch.outputs.branch }}`
**Commit:** `${{ github.sha }}`
**Revision:** `${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}`
**Scaling:** Min instances: 0, Max instances: 1 (scales to zero when not in use)

---

📊 **[View Logs in GCP Console](https://console.cloud.google.com/logs/query;query=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22${{ env.SERVICE }}%22%0Aresource.labels.revision_name%3D%22${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}%22;timeRange=PT1H?project=${{ env.PROJECT_ID }})**

---

ℹ️ Note: This deployment receives no production traffic and will be automatically cleaned up when the PR is closed.

<details>
<summary>📋 Deployment Details</summary>

- **Service:** `${{ env.SERVICE }}`
- **Region:** `${{ env.REGION }}`
- **Image:** `${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE }}/${{ env.SERVICE }}:${{ github.sha }}`
- **Labels:** `branch=${{ steps.extract_branch.outputs.branch }}, commit=${{ github.sha }}, deployed-by=github-actions`

</details>

- name: Health Check Cloud Run
id: healthcheck
run: |
# Check the Cloud Run backend directly
if [ "${{ steps.extract_branch.outputs.branch }}" == "main" ]; then
HEALTH_URL="${{ steps.deploy.outputs.url }}/api/health"
echo "🏥 Starting health check for main branch: $HEALTH_URL"
else
# For PR branches, check the tagged URL directly
HEALTH_URL="${{ steps.tagged_url.outputs.tagged_url }}/api/health"
echo "🏥 Starting health check for Cloud Run: $HEALTH_URL"
fi

# Health check with timeout (3 minutes = 180 seconds)
MAX_ATTEMPTS=36 # 36 attempts * 5 seconds = 180 seconds
ATTEMPT=1

while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS..."

# Make health check request
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$HEALTH_URL" || echo "000")

if [ "$HTTP_STATUS" == "200" ]; then
echo "✅ Health check passed! Server is up and running."
HEALTH_RESPONSE=$(curl -s "$HEALTH_URL")
echo "Health response: $HEALTH_RESPONSE"
echo "health_status=success" >> $GITHUB_OUTPUT
exit 0
else
echo "⏳ Health check returned HTTP $HTTP_STATUS, retrying in 5 seconds..."
fi

ATTEMPT=$((ATTEMPT + 1))
[ $ATTEMPT -le $MAX_ATTEMPTS ] && sleep 5
done

echo "❌ Health check failed after 3 minutes. Server did not respond with HTTP 200."
echo "health_status=failed" >> $GITHUB_OUTPUT
exit 1

- name: Set Traffic to 100% for Main Branch
if: steps.extract_branch.outputs.branch == 'main' && steps.healthcheck.outputs.health_status
== 'success'
run: |
echo "🚦 Setting traffic to 100% for main branch deployment..."

# Update traffic to route 100% to the specific revision that was just deployed
gcloud run services update-traffic ${{ env.SERVICE }} \
--region=${{ env.REGION }} \
--to-revisions=${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}=100 \
--platform=managed

echo "✅ Traffic routing updated: 100% traffic now directed to revision ${{ env.SERVICE }}-${{ steps.extract_branch.outputs.revision_suffix }}"

- name: Show Output
run: |
echo "🚀 Deployment completed!"
echo "Cloud Run Service URL: ${{ steps.deploy.outputs.url }}"
if [ "${{ steps.healthcheck.outputs.health_status }}" == "success" ]; then
echo "✅ Backend health check passed"
if [ "${{ github.event_name }}" == "pull_request" ]; then
echo "🔗 Cloud Run Tagged URL: ${{ steps.tagged_url.outputs.tagged_url }}"
echo "ℹ️ This PR deployment receives no production traffic (--no-traffic)"
else
echo "✅ Main branch deployment receiving production traffic"
fi
else
echo "⚠️ Backend health check failed"
echo "Please check Cloud Run logs for details"
fi
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ gh-service/state.sqlite
**/*.sqlite-journal

# running data
data/
data/

# Terraform
infra/.terraform/
infra/.terraform.lock.hcl
infra/*.tfstate
infra/*.tfstate.*
infra/*.tfvars
infra/*.tfplan
3 changes: 3 additions & 0 deletions app/(dashboard)/cnrepos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { CNRepos } from "@/src/CNRepos";
import { Suspense } from "react";
import { CNReposTableClient } from "./CNReposTableClient";

// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";

interface CNReposPageProps {
searchParams?: Promise<{
page?: string;
Expand Down
3 changes: 3 additions & 0 deletions app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import Link from "next/link";
import { Suspense } from "react";
import DetailsTable from "./DetailsTable";
import TotalsPage from "./totals/page";

// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";
export const revalidate = 60; // seconds
export default async function DashboardPage() {
return (
Expand Down
3 changes: 3 additions & 0 deletions app/(dashboard)/repos/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { CNRepos } from "@/src/CNRepos";
import { Suspense } from "react";
import yaml from "yaml";

// Prevent static generation since this page requires database access
export const dynamic = "force-dynamic";

/**
*
* @author: snomiao <snomiao@gmail.com>
Expand Down
Loading
Loading