fix(examples): cache npm install for mcp.proxy's "everything" service #10713
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 | |
| on: | |
| push: | |
| branches: [ develop, 'support/**', 'feature/**' ] | |
| pull_request: | |
| branches: [ develop, 'support/**', 'feature/**' ] | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| java: [ 25 ] | |
| steps: | |
| - name: Checkout GitHub sources | |
| uses: actions/checkout@v7 | |
| - name: Pin maven.packages.aklivity.io to IPv4 | |
| run: | | |
| ipv4=$(getent ahostsv4 maven.packages.aklivity.io | awk 'NR==1{print $1}') | |
| if [ -n "$ipv4" ]; then | |
| echo "$ipv4 maven.packages.aklivity.io" | sudo tee -a /etc/hosts | |
| fi | |
| - name: Setup JDK ${{ matrix.java }} | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: ${{ matrix.java }} | |
| - name: Cache Maven packages | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/.m2/repository | |
| !~/.m2/repository/io/aklivity/zilla | |
| key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} | |
| restore-keys: ${{ runner.os }}-m2 | |
| - name: Cache Maven wrapper distribution | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.m2/wrapper | |
| key: ${{ runner.os }}-m2-wrapper-${{ hashFiles('**/maven-wrapper.properties') }} | |
| restore-keys: ${{ runner.os }}-m2-wrapper- | |
| - name: Cache ZPM test artifacts | |
| uses: actions/cache@v6 | |
| with: | |
| path: manager/target/zpm/cache | |
| key: zpm-${{ runner.os }}-${{ hashFiles('manager/src/conf/install/zpm.json') }} | |
| restore-keys: zpm-${{ runner.os }}- | |
| - name: Route Docker Hub pulls through mirror.gcr.io | |
| run: | | |
| sudo mkdir -p /etc/docker | |
| if [ -f /etc/docker/daemon.json ]; then | |
| sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json > /tmp/daemon.json | |
| else | |
| echo '{"registry-mirrors": ["https://mirror.gcr.io"]}' > /tmp/daemon.json | |
| fi | |
| sudo mv /tmp/daemon.json /etc/docker/daemon.json | |
| sudo systemctl restart docker | |
| timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done' | |
| - name: Restore base image cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/docker-base-cache/base-images.tar | |
| key: docker-base-${{ runner.os }}-${{ github.run_id }} | |
| restore-keys: | | |
| docker-base-${{ runner.os }}- | |
| - name: Load cached base images | |
| run: | | |
| if [ -f ~/docker-base-cache/base-images.tar ]; then | |
| docker load -i ~/docker-base-cache/base-images.tar || true | |
| fi | |
| - name: Build with Maven | |
| run: ./mvnw -B -nsu -ntp -Ddocker.logStdout -Dfailsafe.skipAfterFailureCount=1 -Ddocker.verbose install jacoco:report-aggregate | |
| env: | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| - name: Save base images | |
| if: always() | |
| run: | | |
| mkdir -p ~/docker-base-cache | |
| imgs=$(docker images --format '{{.Repository}}:{{.Tag}}' \ | |
| | grep -Ev '<none>|ghcr.io/aklivity/zilla' \ | |
| | grep -E '^(eclipse-temurin|ubuntu|alpine|ghcr\.io/aklivity/tshark):') | |
| if [ -n "$imgs" ]; then | |
| docker save -o ~/docker-base-cache/base-images.tar $imgs | |
| fi | |
| - name: Save base image cache | |
| if: always() | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: ~/docker-base-cache/base-images.tar | |
| key: docker-base-${{ runner.os }}-${{ github.run_id }} | |
| - name: Save Docker image | |
| if: success() | |
| run: | | |
| mkdir -p ~/docker-cache | |
| docker save -o ~/docker-cache/docker.tar $(docker images --format "{{.Repository}}" | grep "zilla") | |
| - name: Upload Docker image | |
| uses: actions/upload-artifact@v7 | |
| if: success() | |
| with: | |
| name: zilla-image | |
| path: ~/docker-cache/docker.tar | |
| retention-days: 1 | |
| - name: Conditional Artifact Upload | |
| uses: actions/upload-artifact@v7 | |
| if: failure() | |
| with: | |
| name: zilla-build-${{ matrix.java }}-${{ github.event.number }} | |
| path: | | |
| **/hs_err_pid*.log | |
| **/target/surefire-reports/ | |
| **/target/zilla-itests/ | |
| **/target/failsafe-reports/ | |
| setup-examples: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| outputs: | |
| all_directories: ${{ steps.all-files.outputs.folders_no_base_path }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get examples dirs | |
| id: all-files | |
| uses: Drafteame/list-folders-action@main | |
| with: | |
| paths: | | |
| examples | |
| omit: | | |
| ^\.github$ | |
| ^\.assets$ | |
| ^\.git$ | |
| ^\.vscode$ | |
| ^tcp.reflect$ | |
| ^ws.proxy$ | |
| ^ws.reflect$ | |
| ^http.echo$ | |
| ^grpc.kafka.proxy$ | |
| cache-images: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Route Docker Hub pulls through mirror.gcr.io | |
| run: | | |
| sudo mkdir -p /etc/docker | |
| if [ -f /etc/docker/daemon.json ]; then | |
| sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json > /tmp/daemon.json | |
| else | |
| echo '{"registry-mirrors": ["https://mirror.gcr.io"]}' > /tmp/daemon.json | |
| fi | |
| sudo mv /tmp/daemon.json /etc/docker/daemon.json | |
| sudo systemctl restart docker | |
| timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done' | |
| - name: Restore shared example image cache | |
| id: cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/docker-images-cache/images.tar | |
| key: example-images-${{ runner.os }}-${{ hashFiles('examples/**/compose.yaml') }} | |
| restore-keys: | | |
| example-images-${{ runner.os }}- | |
| - name: Pull example images and refresh shared cache | |
| if: ${{ steps.cache.outputs.cache-hit != 'true' }} | |
| run: | | |
| mkdir -p ~/docker-images-cache | |
| if [ -f ~/docker-images-cache/images.tar ]; then | |
| docker load -i ~/docker-images-cache/images.tar || true | |
| fi | |
| images=$(grep -rhE '^[[:space:]]*image:' examples/*/compose.yaml \ | |
| | sed -E 's/^[[:space:]]*image:[[:space:]]*//' \ | |
| | tr -d "\"'" \ | |
| | sed -E 's/[[:space:]]*#.*$//; s/[[:space:]]+$//' \ | |
| | grep -v '\$' \ | |
| | grep -v 'ghcr.io/aklivity/zilla' \ | |
| | sort -u) | |
| echo "Ensuring the following example images are present:" | |
| echo "$images" | |
| for img in $images; do | |
| docker pull "$img" || echo "::warning::failed to pull $img" | |
| done | |
| present=$(for img in $images; do docker image inspect "$img" >/dev/null 2>&1 && echo "$img"; done) | |
| docker save -o ~/docker-images-cache/images.tar $present | |
| testing: | |
| strategy: | |
| matrix: | |
| dir: ${{ fromJson(needs.setup-examples.outputs.all_directories) }} | |
| fail-fast: false | |
| needs: | |
| - setup-examples | |
| - cache-images | |
| runs-on: ubuntu-latest | |
| env: | |
| ZILLA_VERSION: develop-SNAPSHOT | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: | | |
| examples/.github | |
| examples/${{ matrix.dir }} | |
| - name: Route Docker Hub pulls through mirror.gcr.io | |
| run: | | |
| sudo mkdir -p /etc/docker | |
| if [ -f /etc/docker/daemon.json ]; then | |
| sudo jq '. + {"registry-mirrors": ["https://mirror.gcr.io"]}' /etc/docker/daemon.json > /tmp/daemon.json | |
| else | |
| echo '{"registry-mirrors": ["https://mirror.gcr.io"]}' > /tmp/daemon.json | |
| fi | |
| sudo mv /tmp/daemon.json /etc/docker/daemon.json | |
| sudo systemctl restart docker | |
| timeout 30 sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done' | |
| - name: Download Docker image | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: zilla-image | |
| path: ~/docker-cache | |
| - name: Load Docker image | |
| run: | | |
| docker load -i ~/docker-cache/docker.tar | |
| - name: Restore shared example image cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: ~/docker-images-cache/images.tar | |
| key: example-images-${{ runner.os }}-${{ hashFiles('examples/**/compose.yaml') }} | |
| restore-keys: | | |
| example-images-${{ runner.os }}- | |
| - name: Load shared example images | |
| run: | | |
| if [ -f ~/docker-images-cache/images.tar ]; then | |
| docker load -i ~/docker-images-cache/images.tar || true | |
| fi | |
| # mcp.proxy's "everything" toolkit is installed via a live `npx -y` at | |
| # container start (no baked-in image), so the cold path -- npm resolving | |
| # and fetching 100+ transitive packages one by one -- takes anywhere | |
| # from ~2 to 5+ minutes depending on npm-registry conditions, observed | |
| # to occasionally exceed even the healthcheck's generous 10-minute | |
| # ceiling under a slow registry. Restoring npm's own cache directory | |
| # (mounted into the container -- see compose.yaml's `everything` | |
| # service) turns that into a few-seconds no-op fetch instead. Bump the | |
| # key's version suffix if the pinned @modelcontextprotocol/server-everything | |
| # version in that compose.yaml ever changes. | |
| - name: Restore npm cache for shared MCP "everything" server | |
| if: ${{ matrix.dir == 'mcp.proxy' }} | |
| uses: actions/cache@v6 | |
| with: | |
| path: examples/${{ matrix.dir }}/.cache/npm-everything | |
| key: npm-everything-2026.7.4-${{ runner.os }} | |
| - name: Start Zilla and wait for it to be healthy | |
| working-directory: examples/${{ matrix.dir }} | |
| run: docker compose up -d --wait | |
| - name: Execute Test | |
| working-directory: examples/${{ matrix.dir }} | |
| # Without a cap this step inherits GitHub's 6h job ceiling, and a test that | |
| # stalls holds a runner that whole time while serving no logs -- in-progress | |
| # jobs have none, so the failure is invisible until it is hours old. The cap | |
| # also converts a stall into failure(), which is what runs the docker log and | |
| # engine directory collection below; a hang collects nothing. Every example | |
| # except mcp.proxy finishes inside two minutes, and mcp.proxy's own bounded | |
| # worst case sits well under this, so 30m only ever fires on a genuine stall. | |
| timeout-minutes: 30 | |
| run: | | |
| set -o pipefail | |
| ./.github/test.sh | tee $GITHUB_STEP_SUMMARY | |
| - name: Collect docker logs on failure | |
| if: failure() | |
| uses: jwalton/gh-docker-logs@v2 | |
| with: | |
| dest: "./logs" | |
| # `zilla dump` reads a collected engine directory, so ship it with the logs. | |
| # Copied out rather than bind-mounted: a host mount over /var/run/zilla can | |
| # collide with the container user's ownership of it and break the example | |
| # itself. Lands inside ./logs, which is already tarred and uploaded below. | |
| - name: Collect zilla engine directory on failure | |
| if: failure() | |
| working-directory: examples/${{ matrix.dir }} | |
| run: | | |
| mkdir -p "$GITHUB_WORKSPACE/logs" | |
| docker compose cp zilla:/var/run/zilla "$GITHUB_WORKSPACE/logs/engine" || true | |
| - name: Tar logs | |
| if: failure() | |
| run: tar cvzf ./logs.tgz ./logs | |
| - name: Upload logs to GitHub | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.dir }}_logs.tgz | |
| path: ./logs.tgz | |
| - name: Teardown | |
| if: ${{ always() && hashFiles(format('examples/{0}/teardown.sh', matrix.dir)) != '' }} | |
| working-directory: examples/${{ matrix.dir }} | |
| run: docker compose down --remove-orphans | |
| cleanup: | |
| if: success() | |
| needs: | |
| - testing | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete Docker image artifact | |
| uses: actions/github-script@v9 | |
| continue-on-error: true | |
| with: | |
| script: | | |
| const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| }); | |
| for (const artifact of artifacts.data.artifacts) { | |
| if (artifact.name === 'zilla-image') { | |
| await github.rest.actions.deleteArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: artifact.id, | |
| }); | |
| } | |
| } |