Repository navigation
Run the worker in a subprocess so remote bugs cost seconds, not an hour #116
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: Docker | |
| # The image is only useful if it is known-good, and "it built" is a weak | |
| # signal: the two bugs this pipeline caught during bring-up (a venv whose | |
| # console-script shebang pointed at a builder-only path, and an editable | |
| # install whose .pth dangled after the stage copy) both produced an image that | |
| # built cleanly and died on first exec. So CI builds *and* drives it over real | |
| # MCP stdio before publishing anything. | |
| on: | |
| push: | |
| branches: [ main ] | |
| tags: [ 'v*' ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: docker-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| build-and-verify: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| # Needed only to push; the PR path never reaches the push step. | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| with: | |
| # The action caches its own buildx binary in the Actions cache by | |
| # default. On a workflow that also publishes release images that is a | |
| # supply-chain path -- a poisoned cache entry could substitute the | |
| # builder itself. Downloading it fresh costs seconds. | |
| cache-binary: false | |
| - name: Build image (load into the local daemon) | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | |
| with: | |
| context: . | |
| # Single-arch here so the image lands in the daemon and can actually | |
| # be run. Multi-arch cannot be --load'ed; that build is a separate | |
| # job below, gated on this one passing. | |
| load: true | |
| tags: uxarray-mcp:ci | |
| # Read the shared cache but never write it. A job that both consumes | |
| # a mutable cache and produces artifacts others trust is the | |
| # cache-poisoning shape zizmor flags: a compromised PR run could seed | |
| # a layer that a later release build silently reuses. Writes happen | |
| # only from the warm-cache job below, which runs on main and | |
| # publishes nothing. | |
| cache-from: type=gha | |
| - name: Report image size | |
| run: | | |
| docker images uxarray-mcp:ci --format 'image size: {{.Size}}' | |
| - name: Verify fixtures match the manifest | |
| run: | | |
| docker run --rm -i --entrypoint python uxarray-mcp:ci - --verify \ | |
| < scripts/generate_container_fixtures.py | |
| - name: Assert the HPC stack is absent | |
| # The image is local-only by design. If a dependency edit ever pulls | |
| # globus-compute-sdk back in, a sealed container silently regains the | |
| # ability to submit remote work -- fail loudly instead. | |
| run: | | |
| docker run --rm --entrypoint python uxarray-mcp:ci -c " | |
| import importlib.util as u, sys | |
| leaked = [m for m in ('globus_compute_sdk', 'academy') if u.find_spec(m)] | |
| if leaked: | |
| sys.exit(f'HPC dependencies leaked into the image: {leaked}') | |
| print('confirmed local-only: no globus_compute_sdk, no academy') | |
| " | |
| - name: Assert the server does not run as root | |
| run: | | |
| uid=$(docker run --rm --entrypoint id uxarray-mcp:ci -u) | |
| test "$uid" != "0" || { echo "image runs as root"; exit 1; } | |
| echo "runs as uid $uid" | |
| - name: Smoke test over MCP stdio | |
| # The real check: handshake, tool surface, and one numerical result | |
| # compared against an analytic ground truth (total area == 4*pi). | |
| run: python3 scripts/container_smoke_test.py --image uxarray-mcp:ci | |
| - name: Verify the HTTP transport serves MCP | |
| run: | | |
| docker run -d --rm --name uxarray-http -p 8001:8001 uxarray-mcp:ci \ | |
| serve --transport http --host 0.0.0.0 | |
| for i in $(seq 1 30); do | |
| if curl -sf -o /dev/null \ | |
| -X POST http://localhost:8001/mcp \ | |
| -H 'Content-Type: application/json' \ | |
| -H 'Accept: application/json, text/event-stream' \ | |
| -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' | |
| then | |
| echo "HTTP transport responded after ${i}s" | |
| docker stop uxarray-http | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "HTTP transport never became ready" | |
| docker logs uxarray-http | |
| docker stop uxarray-http | |
| exit 1 | |
| publish: | |
| # Only after every check above passes, and never from a pull request -- | |
| # a fork PR must not be able to publish to the org's registry. | |
| needs: build-and-verify | |
| if: github.event_name == 'push' && github.repository == 'UXARRAY/uxarray-mcp-server' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 | |
| - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| with: | |
| cache-binary: false | |
| - name: Log in to ghcr.io | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Derive tags | |
| id: meta | |
| uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=ref,event=branch | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha | |
| - name: Build and push (multi-arch) | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | |
| with: | |
| context: . | |
| # arm64 matters: healpix has no wheel there and builds from source, | |
| # which is the platform most likely to break first. | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # No cache on the publishing build, by design. Released images are | |
| # built from source every time so a poisoned or merely stale cache | |
| # entry can never end up inside something users pull. The build is | |
| # a few minutes; a tampered release is forever. | |
| no-cache: true | |
| provenance: true | |
| sbom: true | |
| warm-cache: | |
| # Writes the layer cache that build-and-verify reads. Split out so that the | |
| # only job with cache-write access runs on main, publishes nothing, and is | |
| # never triggered by a pull request from a fork. | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| with: | |
| cache-binary: false | |
| - name: Populate the build cache | |
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | |
| with: | |
| context: . | |
| push: false | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |