Skip to content

web(ci): deploy prebuilt Flutter web directory directly; embed SPA fa… #37

web(ci): deploy prebuilt Flutter web directory directly; embed SPA fa…

web(ci): deploy prebuilt Flutter web directory directly; embed SPA fa… #37

Workflow file for this run

name: Test & Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
permissions:
contents: read
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
jobs:
# -------------------------
# Server: test & build
# -------------------------
test-server:
name: Test Server
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: true
ADMIN_SECRET: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install, lint, test, build
working-directory: server
run: |
npm ci
npm run lint
npm test
npm run build
- name: Upload server build
uses: actions/upload-artifact@v4
with:
name: server-build
path: server/dist
# -------------------------
# Mobile: test Flutter web build
# -------------------------
test-mobile:
name: Test Mobile (Flutter Web)
runs-on: ubuntu-latest
timeout-minutes: 20
env:
CI: true
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: 3.35.3
channel: stable
cache: true
- name: Ensure Flutter web is enabled
working-directory: mobile
run: flutter config --enable-web
- name: Setup environment file
run: cp mobile/.env.example mobile/.env
- name: Test mobile app
working-directory: mobile
run: |
flutter pub get
flutter analyze
flutter test
flutter build web --release --no-wasm-dry-run
- name: Upload web build
uses: actions/upload-artifact@v4
with:
name: web-build
path: mobile/build/web
# -------------------------
# Server: deploy (requires BOTH tests, only on main)
# -------------------------
deploy-server:
name: Deploy Server (traceoffapi)
runs-on: ubuntu-latest
needs: [test-server, test-mobile]
if: github.ref == 'refs/heads/main'
outputs:
deploy-url: ${{ steps.deploy.outputs.deploy_url }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_API_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} # optional for Personal
VERCEL_TELEMETRY_DISABLED: "1"
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# Deploy from server directory
- name: Vercel pull (production)
working-directory: server
run: npx vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Deploy to Vercel (prod)
working-directory: server
id: deploy
run: |
set -e
DEPLOY_OUTPUT=$(npx vercel deploy --prod --yes --token="$VERCEL_TOKEN")
echo "$DEPLOY_OUTPUT"
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -E "Production:" | grep -Eo 'https://[^[:space:]]+' | head -1)
if [ -z "$DEPLOY_URL" ]; then
# Fallback: take last https URL in output
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -Eo 'https://[^[:space:]]+' | tail -1)
fi
echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT
echo "Deployed to: $DEPLOY_URL"
- name: Health check server
run: |
echo "Waiting for deployment to be ready..."
DEPLOY_URL="${{ steps.deploy.outputs.deploy_url }}"
HOST_HEADER=$(echo "$DEPLOY_URL" | awk -F/ '{print $3}')
echo "Testing health at: $DEPLOY_URL/api/health (Host: $HOST_HEADER)"
# Initial wait to allow cold start and DNS propagation
sleep 15
for i in {1..30}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Host: $HOST_HEADER" \
-H "Accept: application/json" \
-H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
"$DEPLOY_URL/api/health")
if [ "$STATUS" = "200" ]; then
echo "✅ Server health check passed (200)"
exit 0
fi
echo "Attempt $i failed; status=$STATUS; retrying in 5s..."
sleep 5
done
echo "❌ Server health check failed"
exit 1
# -------------------------
# Web: deploy Flutter web (requires BOTH tests, only on main)
# -------------------------
deploy-web:
name: Deploy Flutter Web (traceoff)
runs-on: ubuntu-latest
needs: [test-server, test-mobile]
if: github.ref == 'refs/heads/main'
outputs:
deploy-url: ${{ steps.deploy-mobile.outputs.deploy_url }}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_WEB_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} # optional
VERCEL_TELEMETRY_DISABLED: "1"
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
# Build Flutter web bundle directly in this job (tests temporarily disabled)
- uses: subosito/flutter-action@v2
with:
flutter-version: 3.35.3
channel: stable
cache: true
- name: Ensure Flutter web is enabled
working-directory: mobile
run: flutter config --enable-web
- name: Setup environment file
run: cp mobile/.env.example mobile/.env
- name: Build Flutter web
working-directory: mobile
run: |
flutter pub get
flutter build web --release --no-wasm-dry-run
- name: Verify web build files exist
working-directory: mobile
run: |
if [ ! -d "build/web" ]; then
echo "❌ Error: build/web directory not found after build."
exit 1
fi
echo "✅ Web build files found"
ls -la build/web/
- name: Vercel pull (production)
working-directory: mobile
run: npx vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Deploy to Vercel (prod)
working-directory: mobile
id: deploy-mobile
run: |
set -e
# Include SPA fallback + filesystem in the deployed output
cat > build/web/vercel.json <<'JSON'
{
"version": 2,
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]
}
JSON
# Deploy the prebuilt directory directly to avoid rebuilding on Vercel
DEPLOY_OUTPUT=$(npx vercel deploy build/web --prod --yes --token="$VERCEL_TOKEN")
echo "$DEPLOY_OUTPUT"
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -E "Production:" | grep -Eo 'https://[^[:space:]]+' | head -1)
if [ -z "$DEPLOY_URL" ]; then
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -Eo 'https://[^[:space:]]+' | tail -1)
fi
echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT
echo "Deployed to: $DEPLOY_URL"
- name: Health check mobile app
run: |
echo "Waiting for deployment to be ready..."
DEPLOY_URL="${{ steps.deploy-mobile.outputs.deploy_url }}"
HOST_HEADER=$(echo "$DEPLOY_URL" | awk -F/ '{print $3}')
echo "Testing mobile app at: $DEPLOY_URL (Host: $HOST_HEADER)"
sleep 15
for i in {1..30}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Host: $HOST_HEADER" \
-H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
"$DEPLOY_URL")
if [ "$STATUS" = "200" ]; then
echo "✅ Mobile app health check passed (200)"
exit 0
fi
echo "Attempt $i failed; status=$STATUS; retrying in 5s..."
sleep 5
done
echo "❌ Mobile app health check failed"
exit 1
# -------------------------
# Integration tests after deployment
# -------------------------
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [deploy-server, deploy-web]
if: github.ref == 'refs/heads/main'
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
steps:
- uses: actions/checkout@v4
- name: Test API endpoints
run: |
echo "🧪 Testing API endpoints..."
API_URL="${{ needs.deploy-server.outputs.deploy-url }}"
echo "Testing API at: $API_URL"
# Test health endpoint
curl -f \
-H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
-H "Accept: application/json" \
"$API_URL/api/health"
echo "✅ Health endpoint working"
# Test clean endpoint
curl -f -X POST "$API_URL/api/clean" \
-H "Content-Type: application/json" \
-H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
-d '{"url": "https://example.com?utm_source=test"}'
echo "✅ Clean endpoint working"
- name: Test mobile app
run: |
echo "🧪 Testing mobile app..."
MOBILE_URL="${{ needs.deploy-web.outputs.deploy-url }}"
echo "Testing mobile app at: $MOBILE_URL"
# Test mobile app loads
curl -f -H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" "$MOBILE_URL"
echo "✅ Mobile app loading"
# Test mobile app assets
curl -f -H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" "$MOBILE_URL/manifest.json"
echo "✅ Mobile app assets working"
- name: Deployment summary
run: |
echo "🎉 All deployments successful!"
echo "📱 Mobile App: ${{ needs.deploy-web.outputs.deploy-url }}"
echo "🔧 API Server: ${{ needs.deploy-server.outputs.deploy-url }}"
echo "🏥 Health Check: ${{ needs.deploy-server.outputs.deploy-url }}/api/health"
echo ""
echo "Production URLs (after promotion):"
echo "📱 Mobile App: https://traceoff.vercel.app"
echo "🔧 API Server: https://traceoffapi.vercel.app"