name: publish-platform-image

# Builds and pushes the tenant-platform Docker image to GHCR whenever a
# commit lands on main. The private molecule-controlplane provisioner sets
# TENANT_IMAGE=ghcr.io/molecule-ai/platform:<tag> to spawn tenant Fly
# Machines from this image. See molecule-controlplane README for the pairing.

on:
  push:
    branches: [main]
    paths:
      # Only rebuild when something platform-relevant changes — saves GHA
      # minutes on docs-only / canvas-only / MCP-only PRs.
      - 'platform/**'
      - '.github/workflows/publish-platform-image.yml'
  # Manual trigger for re-publishing a tag after a non-platform merge.
  workflow_dispatch:

permissions:
  contents: read
  packages: write   # required to push to ghcr.io/${{ github.repository_owner }}/*

env:
  # GHCR accepts mixed-case, but most tooling lowercases — keep us consistent.
  IMAGE_NAME: ghcr.io/molecule-ai/platform
  # Fly registry mirror — tenant machines provisioned by the private
  # `molecule-controlplane` pull from here (private GHCR image can't be
  # pulled by Fly machines without auth plumbing we don't want to add).
  # Fly auto-authenticates same-org machines against registry.fly.io, so
  # mirroring keeps GHCR private while tenants still boot.
  FLY_IMAGE_NAME: registry.fly.io/molecule-tenant

jobs:
  build-and-push:
    runs-on: [self-hosted, macos, arm64]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU
        # Required on the Apple-silicon self-hosted runner — Fly tenant machines
        # pull linux/amd64, and buildx needs binfmt handlers in Docker Desktop's
        # VM to emulate amd64 during the build.
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/amd64

      - name: Set up Docker Buildx
        # Buildx enables cache-from/cache-to via GHA cache and multi-arch
        # builds without local docker daemon wrangling.
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Log in to Fly registry
        # username MUST be literal "x". Fly's registry returns 401 for any
        # other value (verified locally 2026-04-15 — "molecule-ai" fails,
        # "x" succeeds with the same token). The password is the FLY_API_TOKEN.
        # Rotation: see docs/runbooks/saas-secrets.md — FLY_API_TOKEN lives in
        # two places (GitHub Actions secret here + `fly secrets` on molecule-cp)
        # and MUST be updated in both on rotation.
        uses: docker/login-action@v3
        with:
          registry: registry.fly.io
          username: x
          password: ${{ secrets.FLY_API_TOKEN }}

      - name: Compute tags
        id: tags
        # Emit two tags per build: `latest` (floating, always the main tip)
        # and the short commit SHA (immutable, pin-friendly). Control plane
        # can deploy `latest` today and pin to :sha in Phase H hardening.
        run: |
          echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

      - name: Build & push to GHCR
        # Split from the Fly mirror so a registry.fly.io outage doesn't block
        # GHCR (or vice versa) — each registry's failure mode is isolated.
        # GHA cache is shared because both steps re-use the same Dockerfile
        # context + build args.
        # Explicit linux/amd64 target: the runner is Apple-silicon (arm64),
        # but Fly tenant machines are amd64. QEMU handles the emulation.
        uses: docker/build-push-action@v5
        with:
          context: ./platform
          file: ./platform/Dockerfile
          platforms: linux/amd64
          push: true
          tags: |
            ${{ env.IMAGE_NAME }}:latest
            ${{ env.IMAGE_NAME }}:sha-${{ steps.tags.outputs.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          labels: |
            org.opencontainers.image.source=https://github.com/${{ github.repository }}
            org.opencontainers.image.revision=${{ github.sha }}
            org.opencontainers.image.description=Molecule AI tenant platform (one instance per org)

      - name: Build & push to Fly registry
        # Continues even if GHCR push failed — `if: always()` ensures the
        # private control plane's tenant-image mirror lands regardless of
        # any GHCR-side flakiness.
        if: always()
        uses: docker/build-push-action@v5
        with:
          context: ./platform
          file: ./platform/Dockerfile
          platforms: linux/amd64
          push: true
          tags: |
            ${{ env.FLY_IMAGE_NAME }}:latest
            ${{ env.FLY_IMAGE_NAME }}:sha-${{ steps.tags.outputs.sha }}
          cache-from: type=gha
          labels: |
            org.opencontainers.image.source=https://github.com/${{ github.repository }}
            org.opencontainers.image.revision=${{ github.sha }}
            org.opencontainers.image.description=Molecule AI tenant platform (one instance per org)
