From 425cb2db49773de3c015f7dd6330800053f1b7ef Mon Sep 17 00:00:00 2001 From: evg-g <229170690+evg-g@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:37:48 +0300 Subject: [PATCH 1/4] ci: run the cross-repo telemetry drift check The contract job only ever ran the in-repo checksum guard: the device repo was never checked out, so check_telemetry_contract.py hit its missing-path guard and skipped the byte comparison against the source contract while still reporting success. Check the device repo out alongside when the DEVICE_REPO variable is set, and point the script at it. With the variable unset (a fork) the job keeps the checksum guard only and stays green, as before. The sibling lands inside the working tree, so ignore it in git and Docker: this repo builds its image with COPY . ., which is how a signing key once reached an image in the device repo. Co-Authored-By: Claude Opus 5 --- .dockerignore | 3 +++ .github/workflows/ci.yml | 13 +++++++++++++ .gitignore | 3 +++ 3 files changed, 19 insertions(+) diff --git a/.dockerignore b/.dockerignore index 344df40..3cd3af9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -35,3 +35,6 @@ docs/ .idea/ *.swp .DS_Store + +# Sibling repo checked out into the workspace by CI — must never reach the image. +/aurora-sensor-agent/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77c0b07..c5f7da5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,6 +158,14 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 # need the base commit for the oasdiff comparison + # Cross-repo telemetry drift needs the device repo. Set the DEVICE_REPO variable (e.g. + # owner/aurora-sensor-agent) to enable it; without it the job runs the in-repo checksum + # guard only and stays green, so a fork with no variables set still passes. + - if: vars.DEVICE_REPO != '' + uses: actions/checkout@v7 + with: + repository: ${{ vars.DEVICE_REPO }} + path: aurora-sensor-agent - uses: ./.github/actions/setup-python-uv # 1. Drift: the served schema must equal the committed contracts/openapi.json. - name: OpenAPI drift check @@ -166,6 +174,11 @@ jobs: # checksums (and, when the device repo is checked out alongside, be byte-identical to its # source). The OpenAPI gate in reverse — spec §8 rule 4. - name: Telemetry contract drift check + env: + # Set: the sibling checkout above lands at /aurora-sensor-agent. + # Unset (a fork): falls back to ../, which is absent in CI, so the script skips the + # cross-repo half and keeps the in-repo checksum guard. ../ is also the local-dev layout. + AURORA_DEVICE_REPO: ${{ vars.DEVICE_REPO != '' && 'aurora-sensor-agent' || '../aurora-sensor-agent' }} run: uv run python scripts/check_telemetry_contract.py - name: Publish OpenAPI as a build artifact uses: actions/upload-artifact@v7 diff --git a/.gitignore b/.gitignore index 0ba980f..7e156f3 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ _*.log .vscode/ *.swp .DS_Store + +# Sibling repo checked out into the workspace by CI (cross-repo contract drift gate). +/aurora-sensor-agent/ From b9c41687a3f33c9299ad28774f44741756838f63 Mon Sep 17 00:00:00 2001 From: evg-g <229170690+evg-g@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:42:07 +0300 Subject: [PATCH 2/4] docs: stop the CI/CD gap note reading as "no pipeline" The section listing what could not be executed locally opened straight into the deploy limitations, so a skim-read came away thinking the pipeline is theatre. Say first that every gate not needing a cloud account runs on every PR. Also explain why the local seed login differs from the browser demo's. Co-Authored-By: Claude Opus 5 --- README.md | 4 +++- docs/KNOWN_GAPS.md | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e22ce9..e4e18e2 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ make stack-up # Postgres + Redis + the API on http://localhost:8000 make seed # demo users, clinic, clinician, service ``` -Demo login: `admin@aurora-clinic.com` / `password123` (local demo only). +Demo login: `admin@aurora-clinic.com` / `password123` (local demo only). The browser demo uses +different addresses (`admin@aurora.test`) because it runs on the mock API; this one rejects the +reserved `.test` TLD. | Tool | How | Notes | |---|---|---| diff --git a/docs/KNOWN_GAPS.md b/docs/KNOWN_GAPS.md index c48f5ad..7e5a4ef 100644 --- a/docs/KNOWN_GAPS.md +++ b/docs/KNOWN_GAPS.md @@ -73,6 +73,11 @@ CI/CD in milestone 11. ## CI/CD: what could not be executed in this environment (milestone 6) +**Every gate that can run without a cloud account runs on every pull request** — lint, typecheck, +unit, integration against real Postgres and Redis, contract, coverage, build, security scanning, +and CodeQL. The list below is only the deploy path, which needs infrastructure this environment +does not have. + The workflows are complete and statically valid (`actionlint` + `yamllint` pass locally, wired into `make ci-local`). What could not be *run* here, and why: From 18f286338734b43b1ea5d8c849560f617a5707ae Mon Sep 17 00:00:00 2001 From: evg-g <229170690+evg-g@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:12:45 +0300 Subject: [PATCH 3/4] ci: make a full-history secret scan clean too On a pull request the gitleaks action scans only the pushed commits. On workflow_dispatch -- and for anyone who clones and runs `gitleaks detect` -- it scans the whole history, where one finding surfaces: the webhook fixture's signing secret in tests/integration/test_webhooks_api.py, the literal "0123456789abcdef". It signs nothing outside the test process and is flagged by the generic-api-key rule on entropy alone. Accept it by fingerprint in .gitleaksignore, with a note on why, so the repo is clean under a full scan rather than clean only because of where the scan looked. Verified: gitleaks 8.24.3 over 66 commits, no leaks. Co-Authored-By: Claude Opus 5 --- .gitleaksignore | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..88b9597 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,15 @@ +# Gitleaks findings reviewed and accepted, one fingerprint per line. +# +# Scope note: on a pull request the gitleaks action scans only the pushed commits, so these +# historical findings are invisible there. On workflow_dispatch — and for anyone who clones the +# repo and runs `gitleaks detect` — it scans the full history and surfaces them. This file exists +# so a full-history scan is clean too, rather than the repo merely appearing clean because of +# where the scan happened to look. +# +# Each entry must be a reviewed false positive, never a real credential that has been rotated: +# a committed secret stays in the history and must be revoked at the source, not ignored here. + +# tests/integration/test_webhooks_api.py:27 — the webhook subscription fixture's signing secret, +# the literal "0123456789abcdef". A sequential hex string written to be readable in test output; +# it signs nothing outside the test process. Flagged by the generic-api-key rule on entropy alone. +7a2b78bba835061243e0d8b24acdffd09ffec1d0:tests/integration/test_webhooks_api.py:generic-api-key:27 From 7c78ffc6197b48cf0a63a6f79ff0412cf727822f Mon Sep 17 00:00:00 2001 From: evg-g <229170690+evg-g@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:12:54 +0300 Subject: [PATCH 4/4] build(deps): pytest 9.0.3+ for PYSEC-2026-1845 pip-audit reports PYSEC-2026-1845 (CVE-2025-71176, CVSS 6.8) against pytest 8.4.2: the predictable /tmp/pytest-of-{user} directory lets a local user cause a denial of service or escalate. Local vector only, and pytest is a dev dependency that never reaches the image, so the exposure is small -- but it is a live advisory in a repo whose pitch is security scanning, and the audit step reported it on every run. The fix is in 9.0.3, across the major bound, which drags pytest-asyncio with it: 0.24 pins pytest<9. The 1.x migration is a no-op here because the suite already runs asyncio_mode="auto" and uses no event_loop fixture. Verified on pytest 9.1.1 / pytest-asyncio 1.4.0: 108 unit, 179 security+contract+property, 101 integration against real Postgres and Redis -- 388 passing, the full count the README quotes. pip-audit now reports no known vulnerabilities. Co-Authored-By: Claude Opus 5 --- pyproject.toml | 4 ++-- uv.lock | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 58eac68..d697e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,8 @@ dependencies = [ [project.optional-dependencies] dev = [ - "pytest>=8.3,<9", - "pytest-asyncio>=0.24,<0.25", + "pytest>=9.0.3,<10", + "pytest-asyncio>=1.0,<2", "pytest-cov>=5.0,<8", "pytest-mock>=3.14,<4", "pytest-xdist>=3.6,<4", diff --git a/uv.lock b/uv.lock index 38a935a..3d42f4f 100644 --- a/uv.lock +++ b/uv.lock @@ -131,8 +131,8 @@ requires-dist = [ { name = "pydantic", extras = ["email"], specifier = ">=2.9,<3" }, { name = "pydantic-settings", specifier = ">=2.5,<3" }, { name = "pyjwt", specifier = ">=2.9,<3" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3,<9" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24,<0.25" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3,<10" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0,<2" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0,<8" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14,<4" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6,<4" }, @@ -2125,7 +2125,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2134,21 +2134,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]]