name: CD Apps

on:
  push:
    branches: [main]
    paths:
      - "apps/**"
      - "packages/**"
      - "pnpm-lock.yaml"
  workflow_dispatch: {}

concurrency:
  group: deploy-apps
  cancel-in-progress: true

jobs:
  build-images:
    name: Build & Push Docker Images
    runs-on: ubuntu-latest
    environment: production
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        app: [journal, planner]
    steps:
      - uses: actions/checkout@v6

      - uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Decrypt Sentry auth token
        run: |
          curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
          chmod +x sops-v3.9.4.linux.amd64
          SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > /tmp/secrets.env
          grep SENTRY_AUTH_TOKEN /tmp/secrets.env | cut -d= -f2- | tr -d '\n' > /tmp/sentry_token

      - uses: docker/build-push-action@v7
        with:
          context: .
          file: apps/${{ matrix.app }}/Dockerfile
          push: true
          tags: |
            ghcr.io/trails-cool/${{ matrix.app }}:latest
            ghcr.io/trails-cool/${{ matrix.app }}:${{ github.sha }}
          build-args: |
            SENTRY_RELEASE=${{ github.sha }}
          secrets: |
            SENTRY_AUTH_TOKEN=/tmp/sentry_token

  deploy:
    name: Deploy Apps
    needs: [build-images]
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v6

      - name: Decrypt secrets
        run: |
          curl -sLO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
          chmod +x sops-v3.9.4.linux.amd64
          SOPS_AGE_KEY="${{ secrets.AGE_SECRET_KEY }}" ./sops-v3.9.4.linux.amd64 -d infrastructure/secrets.app.env > infrastructure/app.env
          echo "SENTRY_RELEASE=${{ github.sha }}" >> infrastructure/app.env
          echo "DOMAIN=trails.cool" >> infrastructure/app.env
          # Flagship marker — see cd-infra.yml for what this gates.
          echo "IS_FLAGSHIP=true" >> infrastructure/app.env

      - name: Copy files to server
        uses: appleboy/scp-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: root
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          source: "infrastructure/docker-compose.yml,infrastructure/Caddyfile"
          target: /opt/trails-cool
          strip_components: 1

      - name: Copy secrets to server
        uses: appleboy/scp-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: root
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          source: "infrastructure/app.env"
          target: /opt/trails-cool
          strip_components: 1

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: root
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /opt/trails-cool

            # Login to ghcr.io
            GHCR_TOKEN=$(grep DEPLOY_GHCR_TOKEN app.env | cut -d= -f2-)
            echo "$GHCR_TOKEN" | docker login ghcr.io -u stigi --password-stdin

            # Pull and deploy app containers
            docker compose --env-file app.env pull journal planner
            # Hand-written data migrations (idempotent) run BEFORE drizzle-kit
            # push so unique-key reshapes can collapse duplicate rows first.
            docker compose --env-file app.env run --rm journal node --experimental-strip-types /app/packages/db/src/migrate-data.ts
            docker compose --env-file app.env run --rm journal npx drizzle-kit push --config /app/packages/db/drizzle.config.ts --force
            # --remove-orphans cleans up containers whose service was deleted
            # from the compose file, matching cd-infra's behaviour.
            docker compose --env-file app.env up -d --remove-orphans journal planner

            # Reload Caddy with the Caddyfile we just scp'd. cd-apps
            # ships infrastructure/Caddyfile alongside docker-compose.yml
            # (see scp step above), but containers don't auto-pick-up
            # config changes. cd-infra reloads Caddy as part of its
            # deploy; cd-apps did NOT, which meant any Caddyfile change
            # touching only `apps/`/`packages/` paths sat on disk
            # unapplied until the next cd-infra run. The reload is
            # idempotent (Caddy validates first, swaps live, no
            # downtime) so doing it on every cd-apps deploy is safe
            # even when Caddyfile is unchanged. `|| true` keeps the
            # deploy from failing if Caddy itself is unhealthy.
            docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile || true

            # Clean up
            docker image prune -af
            docker compose ps

            # Annotate deploy in Grafana. GRAFANA_SERVICE_TOKEN lives
            # in secrets.infra.env (decrypted by cd-infra.yml into the
            # merged /opt/trails-cool/.env on the server). cd-apps's
            # own app.env intentionally does NOT carry it — apps don't
            # need it at runtime. So we read from the merged `.env`
            # that cd-infra populated. If cd-infra has never run on
            # this host, .env may not exist; the `2>/dev/null` and
            # the `if -n` guard make the annotation a silent no-op
            # rather than a deploy failure in that case.
            GRAFANA_TOKEN=$(grep GRAFANA_SERVICE_TOKEN .env 2>/dev/null | cut -d= -f2-)
            if [ -n "$GRAFANA_TOKEN" ]; then
              docker compose exec -T grafana curl -sf -X POST \
                -H "Authorization: Bearer $GRAFANA_TOKEN" \
                -H "Content-Type: application/json" \
                -d '{"text":"Deploy ${{ github.sha }}","tags":["deploy","apps"]}' \
                http://localhost:3000/api/annotations || true
            fi
