name: publish-canvas-image

# Ported from .github/workflows/publish-canvas-image.yml on 2026-05-11 per RFC
# internal#219 §1 sweep. Differences from the GitHub version:
#   - Dropped `workflow_dispatch.inputs` (Gitea 1.22.6 parser rejects them
#     per feedback_gitea_workflow_dispatch_inputs_unsupported).
#   - Dropped `merge_group:` (no Gitea merge queue).
#   - Dropped `environment:` blocks (Gitea has no environments).
#   - Workflow-level env.GITHUB_SERVER_URL pinned per
#     feedback_act_runner_github_server_url.
#   - `continue-on-error: true` on each job (RFC §1 contract).
#   - Retargeted the image push from GHCR to ECR. GHCR was retired during
#     the 2026-05-06 Gitea migration, and Gitea's GITHUB_TOKEN cannot
#     authenticate to ghcr.io.
#

# Builds and pushes the canvas Docker image to ECR whenever a commit lands
# on main that touches canvas code. Previously canvas changes were visible in
# CI (npm run build passed) but the live container was never updated —
# operators had to manually run `docker compose build canvas` each time.
#
# Mirror of publish-platform-image.yml, adapted for the Next.js canvas layer.
# See that workflow for inline notes on macOS Keychain isolation and QEMU.

on:
  push:
    branches: [main]
    paths:
      # Only rebuild when canvas source changes — saves GHA minutes on
      # platform-only / docs-only / MCP-only merges.
      - 'canvas/**'
      - '.gitea/workflows/publish-canvas-image.yml'
  # NOTE (Gitea port): the original GitHub workflow had a
  # `workflow_dispatch:` manual trigger for the
  # non-canvas-merge-but-need-fresh-image scenario. Dropped in the
  # Gitea port (1.22.6 parser-finicky). Manual rebuilds require
  # pushing an empty commit to canvas/ or running the operator-host
  # build directly.

permissions:
  contents: read
  packages: write

env:
  # SSOT-Instance-10 (#333): ECR registry triplet (account.dkr.ecr.region.amazonaws.com)
  # sourced from org/repo var `ECR_REGISTRY` with the current prod-account literal as
  # bootstrap fallback. When the org var is set, the fallback becomes dead code and
  # switching accounts/regions is a one-line change at the org level (instead of
  # touching every workflow). Pattern mirrors `vars.CP_URL || 'literal'` already in
  # use below in this repo's staging-verify.yml.
  IMAGE_NAME: ${{ vars.ECR_REGISTRY || '153263036946.dkr.ecr.us-east-2.amazonaws.com' }}/molecule-ai/canvas
  GITHUB_SERVER_URL: https://git.moleculesai.app

jobs:
  # bp-exempt: post-merge image publication side effect; CI / all-required gates source changes.
  build-and-push:
    name: Build & push canvas image
    # Dedicated publish/release lane (internal#462 / #394 / #399). Ship
    # path (on: push:main, canvas/**) — reserved capacity so a merged
    # canvas fix's image build never FIFO-queues behind PR required-CI.
    # The `publish` label resolves ONLY to the molecule-runner-publish-*
    # sub-pool (config.publish.yaml). HARD DEPENDENCY: this MUST land
    # AFTER the publish-lane runners are registered/advertising `publish`
    # — the earlier #599 `docker` label attempt queued indefinitely with
    # zero eligible runners precisely because the label was targeted
    # before any runner advertised it (see #576). The lane is registered
    # in this rollout (internal#462) so the precondition holds.
    runs-on: publish
    # Phase 3 (RFC #219 §1): surface broken workflows without blocking.
    # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
    continue-on-error: true
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      # Keep Docker auth/buildx state inside the job temp dir. Publish
      # runners can inherit a HOME/DOCKER_CONFIG path that is host-owned
      # and not writable from the job container; docker login otherwise
      # fails before the image build starts.
      - name: Prepare writable Docker config
        run: |
          set -euo pipefail
          export DOCKER_CONFIG="$RUNNER_TEMP/docker-config"
          mkdir -p "$DOCKER_CONFIG/buildx/certs"
          echo "DOCKER_CONFIG=$DOCKER_CONFIG" >> "$GITHUB_ENV"

      - name: Log in to ECR
        env:
          IMAGE_NAME: ${{ env.IMAGE_NAME }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: us-east-2
        run: |
          set -euo pipefail
          ECR_REGISTRY="${IMAGE_NAME%%/*}"
          aws ecr get-login-password --region us-east-2 | \
            docker login --username AWS --password-stdin "${ECR_REGISTRY}"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

      - name: Ensure ECR repository exists
        env:
          IMAGE_NAME: ${{ env.IMAGE_NAME }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: us-east-2
        run: |
          set -euo pipefail
          repo_path="${IMAGE_NAME#*/}"
          if ! aws ecr describe-repositories --repository-names "${repo_path}" --region us-east-2 >/dev/null 2>&1; then
            aws ecr create-repository \
              --repository-name "${repo_path}" \
              --image-scanning-configuration scanOnPush=true \
              --region us-east-2 >/dev/null
          fi

      # Health check: verify Docker daemon is accessible before attempting any
      # build steps. This fails loudly at step 1 when the runner's docker.sock
      # is inaccessible rather than silently continuing to the build step
      # where docker build fails deep in ECR auth with a cryptic error.
      - name: Verify Docker daemon access
        run: |
          set -euo pipefail
          echo "::group::Docker daemon health check"
          echo "Runner: ${HOSTNAME:-unknown}"
          docker_info="$(docker info 2>&1)" || {
            echo "::error::Docker daemon is not accessible at /var/run/docker.sock"
            echo "::error::Runner: ${HOSTNAME:-unknown}"
            printf '%s\n' "${docker_info}"
            echo "::error::Check: (1) daemon running, (2) runner user in docker group, (3) sock perms 660+"
            exit 1
          }
          printf '%s\n' "${docker_info}" | sed -n '1,5p'
          echo "Docker daemon OK"
          echo "::endgroup::"

      - name: Compute tags
        id: tags
        shell: bash
        run: |
          echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

      - name: Resolve build args
        id: build_args
        # Priority: workflow_dispatch input > repo secret > hardcoded default.
        # NEXT_PUBLIC_* env vars are baked into the JS bundle at build time by
        # Next.js — they cannot be changed at runtime without a full rebuild.
        # For local docker-compose deployments the defaults (localhost:8080)
        # work as-is; production deployments should set CANVAS_PLATFORM_URL
        # and CANVAS_WS_URL as repository secrets.
        #
        # Inputs are passed via env vars (not direct ${{ }} interpolation) to
        # prevent shell injection from workflow_dispatch string inputs.
        shell: bash
        env:
          INPUT_PLATFORM_URL: ${{ github.event.inputs.platform_url }}
          SECRET_PLATFORM_URL: ${{ secrets.CANVAS_PLATFORM_URL }}
          INPUT_WS_URL: ${{ github.event.inputs.ws_url }}
          SECRET_WS_URL: ${{ secrets.CANVAS_WS_URL }}
        run: |
          PLATFORM_URL="${INPUT_PLATFORM_URL:-${SECRET_PLATFORM_URL:-http://localhost:8080}}"
          WS_URL="${INPUT_WS_URL:-${SECRET_WS_URL:-ws://localhost:8080/ws}}"

          echo "platform_url=${PLATFORM_URL}" >> "$GITHUB_OUTPUT"
          echo "ws_url=${WS_URL}" >> "$GITHUB_OUTPUT"

      - name: Build & push canvas image to ECR
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: ./canvas
          file: ./canvas/Dockerfile
          platforms: linux/amd64
          push: true
          build-args: |
            NEXT_PUBLIC_PLATFORM_URL=${{ steps.build_args.outputs.platform_url }}
            NEXT_PUBLIC_WS_URL=${{ steps.build_args.outputs.ws_url }}
          tags: |
            ${{ env.IMAGE_NAME }}:latest
            ${{ env.IMAGE_NAME }}:sha-${{ steps.tags.outputs.sha }}
          # Gitea artifact-cache reachability is best-effort on the operator
          # runner network. Do not let cache export fail an image that already
          # built and pushed successfully.
          labels: |
            org.opencontainers.image.source=https://git.moleculesai.app/${{ github.repository }}
            org.opencontainers.image.revision=${{ github.sha }}
            org.opencontainers.image.description=Molecule AI canvas (Next.js 15 + React Flow)
