name: Publish EE Artifacts

on:
  workflow_dispatch:
    inputs:
      push:
        description: Push images and the Helm chart to GHCR
        required: false
        default: false
        type: boolean
      chart_version:
        description: Helm chart version for manual publishes, for example 0.17.1
        required: false
        default: ""
        type: string
  push:
    tags:
      - "v*"
  pull_request:
    paths:
      - ".github/workflows/publish-ee-images.yml"
      - "packaging/docker/Dockerfile.den"
      - "packaging/docker/Dockerfile.den-web"
      - "packaging/docker/Dockerfile.inference"
      - "packaging/helm/openwork-ee/**"
      - "ee/apps/den-api/**"
      - "ee/apps/den-web/**"
      - "ee/apps/inference/**"
      - "ee/packages/**"
      - "packages/email/**"
      - "packages/types/**"
      - "packages/ui/**"
      - "pnpm-lock.yaml"
      - "pnpm-workspace.yaml"
      - "package.json"

permissions:
  contents: read
  packages: write

env:
  REGISTRY: ghcr.io

jobs:
  artifact-version:
    name: Resolve artifact version
    runs-on: ubuntu-latest
    outputs:
      chart_version: ${{ steps.version.outputs.chart_version }}
      publish: ${{ steps.version.outputs.publish }}
    steps:
      - name: Resolve version
        id: version
        shell: bash
        env:
          INPUT_CHART_VERSION: ${{ inputs.chart_version }}
          INPUT_PUSH: ${{ inputs.push }}
        run: |
          publish="false"
          if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
            publish="true"
          elif [ "$INPUT_PUSH" = "true" ]; then
            publish="true"
          fi

          chart_version="$INPUT_CHART_VERSION"
          if [[ "${GITHUB_REF_TYPE}" = "tag" && "${GITHUB_REF_NAME}" == v* ]]; then
            chart_version="${GITHUB_REF_NAME#v}"
          fi

          if [ "$publish" = "true" ] && [ -z "$chart_version" ]; then
            echo "chart_version is required when manually publishing from a non-tag ref" >&2
            exit 1
          fi

          echo "chart_version=$chart_version" >> "$GITHUB_OUTPUT"
          echo "publish=$publish" >> "$GITHUB_OUTPUT"

  helm-chart:
    name: Validate Helm chart
    runs-on: ubuntu-latest
    needs: artifact-version
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up Helm
        uses: azure/setup-helm@v4

      - name: Lint chart
        run: helm lint packaging/helm/openwork-ee

      - name: Render chart
        run: helm template openwork-ee packaging/helm/openwork-ee --set ingress.enabled=true --set inference.enabled=true

      - name: Run chart tests
        run: |
          for test in packaging/helm/openwork-ee/tests/*.sh; do
            bash "$test"
          done

      - name: Package chart
        if: ${{ needs.artifact-version.outputs.chart_version != '' }}
        run: |
          helm package packaging/helm/openwork-ee \
            --version "${{ needs.artifact-version.outputs.chart_version }}" \
            --app-version "${{ needs.artifact-version.outputs.chart_version }}" \
            --destination dist/charts

      - name: Log in to GHCR for Helm
        if: ${{ needs.artifact-version.outputs.publish == 'true' }}
        run: |
          printf '%s' "${{ secrets.GITHUB_TOKEN }}" | helm registry login "${{ env.REGISTRY }}" \
            --username "${{ github.actor }}" \
            --password-stdin

      - name: Publish chart
        if: ${{ needs.artifact-version.outputs.publish == 'true' }}
        run: helm push "dist/charts/openwork-ee-${{ needs.artifact-version.outputs.chart_version }}.tgz" "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts"

  ee-images:
    name: Build ${{ matrix.image }}
    runs-on: ubuntu-latest
    needs: artifact-version
    strategy:
      fail-fast: false
      matrix:
        include:
          - image: openwork-den-api
            dockerfile: packaging/docker/Dockerfile.den
            port: 8788
            health_path: /health
            smoke_env: >-
              -e DB_MODE=mysql
              -e DATABASE_URL=mysql://smoke:smoke@127.0.0.1:3306/smoke
              -e DEN_DB_ENCRYPTION_KEY=ci-den-db-encryption-key-value-123456
              -e BETTER_AUTH_SECRET=ci-better-auth-secret-value-123456
              -e BETTER_AUTH_URL=http://127.0.0.1:8788
          - image: openwork-den-web
            dockerfile: packaging/docker/Dockerfile.den-web
            port: 3005
            health_path: /api/health
            smoke_env: ""
          - image: openwork-inference
            dockerfile: packaging/docker/Dockerfile.inference
            port: 8791
            health_path: /health
            smoke_env: >-
              -e DB_MODE=mysql
              -e DATABASE_URL=mysql://smoke:smoke@127.0.0.1:3306/smoke
              -e DEN_DB_ENCRYPTION_KEY=ci-den-db-encryption-key-value-123456
          # The gateway's own liveness lives at /__gw/health because /health is
          # proxied through to the user's instance.
          - image: openwork-den-gateway
            dockerfile: packaging/docker/Dockerfile.den-gateway
            port: 8788
            health_path: /__gw/health
            smoke_env: >-
              -e DEN_API_BASE=http://127.0.0.1:9
              -e DEN_GATEWAY_KEY=ci-den-gateway-key-value-123456

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        if: ${{ github.event_name == 'push' || inputs.push == true }}
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.image }}
          tags: |
            type=ref,event=tag
            type=semver,pattern={{version}}
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
          labels: |
            org.opencontainers.image.title=${{ matrix.image }}
            org.opencontainers.image.source=https://github.com/${{ github.repository }}

      - name: Resolve Sentry source-map upload
        id: sentry_sourcemaps
        if: ${{ needs.artifact-version.outputs.publish == 'true' && github.event_name != 'pull_request' }}
        shell: bash
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
          SENTRY_RELEASE: ${{ secrets.SENTRY_RELEASE }}
        run: |
          set -euo pipefail

          has_value() {
            [[ "$1" =~ [^[:space:]] ]]
          }

          den_api_upload=false
          den_web_upload=false

          if has_value "$SENTRY_AUTH_TOKEN" && has_value "$SENTRY_ORG" && has_value "$SENTRY_PROJECT" && has_value "$SENTRY_RELEASE"; then
            den_api_upload=true
          else
            echo "Den API Sentry source-map upload disabled: configure SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, and SENTRY_RELEASE to enable it."
          fi

          if has_value "$SENTRY_AUTH_TOKEN" && has_value "$SENTRY_ORG" && has_value "$SENTRY_PROJECT"; then
            den_web_upload=true
          else
            echo "Den Web Sentry source-map upload disabled: configure SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT to enable it."
          fi

          echo "den_api_upload=$den_api_upload" >> "$GITHUB_OUTPUT"
          echo "den_web_upload=$den_web_upload" >> "$GITHUB_OUTPUT"

      - name: Build and optionally push without Sentry source-map upload
        # Build every image except the ones whose Sentry-enabled step below owns
        # them. Stated as an exclusion so a newly added matrix entry builds by
        # default instead of silently skipping and failing the smoke step.
        if: ${{ !((matrix.image == 'openwork-den-api' && steps.sentry_sourcemaps.outputs.den_api_upload == 'true') || (matrix.image == 'openwork-den-web' && steps.sentry_sourcemaps.outputs.den_web_upload == 'true')) }}
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ${{ matrix.dockerfile }}
          platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
          load: ${{ github.event_name == 'pull_request' }}
          push: ${{ needs.artifact-version.outputs.publish == 'true' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha,scope=${{ matrix.image }}
          cache-to: type=gha,mode=max,scope=${{ matrix.image }}
          build-args: |
            ${{ matrix.image == 'openwork-den-api' && format('DEN_API_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
            ${{ matrix.image == 'openwork-den-api' && 'DEN_UPLOAD_SENTRY_SOURCEMAPS=0' || '' }}
            ${{ matrix.image == 'openwork-den-web' && 'DEN_WEB_UPLOAD_SENTRY_SOURCEMAPS=0' || '' }}

      - name: Build and push with Sentry source-map upload
        if: ${{ (matrix.image == 'openwork-den-api' && steps.sentry_sourcemaps.outputs.den_api_upload == 'true') || (matrix.image == 'openwork-den-web' && steps.sentry_sourcemaps.outputs.den_web_upload == 'true') }}
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ${{ matrix.dockerfile }}
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha,scope=${{ matrix.image }}
          cache-to: type=gha,mode=max,scope=${{ matrix.image }}
          build-args: |
            ${{ matrix.image == 'openwork-den-api' && format('DEN_API_VERSION={0}', needs.artifact-version.outputs.chart_version || github.sha) || '' }}
            ${{ matrix.image == 'openwork-den-api' && 'DEN_UPLOAD_SENTRY_SOURCEMAPS=1' || '' }}
            ${{ matrix.image == 'openwork-den-web' && 'DEN_WEB_UPLOAD_SENTRY_SOURCEMAPS=1' || '' }}
            ${{ matrix.image == 'openwork-den-api' && format('DEN_SENTRY_UPLOAD_BUILD_ID={0}-{1}', github.run_id, github.run_attempt) || '' }}
            ${{ matrix.image == 'openwork-den-web' && format('DEN_WEB_SENTRY_UPLOAD_BUILD_ID={0}-{1}', github.run_id, github.run_attempt) || '' }}
          secrets: |
            sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
            sentry_org=${{ secrets.SENTRY_ORG }}
            sentry_project=${{ secrets.SENTRY_PROJECT }}
            sentry_url=${{ secrets.SENTRY_URL }}
            sentry_release=${{ secrets.SENTRY_RELEASE }}
            sentry_dist=${{ secrets.SENTRY_DIST }}

      - name: Assert Den DB migration assets
        if: ${{ github.event_name == 'pull_request' && matrix.image == 'openwork-den-api' }}
        shell: bash
        env:
          IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
        run: |
          set -euo pipefail
          image_tag="${IMAGE_TAGS%%$'\n'*}"
          docker run --rm --entrypoint sh "$image_tag" -lc '
            test -s /app/ee/packages/den-db/dist/scripts/bootstrap.js
            test -s /app/ee/packages/den-db/dist/current-schema.sql
            test -s /app/ee/packages/den-db/dist/drizzle/meta/_journal.json
          '

      - name: Smoke health endpoint
        if: ${{ github.event_name == 'pull_request' }}
        shell: bash
        env:
          IMAGE_TAGS: ${{ steps.meta.outputs.tags }}
        run: |
          image_tag="$(printf '%s\n' "$IMAGE_TAGS" | head -n 1)"
          container_id="$(docker run -d --rm -P ${{ matrix.smoke_env }} "$image_tag")"
          cleanup() {
            docker stop "$container_id" >/dev/null || true
          }
          trap cleanup EXIT

          host_port=""
          for attempt in {1..10}; do
            host_port="$(docker port "$container_id" "${{ matrix.port }}/tcp" 2>/dev/null | awk -F: 'NR == 1 { print $NF }')"
            if [ -n "$host_port" ]; then
              break
            fi
            sleep 1
          done
          if [ -z "$host_port" ]; then
            docker logs "$container_id" || true
            exit 1
          fi

          url="http://127.0.0.1:${host_port}${{ matrix.health_path }}"

          for attempt in {1..30}; do
            if node -e "fetch(process.argv[1]).then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))" "$url"; then
              exit 0
            fi
            sleep 2
          done

          docker logs "$container_id" || true
          exit 1
