Repository navigation
receipt binds the staged bundle's modified time, target label and source commit (#677) #1982
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: Delphi Python Tests | |
| concurrency: | |
| group: python-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| on: | |
| push: | |
| branches: [ edge, stable ] | |
| paths: &paths | |
| - 'delphi/**/*.py' | |
| - 'delphi/requirements*.txt' | |
| - 'delphi/Dockerfile' | |
| - '.github/workflows/python-ci.yml' | |
| # server/src so a new server-side wildcard runs the projection-gate sweep | |
| - 'server/src/**' | |
| # Representative payload tests execute the probe planner and independent gate. | |
| - 'ci/private_cert/**' | |
| - 'ci/probe_box/**' | |
| pull_request: | |
| branches: | |
| - edge | |
| - stable | |
| - 'jc/**' | |
| paths: *paths | |
| # Manual trigger: lets repo write-access users run CI on any branch | |
| # (including spr/edge/* stack branches, which `pull_request` no longer | |
| # auto-triggers for). UI: Actions → Delphi Python Tests → "Run workflow". | |
| # CLI: `gh workflow run "Delphi Python Tests" --ref <branch>`. | |
| workflow_dispatch: | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: 1. Free up disk space on runner | |
| run: | | |
| echo "Starting disk cleanup..." | |
| df -h | |
| sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/powershell | |
| echo "Disk space after cleanup:" | |
| df -h | |
| - name: 2. Create .env file for CI | |
| run: | | |
| if [ -f "test.env" ]; then | |
| cp test.env .env | |
| echo "Using test.env for CI." | |
| else | |
| echo "::error::test.env file not found!" | |
| exit 1 | |
| fi | |
| # Add hostnames for container-to-container networking | |
| # The 'delphi' service in 'docker-compose.test.yml' reads these | |
| echo "POSTGRES_HOST=postgres" >> .env | |
| echo "DYNAMODB_ENDPOINT=http://dynamodb:8000" >> .env | |
| echo "AWS_S3_ENDPOINT=http://minio:9000" >> .env | |
| # BuildKit is required by `RUN --mount=type=cache` in the npm Dockerfiles. | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: 3. Build Docker images | |
| run: | | |
| # Build all services in the test file (including delphi) | |
| docker compose -f docker-compose.test.yml --env-file .env build | |
| - name: 4. Start all services | |
| run: | | |
| # Start all services (including delphi) in detached mode | |
| # The 'delphi' container will start and run 'tail -f /dev/null' | |
| docker compose -f docker-compose.test.yml --env-file .env up -d | |
| echo "Waiting for services to be healthy..." | |
| sleep 20 # Wait for containers to start | |
| docker compose -f docker-compose.test.yml ps | |
| - name: 5. Check service health | |
| run: | | |
| # Wait for postgres to be ready | |
| echo "Checking postgres..." | |
| docker compose -f docker-compose.test.yml exec -T postgres \ | |
| bash -c 'until pg_isready -U $POSTGRES_USER; do sleep 5; done' | |
| echo "Postgres is ready." | |
| # The opt-in Postgres integration tests (require_polis_postgres) run here | |
| # against the compose `postgres` service, whose image bakes the polis | |
| # migrations (server/postgres/migrations/*.sql via docker-entrypoint-initdb.d), | |
| # so the votes / votes_latest_unique schema + on_vote_insert_update_unique_table | |
| # rule are already applied. The pytest step exports POLIS_TEST_POSTGRES_URL; | |
| # the cold-start generator under test is already baked into the delphi image | |
| # (Dockerfile `COPY scripts/ ./scripts/`), built from this checkout. | |
| - name: 6. Run Delphi Pytest | |
| run: | | |
| echo "Copying test files into container..." | |
| docker compose -f docker-compose.test.yml cp delphi/tests delphi:/app/tests | |
| docker compose -f docker-compose.test.yml cp delphi/real_data delphi:/app/real_data | |
| echo "Copying coverage script into container..." | |
| docker compose -f docker-compose.test.yml cp delphi/generate_coverage_md.py delphi:/app/generate_coverage_md.py | |
| echo "Copying script to be tested into container..." | |
| docker compose -f docker-compose.test.yml cp delphi/polismath/run_math_pipeline.py delphi:/app/run_math_pipeline.py | |
| docker compose -f docker-compose.test.yml cp delphi/umap_narrative delphi:/app/umap_narrative | |
| echo "Copying the compose files into container..." | |
| # tests/test_compose_math_env.py asserts that every stack's delphi service | |
| # receives the MATH_ENV its math service writes under. /app/tests has no | |
| # checkout above it, so put the two files it parses beside it; without | |
| # them the test skips instead of guarding the report pipeline's math_env. | |
| docker compose -f docker-compose.test.yml cp docker-compose.yml delphi:/app/docker-compose.yml | |
| docker compose -f docker-compose.test.yml cp docker-compose.test.yml delphi:/app/docker-compose.test.yml | |
| # Wire a checkout-shaped root so delphi/tests/scripts collects AND RUNS the | |
| # pure-Python inventory sweep + the DB-free gate unit tests (the conftest | |
| # locator resolves POLIS_CHECKOUT_DIR). The scan inputs are copied in as REAL, | |
| # traversable directories (NOT symlinks — os.walk would skip those). The set | |
| # of inputs is DERIVED from the scanner's own declaration | |
| # (`--print-scan-inputs`) so it can never diverge from what the sweep scans; | |
| # a test asserts every declared input is present under /app/projgate. | |
| echo "Wiring projection-gate scan inputs (derived from the scanner declaration)..." | |
| docker compose -f docker-compose.test.yml exec -T delphi mkdir -p /app/projgate | |
| # Iterate a CAPTURED list, not `... | while read`: a `docker compose exec` | |
| # inside a while-read loop drains the piped stdin and truncates the list to | |
| # its first item (observed: only server/src copied). | |
| # Recordings tests also load the packer and its digest helper from ci/. | |
| # battery_coverage.py is already included in the declared delphi/scripts. | |
| # test_compose_math_env.py reads the deploy hook's per-role compose lines. | |
| # test_before_install_hook.py runs scripts/before_install.sh against a fake docker. | |
| # test_compose_math_env.py also reads the stop hook's per-role stop lines. | |
| # Representative payload tests import the box planner/gate and their | |
| # helpers from this same checkout root; keep their relative paths intact. | |
| # Light-shadow triage tests also import the compare/triage modules. | |
| for rel in $(python3 delphi/scripts/projection_inventory.py --print-scan-inputs) \ | |
| ci/p022_recordings_manifest.py ci/p022_battery_digest.py \ | |
| ci/private_cert/images/gate.py ci/private_cert/images/probe.py \ | |
| ci/private_cert/images/attribution.py ci/private_cert/images/diagnostic_projection.py ci/private_cert/images/selection_context.py \ | |
| ci/private_cert/images/g12.py ci/private_cert/images/near_ties.py ci/private_cert/control.py \ | |
| ci/private_cert/image_admission.py ci/probe_box/receipt.py \ | |
| ci/probe_box/contracts.py ci/probe_box/light_shadow.py ci/probe_box/light_shadow_queries.py \ | |
| ci/private_cert/images/light_shadow_compare.py ci/private_cert/images/light_shadow_triage.py \ | |
| ci/private_cert/images/recipe.py \ | |
| scripts/after_install.sh scripts/before_install.sh scripts/application_stop.sh; do | |
| docker compose -f docker-compose.test.yml exec -T delphi mkdir -p "/app/projgate/$(dirname "$rel")" | |
| docker compose -f docker-compose.test.yml cp "$rel" "delphi:/app/projgate/$rel" \ | |
| || { echo "failed to copy scan input: $rel"; exit 1; } | |
| done | |
| echo "Running tests and generating coverage report..." | |
| docker compose \ | |
| -f docker-compose.test.yml \ | |
| --env-file .env \ | |
| exec -T \ | |
| -e AWS_DEFAULT_REGION=us-east-1 \ | |
| -e AWS_REGION=us-east-1 \ | |
| -e AWS_ACCESS_KEY_ID=dummy \ | |
| -e AWS_SECRET_ACCESS_KEY=dummy \ | |
| -e SKIP_GOLDEN=1 \ | |
| -e POSTGRES_CONNECT_TIMEOUT=5 \ | |
| -e POLIS_CHECKOUT_DIR=/app/projgate \ | |
| delphi \ | |
| bash -c " \ | |
| set -e; \ | |
| echo '--- Setting up DynamoDB Tables ---'; \ | |
| python create_dynamodb_tables.py --region us-east-1; \ | |
| echo '--- Running Pytest ---'; \ | |
| export PYTHONPATH=\$PYTHONPATH:/app; \ | |
| export POLIS_TEST_POSTGRES_URL=\"postgresql://\$DATABASE_USER:\$DATABASE_PASSWORD@\$DATABASE_HOST/\$DATABASE_NAME\"; \ | |
| pytest --cov=polismath --cov=run_math_pipeline --cov=./umap_narrative --cov-report=xml:/app/coverage.xml /app/tests --ignore=/app/tests/test_pakistan_conversation.py | |
| echo '--- Generating Coverage Comment Text ---'; \ | |
| python /app/generate_coverage_md.py > /app/coverage-comment.md \ | |
| " | |
| - name: 7. Copy coverage report from container | |
| if: success() | |
| run: | | |
| echo "Copying coverage-comment.md from delphi container..." | |
| docker compose -f docker-compose.test.yml cp delphi:/app/coverage-comment.md . | |
| echo "=== Coverage Report ===" | |
| cat coverage-comment.md | |
| - name: 8. Upload Coverage Report | |
| if: success() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: coverage-comment.md | |
| - name: 9. Post Coverage Comment | |
| if: success() && github.event_name == 'pull_request' | |
| uses: actions/github-script@v6 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const commentBody = fs.readFileSync('coverage-comment.md', 'utf8'); | |
| const marker = '<!-- delphi-coverage-report -->'; | |
| const body = `${marker}\n## Delphi Coverage Report\n\n${commentBody}`; | |
| try { | |
| // Find existing coverage comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const existingComment = comments.find(c => c.body.includes(marker)); | |
| if (existingComment) { | |
| // Delete existing comment so new one appears after latest commit | |
| await github.rest.issues.deleteComment({ | |
| comment_id: existingComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| console.log('Existing coverage comment deleted.'); | |
| } | |
| // Always create a new comment | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| console.log('Coverage comment posted successfully.'); | |
| } catch (error) { | |
| if (error.status === 403) { | |
| console.log('Note: Could not post coverage comment to PR.'); | |
| console.log('This is expected for PRs from forks due to GitHub token permissions.'); | |
| console.log('Coverage report is available in the workflow logs (step 7) and as a downloadable artifact (step 8).'); | |
| } else { | |
| console.log(`Unexpected error posting comment: ${error.message}`); | |
| } | |
| } | |
| - name: 10. Show service logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Delphi service logs ===" | |
| docker compose -f docker-compose.test.yml logs delphi || true | |
| echo "=== Postgres service logs ===" | |
| docker compose -f docker-compose.test.yml logs postgres || true | |
| echo "=== DynamoDB service logs ===" | |
| docker compose -f docker-compose.test.yml logs dynamodb || true | |
| - name: 11. Clean up services | |
| if: always() | |
| run: | | |
| echo "Cleaning up services..." | |
| docker compose -f docker-compose.test.yml down -v |