diff --git a/.github/workflows/build-embeddings.yml b/.github/workflows/build-embeddings.yml index ba0f548..12e2f0d 100644 --- a/.github/workflows/build-embeddings.yml +++ b/.github/workflows/build-embeddings.yml @@ -1,6 +1,8 @@ name: Build Offline Embedding Pipeline on: + schedule: + - cron: "0 9 * * 0" workflow_dispatch: permissions: read-all diff --git a/.github/workflows/build-mcp-image.yml b/.github/workflows/build-mcp-image.yml index 3074a2d..121c21e 100644 --- a/.github/workflows/build-mcp-image.yml +++ b/.github/workflows/build-mcp-image.yml @@ -167,6 +167,7 @@ jobs: context: . file: mcp-local/Dockerfile platforms: ${{ matrix.platform }} + network: none build-args: | UBUNTU_IMAGE=${{ steps.locked_images.outputs.ubuntu_image }} EMBEDDINGS_IMAGE=${{ steps.locked_images.outputs.embeddings_image }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f335fde..06207a1 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -72,6 +72,7 @@ jobs: run: | docker buildx build \ --platform ${{ matrix.platform }} \ + --network none \ -f mcp-local/Dockerfile \ -t arm-mcp:latest \ --output type=docker \ diff --git a/README.md b/README.md index 8a6902c..38d08d2 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,7 @@ embedding images are private: ```bash docker login ghcr.io docker buildx build \ + --network none \ --file mcp-local/Dockerfile \ --tag arm-mcp:local \ --load \ @@ -426,7 +427,8 @@ Production must never consume a mutable tag. Embedding updates use an automated promotion PR instead of being copied into the MCP release directly: -1. Manually run **Build Offline Embedding Pipeline** from `main`. +1. Let **Build Offline Embedding Pipeline** run from `main` every Sunday at + 09:00 UTC, or start it manually for an out-of-band update. 2. The workflow publishes an immutable candidate vector-store image and opens or updates `automation/pin-embedding-vectorstore`. 3. The promotion branch updates both `container_images.embeddings` in @@ -469,6 +471,15 @@ The publication workflow also creates a tag containing the source commit, workflow run ID, and attempt. That tag is only a discovery aid; production builds always use the digest. +#### Rolling Back an Input Update + +Rollback is a reviewed pin change. Restore the last approved image references +and metadata in `mcp-local/build-inputs.lock.json`, and keep the corresponding +image defaults in `mcp-local/Dockerfile` synchronized. Submit the rollback +through the normal pull-request process and run the AMD64 and Arm64 integration +builds before release. Do not delete, overwrite, or retag the immutable GHCR +artifacts. + ## Troubleshooting ### Accessing the Container Shell diff --git a/embedding-generation/README.md b/embedding-generation/README.md index dfba328..4ab801f 100644 --- a/embedding-generation/README.md +++ b/embedding-generation/README.md @@ -39,7 +39,8 @@ one immutable artifact. It is published privately as The embedding pipeline publishes candidates; it does not cause the MCP image to consume the newest registry artifact automatically. To promote a candidate: -1. Run **Build Offline Embedding Pipeline** from `main`. +1. Let **Build Offline Embedding Pipeline** run from `main` every Sunday at + 09:00 UTC, or start it manually for an out-of-band update. 2. After publishing the vector store, the workflow opens or updates the `automation/pin-embedding-vectorstore` PR with the immutable digest in both `mcp-local/build-inputs.lock.json` and `mcp-local/Dockerfile`. diff --git a/mcp-local/Dockerfile b/mcp-local/Dockerfile index 823f611..bfb6748 100644 --- a/mcp-local/Dockerfile +++ b/mcp-local/Dockerfile @@ -40,7 +40,7 @@ RUN --network=none apt-get install -y --no-download --no-install-recommends \ rm -rf /var/lib/apt/lists/* WORKDIR /app -RUN python3 -m venv "$VIRTUAL_ENV" +RUN --network=none python3 -m venv "$VIRTUAL_ENV" # The requirements file and wheelhouse come from the same immutable input # bundle. Network access is disabled for installation. @@ -81,7 +81,7 @@ RUN --network=none mkdir -p /opt/ArmPerformix-cli-current && \ # Keep the model, metadata, and index from the same immutable vector-store # artifact. The model is architecture-independent even though generation ran # on arm64. -RUN mkdir -p /app/data +RUN --network=none mkdir -p /app/data COPY --from=embeddings /embedding-data/embedding-model/ /app/embedding-model/ COPY --from=embeddings /embedding-data/metadata.json /app/data/metadata.json COPY --from=embeddings /embedding-data/usearch_index.bin /app/data/usearch_index.bin @@ -122,7 +122,7 @@ COPY --from=builder /opt/arm-migration-tools/migrate-ease /opt/arm-migration-too COPY --from=builder /usr/local/bin/migrate-ease-* /usr/local/bin/ COPY --from=builder /app /app -RUN ln -sf "${APX_BIN}" /usr/local/bin/apx +RUN --network=none ln -sf "${APX_BIN}" /usr/local/bin/apx WORKDIR /app diff --git a/mcp-local/tests/test_build_inputs.py b/mcp-local/tests/test_build_inputs.py index b48ec4e..4b695a8 100644 --- a/mcp-local/tests/test_build_inputs.py +++ b/mcp-local/tests/test_build_inputs.py @@ -131,6 +131,21 @@ def test_final_builds_do_not_acquire_inputs_live() -> None: assert "python -m pip install --upgrade pip" not in INTEGRATION_WORKFLOW +def test_final_builds_disable_network_for_every_run_instruction() -> None: + run_instructions = [ + line.strip() + for line in DOCKERFILE.splitlines() + if re.match(r"(?i:RUN)(?:\s|$)", line.strip()) + ] + assert run_instructions + assert all( + re.match(r"(?i:RUN)\s+--network=none(?:\s|$)", instruction) + for instruction in run_instructions + ) + assert " network: none\n" in IMAGE_WORKFLOW + assert " --network none \\\n" in INTEGRATION_WORKFLOW + + def test_release_build_loads_image_arguments_from_manifest() -> None: assert 'lock_file="mcp-local/build-inputs.lock.json"' in IMAGE_WORKFLOW assert "@sha256:[0-9a-f]{64}" in IMAGE_WORKFLOW @@ -272,13 +287,3 @@ def test_input_publication_is_manual_private_and_multi_architecture() -> None: assert '"export",' in STAGE_INPUTS assert 'output / "requirements.lock"' in STAGE_INPUTS assert 'echo "- MCP build input: \\`${IMAGE}@${digest}\\`"' in INPUT_WORKFLOW - - -def test_input_publication_uses_pinned_build_actions() -> None: - action_lines = [ - line.strip() - for line in INPUT_WORKFLOW.splitlines() - if line.strip().startswith("uses:") - ] - assert action_lines - assert all(re.fullmatch(r"uses: [^@]+@[0-9a-f]{40}(?: # .+)?", line) for line in action_lines)