Skip to content

receipt binds the staged bundle's modified time, target label and source commit (#677) #1982

receipt binds the staged bundle's modified time, target label and source commit (#677)

receipt binds the staged bundle's modified time, target label and source commit (#677) #1982

Workflow file for this run

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