name: Deploy Frontend

# Multi-tenant: one bundle serves every customer domain, so a single deploy
# updates all of them at once. There is no per-tenant build or per-tenant deploy.

on:
  push:
    branches:
      - main
  workflow_dispatch:
    inputs:
      ref:
        description: Branch or SHA to deploy
        required: false
        default: main

concurrency:
  group: deploy-frontend
  cancel-in-progress: false # never abandon a half-finished release swap

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.ref || github.ref }}

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Enforce boundaries
        run: npm run arch:check

      - name: Build
        run: npm run build -- --configuration production

      - name: Resolve build output
        id: dist
        run: |
          set -euo pipefail
          # @angular/build:application emits into dist/<name>/browser.
          # Fall back to the flat layout so this survives a builder change.
          if [ -d dist/dexarmarket/browser ]; then
            DIR=dist/dexarmarket/browser
          elif [ -f dist/dexarmarket/index.html ]; then
            DIR=dist/dexarmarket
          else
            echo "no build output found under dist/dexarmarket" >&2
            ls -R dist || true
            exit 1
          fi
          test -f "$DIR/index.html" || { echo "$DIR has no index.html" >&2; exit 1; }
          echo "dir=$DIR" >> "$GITHUB_OUTPUT"
          echo "Deploying from $DIR ($(find "$DIR" -type f | wc -l) files)"

      - name: Configure SSH
        env:
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
        run: |
          set -euo pipefail
          test -n "$DEPLOY_SSH_KEY" || { echo "secret DEPLOY_SSH_KEY is empty" >&2; exit 1; }
          test -n "$DEPLOY_KNOWN_HOSTS" || { echo "secret DEPLOY_KNOWN_HOSTS is empty" >&2; exit 1; }
          mkdir -p ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          # Pinned host key, so a MITM or a rebuilt server fails the deploy
          # instead of being trusted silently.
          printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts

      - name: Reconcile tenant API domains
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
          USER: ${{ secrets.DEPLOY_USER }}
          STOREFRONT_DOMAINS: ${{ secrets.STOREFRONT_DOMAINS }}
          CERTBOT_EMAIL: ${{ secrets.CERTBOT_EMAIL }}
          BACKEND_UPSTREAM: ${{ secrets.BACKEND_UPSTREAM }}
        run: |
          set -euo pipefail
          test -n "$HOST" || { echo "secret DEPLOY_HOST is empty" >&2; exit 1; }
          test -n "$USER" || { echo "secret DEPLOY_USER is empty" >&2; exit 1; }
          test -n "$STOREFRONT_DOMAINS" || { echo "secret STOREFRONT_DOMAINS is empty" >&2; exit 1; }
          test -n "$CERTBOT_EMAIL" || { echo "secret CERTBOT_EMAIL is empty" >&2; exit 1; }
          BACKEND_UPSTREAM="${BACKEND_UPSTREAM:-https://127.0.0.1:445}"
          [[ "$CERTBOT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || {
            echo "CERTBOT_EMAIL is invalid" >&2; exit 1;
          }
          [[ "$BACKEND_UPSTREAM" =~ ^https?://[A-Za-z0-9.:-]+$ ]] || {
            echo "BACKEND_UPSTREAM is invalid" >&2; exit 1;
          }

          declare -A API_BASE_DOMAINS=()
          for storefront in $STOREFRONT_DOMAINS; do
            [[ "$storefront" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]] || {
              echo "invalid storefront domain: $storefront" >&2; exit 1;
            }
            IFS=. read -ra labels <<< "$storefront"
            label_count=${#labels[@]}
            take=2
            tld=${labels[label_count-1]}
            second_level=${labels[label_count-2]}
            if (( label_count >= 3 && ${#tld} == 2 && ${#second_level} <= 3 )); then
              take=3
            fi
            start=$((label_count - take))
            base_domain=$(IFS=.; echo "${labels[*]:start}")
            API_BASE_DOMAINS["$base_domain"]=1
          done

          SSH="ssh -i ~/.ssh/deploy_key -o BatchMode=yes"
          for domain in "${!API_BASE_DOMAINS[@]}"; do
            $SSH "$USER@$HOST" sudo /usr/local/sbin/marketplaces-configure-api-domain \
              --domain "$domain" --email "$CERTBOT_EMAIL" --upstream "$BACKEND_UPSTREAM"
          done

      - name: Upload release
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
          USER: ${{ secrets.DEPLOY_USER }}
          SRC: ${{ steps.dist.outputs.dir }}
        run: |
          set -euo pipefail
          RELEASE="${GITHUB_SHA::12}"
          echo "RELEASE=$RELEASE" >> "$GITHUB_ENV"
          SSH="ssh -i ~/.ssh/deploy_key -o BatchMode=yes"
          $SSH "$USER@$HOST" "mkdir -p /srv/marketplaces/releases/$RELEASE/frontend"
          tar -C "$SRC" -czf - . | $SSH "$USER@$HOST" \
            "tar -xzf - -C /srv/marketplaces/releases/$RELEASE/frontend"

      - name: Activate release
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
          USER: ${{ secrets.DEPLOY_USER }}
        run: |
          set -euo pipefail
          ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$USER@$HOST" bash -euo pipefail <<EOSSH
            BASE=/srv/marketplaces
            REL="\$BASE/releases/$RELEASE"
            test -f "\$REL/frontend/index.html" || { echo "upload incomplete, refusing to swap" >&2; exit 1; }
            # ln -T onto a temp name then mv: the swap is atomic, so no request
            # is ever served from a half-updated root.
            ln -sfnT "\$REL" "\$BASE/current.new"
            mv -Tf "\$BASE/current.new" "\$BASE/current"
            sudo /bin/systemctl reload nginx
            # Keep the last 5 releases so a rollback is a symlink change.
            ls -1dt "\$BASE"/releases/*/ | tail -n +6 | xargs -r rm -rf
            echo "active: \$(readlink -f \$BASE/current)"
          EOSSH

      - name: Verify
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
          USER: ${{ secrets.DEPLOY_USER }}
        run: |
          set -euo pipefail
          ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$USER@$HOST" \
            'curl -fsS -o /dev/null -w "health=%{http_code}\n" http://127.0.0.1/health &&
             curl -fsS -o /dev/null -w "index=%{http_code}\n" http://127.0.0.1/'

      - name: Report
        if: always()
        run: |
          if [ "${{ job.status }}" = "success" ]; then
            echo "Deployed ${GITHUB_SHA::12} to ${{ secrets.DEPLOY_HOST }}" >> "$GITHUB_STEP_SUMMARY"
          else
            echo "Deploy of ${GITHUB_SHA::12} FAILED. The previous release is still active — the symlink only moves after a successful upload." >> "$GITHUB_STEP_SUMMARY"
          fi
