prod-deploy.yml10.2 KBView on GitHub
name: Deploy AWS Prod

on:
  push:
    branches:
      - main
  workflow_dispatch:
    inputs:
      deploy_mode:
        description: Deployment mode
        required: true
        default: auto
        type: choice
        options:
          - auto
          - full

permissions:
  contents: read
  id-token=[redacted]

concurrency:
  group: aws-prod-deploy
  cancel-in-progress: false

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      # Reading which commit each deploy job last succeeded on.
      actions: read
    outputs:
      should_deploy: ${{ steps.plan.outputs.should_deploy }}
      deploy_stack: ${{ steps.plan.outputs.deploy_stack }}
      deploy_frontend: ${{ steps.plan.outputs.deploy_frontend }}
      publish_api: ${{ steps.plan.outputs.publish_api }}
      publish_chat: ${{ steps.plan.outputs.publish_chat }}
      publish_worker: ${{ steps.plan.outputs.publish_worker }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Determine deploy scope
        id: plan
        env:
          CEDAR_COMPARE_FROM_SHA: ${{ github.event.before }}
          CEDAR_COMPARE_TO_SHA: ${{ github.sha }}
          INPUT_DEPLOY_MODE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_mode || 'auto' }}
          # Scope each surface off the commit it was last deployed from rather than
          # off the previous push, so a cancelled or failed run doesn't strand its
          # changes. See aws/scripts/last-deployed-sha.sh.
          CEDAR_DEPLOY_BASELINE_WORKFLOW: prod-deploy.yml
          CEDAR_DEPLOY_BASELINE_REF: main
          GH_TOKEN: ${{ github.token }}
        run: bash ./aws/scripts/plan-surface-scope.sh

  deploy-backend:
    needs: plan
    if: needs.plan.outputs.deploy_stack == 'true' || needs.plan.outputs.publish_api == 'true' || needs.plan.outputs.publish_chat == 'true' || needs.plan.outputs.publish_worker == 'true'
    runs-on: ubuntu-latest
    environment: production
    env:
      AWS_REGION: us-east-1
      AWS_DEFAULT_REGION: us-east-1
      CEDAR_AWS_ACCOUNT_ID: '619071350399'
      CEDAR_AWS_ENV: prod
      CEDAR_AWS_REGION: us-east-1
      AWS_PROD_DEPLOY_ROLE_ARN: ${{ vars.AWS_PROD_DEPLOY_ROLE_ARN }}
      CEDAR_AWS_PROD_APP_DOMAIN: ${{ vars.CEDAR_AWS_PROD_APP_DOMAIN }}
      CEDAR_AWS_PROD_API_DOMAIN: ${{ vars.CEDAR_AWS_PROD_API_DOMAIN }}
      CEDAR_AWS_PROD_CERTIFICATE_ARN: ${{ vars.CEDAR_AWS_PROD_CERTIFICATE_ARN }}
      CEDAR_AWS_PROD_MARKETING_DOMAINS: ${{ vars.CEDAR_AWS_PROD_MARKETING_DOMAINS || 'cedarcopilot.com,www.cedarcopilot.com' }}
      # Unset until the Manufact domain cutover (docs/design — Cedar MCP via Manufact proxy);
      # buildEnvironmentConfig only sets CEDAR_MCP_PUBLIC_URL in the ECS task when this resolves
      # to a non-empty value, so an unset GitHub variable here is a safe, inert default.
      CEDAR_AWS_PROD_MCP_PUBLIC_URL: ${{ vars.CEDAR_AWS_PROD_MCP_PUBLIC_URL }}
      CEDAR_AWS_SHARED_FILES_BUCKET_NAME: ${{ vars.CEDAR_AWS_SHARED_FILES_BUCKET_NAME }}
      CEDAR_AWS_VPC_ID: ${{ vars.CEDAR_AWS_VPC_ID }}
      CEDAR_AWS_DEPLOY_STACK: ${{ needs.plan.outputs.deploy_stack }}
      CEDAR_AWS_DEPLOY_FRONTEND: 'false'
      CEDAR_AWS_PUBLISH_API: ${{ needs.plan.outputs.publish_api }}
      CEDAR_AWS_PUBLISH_CHAT: ${{ needs.plan.outputs.publish_chat }}
      CEDAR_AWS_PUBLISH_WORKER: ${{ needs.plan.outputs.publish_worker }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Cache node_modules
        id: cache-deps
        uses: actions/cache@v4
        with:
          path: |
            node_modules
            apps/*/node_modules
            packages/*/node_modules
          key: ${{ runner.os }}-deploy-deps-${{ hashFiles('pnpm-lock.yaml') }}

      - name: Cache AWS CDK dependencies
        id: cache-aws
        uses: actions/cache@v4
        with:
          path: aws/node_modules
          key: ${{ runner.os }}-deploy-aws-${{ hashFiles('aws/package.json', 'aws/pnpm-lock.yaml') }}

      - name: Install workspace dependencies
        if: steps.cache-deps.outputs.cache-hit != 'true'
        run: pnpm install --frozen-lockfile

      - name: Install AWS deployment dependencies
        if: steps.cache-aws.outputs.cache-hit != 'true'
        run: pnpm --ignore-workspace --dir aws install --no-frozen-lockfile

      - name: Validate prod deploy configuration
        run: |
          set -euo pipefail

          required_vars=(
            AWS_PROD_DEPLOY_ROLE_ARN
            CEDAR_AWS_VPC_ID
          )

          for var_name in "${required_vars[@]}"; do
            if [[ -z "${!var_name:-}" ]]; then
              echo "Missing required GitHub production environment variable: ${var_name}" >&2
              exit 1
            fi
          done

          if [[ "$AWS_PROD_DEPLOY_ROLE_ARN" != "arn:aws:iam::${CEDAR_AWS_ACCOUNT_ID}:role/"* ]]; then
            echo "AWS_PROD_DEPLOY_ROLE_ARN must point at Cedar account ${CEDAR_AWS_ACCOUNT_ID}" >&2
            exit 1
          fi

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
          role-to-assume: ${{ env.AWS_PROD_DEPLOY_ROLE_ARN }}

      - name: Verify Cedar AWS account
        run: |
          set -euo pipefail

          account_id="$(aws sts get-caller-identity --query 'Account' --output text)"
          if [[ "$account_id" != "$CEDAR_AWS_ACCOUNT_ID" ]]; then
            echo "Expected Cedar AWS account ${CEDAR_AWS_ACCOUNT_ID}, got ${account_id}" >&2
            exit 1
          fi

      - name: Validate Secrets Manager has all declared fields
        run: bash ./aws/scripts/validate-secrets.sh prod

      - name: Preflight CDK validation
        if: needs.plan.outputs.deploy_stack == 'true'
        run: |
          set -euo pipefail
          echo "Typechecking CDK and synthesizing template in parallel..."
          pnpm --ignore-workspace --dir aws exec tsc -p tsconfig.json --noEmit &
          PID_TC=$!
          pnpm --ignore-workspace --dir aws exec cdk synth CedarAwsProdEnvironmentStack --quiet >/dev/null &
          PID_SYNTH=$!
          wait $PID_TC
          wait $PID_SYNTH
          echo "CDK preflight passed"

      - name: Deploy backend
        env:
          NODE_OPTIONS: --max-old-space-size=8192
          DOCKER_BUILDKIT: '1'
          CEDAR_AWS_RUN_TYPECHECK: 'false'
        run: bash ./aws/scripts/deploy-surface.sh prod

  deploy-frontend:
    needs: [plan, deploy-backend]
    if: always() && needs.plan.outputs.deploy_frontend == 'true' && (needs.deploy-backend.result == 'success' || needs.deploy-backend.result == 'skipped')
    runs-on: ubuntu-latest
    environment: production
    env:
      AWS_REGION: us-east-1
      AWS_DEFAULT_REGION: us-east-1
      CEDAR_AWS_ACCOUNT_ID: '619071350399'
      CEDAR_AWS_ENV: prod
      CEDAR_AWS_REGION: us-east-1
      AWS_PROD_DEPLOY_ROLE_ARN: ${{ vars.AWS_PROD_DEPLOY_ROLE_ARN }}
      CEDAR_AWS_PROD_APP_DOMAIN: ${{ vars.CEDAR_AWS_PROD_APP_DOMAIN }}
      CEDAR_AWS_PROD_API_DOMAIN: ${{ vars.CEDAR_AWS_PROD_API_DOMAIN }}
      CEDAR_AWS_PROD_CERTIFICATE_ARN: ${{ vars.CEDAR_AWS_PROD_CERTIFICATE_ARN }}
      CEDAR_AWS_VPC_ID: ${{ vars.CEDAR_AWS_VPC_ID }}
      VITE_PUBLIC_POSTHOG_KEY: ${{ vars.VITE_PUBLIC_POSTHOG_KEY }}
      # VITE_PUBLIC_POSTHOG_HOST is intentionally unset: publish-frontend-assets.sh derives it
      # as <app-origin>/ingest (the same-origin reverse proxy on the CloudFront distribution),
      # so analytics isn't dropped by browser tracker blockers.
      VITE_PUBLIC_SENTRY_DSN: ${{ vars.VITE_PUBLIC_SENTRY_DSN }}
      VITE_PUBLIC_SENTRY_ENVIRONMENT: production
      VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: '0.1'
      VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: '0'
      VITE_PUBLIC_SENTRY_REPLAYS_ERROR_SAMPLE_RATE: '1'
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      SENTRY_ORG: cedar-2k
      SENTRY_PROJECT: scm-platform-features
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Cache node_modules
        id: cache-deps
        uses: actions/cache@v4
        with:
          path: |
            node_modules
            apps/*/node_modules
            packages/*/node_modules
          key: ${{ runner.os }}-deploy-deps-${{ hashFiles('pnpm-lock.yaml') }}

      - name: Install workspace dependencies
        if: steps.cache-deps.outputs.cache-hit != 'true'
        run: pnpm install --frozen-lockfile

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
          role-to-assume: ${{ env.AWS_PROD_DEPLOY_ROLE_ARN }}

      - name: Deploy frontend
        env:
          NODE_OPTIONS: --max-old-space-size=8192
        run: bash ./aws/scripts/publish-frontend-assets.sh prod

  smoke-test:
    needs: [deploy-backend, deploy-frontend]
    if: always() && (needs.deploy-backend.result == 'success' || needs.deploy-backend.result == 'skipped') && (needs.deploy-frontend.result == 'success' || needs.deploy-frontend.result == 'skipped') && !(needs.deploy-backend.result == 'skipped' && needs.deploy-frontend.result == 'skipped')
    runs-on: ubuntu-latest
    environment: production
    env:
      AWS_REGION: us-east-1
      AWS_DEFAULT_REGION: us-east-1
      CEDAR_AWS_ENV: prod
      CEDAR_AWS_REGION: us-east-1
      AWS_PROD_DEPLOY_ROLE_ARN: ${{ vars.AWS_PROD_DEPLOY_ROLE_ARN }}
      CEDAR_AWS_PROD_APP_DOMAIN: ${{ vars.CEDAR_AWS_PROD_APP_DOMAIN }}
      CEDAR_AWS_PROD_API_DOMAIN: ${{ vars.CEDAR_AWS_PROD_API_DOMAIN }}
      CEDAR_AWS_PROD_CERTIFICATE_ARN: ${{ vars.CEDAR_AWS_PROD_CERTIFICATE_ARN }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: us-east-1
          role-to-assume: ${{ env.AWS_PROD_DEPLOY_ROLE_ARN }}

      - name: Smoke test prod deployment
        run: bash ./aws/scripts/smoke-test.sh prod