diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c30de65 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Repository metadata and local credentials are not runtime inputs. +.git +**/.git +.env +**/.env +.env.* +**/.env.* +.ssh +**/.ssh +.aws +**/.aws +.netrc +**/.netrc +.pypirc +**/.pypirc +**/github_token +**/hf_token +.mcp-tasks +**/.mcp-tasks + +# Local development environments and generated caches. +.venv +venv +**/__pycache__ +.pytest_cache +.mypy_cache diff --git a/.github/workflows/sensevoice-container.yml b/.github/workflows/sensevoice-container.yml index 7c32114..9131f95 100644 --- a/.github/workflows/sensevoice-container.yml +++ b/.github/workflows/sensevoice-container.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - Dockerfile + - .dockerignore - docker-compose.yaml - requirements.txt - api.py @@ -15,6 +16,7 @@ on: - CONTRIBUTING.md - .github/workflows/sensevoice-container.yml - tests/test_container_contract.py + - tests/test_docker_context.sh - tests/test_device_env.py push: branches: @@ -23,6 +25,7 @@ on: - "v*" paths: - Dockerfile + - .dockerignore - docker-compose.yaml - requirements.txt - api.py @@ -34,6 +37,7 @@ on: - CONTRIBUTING.md - .github/workflows/sensevoice-container.yml - tests/test_container_contract.py + - tests/test_docker_context.sh - tests/test_device_env.py workflow_dispatch: @@ -50,6 +54,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Validate container contract run: python -m unittest tests.test_container_contract tests.test_device_env @@ -58,8 +64,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 + - name: Verify Docker build context exclusions + run: bash tests/test_docker_context.sh + - name: Log in to GitHub Container Registry if: github.event_name != 'pull_request' uses: docker/login-action@v3 diff --git a/tests/test_container_contract.py b/tests/test_container_contract.py index c14cb48..563ea5d 100644 --- a/tests/test_container_contract.py +++ b/tests/test_container_contract.py @@ -6,6 +6,26 @@ class ContainerContractTest(unittest.TestCase): + def test_build_context_excludes_local_credentials_and_git_metadata(self): + ignore = ROOT / '.dockerignore' + self.assertTrue(ignore.is_file(), 'COPY . requires an explicit context boundary') + patterns = {line.strip() for line in ignore.read_text().splitlines() + if line.strip() and not line.lstrip().startswith('#')} + required = {'.git', '**/.git', '.env', '**/.env', '.env.*', '**/.env.*', + '.ssh', '**/.ssh', '.aws', '**/.aws', '.netrc', '**/.netrc', + '.pypirc', '**/.pypirc', '**/github_token', '**/hf_token', + '.mcp-tasks', '**/.mcp-tasks'} + self.assertTrue(required <= patterns, required - patterns) + self.assertFalse(any(pattern.startswith('!') for pattern in patterns)) + + def test_context_changes_run_ci_without_persisting_checkout_credentials(self): + workflow = (ROOT / '.github/workflows/sensevoice-container.yml').read_text() + self.assertEqual(workflow.count('- .dockerignore'), 2) + self.assertEqual(workflow.count('persist-credentials: false'), + workflow.count('uses: actions/checkout@v4')) + self.assertEqual(workflow.count('- tests/test_docker_context.sh'), 2) + self.assertIn('run: bash tests/test_docker_context.sh', workflow) + def test_container_base_matches_repository_torch_floor(self): dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") diff --git a/tests/test_docker_context.sh b/tests/test_docker_context.sh new file mode 100644 index 0000000..ac68fa9 --- /dev/null +++ b/tests/test_docker_context.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +probe=$(mktemp -d .container-context-probe.XXXXXX) +output=$(mktemp -d "${TMPDIR:-/tmp}/sensevoice-context.XXXXXX") +trap 'rm -rf -- "$probe" "$output"' EXIT + +# Harmless sentinels exercise Docker's matcher without reading credentials. +mkdir -p "$probe/.git" "$probe/.ssh" "$probe/.aws" +touch "$probe/.git/sentinel" "$probe/.ssh/sentinel" "$probe/.aws/sentinel" +touch "$probe/.env" "$probe/.env.test" "$probe/.netrc" "$probe/.pypirc" +touch "$probe/github_token" "$probe/hf_token" "$probe/runtime-sentinel.txt" + +docker buildx build --file - --output "type=local,dest=$output" . <<'DOCKERFILE' +FROM scratch +COPY . / +DOCKERFILE + +test ! -e "$output/.git" +for excluded in .git .ssh .aws .env .env.test .netrc .pypirc github_token hf_token; do + test ! -e "$output/$probe/$excluded" +done +for required in api.py model.py requirements.txt utils/device_env.py "$probe/runtime-sentinel.txt"; do + test -f "$output/$required" +done +printf '%s\n' 'Docker context exclusions and runtime inputs verified.'