fix(reporting): validate buyer frozen histories #2769
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: CI | |
| on: | |
| push: | |
| branches: [main, python-adcp-sdk-setup] | |
| pull_request: | |
| branches: | |
| - main | |
| - conductor/reporting-webhook-activity-1168b | |
| - conductor/reporting-status-notifications-1168c | |
| - conductor/1167b1-materializer-contracts | |
| - conductor/1167b2-durable-managed-reporting | |
| - conductor/reporting-receipt-ingress-b22 | |
| - conductor/reporting-frozen-account-feed-b23 | |
| - conductor/reporting-schema-proof-receipt-diagnostics-hardening | |
| - conductor/reporting-production-tier-capabilities-b24 | |
| - conductor/reporting-adcp-rc4-adoption | |
| # Default @adcp/sdk runner alias for storyboard jobs. Tracks the current | |
| # stable @adcp/sdk release via the ``latest`` npm dist-tag. | |
| env: | |
| ADCP_SDK_VERSION: "latest" | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| workflow-security: | |
| name: Workflow security | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: Lint workflows | |
| uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 | |
| with: | |
| version: 1.7.7 | |
| shellcheck: false | |
| # actionlint v1.7.x predates GitHub's `queue: max` concurrency | |
| # syntax. Keep linting every other workflow field until its parser | |
| # catches up. | |
| flags: >- | |
| -ignore ^unexpected\s+key\s+\x22queue\x22\s+for\s+\x22concurrency\x22\s+section | |
| - name: Test Ladon review-state helper | |
| run: node --test scripts/retire-superseded-ladon-reviews.test.cjs | |
| - name: Audit workflows | |
| uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 | |
| with: | |
| inputs: >- | |
| .github/workflows/claude-issue-triage.yml | |
| .github/workflows/ipr-agreement.yml | |
| .github/workflows/sync-agent-roles.yml | |
| .github/workflows/slash-command-dispatch.yml | |
| .github/workflows/ai-review.yml | |
| .github/workflows/release-please.yml | |
| .github/workflows/release-proposal.yml | |
| .github/workflows/release-acceptance.yml | |
| .github/workflows/release-publish.yml | |
| version: 1.29.0 | |
| min-severity: high | |
| min-confidence: high | |
| advanced-security: false | |
| annotations: true | |
| test: | |
| name: Test Python ${{ matrix.python-version }} | |
| runs-on: ubuntu-latest | |
| # The job ceiling is not the suite's budget: it also has to absorb | |
| # checkout, Python setup, the editable [dev] install and, on 3.12 only, | |
| # ruff/mypy/mypy --strict/the type-ignore contract before pytest starts, | |
| # then post-job cleanup after it ends. On an ubuntu-latest runner that | |
| # pre-test work is ~1m30s and the plain suite is ~16m, but 3.12 adds | |
| # coverage tracing on ~9.4k tests: at 30 minutes that leg was cancelled | |
| # mid-run at 99% with no failing test, which reports as a red matrix and | |
| # hides real signal. Bound the suite itself below (so a hang fails one | |
| # named step with its own message instead of silently taking the job), and | |
| # leave the job enough room that step bound plus setup and cleanup still | |
| # fit with margin for runner variance. | |
| timeout-minutes: 60 | |
| strategy: | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Run linter | |
| if: matrix.python-version == '3.12' | |
| run: ruff check src/ | |
| - name: Run type checker | |
| if: matrix.python-version == '3.12' | |
| run: mypy src/adcp/ | |
| - name: Run adopter type-check suite | |
| if: matrix.python-version == '3.12' | |
| run: mypy --strict tests/type_checks/ examples/reporting_webhook_activity.py examples/reporting_status_notifications.py examples/reporting_destination_writer.py examples/reporting_durable_materializer.py examples/reporting_receipt_ingress.py examples/reporting_production.py | |
| - name: Enforce adopter type-check fixture contract | |
| if: matrix.python-version == '3.12' | |
| run: python scripts/check_type_ignore_contract.py | |
| # Bounded well above the observed ~16m (plain) and ~30m (coverage) runs | |
| # so ordinary variance never trips it, and well below the job ceiling so | |
| # a genuinely stuck suite still fails *this* step with a timeout rather | |
| # than being cancelled as a whole job. | |
| - name: Run tests | |
| if: matrix.python-version != '3.12' | |
| timeout-minutes: 45 | |
| run: python scripts/reporting_test_harness.py pytest tests/ -v -ra | |
| - name: Run tests with coverage | |
| if: matrix.python-version == '3.12' | |
| timeout-minutes: 45 | |
| run: python scripts/reporting_test_harness.py pytest tests/ -v -ra --cov=src/adcp --cov-report=term-missing | |
| pg-conformance: | |
| name: Postgres conformance tests (Postgres 16, ${{ matrix.lane }}) | |
| runs-on: ubuntu-latest | |
| # The core lane was still passing at 90% when its 15-minute job expired. | |
| # Leave room for dependency installation and the complete conformance suite. | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| lane: [core, process] | |
| services: | |
| postgres: | |
| # CI-local ephemeral database. POSTGRES_HOST_AUTH_METHOD=trust | |
| # avoids shipping any password literal (real or placeholder) in | |
| # this workflow — GitHub's default CI network is already the | |
| # trust boundary for this throwaway service. | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch reviewed reporting baseline for rolling binary tests | |
| timeout-minutes: 1 | |
| run: git fetch --no-tags --depth=1 origin 17ee407ae3978c8a2bb54437287afbf9dafb8130 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies (with [pg] extra) | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev,pg]" | |
| - name: Run Postgres conformance tests | |
| timeout-minutes: 25 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_test | |
| PG_LANE: ${{ matrix.lane }} | |
| run: | | |
| # Keep every case and its per-test deadline. Separate process-crash | |
| # controls so a test timeout is reported before the job deadline. | |
| case "$PG_LANE" in | |
| core) | |
| python scripts/reporting_test_harness.py pytest tests/conformance/signing/test_pg_replay_store.py \ | |
| tests/conformance/signing/test_pg_replay_store_e2e.py \ | |
| tests/conformance/decisioning/test_pg_buyer_agent_registry.py \ | |
| tests/conformance/decisioning/test_pg_idempotency_backend.py \ | |
| tests/conformance/decisioning/test_pg_task_webhook_outbox.py \ | |
| tests/conformance/decisioning/test_pg_reference_workflow_queue.py \ | |
| tests/conformance/reporting/ \ | |
| --ignore=tests/conformance/reporting/test_reporting_notification_process_matrix.py \ | |
| --ignore-glob='tests/conformance/reporting/test_reporting_status_*.py' \ | |
| --ignore=tests/conformance/reporting/test_reporting_materializer_rolling.py \ | |
| --ignore=tests/conformance/reporting/test_reporting_materializer_process.py \ | |
| --ignore=tests/conformance/reporting/test_reporting_materializer_migration.py \ | |
| --ignore=tests/conformance/reporting/test_reporting_materializer_installed_pg.py \ | |
| --ignore-glob='tests/conformance/reporting/test_reporting_receipt_*.py' \ | |
| --ignore-glob='tests/conformance/reporting/test_reporting_feed_*.py' \ | |
| --ignore-glob='tests/conformance/reporting/test_reporting_production*.py' \ | |
| --ignore-glob='tests/conformance/reporting/test_reporting_projection*.py' \ | |
| --ignore=tests/conformance/reporting/test_reporting_tier_projection.py \ | |
| --ignore=tests/conformance/reporting/test_reporting_schedule_schema.py \ | |
| --ignore=tests/conformance/reporting/test_reporting_rc6_rolling.py \ | |
| -v -ra | |
| ;; | |
| process) | |
| python scripts/reporting_test_harness.py pytest tests/conformance/reporting/test_reporting_notification_process_matrix.py -v -ra | |
| ;; | |
| *) | |
| echo "Unknown Postgres conformance lane" | |
| exit 1 | |
| ;; | |
| esac | |
| pg-conformance-required-gate: | |
| name: Postgres conformance tests (Postgres 16) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: {} | |
| needs: [pg-conformance, pg-reporting-status, pg-reporting-materializer, pg-reporting-receipts, pg-reporting-receipt-compatibility, pg-reporting-feed, pg-reporting-feed-compatibility, pg-reporting-feed-installed, pg-reporting-production, pg-reporting-production-compatibility, pg-reporting-production-installed] | |
| if: ${{ always() }} | |
| steps: | |
| - name: Require every Postgres conformance lane | |
| env: | |
| PG_RESULT: ${{ needs.pg-conformance.result }} | |
| STATUS_RESULT: ${{ needs.pg-reporting-status.result }} | |
| MATERIALIZER_RESULT: ${{ needs.pg-reporting-materializer.result }} | |
| RECEIPTS_RESULT: ${{ needs.pg-reporting-receipts.result }} | |
| RECEIPT_COMPATIBILITY_RESULT: ${{ needs.pg-reporting-receipt-compatibility.result }} | |
| FEED_RESULT: ${{ needs.pg-reporting-feed.result }} | |
| FEED_COMPATIBILITY_RESULT: ${{ needs.pg-reporting-feed-compatibility.result }} | |
| FEED_INSTALLED_RESULT: ${{ needs.pg-reporting-feed-installed.result }} | |
| PRODUCTION_RESULT: ${{ needs.pg-reporting-production.result }} | |
| PRODUCTION_COMPATIBILITY_RESULT: ${{ needs.pg-reporting-production-compatibility.result }} | |
| PRODUCTION_INSTALLED_RESULT: ${{ needs.pg-reporting-production-installed.result }} | |
| run: | | |
| if [ "$PG_RESULT" != "success" ] || [ "$STATUS_RESULT" != "success" ] || [ "$MATERIALIZER_RESULT" != "success" ] || [ "$RECEIPTS_RESULT" != "success" ] || [ "$RECEIPT_COMPATIBILITY_RESULT" != "success" ] || [ "$FEED_RESULT" != "success" ] || [ "$FEED_COMPATIBILITY_RESULT" != "success" ] || [ "$FEED_INSTALLED_RESULT" != "success" ] || [ "$PRODUCTION_RESULT" != "success" ] || [ "$PRODUCTION_COMPATIBILITY_RESULT" != "success" ] || [ "$PRODUCTION_INSTALLED_RESULT" != "success" ]; then | |
| echo "Postgres conformance matrix result: $PG_RESULT" | |
| echo "Reporting status conformance result: $STATUS_RESULT" | |
| echo "Durable materializer conformance result: $MATERIALIZER_RESULT" | |
| echo "Authenticated receipt conformance result: $RECEIPTS_RESULT" | |
| echo "Receipt rolling compatibility result: $RECEIPT_COMPATIBILITY_RESULT" | |
| echo "Frozen feed conformance result: $FEED_RESULT" | |
| echo "Feed rolling compatibility result: $FEED_COMPATIBILITY_RESULT" | |
| echo "Installed frozen feed result: $FEED_INSTALLED_RESULT" | |
| echo "pg-reporting-production result: $PRODUCTION_RESULT" | |
| echo "pg-reporting-production-compatibility result: $PRODUCTION_COMPATIBILITY_RESULT" | |
| echo "pg-reporting-production-installed result: $PRODUCTION_INSTALLED_RESULT" | |
| exit 1 | |
| fi | |
| echo "All Postgres conformance lanes passed" | |
| pg-reporting-status: | |
| name: Reporting status conformance (Postgres 16) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_status_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch exact reviewed A, B and C compatibility artifacts | |
| timeout-minutes: 1 | |
| run: | | |
| git fetch --no-tags --depth=1 origin \ | |
| 17ee407ae3978c8a2bb54437287afbf9dafb8130 \ | |
| 0f34c666ac1961e9832fce43ef0ef6937b3c1dde \ | |
| 967b6e286301d7e5d089aea6fdbb90bea8ee5a16 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies (with [pg] extra) | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev,pg]" | |
| - name: Run status semantics, transaction, clock/process and rolling gates | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_status_test | |
| run: python scripts/reporting_test_harness.py pytest tests/conformance/reporting/test_reporting_status_*.py -v -ra | |
| pg-reporting-materializer: | |
| name: Durable materializer and frozen artifacts (Postgres 16) | |
| runs-on: ubuntu-latest | |
| # Actual wheel builds/installations plus isolated worker processes are a | |
| # separate bounded job; do not consume the existing conformance headroom. | |
| # Seven real installations plus process/crash and Python 3.10 distribution | |
| # cells approach twenty minutes locally; allow runner variance and retain | |
| # five minutes outside the suite for setup and cleanup. | |
| timeout-minutes: 35 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_materializer_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch exact frozen reporting artifacts | |
| timeout-minutes: 2 | |
| run: | | |
| git fetch --no-tags --depth=1 origin \ | |
| 3e76aa54623529a3dda01cd690b8a5c287c75641 \ | |
| 3c405a21f978ed9d3208611bb4a7a8434a056933 \ | |
| 037de4ac822ecefb2f95d32c15c297fb4c45d683 \ | |
| 17ee407ae3978c8a2bb54437287afbf9dafb8130 \ | |
| 0f34c666ac1961e9832fce43ef0ef6937b3c1dde \ | |
| 967b6e286301d7e5d089aea6fdbb90bea8ee5a16 \ | |
| 5487f2bdef23c5102118b305be9e868228f6ce61 | |
| - uses: actions/setup-python@v6 | |
| id: materializer-python310 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run installed frozen binaries and materializer migrations | |
| # Propagate pytest failures through tee while retaining evidence for upload. | |
| shell: bash | |
| timeout-minutes: 30 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_materializer_test | |
| ADCP_PYTHON310: ${{ steps.materializer-python310.outputs.python-path }} | |
| run: | | |
| python scripts/reporting_test_harness.py pytest \ | |
| tests/conformance/reporting/test_reporting_materializer_rolling.py \ | |
| tests/conformance/reporting/test_reporting_materializer_migration.py \ | |
| tests/conformance/reporting/test_reporting_materializer_process.py \ | |
| tests/conformance/reporting/test_reporting_materializer_installed_pg.py \ | |
| -v -s -ra | tee materializer-evidence.log | |
| - name: Preserve installed module and worker evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: materializer-evidence-${{ github.run_attempt }} | |
| path: materializer-evidence.log | |
| if-no-files-found: error | |
| pg-reporting-receipts: | |
| name: Authenticated receipts and installed replay (Postgres 16) | |
| runs-on: ubuntu-latest | |
| # Separate databases avoid shared account advisory locks. Keep five minutes | |
| # outside the bounded suite for checkout, installations and cleanup. | |
| timeout-minutes: 35 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_receipts_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| id: receipt-python310 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run receipt transactions, real mounts, crash and installed gates | |
| shell: bash | |
| timeout-minutes: 30 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_receipts_test | |
| ADCP_PYTHON310: ${{ steps.receipt-python310.outputs.python-path }} | |
| run: | | |
| # Explicit pytest paths bypass --ignore; exclude rolling before invocation. | |
| shopt -s nullglob | |
| receipt_tests=() | |
| for test_file in tests/conformance/reporting/test_reporting_receipt_*.py; do | |
| if [[ "$test_file" != tests/conformance/reporting/test_reporting_receipt_rolling.py ]]; then | |
| receipt_tests+=("$test_file") | |
| fi | |
| done | |
| if (( ${#receipt_tests[@]} == 0 )); then | |
| echo "No receipt test files selected" >&2 | |
| exit 1 | |
| fi | |
| python scripts/reporting_test_harness.py pytest \ | |
| "${receipt_tests[@]}" \ | |
| -v -s -ra | tee pg-reporting-receipts-evidence.log | |
| - name: Preserve commands, counts, installed provenance and skip reasons | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-receipts-evidence-${{ github.run_attempt }} | |
| path: pg-reporting-receipts-evidence.log | |
| if-no-files-found: error | |
| pg-reporting-receipt-compatibility-shard: | |
| name: Receipt rolling shard (${{ matrix.artifact }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| artifact: [beta15, records, integration, a, b, c, b1, b21] | |
| env: | |
| RECEIPT_ARTIFACT: ${{ matrix.artifact }} | |
| RECEIPT_SOURCE_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_receipt_rolling_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| # runner.temp survives checkout cleanup. Setup failures still identify the | |
| # attempted shard; an incomplete artifact can never satisfy the aggregate. | |
| - name: Record attempted shard identity before checkout | |
| timeout-minutes: 1 | |
| shell: bash | |
| run: | | |
| python3 - <<'PYTHON' | |
| import json | |
| import os | |
| import re | |
| from pathlib import Path | |
| root = Path(os.environ["RUNNER_TEMP"]) / "receipt-rolling-evidence" | |
| root.mkdir(mode=0o700) | |
| fields = { | |
| "source_head": "RECEIPT_SOURCE_HEAD", "event_sha": "GITHUB_SHA", | |
| "event_name": "GITHUB_EVENT_NAME", "run_id": "GITHUB_RUN_ID", | |
| "run_attempt": "GITHUB_RUN_ATTEMPT", "repository": "GITHUB_REPOSITORY", | |
| "artifact": "RECEIPT_ARTIFACT", | |
| } | |
| values = {key: os.environ[value] for key, value in fields.items()} | |
| valid = ( | |
| all(re.fullmatch(r"[0-9a-f]{40}", values[k]) for k in ("source_head", "event_sha")) | |
| and all(re.fullmatch(r"[1-9][0-9]{0,19}", values[k]) for k in ("run_id", "run_attempt")) | |
| and values["repository"] == "adcontextprotocol/adcp-client-python" | |
| and values["event_name"] in {"pull_request", "push"} | |
| and values["artifact"] in {"beta15", "records", "integration", "a", "b", "c", "b1", "b21"} | |
| and (values["event_name"] != "push" or values["source_head"] == values["event_sha"]) | |
| ) | |
| if not valid: | |
| raise SystemExit("invalid shard attempt identity") | |
| (root / "attempted.json").write_text(json.dumps(values) + "\n") | |
| PYTHON | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| timeout-minutes: 2 | |
| with: | |
| ref: ${{ env.RECEIPT_SOURCE_HEAD }} | |
| persist-credentials: false | |
| - name: Fetch all eight exact approved historical artifacts | |
| timeout-minutes: 2 | |
| run: | | |
| git fetch --no-tags --depth=1 origin \ | |
| 3e76aa54623529a3dda01cd690b8a5c287c75641 \ | |
| 3c405a21f978ed9d3208611bb4a7a8434a056933 \ | |
| 037de4ac822ecefb2f95d32c15c297fb4c45d683 \ | |
| 17ee407ae3978c8a2bb54437287afbf9dafb8130 \ | |
| 0f34c666ac1961e9832fce43ef0ef6937b3c1dde \ | |
| 967b6e286301d7e5d089aea6fdbb90bea8ee5a16 \ | |
| 5487f2bdef23c5102118b305be9e868228f6ce61 \ | |
| 3fd62121c96a074e3ea458c30c5224d6a586f169 | |
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | |
| timeout-minutes: 2 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test and workflow-control dependencies | |
| timeout-minutes: 5 | |
| run: pip install -e ".[dev,pg]" "PyYAML==6.0.3" | |
| # The unchanged single node includes its notification branches and all | |
| # builds/migrations/recovery. Inner timeout is 15m plus <=30s cleanup; | |
| # all step ceilings total 33m, below this job's unchanged 35m ceiling. | |
| - name: Verify collection and run one complete historical parameter | |
| shell: bash | |
| timeout-minutes: 18 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_receipt_rolling_test | |
| run: | | |
| timeout --signal=TERM --kill-after=5s 60s python -m unittest discover -s scripts -p test_receipt_rolling_ci.py -v | |
| python scripts/receipt_rolling_ci.py run | |
| - name: Preserve safe partial output and exact shard result | |
| if: always() | |
| timeout-minutes: 3 | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: receipt-rolling-shard-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.artifact }} | |
| path: ${{ runner.temp }}/receipt-rolling-evidence/ | |
| if-no-files-found: error | |
| pg-reporting-receipt-compatibility: | |
| name: Receipt rolling compatibility (eight actual artifacts) | |
| needs: pg-reporting-receipt-compatibility-shard | |
| if: ${{ always() && !cancelled() }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 8 | |
| permissions: | |
| contents: read | |
| actions: read | |
| env: | |
| RECEIPT_SOURCE_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| timeout-minutes: 2 | |
| with: | |
| ref: ${{ env.RECEIPT_SOURCE_HEAD }} | |
| persist-credentials: false | |
| - name: Download this attempt's unmerged shard directories | |
| id: shard-download | |
| timeout-minutes: 3 | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: receipt-rolling-shard-${{ github.run_id }}-${{ github.run_attempt }}-* | |
| path: receipt-rolling-aggregate | |
| merge-multiple: false | |
| digest-mismatch: error | |
| # Hard job cancellation can prevent an always-upload step. Missing, | |
| # incomplete, duplicate or mismatched evidence is deliberately fatal. | |
| - name: Require every shard and verify exact run evidence | |
| if: ${{ always() && !cancelled() }} | |
| timeout-minutes: 2 | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RECEIPT_MATRIX_RESULT: ${{ needs.pg-reporting-receipt-compatibility-shard.result }} | |
| RECEIPT_DOWNLOAD_RESULT: ${{ steps.shard-download.outcome }} | |
| run: python scripts/receipt_rolling_ci.py aggregate | |
| pg-reporting-feed: | |
| name: Frozen authorized feed and mounted isolation (Postgres 16) | |
| runs-on: ubuntu-latest | |
| # Crash processes and shared memory/SQL vectors have their own database | |
| # and bounded suite; setup and cleanup have five minutes of headroom. | |
| timeout-minutes: 25 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_feed_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run frozen feeds, checkpoints, mounts, transaction faults and migrations | |
| shell: bash | |
| timeout-minutes: 20 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_feed_test | |
| run: | | |
| # Filter before pytest receives explicit paths; --ignore cannot do this. | |
| shopt -s nullglob | |
| feed_tests=() | |
| for test_file in tests/conformance/reporting/test_reporting_feed_*.py; do | |
| case "$test_file" in | |
| tests/conformance/reporting/test_reporting_feed_rolling.py|tests/conformance/reporting/test_reporting_feed_packaging.py|tests/conformance/reporting/test_reporting_feed_installed_pg.py|tests/conformance/reporting/test_reporting_feed_hardening_installed.py) ;; | |
| *) feed_tests+=("$test_file") ;; | |
| esac | |
| done | |
| if (( ${#feed_tests[@]} == 0 )); then | |
| echo "No native feed test files selected" >&2 | |
| exit 1 | |
| fi | |
| python scripts/reporting_test_harness.py pytest \ | |
| "${feed_tests[@]}" \ | |
| -v -s -ra | tee pg-reporting-feed-evidence.log | |
| - name: Preserve exact public, private, restart and fault evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-feed-evidence-${{ github.run_attempt }} | |
| path: pg-reporting-feed-evidence.log | |
| if-no-files-found: error | |
| pg-reporting-feed-compatibility: | |
| name: Feed rolling compatibility (nine actual artifacts) | |
| runs-on: ubuntu-latest | |
| # Actual B2.1 produces the artifact and actual B2.2 mounts receipts on | |
| # both sides of migration for every historical reader/writer cell. | |
| timeout-minutes: 50 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_feed_rolling_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch all nine exact approved historical artifacts | |
| timeout-minutes: 2 | |
| run: | | |
| git fetch --no-tags --depth=1 origin \ | |
| 3e76aa54623529a3dda01cd690b8a5c287c75641 \ | |
| 3c405a21f978ed9d3208611bb4a7a8434a056933 \ | |
| 037de4ac822ecefb2f95d32c15c297fb4c45d683 \ | |
| 17ee407ae3978c8a2bb54437287afbf9dafb8130 \ | |
| 0f34c666ac1961e9832fce43ef0ef6937b3c1dde \ | |
| 967b6e286301d7e5d089aea6fdbb90bea8ee5a16 \ | |
| 5487f2bdef23c5102118b305be9e868228f6ce61 \ | |
| 3fd62121c96a074e3ea458c30c5224d6a586f169 \ | |
| 09fd87f79a746665d828dea66b3a1dd9d1fc189e | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run historical binaries before and after the isolated feed migration | |
| shell: bash | |
| timeout-minutes: 45 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_feed_rolling_test | |
| run: | | |
| python scripts/reporting_test_harness.py pytest \ | |
| tests/conformance/reporting/test_reporting_feed_rolling.py \ | |
| -v -s -ra | tee pg-reporting-feed-compatibility-evidence.log | |
| - name: Preserve exact artifact origins, catalogs and original replay responses | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-feed-compatibility-evidence-${{ github.run_attempt }} | |
| path: pg-reporting-feed-compatibility-evidence.log | |
| if-no-files-found: error | |
| pg-reporting-feed-installed: | |
| name: Installed frozen feed (Python 3.10 VCS and sdist) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 50 | |
| permissions: | |
| contents: read | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_feed_installed_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch the exact integrated B2.3 hardening comparison artifact | |
| timeout-minutes: 1 | |
| run: git fetch --no-tags origin 2d777ace7b4bf8be519ce0abd4fd0a25ed4f1da7 | |
| - uses: actions/setup-python@v6 | |
| id: feed-python310 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run installed base and PostgreSQL restart cells | |
| shell: bash | |
| timeout-minutes: 40 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_feed_installed_test | |
| ADCP_PYTHON310: ${{ steps.feed-python310.outputs.python-path }} | |
| ADCP_HARDENING_EVIDENCE: ${{ runner.temp }}/hardening-installed-evidence | |
| run: | | |
| python scripts/reporting_test_harness.py pytest \ | |
| tests/conformance/reporting/test_reporting_feed_packaging.py \ | |
| tests/conformance/reporting/test_reporting_feed_installed_pg.py \ | |
| tests/conformance/reporting/test_reporting_feed_hardening_installed.py \ | |
| -v -s -ra | tee pg-reporting-feed-installed-evidence.log | |
| - name: Preserve installed origins, SQL, strict adopter and cold page evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-feed-installed-evidence-${{ github.run_attempt }} | |
| path: | | |
| pg-reporting-feed-installed-evidence.log | |
| ${{ runner.temp }}/hardening-installed-evidence | |
| if-no-files-found: error | |
| pg-reporting-production: | |
| name: Production reporting status, ownership and notification contracts | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 35 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_production_test | |
| ports: ["5432:5432"] | |
| options: >- | |
| --health-cmd pg_isready --health-interval 5s | |
| --health-timeout 5s --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run complete production and projection conformance | |
| shell: bash | |
| timeout-minutes: 30 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_production_test | |
| run: | | |
| # Explicit pytest paths bypass --ignore; preselect and reject empty globs. | |
| shopt -s nullglob | |
| source_tests=() | |
| for test in tests/conformance/reporting/test_reporting_production*.py; do | |
| case "$test" in | |
| *test_reporting_production_packaging.py|*test_reporting_production_rolling.py) continue ;; | |
| esac | |
| source_tests+=("$test") | |
| done | |
| projection_tests=(tests/conformance/reporting/test_reporting_projection*.py) | |
| if (( ${#source_tests[@]} == 0 || ${#projection_tests[@]} == 0 )); then | |
| echo "Production or projection test selection is empty" | |
| exit 1 | |
| fi | |
| python scripts/reporting_test_harness.py pytest \ | |
| "${source_tests[@]}" \ | |
| "${projection_tests[@]}" \ | |
| tests/conformance/reporting/test_reporting_tier_projection.py \ | |
| tests/conformance/reporting/test_reporting_schedule_schema.py \ | |
| tests/test_reporting_capability_models.py \ | |
| tests/test_reporting_revision_ownership.py \ | |
| tests/test_reporting_production_public.py \ | |
| tests/test_schema_datetime_formats.py \ | |
| tests/test_rc6_adoption.py \ | |
| -v -s -ra | tee pg-reporting-production-evidence.log | |
| - name: Preserve production contract evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-production-evidence-${{ github.run_attempt }} | |
| path: pg-reporting-production-evidence.log | |
| if-no-files-found: error | |
| pg-reporting-production-installed: | |
| name: Installed production reporting (${{ matrix.cell }}, Python 3.10) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 40 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| cell: [base-vcs, base-sdist, pg-vcs, pg-sdist] | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_production_installed_test | |
| ports: ["5432:5432"] | |
| options: >- | |
| --health-cmd pg_isready --health-interval 5s | |
| --health-timeout 5s --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| id: production-python310 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run installed source-equivalent production contract | |
| shell: bash | |
| timeout-minutes: 35 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_production_installed_test | |
| ADCP_PYTHON310: ${{ steps.production-python310.outputs.python-path }} | |
| ADCP_PRODUCTION_EVIDENCE: ${{ runner.temp }}/production-installed-evidence | |
| PRODUCTION_CELL: ${{ matrix.cell }} | |
| run: | | |
| python scripts/reporting_test_harness.py pytest \ | |
| tests/conformance/reporting/test_reporting_production_packaging.py \ | |
| -k "$PRODUCTION_CELL" -v -s -ra | tee pg-reporting-production-installed.log | |
| - name: Preserve installed origins, original inner logs and exact asset hashes | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-production-installed-${{ matrix.cell }}-${{ github.run_attempt }} | |
| path: | | |
| pg-reporting-production-installed.log | |
| ${{ runner.temp }}/production-installed-evidence | |
| if-no-files-found: error | |
| pg-reporting-production-compatibility: | |
| name: B2.3 and hardening to installed B2.4 activation and restart | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 50 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp_production_rolling_test | |
| ports: ["5432:5432"] | |
| options: >- | |
| --health-cmd pg_isready --health-interval 5s | |
| --health-timeout 5s --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Fetch integrated feed, hardening and production comparison artifacts | |
| timeout-minutes: 1 | |
| run: git fetch --no-tags origin 2d777ace7b4bf8be519ce0abd4fd0a25ed4f1da7 e16eb8cf3074cabd45aab42840950f05ad6d2b43 34c8f6d929aeac3407e2f595104a8e903e572623 | |
| - uses: actions/setup-python@v6 | |
| id: production-python310 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install test dependencies | |
| run: pip install -e ".[dev,pg]" | |
| - name: Run actual historical page one, activation, SIGKILL and cold continuations | |
| shell: bash | |
| timeout-minutes: 42 | |
| env: | |
| ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_production_rolling_test | |
| ADCP_PYTHON310: ${{ steps.production-python310.outputs.python-path }} | |
| ADCP_PRODUCTION_EVIDENCE: ${{ runner.temp }}/production-rolling-evidence | |
| run: | | |
| python scripts/reporting_test_harness.py pytest \ | |
| tests/conformance/reporting/test_reporting_production_rolling.py \ | |
| tests/conformance/reporting/test_reporting_rc6_rolling.py \ | |
| -v -s -ra | tee pg-reporting-production-rolling.log | |
| - name: Preserve exact installed historical continuity and fence evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: pg-reporting-production-rolling-${{ github.run_attempt }} | |
| path: | | |
| pg-reporting-production-rolling.log | |
| ${{ runner.temp }}/production-rolling-evidence | |
| if-no-files-found: error | |
| conventional-commits: | |
| name: Validate conventional commit format | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate PR commits | |
| uses: amannn/action-semantic-pull-request@v6 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate individual commits | |
| run: | | |
| # Get the base branch | |
| BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD) | |
| # Check each commit since the base | |
| echo "Validating commits since $BASE_SHA..." | |
| git log --format="%H %s" $BASE_SHA..HEAD | while read sha message; do | |
| # Skip merge commits. Three GitHub/git-created shapes: | |
| # - "Merge <sha> into <sha>" — the merge-queue API path | |
| # (clicking "Update branch" on a PR) | |
| # - "Merge branch '<name>' [into <name>]" — `gh pr update-branch` | |
| # and `git merge <local-branch>` defaults | |
| # - "Merge remote-tracking branch '<name>' [into <name>]" — | |
| # `git merge origin/<branch>` default | |
| if echo "$message" | grep -qE "^Merge ([0-9a-f]+ into [0-9a-f]+|(remote-tracking )?branch '[^']+')"; then | |
| echo "⊙ Skipping merge commit: $sha" | |
| continue | |
| fi | |
| # Check if message matches conventional commit format | |
| if ! echo "$message" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: .+'; then | |
| echo "❌ Commit $sha does not follow Conventional Commits format:" | |
| echo " $message" | |
| echo "" | |
| echo "Expected format: <type>[optional scope]: <description>" | |
| echo "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert" | |
| echo "" | |
| echo "Examples:" | |
| echo " feat: add new feature" | |
| echo " fix: resolve bug in parser" | |
| echo " feat(api): add new endpoint" | |
| echo " feat!: breaking change" | |
| exit 1 | |
| else | |
| echo "✓ $sha: $message" | |
| fi | |
| done | |
| echo "" | |
| echo "✅ All commits follow Conventional Commits format" | |
| downstream-imports: | |
| name: Downstream import smoke (representative consumer symbols) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Build sdist and install its wheel | |
| run: | | |
| python -m pip install --upgrade pip build | |
| test ! -e src/adcp/_schemas | |
| # The default build path creates the sdist first, then builds the | |
| # wheel from that sdist. This covers both VCS and sdist consumers. | |
| python -m build --outdir dist/ | |
| pip install dist/*.whl | |
| - name: Verify VCS-built wheel schemas | |
| working-directory: /tmp | |
| run: | | |
| python - <<'PY' | |
| import json | |
| from importlib.resources import files | |
| from adcp import get_adcp_spec_version | |
| from adcp.server.helpers import STANDARD_ERROR_CODES, adcp_error | |
| from adcp.types import ReportingCapabilities | |
| from adcp.types.versioned import make_versioned_base | |
| spec_version = get_adcp_spec_version() | |
| current_bundle = ( | |
| spec_version | |
| if "-" in spec_version | |
| else ".".join(spec_version.split(".")[:2]) | |
| ) | |
| schema_root = files("adcp") / "_schemas" | |
| packaged_versions = { | |
| child.name | |
| for child in schema_root.iterdir() | |
| if child.is_dir() | |
| } | |
| # Preserve explicit rc.3 and the frozen v32 (beta.6) schemas offline. | |
| assert packaged_versions == { | |
| "2.5", "3.0", "3.1", "3.2.0-beta.6", "3.2.0-rc.3", current_bundle | |
| } | |
| error_schema = json.loads( | |
| (schema_root / current_bundle / "enums/error-code.json").read_text(encoding="utf-8") | |
| ) | |
| # NOT_SUPPORTED is the SDK's explicit extension to the pinned vocabulary. | |
| assert set(STANDARD_ERROR_CODES) == {*error_schema["enum"], "NOT_SUPPORTED"} | |
| assert adcp_error("AUTH_MISSING", "x")["errors"][0]["recovery"] == "correctable" | |
| assert make_versioned_base("3.1", "ListCreativesRequest") | |
| assert ReportingCapabilities.__name__ == "ReportingCapabilities" | |
| PY | |
| # Proxy for real downstream import sites (salesagent, creative agents, | |
| # signals agents). Any ImportError here means we broke the public API | |
| # surface without a migration pointer — failing CI is the goal. | |
| - name: Import representative public-API symbols | |
| run: | | |
| python - <<'PY' | |
| from adcp import ( | |
| ADCPClient, | |
| AgentConfig, | |
| BrandReference, | |
| CpmPricingOption, | |
| CreateMediaBuyRequest, | |
| Error, | |
| GetProductsRequest, | |
| ListCreativesRequest, | |
| MediaBuyStatus, | |
| Package, | |
| PackageRequest, | |
| PublisherPropertiesAll, | |
| SyncCatalogsRequest, | |
| ) | |
| from adcp.types import ( | |
| AudioFormatAsset, | |
| BriefFormatAsset, | |
| CatalogFormatAsset, | |
| ContextObject, | |
| CreativeAsset, | |
| CssFormatAsset, | |
| DaastFormatAsset, | |
| HtmlFormatAsset, | |
| ImageFormatAsset, | |
| JavascriptFormatAsset, | |
| MarkdownFormatAsset, | |
| RepeatableAssetGroup, | |
| TargetingOverlay, | |
| TextFormatAsset, | |
| UrlFormatAsset, | |
| VastFormatAsset, | |
| VideoFormatAsset, | |
| WebhookFormatAsset, | |
| ) | |
| # Removed-type shims: old import paths must raise a guided | |
| # ImportError pointing at the migration guide. | |
| import adcp | |
| for name in ("BrandManifest", "FormatCategory", "DeliverTo"): | |
| try: | |
| getattr(adcp, name) | |
| except ImportError as exc: | |
| assert "MIGRATION_v3_to_v4" in str(exc), ( | |
| f"{name} deprecation shim dropped migration pointer: {exc}" | |
| ) | |
| else: | |
| raise AssertionError( | |
| f"{name} import should raise ImportError with migration pointer" | |
| ) | |
| # The deep submodule path (some older import sites reach this far) | |
| # must also surface the migration pointer, not a bare ModuleNotFoundError. | |
| try: | |
| from adcp.types.generated_poc.enums.format_category import FormatCategory # noqa: F401 | |
| except ImportError as exc: | |
| assert "MIGRATION_v3_to_v4" in str(exc), exc | |
| else: | |
| raise AssertionError( | |
| "format_category submodule should raise ImportError with migration pointer" | |
| ) | |
| assert adcp.__version__ and adcp.__version__ != "3.12.0", ( | |
| f"adcp.__version__={adcp.__version__!r} — expected real pkg metadata" | |
| ) | |
| assert adcp.get_adcp_version(), "ADCP_VERSION file is empty" | |
| print(f"OK — adcp=={adcp.__version__}, spec={adcp.get_adcp_version()}") | |
| PY | |
| schema-check: | |
| name: Validate schemas are up-to-date | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Check ADCP version | |
| id: version-check | |
| run: | | |
| VERSION=$(cat src/adcp/ADCP_VERSION) | |
| echo "ADCP_VERSION=$VERSION" | |
| # Skip regeneration + drift check for pre-release tags (alpha/beta/rc) | |
| # and for `latest`, which is a moving dev snapshot — the committed | |
| # generated types are frozen against the bundle we last synced, and | |
| # CI's fresh sync against today's `latest.tgz` is expected to drift. | |
| if echo "$VERSION" | grep -qE '(alpha|beta|rc)' || [ "$VERSION" = "latest" ]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "Pre-release / latest version detected - will skip schema sync" | |
| else | |
| echo "is_prerelease=false" >> $GITHUB_OUTPUT | |
| echo "Stable version - will sync schemas from upstream" | |
| fi | |
| # Stable upstream tags ship Sigstore sidecars; sync_schemas.py | |
| # verifies the bundle via `cosign verify-blob` before extraction. | |
| # Installs cosign from the official sigstore/cosign-installer action. | |
| - name: Install cosign (for signature verification) | |
| if: steps.version-check.outputs.is_prerelease != 'true' | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Download latest schemas | |
| if: steps.version-check.outputs.is_prerelease != 'true' | |
| run: python scripts/sync_schemas.py | |
| - name: Fix schema references | |
| if: steps.version-check.outputs.is_prerelease != 'true' | |
| run: python scripts/fix_schema_refs.py | |
| - name: Bundle schemas into package | |
| if: steps.version-check.outputs.is_prerelease != 'true' | |
| run: python scripts/bundle_schemas.py | |
| - name: Generate models | |
| run: python scripts/generate_types.py | |
| - name: Validate generated code syntax | |
| run: | | |
| echo "Validating generated code can be parsed..." | |
| python -m py_compile src/adcp/types/_generated.py | |
| echo "✓ Syntax validation passed" | |
| - name: Validate generated code imports | |
| run: | | |
| echo "Validating generated code can be imported..." | |
| python -c "from adcp.types import _generated as generated; print(f'✓ Successfully imported {len(dir(generated))} symbols')" | |
| - name: Run code generation tests | |
| run: | | |
| echo "Running code generation test suite..." | |
| pytest tests/test_code_generation.py -v --tb=short | |
| - name: Check for schema drift | |
| if: steps.version-check.outputs.is_prerelease != 'true' | |
| run: | | |
| # datamodel-codegen's numbered-variant class names | |
| # (Pass1/Pass4, Status16/Status17, StatusFilter1/StatusFilter4, | |
| # Type80, etc.) shift between regens because the generator | |
| # walks the schema graph in filesystem-iteration order and | |
| # APFS (macOS) vs. ext4 (Linux CI) sort differently. The | |
| # numbers are an implementation detail; semantic aliases in | |
| # ``src/adcp/types/aliases.py`` pin the names downstream | |
| # actually uses. | |
| # | |
| # The real drift guarantees we need are enforced elsewhere: | |
| # * ``tests/test_schemas_version_pin.py`` — ADCP_VERSION | |
| # matches ``schemas/cache/index.json.adcp_version`` on | |
| # every test run. | |
| # * This job's "Validate generated code syntax/imports" | |
| # steps above — the regenerated code compiles and imports. | |
| # * ``tests/test_asset_aliases_stable.py`` — the semantic | |
| # aliases still point at valid classes. | |
| # | |
| # We keep this step as a "regen runs without error on stable | |
| # tags" smoke — but don't fail on line-level diff, because | |
| # the non-determinism produces false positives that block | |
| # release PRs for cosmetic churn. | |
| if git diff --quiet src/adcp/types/_generated.py schemas/cache/; then | |
| echo "✓ Schemas are up-to-date (no diff)" | |
| else | |
| echo "ℹ Regen produced cosmetic diff — see aliases.py for stable names" | |
| echo " Numbered-variant class-name churn is expected; the semantic" | |
| echo " alias tests and drift-version-pin test guard the real surface." | |
| fi | |
| storyboard: | |
| name: AdCP storyboard runner — examples/seller_agent.py (@adcp/sdk ${{ matrix.adcp-sdk-tag }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Blocking gate: examples/seller_agent.py is the Python-owned | |
| # reference target for bidirectional storyboard interop. The matrix | |
| # runs two legs: the sticky ``adcp-3.0`` tag is a fixed, reproducible | |
| # backwards-compat floor (the AdCP 3.0 runner line), while ``latest`` | |
| # tracks the current stable @adcp/sdk release (the AdCP 3.1 line). | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| adcp-sdk-tag: ["adcp-3.0", "latest"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Set up Node 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| # Cache the npm tarball + extracted package directory so the | |
| # storyboard runner install isn't a cold network fetch every run. | |
| # The cache key is salted by the npm dist-tag; npm still resolves | |
| # the current concrete package for that tag on install. | |
| - name: Cache ~/.npm | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-npm-adcp-sdk-${{ matrix.adcp-sdk-tag }} | |
| restore-keys: | | |
| ${{ runner.os }}-npm- | |
| - name: Run reference seller storyboard harness | |
| timeout-minutes: 15 | |
| env: | |
| PYTHON: python | |
| ADCP_SDK_VERSION: ${{ matrix.adcp-sdk-tag }} | |
| STORYBOARD_RESULT_PATH: storyboard-result-${{ matrix.adcp-sdk-tag }}.json | |
| run: | | |
| scripts/ci/run_storyboard_reference_seller.sh | |
| - if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: storyboard-result-${{ matrix.adcp-sdk-tag }}-${{ github.run_attempt }} | |
| path: storyboard-result-${{ matrix.adcp-sdk-tag }}.json | |
| if-no-files-found: warn | |
| storyboard-required-gate: | |
| name: AdCP storyboard runner — examples/seller_agent.py | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: storyboard | |
| # Run after a failed matrix so the aggregate required check fails, but | |
| # do not keep superseded workflow runs alive after concurrency cancels them. | |
| if: ${{ !cancelled() }} | |
| steps: | |
| - name: Require all reference seller storyboard matrix runs | |
| run: | | |
| if [ "${{ needs.storyboard.result }}" != "success" ]; then | |
| echo "Reference seller storyboard matrix result: ${{ needs.storyboard.result }}" | |
| exit 1 | |
| fi | |
| echo "Reference seller storyboard matrix passed" | |
| v3-reference-seller-tests: | |
| name: v3 reference seller — pytest (respx-mocked upstream) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev,pg]" | |
| # Example-local deps: the v3 reference seller imports | |
| # sqlalchemy + asyncpg + httpx-respx but those aren't in the | |
| # SDK's [dev] extras. Install them inline rather than adding | |
| # a separate optional-dependencies group for a single example. | |
| pip install "sqlalchemy[asyncio]>=2.0" "asyncpg>=0.29" "respx>=0.20" | |
| - name: Run translator-pattern tests | |
| # The tests respx-mock the JS mock-server upstream so we don't | |
| # need to boot Node here. Storyboard CI (below) covers the | |
| # real boot-the-upstream path. | |
| run: | | |
| pytest examples/v3_reference_seller/tests/ -v | |
| storyboard-v3-reference-seller: | |
| name: AdCP storyboard runner — v3 reference seller (translator) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| # Intentionally non-secret: gates loopback-only debug counters | |
| # inside this CI job. | |
| ADCP_DEBUG_TOKEN: storyboard-debug-token | |
| # Required as of @adcp/sdk@6.7.0 (sales-guaranteed mock-server | |
| # canonicalized; closes #449). Storyboard run + traffic-counter | |
| # assertions gate every PR's translator-pattern conformance. | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| POSTGRES_DB: adcp | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Set up Node 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| # Same cache pattern as the storyboard job: keyed by | |
| # ADCP_SDK_VERSION so a bump invalidates deterministically. | |
| - name: Cache ~/.npm | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-npm-adcp-sdk-${{ env.ADCP_SDK_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-npm- | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev,pg]" | |
| # Example-local deps: the v3 reference seller imports | |
| # sqlalchemy + asyncpg (async Postgres driver) which aren't | |
| # in the SDK's [dev,pg] extras. Install inline rather than | |
| # adding an example-only optional-dependencies group. | |
| pip install "sqlalchemy[asyncio]>=2.0" "asyncpg>=0.29" "respx>=0.20" | |
| - name: Pre-install @adcp/sdk (once, then call binary directly) | |
| # See the comment on the storyboard job's install step for the | |
| # AAO reference-formats fixture rationale (upstream adcp#3307). | |
| run: | | |
| npm install -g @adcp/sdk@${ADCP_SDK_VERSION} | |
| adcp --version | |
| SDK_ROOT="$(npm root -g)/@adcp/sdk" | |
| mkdir -p "${SDK_ROOT}/test/lib/v2-projection-fixtures" | |
| cp tests/fixtures/aao-reference-formats.json "${SDK_ROOT}/test/lib/v2-projection-fixtures/aao-reference-formats.json" | |
| mkdir -p "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries" | |
| cp tests/fixtures/v1-canonical-mapping.json "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries/v1-canonical-mapping.json" | |
| - name: Start JS mock-server upstream | |
| run: | | |
| # Cached install above means this is a hot-start (~2-3s on | |
| # GHA-hosted runners) — no npm install delay. Tee output to | |
| # /tmp/mock-server.log so failures surface their stack traces | |
| # (otherwise the readiness loop just times out silently). | |
| adcp mock-server sales-guaranteed --port 4503 --api-key test-key \ | |
| > /tmp/mock-server.log 2>&1 & | |
| MOCK_PID=$! | |
| echo "MOCK_PID=$MOCK_PID" >> "$GITHUB_ENV" | |
| # Health-check via /_debug/traffic — non-network-scoped and | |
| # no-auth, so it doesn't break when the JS mock's seed-data | |
| # renames or removes a specific network. The endpoint is | |
| # always present on the harness-side mock. | |
| for i in $(seq 1 120); do | |
| # ``||`` runs on the assignment so curl's "000" stdout and the | |
| # fallback don't concatenate when the connection is refused. | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \ | |
| http://127.0.0.1:4503/_debug/traffic 2>/dev/null) || HTTP_CODE="000" | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "Upstream mock ready (HTTP 200, pid $MOCK_PID, $i polls)" | |
| break | |
| fi | |
| if [ "$i" -eq 120 ]; then | |
| echo "Upstream mock failed to start within 60s" | |
| echo "--- mock-server.log ---" | |
| cat /tmp/mock-server.log || echo "(log unavailable)" | |
| kill "$MOCK_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| sleep 0.5 | |
| done | |
| - name: Seed Postgres fixtures | |
| env: | |
| DATABASE_URL: postgresql+asyncpg://postgres@127.0.0.1:5432/adcp | |
| run: | | |
| cd examples/v3_reference_seller | |
| python -m seed | |
| - name: Boot v3 reference seller (translator) | |
| env: | |
| DATABASE_URL: postgresql+asyncpg://postgres@127.0.0.1:5432/adcp | |
| MOCK_AD_SERVER_URL: http://127.0.0.1:4503 | |
| MOCK_AD_SERVER_API_KEY: test-key | |
| PORT: "3001" | |
| run: | | |
| cd examples/v3_reference_seller | |
| python -m src.app > /tmp/v3-reference-seller.log 2>&1 & | |
| SELLER_PID=$! | |
| echo "SELLER_PID=$SELLER_PID" >> "$GITHUB_ENV" | |
| for i in $(seq 1 120); do | |
| # ``||`` runs on the assignment so curl's "000" stdout and the | |
| # fallback don't concatenate when the connection is refused. | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \ | |
| http://127.0.0.1:3001/mcp 2>/dev/null) || HTTP_CODE="000" | |
| if [ "$HTTP_CODE" != "000" ]; then | |
| echo "Seller ready (HTTP ${HTTP_CODE}, pid ${SELLER_PID})" | |
| break | |
| fi | |
| if [ "$i" -eq 120 ]; then | |
| echo "Seller failed to start within 60s" | |
| echo "--- v3-reference-seller.log ---" | |
| cat /tmp/v3-reference-seller.log || echo "(log unavailable)" | |
| kill "$SELLER_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| sleep 0.5 | |
| done | |
| # Upstream-still-alive probe — guard against the upstream | |
| # dying during seller startup (e.g. seller's connection | |
| # handshake crashes the mock). If the upstream is gone, | |
| # the storyboard run will fail in confusing ways; fail | |
| # here with a clear diagnostic instead. | |
| UPSTREAM_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \ | |
| -H "Authorization: Bearer test-key" \ | |
| -H "X-Network-Code: net_premium_us" \ | |
| http://127.0.0.1:4503/v1/products 2>/dev/null || echo "000") | |
| if [ "$UPSTREAM_CODE" != "200" ]; then | |
| echo "Upstream mock no longer responding after seller boot (HTTP ${UPSTREAM_CODE})" | |
| echo "The seller likely crashed the upstream during connection handshake." | |
| kill "$SELLER_PID" 2>/dev/null || true | |
| kill "$MOCK_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "Upstream still alive after seller boot (HTTP ${UPSTREAM_CODE})" | |
| - name: Run storyboard suite | |
| timeout-minutes: 5 | |
| run: | | |
| # /etc/hosts override so the buyer can reach acme.localhost | |
| # (the seeded tenant subdomain). | |
| echo "127.0.0.1 acme.localhost" | sudo tee -a /etc/hosts | |
| cd examples/v3_reference_seller | |
| # ``adcp`` was installed once at job start — call the binary | |
| # directly to skip per-invocation ``npx`` extract+link. | |
| # --auth carries the seeded bearer credential (seed.py:74) so the | |
| # storyboard runner's requests resolve through the v3 ref seller's | |
| # PgBuyerAgentRegistry to ba_acme_bearer. Without this, every | |
| # request hits the Tier 2 commercial-allowlist gate and storyboard | |
| # steps fail with PERMISSION_DENIED across the board. | |
| adcp storyboard run \ | |
| http://acme.localhost:3001/mcp media_buy_seller \ | |
| --auth dev-bearer-token-acme-1 \ | |
| --test-kit ../../tests/fixtures/storyboard-test-kit.yaml \ | |
| --json --allow-http \ | |
| > v3-storyboard-result.json | |
| - name: Assert storyboard runner produced output | |
| run: | | |
| # Hard gate: the runner must produce a non-empty result file. | |
| # The anti-façade invariant (the seller actually called upstream) | |
| # is enforced by "Assert upstream traffic" below — controller_detected | |
| # is observational; it has never been true for the v3 reference | |
| # seller's translator topology (the runner detects it for the | |
| # examples/seller_agent.py stub-mode topology only). | |
| python -c " | |
| import json, sys, pathlib | |
| p = pathlib.Path('examples/v3_reference_seller/v3-storyboard-result.json') | |
| if not p.exists() or p.stat().st_size == 0: | |
| print('v3-storyboard-result.json missing or empty — runner produced no output') | |
| sys.exit(1) | |
| with p.open() as f: | |
| d = json.load(f) | |
| print('overall_status:', d.get('overall_status')) | |
| print('controller_detected:', d.get('controller_detected')) | |
| print('summary:', json.dumps(d.get('summary', {}), indent=2)) | |
| " | |
| - name: Assert storyboard passed | |
| # Hard gate. This translator intentionally has no | |
| # ``comply_test_controller``: it exercises a real upstream rather | |
| # than owning the runner's state-control API. SDK 13 therefore grades | |
| # its coverage ``partial`` even when every executable step passes. | |
| # Accept only that exact topology — zero failed steps/tracks, useful | |
| # executed coverage, and no skip class outside controller absence, | |
| # its dependent steps, missing controller tools, or not-applicable | |
| # capabilities. Any real failure or new skip class still blocks CI. | |
| run: | | |
| python -c " | |
| import json, sys, pathlib | |
| p = pathlib.Path('examples/v3_reference_seller/v3-storyboard-result.json') | |
| with p.open() as f: | |
| d = json.load(f) | |
| status = d.get('overall_status') | |
| summary = d.get('summary') or {} | |
| skip_counts = summary.get('skipped_by_reason') or {} | |
| allowed_partial_skips = { | |
| 'missing_test_controller', | |
| 'missing_tool', | |
| 'prerequisite_failed', | |
| 'not_applicable', | |
| } | |
| unexpected_skips = sorted(set(skip_counts) - allowed_partial_skips) | |
| steps = [ | |
| step | |
| for track in d.get('tracks', []) | |
| for scenario in track.get('scenarios', []) | |
| for step in scenario.get('steps', []) | |
| ] | |
| unexpected_missing_tools = [ | |
| step.get('task') | |
| for step in steps | |
| if step.get('skip_reason') == 'missing_tool' | |
| and step.get('task') != 'comply_test_controller' | |
| ] | |
| unexpected_prerequisites = [ | |
| step.get('error') | |
| for step in steps | |
| if step.get('skip_reason') == 'prerequisite_failed' | |
| and 'skipped (missing_tool)' not in (step.get('error') or '') | |
| ] | |
| partial_is_expected = ( | |
| status == 'partial' | |
| and summary.get('steps_failed') == 0 | |
| and summary.get('tracks_failed') == 0 | |
| and (summary.get('steps_passed') or 0) > 0 | |
| and (skip_counts.get('missing_test_controller') or 0) > 0 | |
| and not unexpected_skips | |
| and not unexpected_missing_tools | |
| and not unexpected_prerequisites | |
| and d.get('controller_detected') is False | |
| ) | |
| if status != 'passing' and not partial_is_expected: | |
| print(json.dumps(d, indent=2)) | |
| if unexpected_skips: | |
| print(f'Unexpected partial skip classes: {unexpected_skips}') | |
| if unexpected_missing_tools: | |
| print(f'Unexpected missing tools: {unexpected_missing_tools}') | |
| if unexpected_prerequisites: | |
| print(f'Unexpected prerequisite skips: {unexpected_prerequisites}') | |
| sys.exit(1) | |
| print(f'Storyboard accepted: status={status}, summary={summary}') | |
| " | |
| - name: Assert upstream traffic (anti-façade gate) | |
| run: | | |
| # The v3 reference seller exposes /_debug/traffic with per-method | |
| # upstream-call counts. If the storyboard passed but the counts | |
| # are empty/zero, the seller served stub data without translating | |
| # to upstream — the façade-mode failure this job exists to catch | |
| # (see #410). | |
| curl -sf -H "X-Debug-Token: ${ADCP_DEBUG_TOKEN}" \ | |
| http://127.0.0.1:3001/_debug/traffic > traffic.json | |
| python -c " | |
| import json, sys | |
| with open('traffic.json') as f: | |
| counts = json.load(f) | |
| if not counts: | |
| print('Empty traffic snapshot — seller invoked no upstream methods.') | |
| print('This is the façade-mode failure #410 gates against.') | |
| sys.exit(1) | |
| total = sum(counts.values()) | |
| if total == 0: | |
| print(f'All upstream-method counts zero: {counts!r}') | |
| print('Seller served stub data without calling upstream — façade failure.') | |
| sys.exit(1) | |
| print(f'Upstream traffic: total={total} per-method={counts!r}') | |
| " | |
| - if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: v3-storyboard-result-${{ github.run_attempt }} | |
| path: examples/v3_reference_seller/v3-storyboard-result.json | |
| if-no-files-found: warn | |
| storyboard-multi-platform-seller: | |
| name: AdCP storyboard runner — examples/multi_platform_seller (PlatformRouter) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Multi-tenant proof: one process, two tenants, one router. Each | |
| # tenant's storyboard runs against its own subdomain | |
| # (tenant-a.localhost / tenant-b.localhost). Blocking gate — both | |
| # tenants must pass the AdCP storyboard suite for the example to | |
| # remain a credible reference. continue-on-error was dropped after | |
| # the mocks were aligned with the wire contract; future drift now | |
| # blocks CI. | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Set up Node 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - name: Cache ~/.npm | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-npm-adcp-sdk-${{ env.ADCP_SDK_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-npm- | |
| - name: Pre-install @adcp/sdk | |
| # See the comment on the storyboard job's install step for the | |
| # AAO reference-formats fixture rationale (upstream adcp#3307). | |
| run: | | |
| npm install -g @adcp/sdk@${ADCP_SDK_VERSION} | |
| adcp --version | |
| SDK_ROOT="$(npm root -g)/@adcp/sdk" | |
| mkdir -p "${SDK_ROOT}/test/lib/v2-projection-fixtures" | |
| cp tests/fixtures/aao-reference-formats.json "${SDK_ROOT}/test/lib/v2-projection-fixtures/aao-reference-formats.json" | |
| mkdir -p "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries" | |
| cp tests/fixtures/v1-canonical-mapping.json "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries/v1-canonical-mapping.json" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Map tenant subdomains to localhost | |
| run: | | |
| # The storyboard runner connects to ``tenant-x.localhost`` — | |
| # /etc/hosts gives those names a 127.0.0.1 mapping so the | |
| # subdomain middleware on the seller process resolves the | |
| # right tenant from the Host header. | |
| echo "127.0.0.1 tenant-a.localhost tenant-b.localhost" \ | |
| | sudo tee -a /etc/hosts | |
| - name: Boot multi-platform seller | |
| run: | | |
| ADCP_PORT=3001 python -m examples.multi_platform_seller.src.app & | |
| SELLER_PID=$! | |
| echo "SELLER_PID=$SELLER_PID" >> "$GITHUB_ENV" | |
| for i in $(seq 1 60); do | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \ | |
| http://127.0.0.1:3001/mcp 2>/dev/null) || HTTP_CODE="000" | |
| if [ "$HTTP_CODE" != "000" ]; then | |
| echo "Seller ready (HTTP ${HTTP_CODE}, pid ${SELLER_PID})" | |
| break | |
| fi | |
| if ! kill -0 "$SELLER_PID" 2>/dev/null; then | |
| echo "Seller process died during startup" | |
| exit 1 | |
| fi | |
| if [ "$i" -eq 60 ]; then | |
| echo "Seller failed to start within 30s" | |
| kill "$SELLER_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| sleep 0.5 | |
| done | |
| - name: Run storyboard — tenant-a (sales-guaranteed) | |
| timeout-minutes: 5 | |
| run: | | |
| adcp storyboard run \ | |
| http://tenant-a.localhost:3001/mcp media_buy_seller \ | |
| --json --allow-http \ | |
| > tenant-a-storyboard.json | |
| cat tenant-a-storyboard.json | head -50 | |
| - name: Run storyboard — tenant-b (sales-non-guaranteed) | |
| timeout-minutes: 5 | |
| run: | | |
| adcp storyboard run \ | |
| http://tenant-b.localhost:3001/mcp media_buy_seller \ | |
| --json --allow-http \ | |
| > tenant-b-storyboard.json | |
| cat tenant-b-storyboard.json | head -50 | |
| - if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: multi-platform-storyboards-${{ github.run_attempt }} | |
| path: | | |
| tenant-a-storyboard.json | |
| tenant-b-storyboard.json | |
| if-no-files-found: warn | |
| storyboard-sales-proposal-mode: | |
| name: AdCP storyboard runner — sales-proposal-mode (proposal_finalize) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # v1.5 ProposalManager finalize lifecycle proof. The mock seller | |
| # declares ``finalize=True`` + wires an ``InMemoryProposalStore``; | |
| # the framework's dispatch wiring intercepts ``refine[i].action='finalize'`` | |
| # requests, runs ``finalize_proposal``, commits via the store, and | |
| # auto-hydrates ``ctx.recipes`` on subsequent ``create_media_buy`` | |
| # calls. This job is the storyboard-level proof that the design | |
| # works end-to-end. Blocking gate — no continue-on-error. | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: pyproject.toml | |
| - name: Set up Node 22 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "22" | |
| - name: Cache ~/.npm | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.npm | |
| key: ${{ runner.os }}-npm-adcp-sdk-${{ env.ADCP_SDK_VERSION }} | |
| restore-keys: | | |
| ${{ runner.os }}-npm- | |
| - name: Pre-install @adcp/sdk | |
| # See the comment on the storyboard job's install step for the | |
| # AAO reference-formats fixture rationale (upstream adcp#3307). | |
| run: | | |
| npm install -g @adcp/sdk@${ADCP_SDK_VERSION} | |
| adcp --version | |
| SDK_ROOT="$(npm root -g)/@adcp/sdk" | |
| mkdir -p "${SDK_ROOT}/test/lib/v2-projection-fixtures" | |
| cp tests/fixtures/aao-reference-formats.json "${SDK_ROOT}/test/lib/v2-projection-fixtures/aao-reference-formats.json" | |
| mkdir -p "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries" | |
| cp tests/fixtures/v1-canonical-mapping.json "${SDK_ROOT}/schemas/cache/3.1.0-beta.2/registries/v1-canonical-mapping.json" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Boot sales-proposal-mode seller | |
| run: | | |
| ADCP_PORT=3003 python -m examples.sales_proposal_mode_seller.src.app & | |
| SELLER_PID=$! | |
| echo "SELLER_PID=$SELLER_PID" >> "$GITHUB_ENV" | |
| for i in $(seq 1 60); do | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \ | |
| http://127.0.0.1:3003/mcp 2>/dev/null) || HTTP_CODE="000" | |
| if [ "$HTTP_CODE" != "000" ]; then | |
| echo "Seller ready (HTTP ${HTTP_CODE}, pid ${SELLER_PID})" | |
| break | |
| fi | |
| if ! kill -0 "$SELLER_PID" 2>/dev/null; then | |
| echo "Seller process died during startup" | |
| exit 1 | |
| fi | |
| if [ "$i" -eq 60 ]; then | |
| echo "Seller failed to start within 30s" | |
| kill "$SELLER_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| sleep 0.5 | |
| done | |
| - name: Run storyboard — proposal_finalize | |
| timeout-minutes: 5 | |
| # Full proposal_finalize storyboard. With @adcp/sdk@6.10.0 the | |
| # runner's stateful-chain controller exercises every phase | |
| # (setup, brief, refine, finalize, accept) end-to-end against | |
| # the framework's intercept seam. Earlier SDK versions skipped | |
| # refine/finalize/accept due to a sync_accounts cascade gap | |
| # that 6.10.0 closed (adcp#4053, adcp-client#1146/#1545). | |
| run: | | |
| adcp storyboard run \ | |
| http://127.0.0.1:3003/mcp media_buy_seller/proposal_finalize \ | |
| --json --allow-http \ | |
| > proposal-finalize-storyboard.json | |
| cat proposal-finalize-storyboard.json | head -200 | |
| - name: Assert v1.5 dispatch path scenarios pass | |
| run: | | |
| python -c " | |
| import json, sys, pathlib | |
| p = pathlib.Path('proposal-finalize-storyboard.json') | |
| if not p.exists() or p.stat().st_size == 0: | |
| print('storyboard result missing or empty') | |
| sys.exit(1) | |
| with p.open() as f: | |
| d = json.load(f) | |
| # Every phase of the proposal_finalize storyboard must pass: | |
| # setup → brief_with_proposals → refine_proposal → | |
| # finalize_proposal → accept_proposal. Each exercises a | |
| # different framework seam — see the per-scenario hints | |
| # below for what's wired where. | |
| required_passing = { | |
| 'media_buy_seller/proposal_finalize/setup', | |
| 'media_buy_seller/proposal_finalize/brief_with_proposals', | |
| 'media_buy_seller/proposal_finalize/refine_proposal', | |
| 'media_buy_seller/proposal_finalize/finalize_proposal', | |
| 'media_buy_seller/proposal_finalize/accept_proposal', | |
| } | |
| passed = set() | |
| for track in d.get('tracks', []) or []: | |
| for s in track.get('scenarios', []) or []: | |
| if s.get('overall_passed'): | |
| passed.add(s.get('scenario')) | |
| missing = required_passing - passed | |
| if missing: | |
| # Per-scenario likely-cause hints. A contributor breaks the | |
| # framework finalize wiring; CI tells them what to look at | |
| # rather than just naming a scenario. | |
| hints = { | |
| 'media_buy_seller/proposal_finalize/setup': ( | |
| 'sync_accounts dispatch failed (or the runner stopped ' | |
| 'applying the sole-stateful-step exemption). See ' | |
| 'docs/proposals/proposal-manager-v15-design.md § D5.' | |
| ), | |
| 'media_buy_seller/proposal_finalize/brief_with_proposals': ( | |
| 'Manager.get_products + framework draft persistence ' | |
| 'broke. Check maybe_persist_draft_after_get_products ' | |
| 'in src/adcp/decisioning/proposal_dispatch.py. See ' | |
| 'docs/proposals/proposal-manager-v15-design.md § D1.' | |
| ), | |
| 'media_buy_seller/proposal_finalize/refine_proposal': ( | |
| 'Refine iteration broke. Check refine_products on ' | |
| 'examples/sales_proposal_mode_seller/src/proposal_manager.py ' | |
| 'and the maybe_persist_draft_after_get_products ' | |
| 'overwrite path. See § D1 (refine iteration).' | |
| ), | |
| 'media_buy_seller/proposal_finalize/finalize_proposal': ( | |
| 'Finalize interception broke. Check ' | |
| 'maybe_intercept_finalize in proposal_dispatch.py + ' | |
| 'manager.finalize_proposal in the example. The seam ' | |
| 'must commit via store.commit before projecting. See § D2.' | |
| ), | |
| 'media_buy_seller/proposal_finalize/accept_proposal': ( | |
| 'create_media_buy(proposal_id=...) or ' | |
| 'create_media_buy(packages=[...]) broke. Check ' | |
| 'maybe_hydrate_recipes_for_create_media_buy + ' | |
| 'mark_proposal_consumed in proposal_dispatch.py and ' | |
| 'the platform.create_media_buy adapter. See § D3 + D7.' | |
| ), | |
| } | |
| print('FAIL: required scenarios did not pass:') | |
| for s in sorted(missing): | |
| hint = hints.get(s, 'no hint registered for this scenario') | |
| print(f' - {s}') | |
| print(f' likely cause: {hint}') | |
| print() | |
| print('--- raw storyboard result ---') | |
| print(json.dumps(d, indent=2)) | |
| sys.exit(1) | |
| print('PASS: full proposal_finalize storyboard chain (5/5 phases)') | |
| " | |
| - if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sales-proposal-mode-storyboard-${{ github.run_attempt }} | |
| path: proposal-finalize-storyboard.json | |
| if-no-files-found: warn |