# Sandbox deploy (reusable). A COPY of this lives in every repo at
# .github/workflows/_deploy-sandbox.yml, distributed by the ci-cd file sync.
# The repo's sandbox.yml trigger calls it locally
# (uses: ./.github/workflows/_deploy-sandbox.yml), so no repo depends on ci-cd
# at runtime. See ../../knowledge/architecture/prototype-publishing.md.
name: Deploy sandbox (reusable)

on:
  # No inputs: every knob comes from a sandbox.json committed in the prototype
  # (self-contained + versioned), so a designer never needs GitHub settings
  # access to configure a deploy.
  workflow_call:

# ── Sandbox AWS account (727646493730) — role names/ECR/region from sandbox-infra ─
env:
  AWS_REGION: us-east-1
  ECR_REPO: es-sandboxes # single shared ECR repository
  DEPLOY_ROLE_ARN: arn:aws:iam::727646493730:role/github-sandbox-ci-cd
  # ECS Express Mode roles the deploy passes to ecs.amazonaws.com:
  EXECUTION_ROLE_ARN: arn:aws:iam::727646493730:role/ecsTaskExecutionRole
  INFRA_ROLE_ARN: arn:aws:iam::727646493730:role/ecsInfrastructureRoleForExpressServices

permissions:
  id-token: write # assume the AWS role via OIDC
  contents: read
  deployments: write # record the live URL on a GitHub Deployment

jobs:
  deploy:
    # Belt-and-suspenders with the IAM trust policy: only sandbox/* branches.
    if: startsWith(github.ref, 'refs/heads/sandbox/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # All config lives in the prototype's own sandbox.json (committed on the
      # branch), so nothing here depends on GitHub variables a designer can't set.
      #   { "stack": "spa" | "static" | "node", "apiBaseUrl": "https://…" }
      # stack defaults to spa; apiBaseUrl is optional (used by front-ends).
      - name: Read sandbox config
        run: |
          set -eu
          if [ -f sandbox.json ]; then
            STACK="$(jq -r '.stack // "spa"' sandbox.json)"
            API_BASE_URL="$(jq -r '.apiBaseUrl // ""' sandbox.json)"
          else
            STACK="spa"; API_BASE_URL=""
            echo "No sandbox.json found — using defaults (stack=spa)."
          fi
          echo "STACK=$STACK" >> "$GITHUB_ENV"
          echo "API_BASE_URL=$API_BASE_URL" >> "$GITHUB_ENV"
          echo "Sandbox config: stack=$STACK apiBaseUrl=${API_BASE_URL:-<none>}"

      # Stable, DNS-safe names derived from the branch. Service names are capped
      # at 40 chars, so hash long ones.
      - name: Derive names
        id: names
        run: |
          SLUG="${GITHUB_REF_NAME#sandbox/}"          # e.g. dana-ruiz/quiz-builder
          SAFE="$(echo "$SLUG" | tr '/' '-' | tr -cd 'a-zA-Z0-9-')"
          REPO="${GITHUB_REPOSITORY##*/}"
          NAME="sb-${REPO}-${SAFE}"
          if [ ${#NAME} -gt 40 ]; then
            NAME="sb-${REPO}-$(echo "$SAFE" | shasum | cut -c1-8)"
          fi
          echo "service=$NAME" >> "$GITHUB_OUTPUT"
          echo "tag=${REPO}-${SAFE}" >> "$GITHUB_OUTPUT"

      # Start a GitHub Deployment so the toolbox can track this (it reads the
      # deployment's environment_url to report the live link).
      - name: Open deployment
        id: deployment
        uses: actions/github-script@v7
        with:
          script: |
            // Use the SHORT branch name (not refs/heads/…) so get_deploy_status,
            // which queries deployments?ref=<branch>, finds this deployment.
            const branch = context.ref.replace('refs/heads/', '');
            const d = await github.rest.repos.createDeployment({
              owner: context.repo.owner, repo: context.repo.repo,
              ref: branch, environment: 'sandbox',
              auto_merge: false, required_contexts: [], transient_environment: true,
            });
            core.setOutput('id', d.data.id);
            await github.rest.repos.createDeploymentStatus({
              owner: context.repo.owner, repo: context.repo.repo,
              deployment_id: d.data.id, state: 'in_progress',
              log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
            });

      # Use the repo's own Dockerfile if it has one; otherwise generate a
      # default for its stack. Self-contained on purpose — no fetch at build
      # time. All lines sit at the block baseline so the heredoc bodies land at
      # column 0 (clean Dockerfiles).
      - name: Containerize
        run: |
          set -eu
          if [ -f Dockerfile ]; then
          echo "Using the repo's own Dockerfile."
          else
          echo "No Dockerfile in the repo — generating the default for stack '$STACK'."
          case "$STACK" in
          node)
          # Any Node server, incl. a NestJS backend: it must listen on
          # process.env.PORT (the container serves on 8080).
          cat > Dockerfile <<'DOCKERFILE'
          FROM node:22-alpine
          WORKDIR /app
          COPY package*.json ./
          RUN npm ci
          COPY . .
          RUN npm run build || echo "no build step"
          ENV PORT=8080
          EXPOSE 8080
          CMD ["npm", "start"]
          DOCKERFILE
          ;;
          static)
          # Plain files, already built — no npm step, so a lone index.html works.
          cat > Dockerfile <<'DOCKERFILE'
          FROM node:22-alpine
          WORKDIR /app
          RUN npm i -g serve
          COPY . ./site
          EXPOSE 8080
          CMD ["serve", "-l", "8080", "site"]
          DOCKERFILE
          ;;
          spa|*)
          cat > Dockerfile <<'DOCKERFILE'
          FROM node:22-alpine AS build
          WORKDIR /app
          COPY package*.json ./
          RUN npm ci
          COPY . .
          RUN npm run build
          FROM node:22-alpine
          WORKDIR /app
          RUN npm i -g serve
          COPY --from=build /app/dist ./site
          EXPOSE 8080
          CMD ["serve", "-s", "site", "-l", "8080"]
          DOCKERFILE
          ;;
          esac
          fi
          echo "----- Dockerfile -----"; cat Dockerfile

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - uses: aws-actions/amazon-ecr-login@v2
        id: ecr

      - name: Build & push image
        env:
          REGISTRY: ${{ steps.ecr.outputs.registry }}
        run: |
          IMAGE="$REGISTRY/${ECR_REPO}:${{ steps.names.outputs.tag }}"
          docker build -t "$IMAGE" .
          docker push "$IMAGE"
          echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"

      # ECS Express Mode: give it the image + roles, it provisions the Fargate
      # service, ALB, SSL, autoscaling, waits for stable, and returns the URL.
      - name: Deploy to ECS Express Mode
        id: deploy
        uses: aws-actions/amazon-ecs-deploy-express-service@v1
        with:
          cluster: default # auto-created if missing
          service-name: ${{ steps.names.outputs.service }}
          image: ${{ env.IMAGE }}
          container-port: "8080"
          health-check-path: "/"
          execution-role-arn: ${{ env.EXECUTION_ROLE_ARN }}
          infrastructure-role-arn: ${{ env.INFRA_ROLE_ARN }}
          task-role-arn: ${{ env.EXECUTION_ROLE_ARN }}
          environment-variables: '[{"name":"SANDBOX_API_BASE_URL","value":"${{ env.API_BASE_URL }}"}]'

      - name: Finish deployment
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const ok = '${{ steps.deploy.outcome }}' === 'success';
            // The action returns a bare hostname; the Deployments API needs an http(s) scheme.
            const ep = '${{ steps.deploy.outputs.endpoint }}';
            const url = ep ? (ep.startsWith('http') ? ep : `https://${ep}`) : undefined;
            await github.rest.repos.createDeploymentStatus({
              owner: context.repo.owner, repo: context.repo.repo,
              deployment_id: '${{ steps.deployment.outputs.id }}',
              state: ok ? 'success' : 'failure',
              environment_url: url,
              log_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
            });
