diff --git a/.dockerignore b/.dockerignore index b127506..5ac7fc8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,11 @@ -.git -.github -.venv -.pytest_cache -.ruff_cache -.mypy_cache -__pycache__ -*.py[cod] -*.egg-info -build -dist -htmlcov -.coverage -test-results.xml +# Fail-closed allowlist for the Planfile fleet-runner build context. +* +!.dockerignore +!Dockerfile +!README.md +!pyproject.toml +!uv.lock +!planfile/ +!planfile/** +!scripts/ +!scripts/docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 1228fbc..9ce112e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,25 @@ # Planfile CI/CD Runner Docker Image -FROM python:3.11-slim +# uv 0.11.28 and Python 3.12.14 are selected by immutable multi-platform +# manifest digests. Keep the human-readable versions in this comment only. +FROM ghcr.io/astral-sh/uv@sha256:0f36cb9361a3346885ca3677e3767016687b5a170c1a6b88465ec14aefec90aa AS uv +FROM python@sha256:782412e85d0f0984994c290652577d4018aff08145c85b262bb63dc0c7522254 AS runtime + +COPY --from=uv /uv /uvx /bin/ # Install system dependencies -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ git \ - curl \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Ollama for local LLM support -RUN curl -fsSL https://ollama.ai/install.sh | sh - # Set working directory WORKDIR /app -# Copy requirements -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Install Planfile with all integrations -RUN pip install --no-cache-dir planfile[all] - -# Install LLX -RUN pip install --no-cache-dir llx +# Install the local package and all runner integrations from the committed, +# hash-bearing lockfile. Frozen mode refuses to resolve or rewrite anything. +COPY pyproject.toml uv.lock README.md ./ +COPY planfile/ ./planfile/ +RUN uv sync --frozen --no-dev --extra all --no-editable # Copy entrypoint script COPY scripts/docker-entrypoint.sh /usr/local/bin/ @@ -35,9 +32,8 @@ RUN mkdir -p /workspace /app/results ENV PYTHONPATH=/app ENV WORKSPACE=/workspace ENV RESULTS_DIR=/app/results - -# Expose port for Ollama -EXPOSE 11434 +ENV VIRTUAL_ENV=/app/.venv +ENV PATH="/app/.venv/bin:${PATH}" # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ diff --git a/docker-compose.yml b/docker-compose.yml index fa8e105..6bb69ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,6 @@ services: - MAX_ITERATIONS=5 - AUTO_FIX=false - # Ollama settings - - ENABLE_OLLAMA=true - - OLLAMA_MODEL=qwen2.5:3b - # Strategy file (mount or copy) - STRATEGY_FILE=/app/planfile.yaml @@ -56,9 +52,6 @@ services: # Mount git config if needed - ~/.gitconfig:/root/.gitconfig:ro - ports: - - "11434:11434" # Ollama API - networks: - planfile diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 8733ca3..cdbe876 100644 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -10,17 +10,12 @@ NC='\033[0m' # No Color echo -e "${GREEN}🚀 Planfile CI/CD Runner${NC}" echo "==============================" -# Start Ollama in background if enabled -if [ "$ENABLE_OLLAMA" = "true" ]; then - echo -e "${YELLOW}🤖 Starting Ollama...${NC}" - ollama serve & - sleep 5 - - # Pull default model if specified - if [ -n "$OLLAMA_MODEL" ]; then - echo -e "${YELLOW}📥 Pulling model: $OLLAMA_MODEL${NC}" - ollama pull "$OLLAMA_MODEL" - fi +# The fleet runner no longer downloads or embeds Ollama. Call an external, +# independently managed Ollama endpoint through the normal provider settings. +if [ "${ENABLE_OLLAMA:-false}" = "true" ]; then + echo -e "${RED}❌ Embedded Ollama is not part of the locked runner image.${NC}" >&2 + echo "Configure an external Ollama endpoint instead." >&2 + exit 2 fi # Check required environment variables diff --git a/tests/test_docker_supply_chain.py b/tests/test_docker_supply_chain.py new file mode 100644 index 0000000..c548762 --- /dev/null +++ b/tests/test_docker_supply_chain.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DOCKERFILE = ROOT / "Dockerfile" +DIGEST_FROM = re.compile(r"^FROM\s+\S+@sha256:[0-9a-f]{64}(?:\s+AS\s+\S+)?$", re.MULTILINE) + + +def test_every_runner_stage_uses_an_immutable_image_digest() -> None: + text = DOCKERFILE.read_text(encoding="utf-8") + from_lines = [line for line in text.splitlines() if line.startswith("FROM ")] + + assert from_lines + assert all(DIGEST_FROM.fullmatch(line) for line in from_lines) + + +def test_runner_uses_only_frozen_python_resolution() -> None: + text = DOCKERFILE.read_text(encoding="utf-8") + + assert "uv sync --frozen --no-dev --extra all --no-editable" in text + assert "COPY pyproject.toml uv.lock README.md ./" in text + assert "pip install" not in text + assert "install.sh" not in text + assert "curl " not in text + + +def test_docker_context_is_an_explicit_allowlist() -> None: + entries = (ROOT / ".dockerignore").read_text(encoding="utf-8").splitlines() + active = [line for line in entries if line and not line.startswith("#")] + + assert active[0] == "*" + assert set(active[1:]) == { + "!.dockerignore", + "!Dockerfile", + "!README.md", + "!pyproject.toml", + "!uv.lock", + "!planfile/", + "!planfile/**", + "!scripts/", + "!scripts/docker-entrypoint.sh", + }