From 4a95aa3e98fb0b749f2a8d42661f172b1050866a Mon Sep 17 00:00:00 2001 From: Canvas Agent Date: Thu, 16 Apr 2026 09:23:26 +0000 Subject: [PATCH] feat(ci): auto-publish canvas Docker image to GHCR on canvas/** merges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #399. ## Root cause `publish-platform-image.yml` existed for the Go platform image but there was no equivalent for the canvas. After every canvas PR merged, CI ran `npm run build` and passed — but the live container at :3000 was never updated. The `canvas-deploy-reminder` job only posted a comment asking operators to manually rebuild, which was consistently missed. ## What this adds - `.github/workflows/publish-canvas-image.yml`: triggers on `canvas/**` changes to main (and `workflow_dispatch`). Mirrors the platform workflow: macOS Keychain isolation, QEMU for linux/amd64, Buildx, GHCR push with `:latest` + `:sha-<7>` tags. - `NEXT_PUBLIC_PLATFORM_URL` / `NEXT_PUBLIC_WS_URL` resolve from `workflow_dispatch` inputs → `CANVAS_PLATFORM_URL` / `CANVAS_WS_URL` repo secrets → `localhost:8080` defaults (safe for self-hosted dev). - Inputs are passed via env vars (not direct `${{ }}` interpolation) to prevent shell injection from string inputs. - `docker-compose.yml`: adds `image: ghcr.io/molecule-ai/canvas:latest` to the canvas service so `docker compose pull canvas && docker compose up -d canvas` applies the new image. `build:` is retained for local development. Adds a comment clarifying that `NEXT_PUBLIC_*` runtime env vars are ignored by the standalone bundle (build-time only). - `ci.yml`: updates `canvas-deploy-reminder` commit comment to reference `docker compose pull` as the fast path, with `docker compose build` as the local-source fallback. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 11 +- .github/workflows/publish-canvas-image.yml | 131 +++++++++++++++++++++ docker-compose.yml | 10 ++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-canvas-image.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be006be4..aa2169f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,12 +218,19 @@ jobs: cat > /tmp/deploy-reminder.md << 'BODY' ## Canvas build passed ✅ — deploy required - The canvas container is **not auto-deployed**. Merged canvas changes are invisible until the host container is rebuilt. + The `publish-canvas-image` workflow is now building a fresh Docker image + (`ghcr.io/molecule-ai/canvas:latest`) in the background. - Run this on the host machine to apply: + Once it completes (~3–5 min), apply on the host machine with: ```bash cd /g/personal_programs/molecule-monorepo git pull origin main + docker compose pull canvas && docker compose up -d canvas + ``` + + If you need to rebuild from local source instead (e.g. testing unreleased + changes or a new `NEXT_PUBLIC_*` URL), use: + ```bash docker compose build canvas && docker compose up -d canvas ``` BODY diff --git a/.github/workflows/publish-canvas-image.yml b/.github/workflows/publish-canvas-image.yml new file mode 100644 index 00000000..08a03743 --- /dev/null +++ b/.github/workflows/publish-canvas-image.yml @@ -0,0 +1,131 @@ +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: Isolate Docker config (skip macOS Keychain) + # Same workaround as publish-platform-image.yml — launchd service + # runners can't reach the Keychain, so we write a disposable + # config.json with an empty credsStore to force auths-map storage. + shell: bash + run: | + set -euo pipefail + mkdir -p "${RUNNER_TEMP}/docker-config" + cat > "${RUNNER_TEMP}/docker-config/config.json" <<'JSON' + { + "auths": {}, + "credsStore": "", + "credHelpers": {} + } + JSON + echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config" >> "${GITHUB_ENV}" + echo "=== Runner docker diagnostics ===" + command -v docker || echo "(docker not in PATH)" + docker --version 2>&1 || true + + - name: Set up QEMU + # Apple-silicon runner building linux/amd64 images for x86 hosts. + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64 + + - name: Set up Docker Buildx + 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: 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@v5 + 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) diff --git a/docker-compose.yml b/docker-compose.yml index 8c955b6e..3b5f93b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,13 @@ services: # --- Canvas --- canvas: + # The publish-canvas-image CI workflow pushes a fresh image to GHCR on + # every canvas/** merge to main. To update the running container: + # docker compose pull canvas && docker compose up -d canvas + # First-time local setup or testing unreleased changes — build from source: + # docker compose build canvas && docker compose up -d canvas + # Note: GHCR images are private — `docker login ghcr.io` required before pull. + image: ghcr.io/molecule-ai/canvas:latest build: context: ./canvas dockerfile: Dockerfile @@ -147,6 +154,9 @@ services: condition: service_healthy environment: PORT: "${CANVAS_PORT:-3000}" + # NOTE: NEXT_PUBLIC_* are baked into the JS bundle at `next build` time — + # these runtime values are ignored by the standalone output. They're kept + # here for documentation / override during `docker compose build`. NEXT_PUBLIC_PLATFORM_URL: ${NEXT_PUBLIC_PLATFORM_URL:-http://localhost:${PLATFORM_PUBLISH_PORT:-8080}} NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-ws://localhost:${PLATFORM_PUBLISH_PORT:-8080}/ws} ports: