chore(security): bump next to 16.2.6 to clear critical/high CVEs #397
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy FPAI Frontend to AWS (S3 + CloudFront) | |
| on: | |
| push: | |
| branches: ["main", "prod"] | |
| paths: | |
| - "frontend/**" | |
| - ".github/workflows/deploy-frontend-aws.yml" | |
| workflow_dispatch: | |
| permissions: | |
| id-token: write | |
| contents: read | |
| concurrency: | |
| group: deploy-fpai-frontend-${{ github.ref_name == 'prod' && 'prod' || 'dev' }} | |
| cancel-in-progress: true | |
| env: | |
| AWS_REGION: us-east-1 | |
| AWS_ROLE_TO_ASSUME: arn:aws:iam::675177356722:role/GitHubActionsDeployer | |
| DEPLOY_REF: ${{ github.ref_name }} | |
| ENVIRONMENT: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }} | |
| jobs: | |
| # ============================================ | |
| # PRE-DEPLOYMENT CHECKS (run in parallel) | |
| # ============================================ | |
| lint: | |
| name: Lint & Code Quality | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| - name: Use Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Run ESLint | |
| working-directory: frontend | |
| run: | | |
| npm run lint 2>&1 | tee lint-output.txt || { | |
| echo "::error::ESLint found issues" | |
| exit 1 | |
| } | |
| - name: Check for console.log statements | |
| working-directory: frontend | |
| run: | | |
| if grep -rn "console\.log" src/ --include="*.js" --include="*.jsx" | grep -v "// debug" | head -5; then | |
| echo "::warning::Found console.log statements - consider removing for production" | |
| fi | |
| security: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| - name: Use Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Run npm audit | |
| working-directory: frontend | |
| run: | | |
| set +e | |
| npm audit --audit-level=high --json > audit-report.json | |
| STATUS=$? | |
| set -e | |
| if [ "$STATUS" -ne 0 ]; then | |
| echo "::error::Security vulnerabilities found in dependencies" | |
| jq '.metadata.vulnerabilities' audit-report.json || true | |
| exit "$STATUS" | |
| fi | |
| - name: Upload npm audit report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| if: always() | |
| with: | |
| name: frontend-audit-report | |
| path: frontend/audit-report.json | |
| retention-days: 7 | |
| - name: Check for outdated dependencies | |
| working-directory: frontend | |
| run: | | |
| npm outdated || echo "Some packages are outdated" | |
| secret-scan: | |
| name: Secret Scan (Gitleaks) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| fetch-depth: 0 | |
| - name: Install gitleaks CLI | |
| run: | | |
| go version | |
| go install github.com/zricethezav/gitleaks/v8@v8.24.2 | |
| echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" | |
| - name: Run gitleaks | |
| run: | | |
| gitleaks detect --source . --redact --report-format sarif --report-path gitleaks-report.sarif | |
| - name: Upload gitleaks report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| if: always() | |
| with: | |
| name: frontend-gitleaks-report | |
| path: gitleaks-report.sarif | |
| retention-days: 7 | |
| build-test: | |
| name: Build Verification | |
| runs-on: ubuntu-latest | |
| environment: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }} | |
| env: | |
| NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL || secrets.NEXT_PUBLIC_API_URL || (github.ref_name == 'prod' && 'https://sfc3x4krb1.execute-api.us-east-1.amazonaws.com/prod' || '') }} | |
| NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL || vars.NEXT_PUBLIC_API_URL || secrets.NEXT_PUBLIC_API_BASE_URL || secrets.NEXT_PUBLIC_API_URL || (github.ref_name == 'prod' && 'https://sfc3x4krb1.execute-api.us-east-1.amazonaws.com/prod' || '') }} | |
| NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || secrets.NEXT_PUBLIC_SITE_URL }} | |
| NEXT_PUBLIC_FIREBASE_API_KEY: ${{ vars.NEXT_PUBLIC_FIREBASE_API_KEY || secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} | |
| NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ vars.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} | |
| NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_PROJECT_ID || secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} | |
| NEXT_PUBLIC_FIREBASE_APP_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_APP_ID || secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} | |
| NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} | |
| NEXT_PUBLIC_PREPARE_UPLOADS: ${{ vars.NEXT_PUBLIC_PREPARE_UPLOADS || secrets.NEXT_PUBLIC_PREPARE_UPLOADS || (github.ref_name == 'prod' && 'false' || 'true') }} | |
| NEXT_PUBLIC_ENABLE_JOB_EVENT_STREAMING: ${{ vars.NEXT_PUBLIC_ENABLE_JOB_EVENT_STREAMING || 'true' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| - name: Use Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Validate frontend env | |
| run: | | |
| if [ "${{ env.ENVIRONMENT }}" != "prod" ] && [ -z "$NEXT_PUBLIC_API_URL" ] && [ -z "$NEXT_PUBLIC_API_BASE_URL" ]; then | |
| echo "::error::Missing NEXT_PUBLIC_API_URL (or NEXT_PUBLIC_API_BASE_URL) in GitHub environment vars/secrets" | |
| exit 1 | |
| fi | |
| test -n "$NEXT_PUBLIC_FIREBASE_API_KEY" || (echo "::error::Missing NEXT_PUBLIC_FIREBASE_API_KEY (repo variable or secret)" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN" || (echo "::error::Missing NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN (repo variable or secret)" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_PROJECT_ID" || (echo "::error::Missing NEXT_PUBLIC_FIREBASE_PROJECT_ID (repo variable or secret)" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_APP_ID" || (echo "::error::Missing NEXT_PUBLIC_FIREBASE_APP_ID (repo variable or secret)" && exit 1) | |
| - name: Build (Next.js) | |
| working-directory: frontend | |
| env: | |
| NODE_ENV: production | |
| NEXT_PUBLIC_BUILD_ID: ${{ github.sha }} | |
| run: | | |
| npm run build 2>&1 | tee build-output.txt | |
| if [ ! -d ".next" ]; then | |
| echo "::error::Build failed - .next directory not created" | |
| exit 1 | |
| fi | |
| - name: Export static site | |
| working-directory: frontend | |
| run: | | |
| # Next.js 13+ with output: 'export' in next.config creates 'out' directory | |
| # If not configured, we need to check if static export is set up | |
| if [ -d "out" ]; then | |
| echo "Static export found in 'out' directory" | |
| elif [ -d ".next/static" ]; then | |
| echo "Using .next build output" | |
| else | |
| echo "::error::No static output found" | |
| exit 1 | |
| fi | |
| - name: Verify build output | |
| working-directory: frontend | |
| run: | | |
| echo "Checking build artifacts..." | |
| # Determine output directory (Next.js uses 'out' for static export, or '.next' for server) | |
| if [ -d "out" ]; then | |
| OUTPUT_DIR="out" | |
| else | |
| OUTPUT_DIR=".next" | |
| fi | |
| echo "Using output directory: $OUTPUT_DIR" | |
| # For static export, check index.html | |
| if [ "$OUTPUT_DIR" = "out" ]; then | |
| if [ ! -f "$OUTPUT_DIR/index.html" ]; then | |
| echo "::error::Missing index.html in build output" | |
| exit 1 | |
| fi | |
| fi | |
| # Check for static assets | |
| if [ -d "$OUTPUT_DIR/static" ] || [ -d "$OUTPUT_DIR/_next/static" ]; then | |
| echo "✅ Static assets found" | |
| else | |
| echo "::warning::No static assets directory found" | |
| fi | |
| echo "✅ Build artifacts verified" | |
| - name: Analyze bundle size | |
| working-directory: frontend | |
| run: | | |
| echo "## 📦 Bundle Size Analysis" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| File | Size |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|------|" >> $GITHUB_STEP_SUMMARY | |
| # Determine output directory | |
| if [ -d "out" ]; then | |
| OUTPUT_DIR="out" | |
| ASSETS_DIR="out/_next/static" | |
| else | |
| OUTPUT_DIR=".next" | |
| ASSETS_DIR=".next/static" | |
| fi | |
| # Calculate total JS size | |
| JS_SIZE=$(find $ASSETS_DIR -name "*.js" -exec du -ch {} + 2>/dev/null | tail -1 | cut -f1 || echo "N/A") | |
| echo "| JavaScript (total) | $JS_SIZE |" >> $GITHUB_STEP_SUMMARY | |
| # Calculate total CSS size | |
| CSS_SIZE=$(find $ASSETS_DIR -name "*.css" -exec du -ch {} + 2>/dev/null | tail -1 | cut -f1 || echo "N/A") | |
| echo "| CSS (total) | $CSS_SIZE |" >> $GITHUB_STEP_SUMMARY | |
| # Total output size | |
| TOTAL_SIZE=$(du -sh $OUTPUT_DIR | cut -f1) | |
| echo "| **Total** | **$TOTAL_SIZE** |" >> $GITHUB_STEP_SUMMARY | |
| - name: Check for HTML validity | |
| working-directory: frontend | |
| run: | | |
| # Determine output directory | |
| if [ -d "out" ]; then | |
| HTML_FILE="out/index.html" | |
| else | |
| echo "Skipping HTML validation for non-static export" | |
| exit 0 | |
| fi | |
| # Basic HTML structure check | |
| if ! grep -q "<!DOCTYPE html>" "$HTML_FILE" && ! grep -q "<!doctype html>" "$HTML_FILE"; then | |
| echo "::warning::index.html missing DOCTYPE declaration" | |
| fi | |
| if ! grep -q "<title>" "$HTML_FILE"; then | |
| echo "::warning::index.html missing title tag (SEO issue)" | |
| fi | |
| if ! grep -q 'lang="' "$HTML_FILE"; then | |
| echo "::warning::html tag missing lang attribute (accessibility issue)" | |
| fi | |
| echo "✅ HTML validation complete" | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: frontend-build | |
| path: | | |
| frontend/out | |
| frontend/.next | |
| retention-days: 7 | |
| # ============================================ | |
| # QUALITY GATE (all checks must pass) | |
| # ============================================ | |
| quality-gate: | |
| name: Quality Gate | |
| needs: [lint, security, secret-scan, build-test] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: All checks passed | |
| run: | | |
| echo "## ✅ All Pre-Deployment Checks Passed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint & Code Quality | ✅ Passed |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Security Audit | ✅ Passed |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Secret Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Build Verification | ✅ Passed |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Proceeding to deployment..." >> $GITHUB_STEP_SUMMARY | |
| # ============================================ | |
| # DEPLOYMENT (only after quality gate passes) | |
| # ============================================ | |
| deploy: | |
| name: Deploy to AWS | |
| needs: quality-gate | |
| runs-on: ubuntu-latest | |
| environment: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }} | |
| env: | |
| S3_BUCKET: ${{ vars.FRONTEND_S3_BUCKET }} | |
| CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.FRONTEND_CLOUDFRONT_DISTRIBUTION_ID }} | |
| NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL || secrets.NEXT_PUBLIC_API_URL || (github.ref_name == 'prod' && 'https://sfc3x4krb1.execute-api.us-east-1.amazonaws.com/prod' || '') }} | |
| NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL || vars.NEXT_PUBLIC_API_URL || secrets.NEXT_PUBLIC_API_BASE_URL || secrets.NEXT_PUBLIC_API_URL || (github.ref_name == 'prod' && 'https://sfc3x4krb1.execute-api.us-east-1.amazonaws.com/prod' || '') }} | |
| NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || secrets.NEXT_PUBLIC_SITE_URL }} | |
| NEXT_PUBLIC_FIREBASE_API_KEY: ${{ vars.NEXT_PUBLIC_FIREBASE_API_KEY || secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} | |
| NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ vars.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} | |
| NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_PROJECT_ID || secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} | |
| NEXT_PUBLIC_FIREBASE_APP_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_APP_ID || secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} | |
| NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ vars.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} | |
| NEXT_PUBLIC_PREPARE_UPLOADS: ${{ vars.NEXT_PUBLIC_PREPARE_UPLOADS || secrets.NEXT_PUBLIC_PREPARE_UPLOADS || (github.ref_name == 'prod' && 'false' || 'true') }} | |
| NEXT_PUBLIC_ENABLE_JOB_EVENT_STREAMING: ${{ vars.NEXT_PUBLIC_ENABLE_JOB_EVENT_STREAMING || 'true' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| - name: Validate required config | |
| run: | | |
| test -n "$S3_BUCKET" || (echo "Missing variable: FRONTEND_S3_BUCKET" && exit 1) | |
| test -n "$CLOUDFRONT_DISTRIBUTION_ID" || (echo "Missing variable: FRONTEND_CLOUDFRONT_DISTRIBUTION_ID" && exit 1) | |
| if [ "${{ env.ENVIRONMENT }}" != "prod" ] && [ -z "$NEXT_PUBLIC_API_URL" ] && [ -z "$NEXT_PUBLIC_API_BASE_URL" ]; then | |
| echo "::error::Missing NEXT_PUBLIC_API_URL (or NEXT_PUBLIC_API_BASE_URL) in GitHub environment vars/secrets" | |
| exit 1 | |
| fi | |
| test -n "$NEXT_PUBLIC_FIREBASE_API_KEY" || (echo "Missing variable/secret: NEXT_PUBLIC_FIREBASE_API_KEY" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN" || (echo "Missing variable/secret: NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_PROJECT_ID" || (echo "Missing variable/secret: NEXT_PUBLIC_FIREBASE_PROJECT_ID" && exit 1) | |
| test -n "$NEXT_PUBLIC_FIREBASE_APP_ID" || (echo "Missing variable/secret: NEXT_PUBLIC_FIREBASE_APP_ID" && exit 1) | |
| - name: Use Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install deps | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Build (Next.js) | |
| working-directory: frontend | |
| env: | |
| NODE_ENV: production | |
| NEXT_PUBLIC_BUILD_ID: ${{ github.sha }} | |
| run: | | |
| npm run build | |
| # Verify static export was created | |
| if [ ! -d "out" ]; then | |
| echo "::error::Build failed - 'out' directory not created. Ensure next.config has output: 'export'" | |
| exit 1 | |
| fi | |
| - name: Configure AWS credentials via OIDC | |
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | |
| with: | |
| aws-region: ${{ env.AWS_REGION }} | |
| role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }} | |
| - name: Who am I (debug) | |
| run: aws sts get-caller-identity | |
| - name: Sync static assets to S3 (immutable) | |
| run: | | |
| # Next.js static export puts assets in _next/static | |
| aws s3 sync frontend/out/_next "s3://$S3_BUCKET/_next" \ | |
| --delete \ | |
| --cache-control "public,max-age=31536000,immutable" \ | |
| --only-show-errors | |
| - name: Upload non-asset files to S3 | |
| run: | | |
| aws s3 sync frontend/out "s3://$S3_BUCKET" \ | |
| --delete \ | |
| --exclude "_next/*" \ | |
| --exclude "*.html" \ | |
| --cache-control "public,max-age=86400" \ | |
| --only-show-errors | |
| - name: Create and upload version.json (no-cache) | |
| run: | | |
| echo '{"buildId":"${{ github.sha }}"}' > frontend/out/version.json | |
| aws s3 cp frontend/out/version.json "s3://$S3_BUCKET/version.json" \ | |
| --cache-control "no-cache,no-store,must-revalidate" \ | |
| --content-type "application/json" \ | |
| --only-show-errors | |
| - name: Upload HTML files with no-cache | |
| run: | | |
| HTML_CACHE_CONTROL="no-cache,no-store,must-revalidate" | |
| # Upload all HTML files (Next.js creates one per page) | |
| find frontend/out -name "*.html" | while read html_file; do | |
| relative_path="${html_file#frontend/out/}" | |
| aws s3 cp "$html_file" "s3://$S3_BUCKET/$relative_path" \ | |
| --cache-control "$HTML_CACHE_CONTROL" \ | |
| --content-type "text/html; charset=utf-8" \ | |
| --only-show-errors | |
| # CloudFront + private S3 origins do not resolve /route or /route/ to /route/index.html. | |
| # Publish alias keys so both URL shapes work for exported pages. | |
| if [[ "$relative_path" == */index.html ]] && [ "$relative_path" != "index.html" ]; then | |
| route_key="${relative_path%/index.html}" | |
| # Skip internal/special pages. | |
| case "$route_key" in | |
| _*|404) | |
| continue | |
| ;; | |
| esac | |
| # /about -> object key "about" | |
| aws s3api put-object \ | |
| --bucket "$S3_BUCKET" \ | |
| --key "$route_key" \ | |
| --body "$html_file" \ | |
| --cache-control "$HTML_CACHE_CONTROL" \ | |
| --content-type "text/html; charset=utf-8" \ | |
| >/dev/null | |
| # /about/ -> object key "about/" | |
| aws s3api put-object \ | |
| --bucket "$S3_BUCKET" \ | |
| --key "$route_key/" \ | |
| --body "$html_file" \ | |
| --cache-control "$HTML_CACHE_CONTROL" \ | |
| --content-type "text/html; charset=utf-8" \ | |
| >/dev/null | |
| fi | |
| done | |
| - name: Verify index.html exists in S3 | |
| run: | | |
| aws s3api head-object \ | |
| --bucket "$S3_BUCKET" \ | |
| --key "index.html" \ | |
| >/dev/null | |
| - name: Ensure CloudFront default root object is index.html | |
| run: | | |
| set -euo pipefail | |
| DIST_CONFIG_FILE="$(mktemp)" | |
| UPDATED_CONFIG_FILE="$(mktemp)" | |
| aws cloudfront get-distribution-config \ | |
| --id "$CLOUDFRONT_DISTRIBUTION_ID" \ | |
| > "$DIST_CONFIG_FILE" | |
| ETAG="$(jq -r '.ETag' "$DIST_CONFIG_FILE")" | |
| CURRENT_ROOT_OBJECT="$(jq -r '.DistributionConfig.DefaultRootObject // empty' "$DIST_CONFIG_FILE")" | |
| if [ "$CURRENT_ROOT_OBJECT" = "index.html" ]; then | |
| echo "CloudFront DefaultRootObject is already index.html" | |
| exit 0 | |
| fi | |
| echo "Updating DefaultRootObject to index.html (current: ${CURRENT_ROOT_OBJECT:-<empty>})" | |
| jq '.DistributionConfig | .DefaultRootObject = "index.html"' "$DIST_CONFIG_FILE" > "$UPDATED_CONFIG_FILE" | |
| aws cloudfront update-distribution \ | |
| --id "$CLOUDFRONT_DISTRIBUTION_ID" \ | |
| --if-match "$ETAG" \ | |
| --distribution-config "file://$UPDATED_CONFIG_FILE" \ | |
| >/dev/null | |
| echo "CloudFront distribution update submitted" | |
| - name: Invalidate CloudFront cache | |
| run: | | |
| aws cloudfront create-invalidation \ | |
| --distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" \ | |
| --paths "/*" | |
| - name: Smoke test CloudFront root response | |
| run: | | |
| set -euo pipefail | |
| DISTRIBUTION_DOMAIN="$(aws cloudfront get-distribution --id "$CLOUDFRONT_DISTRIBUTION_ID" --query 'Distribution.DomainName' --output text)" | |
| SITE_URL="https://$DISTRIBUTION_DOMAIN" | |
| MAX_ATTEMPTS=12 | |
| ATTEMPT=1 | |
| echo "Running smoke test against $SITE_URL (/, /about, /team, /app, /demo)" | |
| while [ "$ATTEMPT" -le "$MAX_ATTEMPTS" ]; do | |
| FAILED=0 | |
| for path in / /about /team /app /demo; do | |
| HTTP_CODE="$(curl -sS -L -o /tmp/cloudfront-smoke.html -w '%{http_code}' "$SITE_URL$path" || true)" | |
| if [ "$HTTP_CODE" != "200" ] || grep -q "<Code>AccessDenied</Code>" /tmp/cloudfront-smoke.html; then | |
| echo "Path $path failed (status: ${HTTP_CODE:-unknown})" | |
| FAILED=1 | |
| break | |
| fi | |
| done | |
| if [ "$FAILED" -eq 0 ]; then | |
| echo "Smoke test passed on attempt $ATTEMPT" | |
| exit 0 | |
| fi | |
| if [ "$ATTEMPT" -eq "$MAX_ATTEMPTS" ]; then | |
| echo "::error::CloudFront smoke test failed after $MAX_ATTEMPTS attempts" | |
| echo "First lines of response:" | |
| sed -n '1,30p' /tmp/cloudfront-smoke.html || true | |
| exit 1 | |
| fi | |
| echo "Attempt $ATTEMPT/$MAX_ATTEMPTS not ready; retrying in 20s..." | |
| ATTEMPT=$((ATTEMPT + 1)) | |
| sleep 20 | |
| done | |
| - name: Deployment Summary | |
| run: | | |
| EFFECTIVE_API_URL="${NEXT_PUBLIC_API_URL:-$NEXT_PUBLIC_API_BASE_URL}" | |
| echo "## 🚀 Frontend Deployment Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| S3 Bucket | $S3_BUCKET |" >> $GITHUB_STEP_SUMMARY | |
| echo "| CloudFront | $CLOUDFRONT_DISTRIBUTION_ID |" >> $GITHUB_STEP_SUMMARY | |
| echo "| API URL | $EFFECTIVE_API_URL |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Triggered by | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Commit | ${{ github.sha }} |" >> $GITHUB_STEP_SUMMARY |