docs: README refresh — 'controllable map for AI agents' lead + full-width video banner #18
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Publish Container Image | |
| # Publishes ghcr.io/esipfed/mc2 from main (and version tags) as a MULTI-ARCH | |
| # image (linux/amd64 + linux/arm64), so `docker pull` Just Works on Intel and | |
| # Apple-Silicon machines alike — no `platform:` override needed in compose. | |
| # | |
| # Shape: a matrix job builds each architecture NATIVELY (ubuntu-latest for | |
| # amd64, ubuntu-24.04-arm for arm64 — no QEMU emulation), runs the MCP | |
| # conformance gates INSIDE that arch's image, then pushes it by digest. | |
| # A small merge job stitches the per-arch digests into one multi-arch | |
| # manifest and applies the human-readable tags. PRs stop at the gates | |
| # (nothing is pushed). | |
| # | |
| # Auth: the workflow-scoped GITHUB_TOKEN with `packages: write` — no PAT. | |
| # The first successful push auto-creates the GHCR package, links it to this | |
| # repository, and grants this repo's Actions write access. New GHCR packages | |
| # default to PRIVATE; a maintainer flips visibility to public once in the | |
| # package settings (Package settings → Danger Zone → Change visibility). | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| env: | |
| IMAGE_NAME: ghcr.io/esipfed/mc2 | |
| jobs: | |
| # ---------- Per-arch: build natively, gate, push by digest ---------- # | |
| build-and-gate: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout source | |
| uses: actions/checkout@v4 | |
| # Sanitized platform string (linux/amd64 -> linux-amd64) for cache | |
| # scopes and artifact names, which don't allow slashes. | |
| - name: Prepare platform slug | |
| run: | | |
| platform="${{ matrix.platform }}" | |
| echo "PLATFORM_SLUG=${platform//\//-}" >> "$GITHUB_ENV" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # OCI labels only here — the human-readable tags are applied by the | |
| # merge job on the finished multi-arch manifest. | |
| - name: Compute image metadata (labels) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| # Build first and `load` into the runner's daemon so the conformance | |
| # gates below run against the EXACT artifact we're about to ship | |
| # (same layers, same digest — the digest push below is a pure cache | |
| # hit, not a rebuild). | |
| - name: Build image (load for the conformance gates) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| push: false | |
| load: true | |
| platforms: ${{ matrix.platform }} | |
| tags: ${{ env.IMAGE_NAME }}:gate | |
| cache-from: type=gha,scope=${{ env.PLATFORM_SLUG }} | |
| cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_SLUG }} | |
| # ---------- MCP conformance + auth gates ---------- # | |
| # Run the acceptance suites INSIDE the freshly built image — against the | |
| # deployable artifact with the real runtime env (GDAL, mcp SDK, pyjwt), | |
| # once per architecture, on native hardware. Each suite runs in its own | |
| # container so the Streamable-HTTP session manager's once-per-process | |
| # run() never collides. A failure here fails the job and BLOCKS the | |
| # push step below. | |
| - name: MCP conformance gate (tools/protocol/errors/root-path) | |
| run: | | |
| timeout 120 docker run --rm --workdir /app/server \ | |
| ${{ env.IMAGE_NAME }}:gate \ | |
| python tests/test_mcp.py | |
| - name: MCP authorization gate (§5b Resource Server) | |
| run: | | |
| timeout 120 docker run --rm --workdir /app/server \ | |
| ${{ env.IMAGE_NAME }}:gate \ | |
| python tests/test_mcp_auth.py | |
| - name: MCP standalone auth portal gate | |
| run: | | |
| timeout 120 docker run --rm --workdir /app/server \ | |
| ${{ env.IMAGE_NAME }}:gate \ | |
| python tests/test_portal.py | |
| # ---------- Push by digest (main + tags only; PRs stop above) ---------- # | |
| # push-by-digest uploads the arch image WITHOUT a tag; the merge job | |
| # assembles the digests into the tagged multi-arch manifest. | |
| - name: Push image by digest | |
| id: push | |
| if: github.event_name != 'pull_request' | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| cache-from: type=gha,scope=${{ env.PLATFORM_SLUG }} | |
| - name: Export digest | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| mkdir -p "${{ runner.temp }}/digests" | |
| digest="${{ steps.push.outputs.digest }}" | |
| touch "${{ runner.temp }}/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: digests-${{ env.PLATFORM_SLUG }} | |
| path: ${{ runner.temp }}/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # ---------- Merge per-arch digests into one multi-arch manifest ---------- # | |
| merge-manifest: | |
| if: github.event_name != 'pull_request' | |
| needs: build-and-gate | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Download digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: ${{ runner.temp }}/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - 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 }} | |
| # Tag scheme: latest on main, vX.Y.Z + X.Y on version tags, | |
| # sha-<short> always (immutable pin for deployments). | |
| - name: Compute image metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha,prefix=sha- | |
| - name: Create multi-arch manifest and push tags | |
| working-directory: ${{ runner.temp }}/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect + summary | |
| run: | | |
| docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" | |
| { | |
| echo "### Published :package: (multi-arch: linux/amd64 + linux/arm64)" | |
| echo "" | |
| echo '```' | |
| echo "${{ steps.meta.outputs.tags }}" | |
| echo '```' | |
| echo "" | |
| echo "Pull with: \`docker pull ${{ env.IMAGE_NAME }}:latest\`" | |
| } >> "$GITHUB_STEP_SUMMARY" |