name: publish-canvas-image # Builds and pushes the canvas Docker image to GHCR 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/**' - '.github/workflows/publish-canvas-image.yml' # Manual trigger: use after a non-canvas merge that still needs a fresh # image (e.g. a Dockerfile change lives outside the canvas/ tree). workflow_dispatch: inputs: platform_url: description: 'NEXT_PUBLIC_PLATFORM_URL baked into the bundle (default: http://localhost:8080)' required: false default: '' ws_url: description: 'NEXT_PUBLIC_WS_URL baked into the bundle (default: ws://localhost:8080/ws)' required: false default: '' permissions: contents: read packages: write # required to push to ghcr.io/${{ github.repository_owner }}/* env: IMAGE_NAME: ghcr.io/molecule-ai/canvas jobs: build-and-push: name: Build & push canvas image runs-on: [self-hosted, macos, arm64] steps: - name: Checkout uses: actions/checkout@v4 - name: Configure GHCR auth (write auths map; do NOT call docker login) # `docker login` on macOS unconditionally writes credentials to the # osxkeychain credential helper, even when DOCKER_CONFIG/config.json # declares `credsStore: ""` and even when invoked with `--config`. # Verified locally 2026-04-16 — after a successful login, Docker # rewrites the same config file to: # { "auths": { "ghcr.io": {} }, "credsStore": "osxkeychain" } # i.e. the auth lives in the Keychain, not the config file. The # Mac mini runner is a launchd user agent with a locked Keychain, # so storage fails with `User interaction is not allowed (-25308)`. # # Six prior PRs (#273, #319, #322, #341, #484, #486) all kept calling # `docker login` and tried to coerce credsStore — none worked. # The only reliable fix is to skip `docker login` entirely and write # the auth string directly. `docker/build-push-action@v6` and the # daemon honor the `auths` map for push without needing login. shell: bash env: GHCR_USER: ${{ github.actor }} GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu mkdir -p "${RUNNER_TEMP}/docker-config" AUTH=$(printf '%s:%s' "${GHCR_USER}" "${GHCR_TOKEN}" | base64) umask 077 cat > "${RUNNER_TEMP}/docker-config/config.json" <> "${GITHUB_ENV}" # Diagnostics that don't leak the token. echo "=== docker ===" command -v docker || echo "(docker not in PATH)" docker --version 2>&1 || true ls -la /usr/local/bin/docker /opt/homebrew/bin/docker 2>&1 || true echo "=== auths registries (no values) ===" grep -o '"[a-zA-Z0-9.-]*\.io"' "${RUNNER_TEMP}/docker-config/config.json" || true - name: Set up QEMU # Apple-silicon runner building linux/amd64 images for x86 hosts. uses: docker/setup-qemu-action@v4 with: platforms: linux/amd64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - 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 GHCR uses: docker/build-push-action@v6 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 }} 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 canvas (Next.js 15 + React Flow)