From 0198fd3e5553f1f7675b1c20f68faaf5c1ce2f36 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 19:54:43 +0300 Subject: [PATCH 01/18] ci: add python-core-coverage gate for band-sdk-core-core Resolves the pinned band-sdk-core version from this repo's own dependency lock, checks that exact band-sdk-core-core-v{version} tag out as a sibling, and runs its coverage-python-consumer recipe against this repo's real test suite -- so a PR here fails if it stops exercising a material part of the shared Rust core, not just this SDK's own coverage.py numbers. Companion PR: band-ai/band-sdk-core#71. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- .github/workflows/python-core-coverage.yml | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/python-core-coverage.yml diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml new file mode 100644 index 000000000..b7d34aaec --- /dev/null +++ b/.github/workflows/python-core-coverage.yml @@ -0,0 +1,104 @@ +name: Python Core Coverage + +# Coverage floor for band-sdk-core-core computed from THIS repo's real test +# suite -- band-sdk-core owns the instrumented build + gate (see its +# `coverage-python-consumer` just recipe); this workflow only resolves the +# pinned version, checks that tag out as a sibling, and runs it. +# +# No explicit Rust-toolchain setup step: band-sdk-core/rust-toolchain.toml +# is picked up natively by rustup's own directory-walk resolution the first +# time a `cargo`/`rustup` command runs with that checkout as its cwd, so +# there is nothing to duplicate here. + +on: + pull_request: + branches: [main] + paths: + - 'src/**' + - 'tests/**' + - 'pyproject.toml' + - 'uv.lock' + merge_group: + +permissions: + contents: read + +concurrency: + group: python-core-coverage-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + coverage: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout band-sdk-python + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + path: band-sdk-python + + - name: Configure git to use HTTPS for GitHub + run: git config --global url."https://github.com/".insteadOf "git@github.com:" + + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: "band-sdk-python/**/pyproject.toml" + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: '3.12' + + - name: Install band-sdk-python dependencies + working-directory: band-sdk-python + run: uv sync --all-packages --locked --extra dev + + - name: Resolve pinned band-sdk-core version + id: pin + working-directory: band-sdk-python + run: | + version=$(uv run python -c "import importlib.metadata; print(importlib.metadata.version('band-sdk-core'))") + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Checkout band-sdk-core at the pinned version + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + repository: band-ai/band-sdk-core + ref: band-sdk-core-core-v${{ steps.pin.outputs.version }} + path: band-sdk-core + + - name: Install just and cargo-llvm-cov + uses: taiki-e/install-action@5bf6ce016fd2e72eefc647cbca1e4213f65955b8 # v2 + with: + tool: just,cargo-llvm-cov + + - name: Run band-sdk-python's tests against instrumented band-sdk-core-core + working-directory: band-sdk-core + run: just coverage-python-consumer ../band-sdk-python + + - name: Write coverage summary + if: always() + working-directory: band-sdk-core + run: | + summary=target/python-consumer-coverage/report/summary.txt + if [ -f "$summary" ]; then + { + echo "## band-sdk-core-core coverage from band-sdk-python's tests" + echo '```' + cat "$summary" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: python-core-coverage + path: | + band-sdk-core/target/python-consumer-coverage/report/python-consumer.lcov + band-sdk-core/target/python-consumer-coverage/report/html/ + band-sdk-core/target/python-consumer-coverage/report/summary.txt + if-no-files-found: warn From 10b56228d99e3090fa750d34129090695d8597a4 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 20:16:07 +0300 Subject: [PATCH 02/18] ci: allow manual dispatch of python-core-coverage The path-filtered pull_request trigger correctly doesn't fire on a PR that only touches this workflow file itself, so there was no way to run it on demand to verify the cross-repo checkout/tag-resolution steps under real GitHub Actions before merging. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- .github/workflows/python-core-coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index b7d34aaec..66d9317f8 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -19,6 +19,7 @@ on: - 'pyproject.toml' - 'uv.lock' merge_group: + workflow_dispatch: permissions: contents: read From 4947bb5b07e153c779402c146a71e8ae7690c799 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 20:16:49 +0300 Subject: [PATCH 03/18] test: throwaway trigger for python-core-coverage.yml verification Temporary -- exists only to fire the path-filtered pull_request event so the new coverage workflow gets one real GitHub Actions run before merge. Removed in a follow-up commit once confirmed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- tests/test_int_1304_trigger.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/test_int_1304_trigger.py diff --git a/tests/test_int_1304_trigger.py b/tests/test_int_1304_trigger.py new file mode 100644 index 000000000..60bc42298 --- /dev/null +++ b/tests/test_int_1304_trigger.py @@ -0,0 +1,8 @@ +"""Throwaway: trigger python-core-coverage.yml's path-filtered pull_request +event for a real GitHub Actions verification run. Removed once confirmed.""" + +from __future__ import annotations + + +def test_int_1304_trigger_placeholder() -> None: + assert True From e07764e5fee3cf31214148f8b6ed7939bb531073 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 20:18:53 +0300 Subject: [PATCH 04/18] fix: authenticate the band-sdk-core checkout with a scoped App token The default GITHUB_TOKEN is scoped to this repo only, so the sibling checkout of band-ai/band-sdk-core (private) 404'd on a real run. Mirrors release.yml's existing add-band cross-repo checkout: a create-github-app-token step scoped to just this one repository. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- .github/workflows/python-core-coverage.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index 66d9317f8..445e80657 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -63,12 +63,22 @@ jobs: version=$(uv run python -c "import importlib.metadata; print(importlib.metadata.version('band-sdk-core'))") echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Generate GitHub App Token (scoped to band-sdk-core) + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ secrets.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: band-ai + repositories: band-sdk-core + - name: Checkout band-sdk-core at the pinned version uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: band-ai/band-sdk-core ref: band-sdk-core-core-v${{ steps.pin.outputs.version }} path: band-sdk-core + token: ${{ steps.app-token.outputs.token }} - name: Install just and cargo-llvm-cov uses: taiki-e/install-action@5bf6ce016fd2e72eefc647cbca1e4213f65955b8 # v2 From 259f3183b0a2a8f40c186852f79e4c953c346c71 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 20:20:00 +0300 Subject: [PATCH 05/18] test: remove throwaway python-core-coverage.yml trigger Confirmed via a real run: the workflow's own logic (checkout, version resolution, App-token generation) all succeed. The remaining failure is that the shared GitHub App isn't installed on band-sdk-core yet -- an org-admin action, not something this branch can fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- tests/test_int_1304_trigger.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 tests/test_int_1304_trigger.py diff --git a/tests/test_int_1304_trigger.py b/tests/test_int_1304_trigger.py deleted file mode 100644 index 60bc42298..000000000 --- a/tests/test_int_1304_trigger.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Throwaway: trigger python-core-coverage.yml's path-filtered pull_request -event for a real GitHub Actions verification run. Removed once confirmed.""" - -from __future__ import annotations - - -def test_int_1304_trigger_placeholder() -> None: - assert True From e61dcddc7b9f6da2526bfcb0504f12889b926c93 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 20:57:50 +0300 Subject: [PATCH 06/18] fix: harden python-core-coverage.yml against automated review findings - Reject a pre-release band-sdk-core pin (e.g. a .devN version) before attempting the cross-repo checkout, with a clear message -- dev pre-releases publish to PyPI but are never tagged in git, so the checkout would otherwise fail on a nonexistent ref with a generic git error. - Add tests/test_python_core_coverage_workflow.py asserting the pin step's shape, the checkout ref interpolation, the pre-release guard, and step ordering (mirrors tests/docker/test_kit_publish_workflow.py's existing yaml.BaseLoader pattern). - Add an else branch to the coverage-summary step so a failure before summary.txt exists leaves a diagnostic line in the job summary instead of silently producing nothing. - Extract the duplicated git-HTTPS-auth line into a small composite action (.github/actions/git-https-auth), and set REPORT_DIR once at job level instead of repeating the report path four times. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- .github/actions/git-https-auth/action.yml | 11 +++++ .github/workflows/python-core-coverage.yml | 37 +++++++++++++--- tests/test_python_core_coverage_workflow.py | 47 +++++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .github/actions/git-https-auth/action.yml create mode 100644 tests/test_python_core_coverage_workflow.py diff --git a/.github/actions/git-https-auth/action.yml b/.github/actions/git-https-auth/action.yml new file mode 100644 index 000000000..5a45bafb5 --- /dev/null +++ b/.github/actions/git-https-auth/action.yml @@ -0,0 +1,11 @@ +name: "Configure git for HTTPS GitHub access" +description: >- + Rewrites git@github.com: URLs to https://github.com/ so a later checkout + or clone authenticates over HTTPS (e.g. with a token) instead of SSH. + +runs: + using: composite + steps: + - name: Configure git to use HTTPS for GitHub + shell: bash + run: git config --global url."https://github.com/".insteadOf "git@github.com:" diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index 445e80657..eda43b099 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -32,6 +32,10 @@ jobs: coverage: runs-on: ubuntu-latest timeout-minutes: 30 + env: + # Single source of truth: crates/py/scripts/coverage_python_consumer.py's + # REPORT_DIR, relative to band-sdk-core's checkout root. + REPORT_DIR: target/python-consumer-coverage/report steps: - name: Checkout band-sdk-python uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -39,7 +43,10 @@ jobs: path: band-sdk-python - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + # Path is relative to $GITHUB_WORKSPACE, not this workflow file -- + # band-sdk-python is checked out under path: band-sdk-python above, + # not at the workspace root. + uses: ./band-sdk-python/.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -61,6 +68,24 @@ jobs: working-directory: band-sdk-python run: | version=$(uv run python -c "import importlib.metadata; print(importlib.metadata.version('band-sdk-core'))") + + # A failed command substitution inside an assignment does not trip + # bash's `set -e`, so the pre-release guard below runs as its own + # statement -- a non-zero exit there aborts the step for real, + # instead of silently continuing with an empty $version. + uv run python -c " + import sys + from packaging.version import Version + + if Version('$version').is_prerelease: + sys.exit( + 'band-sdk-core==$version is a pre-release: band-sdk-core-core-v$version ' + 'is never tagged (dev pre-releases publish to PyPI but are never committed ' + 'or tagged in git), so this gate cannot check it out. Pin a stable release ' + 'to run this gate.' + ) + " + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Generate GitHub App Token (scoped to band-sdk-core) @@ -93,7 +118,7 @@ jobs: if: always() working-directory: band-sdk-core run: | - summary=target/python-consumer-coverage/report/summary.txt + summary="$REPORT_DIR/summary.txt" if [ -f "$summary" ]; then { echo "## band-sdk-core-core coverage from band-sdk-python's tests" @@ -101,6 +126,8 @@ jobs: cat "$summary" echo '```' } >> "$GITHUB_STEP_SUMMARY" + else + echo "No coverage summary produced -- see the coverage step's log above." >> "$GITHUB_STEP_SUMMARY" fi - name: Upload coverage report @@ -109,7 +136,7 @@ jobs: with: name: python-core-coverage path: | - band-sdk-core/target/python-consumer-coverage/report/python-consumer.lcov - band-sdk-core/target/python-consumer-coverage/report/html/ - band-sdk-core/target/python-consumer-coverage/report/summary.txt + band-sdk-core/${{ env.REPORT_DIR }}/python-consumer.lcov + band-sdk-core/${{ env.REPORT_DIR }}/html/ + band-sdk-core/${{ env.REPORT_DIR }}/summary.txt if-no-files-found: warn diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py new file mode 100644 index 000000000..a541ed0d3 --- /dev/null +++ b/tests/test_python_core_coverage_workflow.py @@ -0,0 +1,47 @@ +"""Structural contracts for python-core-coverage.yml.""" + +from __future__ import annotations + +from typing import Any + +import yaml + +from tests.paths import REPO_ROOT + +WORKFLOW_PATH = REPO_ROOT / ".github/workflows/python-core-coverage.yml" + + +def load_workflow() -> dict[str, Any]: + return yaml.load(WORKFLOW_PATH.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + + +def _step(workflow: dict[str, Any], name: str) -> dict[str, Any]: + steps = workflow["jobs"]["coverage"]["steps"] + return next(step for step in steps if step.get("name") == name) + + +def test_pin_step_resolves_version_into_github_output() -> None: + step = _step(load_workflow(), "Resolve pinned band-sdk-core version") + assert step["id"] == "pin" + assert "importlib.metadata.version('band-sdk-core')" in step["run"] + assert '>> "$GITHUB_OUTPUT"' in step["run"] + assert "version=$version" in step["run"] + + +def test_pin_step_rejects_prerelease_versions_before_checkout() -> None: + step = _step(load_workflow(), "Resolve pinned band-sdk-core version") + assert "is_prerelease" in step["run"] + assert "packaging.version" in step["run"] + + +def test_checkout_ref_matches_pin_step_output() -> None: + step = _step(load_workflow(), "Checkout band-sdk-core at the pinned version") + assert step["with"]["ref"] == "band-sdk-core-core-v${{ steps.pin.outputs.version }}" + + +def test_prerelease_guard_runs_before_the_cross_repo_checkout() -> None: + steps = load_workflow()["jobs"]["coverage"]["steps"] + names = [step.get("name") for step in steps] + assert names.index("Resolve pinned band-sdk-core version") < names.index( + "Checkout band-sdk-core at the pinned version" + ) From 71f94df1a231e93baebc53c2e74dc57dbd680674 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Mon, 14 Sep 2026 21:10:21 +0300 Subject: [PATCH 07/18] fix: address cycle-2 review findings on python-core-coverage.yml - Guard "Write coverage summary" against a missing band-sdk-core/ checkout: drop its working-directory (which fails the step outright when absent) and reference the path directly, so the graceful fallback message still reaches the job summary instead of erroring. - Pass the resolved version to the pre-release guard via the environment instead of bash-splicing it into the embedded Python three times, removing the (currently unreachable, but unenforced) literal-injection surface. - Hoist the "band-sdk-core-core-v" tag prefix into a job-level env var, single-sourced between the guard's error message and the checkout ref. - Declare packaging as an explicit dev dependency (was only present transitively) since the guard depends on it directly. - Extend the workflow's structural tests: execute the real guard script via subprocess against stable/dev version strings instead of only checking for substrings, and cover the new env-var single sources of truth and the working-directory fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TmtjU5tgPv9Y6bd238JSBL --- .github/workflows/python-core-coverage.yml | 25 ++++-- pyproject.toml | 1 + tests/test_python_core_coverage_workflow.py | 91 +++++++++++++++++++-- uv.lock | 2 + 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index eda43b099..1c6b38546 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -36,6 +36,8 @@ jobs: # Single source of truth: crates/py/scripts/coverage_python_consumer.py's # REPORT_DIR, relative to band-sdk-core's checkout root. REPORT_DIR: target/python-consumer-coverage/report + # Single source of truth for band-sdk-core's git tag naming scheme. + CORE_TAG_PREFIX: band-sdk-core-core-v steps: - name: Checkout band-sdk-python uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -72,14 +74,19 @@ jobs: # A failed command substitution inside an assignment does not trip # bash's `set -e`, so the pre-release guard below runs as its own # statement -- a non-zero exit there aborts the step for real, - # instead of silently continuing with an empty $version. - uv run python -c " + # instead of silently continuing with an empty $version. $version + # is passed via the environment, not spliced into the script text, + # so it can never break out of the embedded Python literal. + VERSION="$version" uv run python -c " + import os import sys from packaging.version import Version - if Version('$version').is_prerelease: + version = os.environ['VERSION'] + prefix = os.environ['CORE_TAG_PREFIX'] + if Version(version).is_prerelease: sys.exit( - 'band-sdk-core==$version is a pre-release: band-sdk-core-core-v$version ' + f'band-sdk-core=={version} is a pre-release: {prefix}{version} ' 'is never tagged (dev pre-releases publish to PyPI but are never committed ' 'or tagged in git), so this gate cannot check it out. Pin a stable release ' 'to run this gate.' @@ -101,7 +108,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: band-ai/band-sdk-core - ref: band-sdk-core-core-v${{ steps.pin.outputs.version }} + ref: ${{ env.CORE_TAG_PREFIX }}${{ steps.pin.outputs.version }} path: band-sdk-core token: ${{ steps.app-token.outputs.token }} @@ -116,9 +123,13 @@ jobs: - name: Write coverage summary if: always() - working-directory: band-sdk-core + # No working-directory: band-sdk-core/ may not exist yet (the + # pin step's pre-release guard, or any earlier failure, can skip + # its checkout) -- a nonexistent step cwd fails the step outright + # before this script runs, before the else branch below ever gets + # a chance to report that gracefully. run: | - summary="$REPORT_DIR/summary.txt" + summary="band-sdk-core/$REPORT_DIR/summary.txt" if [ -f "$summary" ]; then { echo "## band-sdk-core-core coverage from band-sdk-python's tests" diff --git a/pyproject.toml b/pyproject.toml index 8d360d50e..f04b3f7ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,6 +189,7 @@ dev = [ "pytest-rerunfailures>=14.0.0", "tenacity>=8.0.0", "pytest-timeout>=2.4.0", + "packaging>=23.0", # PEP 440 pre-release detection for the CI coverage-gate version guard "band-testing-python==0.1.4", "httpx>=0.24.0", # Already in main deps, for mocking "pytest-httpx>=0.35.0", # httpx_mock fixture: REST header-emission proof diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index a541ed0d3..2e2bd96fb 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -1,7 +1,9 @@ -"""Structural contracts for python-core-coverage.yml.""" +"""Structural contracts and guard-script behavior for python-core-coverage.yml.""" from __future__ import annotations +import os +import subprocess from typing import Any import yaml @@ -20,6 +22,28 @@ def _step(workflow: dict[str, Any], name: str) -> dict[str, Any]: return next(step for step in steps if step.get("name") == name) +def _pin_guard_script(run_text: str) -> str: + """Isolate the pre-release guard invocation from the pin step's run text.""" + lines = run_text.splitlines() + start = next(i for i, line in enumerate(lines) if line.startswith("VERSION=")) + end = next(i for i, line in enumerate(lines) if 'echo "version=$version"' in line) + return "\n".join(lines[start:end]) + + +def _run_pin_guard(version: str) -> subprocess.CompletedProcess[str]: + """Execute the real guard script from the workflow against a given version string.""" + workflow = load_workflow() + run_text = _step(workflow, "Resolve pinned band-sdk-core version")["run"] + core_tag_prefix = workflow["jobs"]["coverage"]["env"]["CORE_TAG_PREFIX"] + script = f'version="{version}"\n{_pin_guard_script(run_text)}' + return subprocess.run( + ["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", script], + capture_output=True, + text=True, + env={**os.environ, "CORE_TAG_PREFIX": core_tag_prefix}, + ) + + def test_pin_step_resolves_version_into_github_output() -> None: step = _step(load_workflow(), "Resolve pinned band-sdk-core version") assert step["id"] == "pin" @@ -28,15 +52,30 @@ def test_pin_step_resolves_version_into_github_output() -> None: assert "version=$version" in step["run"] -def test_pin_step_rejects_prerelease_versions_before_checkout() -> None: - step = _step(load_workflow(), "Resolve pinned band-sdk-core version") - assert "is_prerelease" in step["run"] - assert "packaging.version" in step["run"] +def test_pin_guard_passes_for_a_stable_version() -> None: + result = _run_pin_guard("2.4.0") + assert result.returncode == 0, result.stderr + + +def test_pin_guard_rejects_dev_prerelease_versions() -> None: + result = _run_pin_guard("2.4.0.dev3") + assert result.returncode == 1 + assert "band-sdk-core==2.4.0.dev3 is a pre-release" in result.stderr + assert "band-sdk-core-core-v2.4.0.dev3" in result.stderr + + +def test_pin_guard_rejects_cargo_style_dev_versions() -> None: + result = _run_pin_guard("2.4.0-dev.3") + assert result.returncode == 1 + assert "band-sdk-core==2.4.0-dev.3 is a pre-release" in result.stderr def test_checkout_ref_matches_pin_step_output() -> None: step = _step(load_workflow(), "Checkout band-sdk-core at the pinned version") - assert step["with"]["ref"] == "band-sdk-core-core-v${{ steps.pin.outputs.version }}" + assert ( + step["with"]["ref"] + == "${{ env.CORE_TAG_PREFIX }}${{ steps.pin.outputs.version }}" + ) def test_prerelease_guard_runs_before_the_cross_repo_checkout() -> None: @@ -45,3 +84,43 @@ def test_prerelease_guard_runs_before_the_cross_repo_checkout() -> None: assert names.index("Resolve pinned band-sdk-core version") < names.index( "Checkout band-sdk-core at the pinned version" ) + + +def test_core_tag_prefix_is_a_single_source_of_truth() -> None: + workflow = load_workflow() + prefix = workflow["jobs"]["coverage"]["env"]["CORE_TAG_PREFIX"] + assert prefix == "band-sdk-core-core-v" + checkout_ref = _step(workflow, "Checkout band-sdk-core at the pinned version")[ + "with" + ]["ref"] + pin_run = _step(workflow, "Resolve pinned band-sdk-core version")["run"] + assert "${{ env.CORE_TAG_PREFIX }}" in checkout_ref + assert "os.environ['CORE_TAG_PREFIX']" in pin_run + assert "band-sdk-core-core-v" not in checkout_ref + assert "band-sdk-core-core-v" not in pin_run + + +def test_report_dir_is_a_single_source_of_truth() -> None: + workflow = load_workflow() + report_dir = workflow["jobs"]["coverage"]["env"]["REPORT_DIR"] + summary_run = _step(workflow, "Write coverage summary")["run"] + upload_paths = _step(workflow, "Upload coverage report")["with"]["path"] + assert "$REPORT_DIR" in summary_run + assert "${{ env.REPORT_DIR }}" in upload_paths + assert report_dir not in summary_run + assert report_dir not in upload_paths + + +def test_write_coverage_summary_has_no_working_directory() -> None: + # band-sdk-core/ may not exist (the pin step's guard, or any earlier + # failure, can skip its checkout) -- a nonexistent step cwd fails the + # step outright before its own else-branch fallback can run. + step = _step(load_workflow(), "Write coverage summary") + assert "working-directory" not in step + assert step["run"].strip().startswith('summary="band-sdk-core/') + + +def test_write_coverage_summary_falls_back_when_missing() -> None: + run_text = _step(load_workflow(), "Write coverage summary")["run"] + assert "No coverage summary produced" in run_text + assert '>> "$GITHUB_STEP_SUMMARY"' in run_text diff --git a/uv.lock b/uv.lock index ab9022a0a..0bb341524 100644 --- a/uv.lock +++ b/uv.lock @@ -643,6 +643,7 @@ dev = [ { name = "opentelemetry-instrumentation-logging" }, { name = "opentelemetry-resourcedetector-gcp" }, { name = "opentelemetry-sdk", version = "1.44.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, { name = "pre-commit" }, { name = "pydantic-ai-slim" }, { name = "pydantic-settings", version = "2.14.2", source = { registry = "https://pypi.org/simple" } }, @@ -847,6 +848,7 @@ requires-dist = [ { name = "opentelemetry-resourcedetector-gcp", marker = "extra == 'dev'", specifier = ">=1.12.0a0,!=1.13.0" }, { name = "opentelemetry-resourcedetector-gcp", marker = "extra == 'google-adk'", specifier = ">=1.12.0a0,!=1.13.0" }, { name = "opentelemetry-sdk", marker = "extra == 'dev'", specifier = ">=1.44.0" }, + { name = "packaging", marker = "extra == 'dev'", specifier = ">=23.0" }, { name = "parlant", marker = "extra == 'dev-parlant'", specifier = ">=3.3.2" }, { name = "parlant", marker = "extra == 'parlant'", specifier = ">=3.3.2" }, { name = "phoenix-channels-python-client", specifier = ">=0.2.4" }, From f4ed698447e5e6838909647ea7bf894ed4adaea9 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 14:33:35 +0300 Subject: [PATCH 08/18] fix: run coverage guard test with Git Bash on Windows --- tests/test_python_core_coverage_workflow.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 2e2bd96fb..8e62f51ef 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -4,6 +4,8 @@ import os import subprocess +import sys +from pathlib import Path from typing import Any import yaml @@ -36,8 +38,12 @@ def _run_pin_guard(version: str) -> subprocess.CompletedProcess[str]: run_text = _step(workflow, "Resolve pinned band-sdk-core version")["run"] core_tag_prefix = workflow["jobs"]["coverage"]["env"]["CORE_TAG_PREFIX"] script = f'version="{version}"\n{_pin_guard_script(run_text)}' + bash = "bash" + if sys.platform == "win32": + bash = str(Path(os.environ["ProgramFiles"]) / "Git" / "bin" / "bash.exe") + return subprocess.run( - ["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", script], + [bash, "--noprofile", "--norc", "-eo", "pipefail", "-c", script], capture_output=True, text=True, env={**os.environ, "CORE_TAG_PREFIX": core_tag_prefix}, From a7651e219c97ea3ba0bcc93dce45615f3efe7a2b Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 16:22:49 +0300 Subject: [PATCH 09/18] fix: use scoped key for Core coverage checkout --- .github/workflows/python-core-coverage.yml | 11 +---------- tests/test_python_core_coverage_workflow.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index 1c6b38546..c34a30f80 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -95,22 +95,13 @@ jobs: echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Generate GitHub App Token (scoped to band-sdk-core) - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 - with: - client-id: ${{ secrets.APP_CLIENT_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: band-ai - repositories: band-sdk-core - - name: Checkout band-sdk-core at the pinned version uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: band-ai/band-sdk-core ref: ${{ env.CORE_TAG_PREFIX }}${{ steps.pin.outputs.version }} path: band-sdk-core - token: ${{ steps.app-token.outputs.token }} + token: ${{ secrets.CORE_SDK_READ_KEY }} - name: Install just and cargo-llvm-cov uses: taiki-e/install-action@5bf6ce016fd2e72eefc647cbca1e4213f65955b8 # v2 diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 8e62f51ef..774e7da62 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -84,6 +84,16 @@ def test_checkout_ref_matches_pin_step_output() -> None: ) +def test_core_checkout_uses_the_scoped_read_secret() -> None: + workflow = load_workflow() + steps = workflow["jobs"]["coverage"]["steps"] + checkout = _step(workflow, "Checkout band-sdk-core at the pinned version") + names = [step.get("name") for step in steps] + + assert checkout["with"]["token"] == "${{ secrets.CORE_SDK_READ_KEY }}" + assert "Generate GitHub App Token (scoped to band-sdk-core)" not in names + + def test_prerelease_guard_runs_before_the_cross_repo_checkout() -> None: steps = load_workflow()["jobs"]["coverage"]["steps"] names = [step.get("name") for step in steps] From a0bf60c3ad6c3566e3fd4719e3de87f6dc303b75 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 16:45:21 +0300 Subject: [PATCH 10/18] fix: bump Core coverage dependency --- pyproject.toml | 2 +- uv.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f04b3f7ef..ffe54d6cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ dependencies = [ "band-client-rest==0.0.27", - "band-sdk-core==2.2.0", + "band-sdk-core==2.5.0", "phoenix-channels-python-client>=0.2.4", "python-dotenv>=1.2.2", "pydantic>=2.0", diff --git a/uv.lock b/uv.lock index 0bb341524..ca8d19ef4 100644 --- a/uv.lock +++ b/uv.lock @@ -781,7 +781,7 @@ requires-dist = [ { name = "anthropic", marker = "extra == 'dev-parlant'", specifier = ">=0.75.0,<1" }, { name = "async-lru", specifier = ">=2.3.0" }, { name = "band-client-rest", specifier = "==0.0.27" }, - { name = "band-sdk-core", specifier = "==2.2.0" }, + { name = "band-sdk-core", specifier = "==2.5.0" }, { name = "band-testing-python", marker = "extra == 'dev'", specifier = "==0.1.4" }, { name = "band-testing-python", marker = "extra == 'dev-crewai'", specifier = "==0.1.4" }, { name = "band-testing-python", marker = "extra == 'dev-parlant'", specifier = "==0.1.4" }, @@ -927,17 +927,17 @@ provides-extras = ["logging", "desktop", "codex", "opencode", "letta", "pydantic [[package]] name = "band-sdk-core" -version = "2.2.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/b6/6432cd80745c68517ffb2efb9f8dc5f5d2ad423eaccb002d7a14dd1d28f9/band_sdk_core-2.2.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2eaa8f62a5b806d10993ea4feb73f1c48d3c959b9539bfbcdbd1ffa1f426b998", size = 475871, upload-time = "2026-09-01T11:30:26.358Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1d/a20cbfce00fd32ec5e1145528138fd2b0d8af12dd09c337a59c6c4b79f69/band_sdk_core-2.2.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:52c4c6af089e81d877e34b839694e8b01547d0f25891cf1bc9c717cf32278f59", size = 477493, upload-time = "2026-09-01T11:30:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d8/4ec2c9a0b37027072e92dbe2912fce0d593ee88c3f96e1281882429588ff/band_sdk_core-2.2.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c99fad0f3edb0fc9a9d42079a2be76d573c4dd7a9579ce4b5aea1d5fabaaf9e", size = 526652, upload-time = "2026-09-01T11:30:29.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/548e4f355517ee7cd4422906055765b955ec543efc95ef4993ac8a2a84ed/band_sdk_core-2.2.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:431e7185d2a8659371a6fb22aa0a3383e935607d1c8944f488eebf70b0821cf7", size = 528152, upload-time = "2026-09-01T11:30:30.953Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/6b19bf76fc55d4f5a1315ac036506aea71c499651b0fbe328004c9e40011/band_sdk_core-2.2.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1996fb61fb22d5750df214b3d180e183cdbf3f7e36cea960ca8ca2442a93b888", size = 705290, upload-time = "2026-09-01T11:30:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/5bceb2a5732fb51b1fa62d6dea172104ea79080c768a0aa4a4d42ddf5d5c/band_sdk_core-2.2.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8688f4c55c7cd778e5bb2846b6ada03e7c392d9eb8565a698fdb3df6e29faa8b", size = 743509, upload-time = "2026-09-01T11:30:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/0e/00/c267b0fc208f121ad25b41f41a18c5d77c9280049eae8a359821d37c2a33/band_sdk_core-2.2.0-cp311-abi3-win_amd64.whl", hash = "sha256:704bff82b7494f1997df1aa20def09d7431075f0ad0a1dd4895a4b916409a9f2", size = 348322, upload-time = "2026-09-01T11:30:35.664Z" }, - { url = "https://files.pythonhosted.org/packages/79/d5/46a9dce9b20509904d69b181685fd2d1d498c657466dcd58ba0fedb78248/band_sdk_core-2.2.0-cp311-abi3-win_arm64.whl", hash = "sha256:7f49e75f6f890f38ba7366fb9264c8ca01c34f231db7ab8f08f4c6cc9c161295", size = 331334, upload-time = "2026-09-01T11:30:36.826Z" }, + { url = "https://files.pythonhosted.org/packages/41/e9/086ccc58b8355d06910803753a1b82a0cbaa85a4b1e9159aced764712474/band_sdk_core-2.5.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a7075054abfe11142f3142649f10a529bbbbd075d8f0f0c6b7b39f56cd421807", size = 478010, upload-time = "2026-09-15T12:24:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/65/4f/72a6e4a50803d7f46f04f9031d5391133dd4d7bd503725b793da76e6b112/band_sdk_core-2.5.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:f860bb80fb16b45c90e60bab8867840e14cb832f965289279b591ade1404e29a", size = 479901, upload-time = "2026-09-15T12:24:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/f7e29c6845f04f7ebe8ee8753689f5a3031a54754372f6d37f6f5a5df615/band_sdk_core-2.5.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:581aa0be042625ea30be3854da472865c056604a99f901d46c881d65295a8ac4", size = 531534, upload-time = "2026-09-15T12:24:23.301Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/0dc4072394cbc5d5577e28f13c2fa7ba25747e00ccc3715a518f490168b8/band_sdk_core-2.5.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6c323bde9df4410eec81129fb3cb18fd62fc3d77bc1532863e1b1078afcf3038", size = 530187, upload-time = "2026-09-15T12:24:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/0b3f3b1d4f3439dec091e984b0b8a479476ad9211f29b9a978da3f5f7723/band_sdk_core-2.5.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:54241dc1d3b78e606e2299dbcdf526c5fe54a7600fde919815e1879e813a1380", size = 709933, upload-time = "2026-09-15T12:24:26.762Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/52bb27eabbc036a48e84a59a2c943556d894bc38281606a390b5340abbbb/band_sdk_core-2.5.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:60d684e4a12bec4fad30eca33249c3b6222e9a6a65dc582a4ac5e145605975e9", size = 746454, upload-time = "2026-09-15T12:24:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/eb/14/dbf8075e7a8902523d2c4736dc16f305d3e778e310e87417ec0a3f328fe2/band_sdk_core-2.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ed4e4a15b0eab970dee152a39388852f6ba25567de4d783abbcd082cc4964e45", size = 351353, upload-time = "2026-09-15T12:24:29.617Z" }, + { url = "https://files.pythonhosted.org/packages/8d/88/1127d1917a8f0cacbef8f3d510f17f6b71ecfc25ed2f1f16982c48f3cde4/band_sdk_core-2.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:63ff44ff7514e81f951b6b164db484a992006d6915fcc2624e46ffdfcbe12cee", size = 333829, upload-time = "2026-09-15T12:24:31.273Z" }, ] [[package]] From ea8b00e51d70eb30f2ba6191fe8a1fbd9c993188 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 17:05:55 +0300 Subject: [PATCH 11/18] ci: report weekly Core coverage --- .github/scripts/post-core-coverage-digest.py | 126 +++++++++++++++++++ .github/workflows/python-core-coverage.yml | 40 ++++++ tests/test_python_core_coverage_workflow.py | 67 ++++++++++ 3 files changed, 233 insertions(+) create mode 100644 .github/scripts/post-core-coverage-digest.py diff --git a/.github/scripts/post-core-coverage-digest.py b/.github/scripts/post-core-coverage-digest.py new file mode 100644 index 000000000..a2b5a9d45 --- /dev/null +++ b/.github/scripts/post-core-coverage-digest.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Post a compact weekly digest for a Core consumer-coverage artifact.""" + +from __future__ import annotations + +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +LOW_COVERAGE_PERCENT = 80.0 + + +@dataclass(frozen=True) +class FileCoverage: + path: str + found: int + hit: int + + @property + def percent(self) -> float: + return 100 * self.hit / self.found if self.found else 100.0 + + @property + def missed(self) -> int: + return self.found - self.hit + + +def display_path(path: str) -> str: + marker = "/crates/" + return path[path.index(marker) + 1 :] if marker in path else Path(path).name + + +def parse_lcov(path: Path) -> list[FileCoverage]: + records: list[FileCoverage] = [] + source: str | None = None + found = hit = 0 + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith("SF:"): + source = line[3:] + elif line.startswith("LF:"): + found = int(line[3:]) + elif line.startswith("LH:"): + hit = int(line[3:]) + elif line == "end_of_record" and source is not None: + records.append(FileCoverage(display_path(source), found, hit)) + source = None + found = hit = 0 + return records + + +def render_digest( + *, lcov_path: Path, label: str, recipients: str, run_url: str, result: str +) -> str: + status = "PASS" if result == "success" else result.upper() + header = f"## Weekly Core coverage: {status}" + if not lcov_path.is_file(): + return "\n".join( + [ + header, + recipients, + "", + "No LCOV report was produced. See the failed run for details.", + "", + f"[Open run]({run_url})", + ] + ) + + files = parse_lcov(lcov_path) + found = sum(item.found for item in files) + hit = sum(item.hit for item in files) + percent = 100 * hit / found if found else 0.0 + gaps = sorted( + (item for item in files if item.percent < LOW_COVERAGE_PERCENT), + key=lambda item: (item.percent, -item.found, item.path), + ) + lines = [ + header, + recipients, + "", + f"**{label}: {percent:.2f}% lines** ({hit}/{found}). Low coverage is below {LOW_COVERAGE_PERCENT:.0f}%.", + "", + ] + if gaps: + lines.extend( + [ + "### Low or uncovered files", + "", + "| File | Lines | Missed |", + "| --- | ---: | ---: |", + ] + ) + lines.extend( + f"| `{item.path}` | {item.percent:.2f}% | {item.missed}/{item.found} |" + for item in gaps + ) + else: + lines.append("All measured files meet the coverage floor.") + lines.extend(["", f"[Open run and coverage artifact]({run_url}#artifacts)"]) + return "\n".join(lines) + + +def main() -> None: + digest = render_digest( + lcov_path=Path(os.environ["LCOV_PATH"]), + label=os.environ["REPORT_LABEL"], + recipients=os.environ["RECIPIENTS"], + run_url=os.environ["RUN_URL"], + result=os.environ["WORKFLOW_RESULT"], + ) + subprocess.run( + [ + "gh", + "api", + f"repos/{os.environ['REPO']}/commits/{os.environ['SHA']}/comments", + "--method", + "POST", + "-f", + f"body={digest}", + ], + check=True, + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index c34a30f80..e16c8496f 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -11,6 +11,10 @@ name: Python Core Coverage # there is nothing to duplicate here. on: + # Weekly consumer coverage report. This is separate from the PR gate so it + # observes main's current coverage without adding PR notifications. + schedule: + - cron: "33 4 * * 1" # Mondays 04:33 UTC pull_request: branches: [main] paths: @@ -142,3 +146,39 @@ jobs: band-sdk-core/${{ env.REPORT_DIR }}/html/ band-sdk-core/${{ env.REPORT_DIR }}/summary.txt if-no-files-found: warn + + report-weekly: + name: report weekly coverage + needs: coverage + if: always() && github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_RESULT: ${{ needs.coverage.result }} + REPORT_LABEL: band-sdk-core Python bindings + LCOV_PATH: artifacts/python-consumer.lcov + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Download coverage report + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: python-core-coverage + path: artifacts + if-no-files-found: warn + + - name: Read integrations mentions list + id: mentions + run: .github/scripts/read-integrations-mentions.sh + + - name: Post the weekly coverage digest + env: + RECIPIENTS: ${{ steps.mentions.outputs.mentions }} + run: python .github/scripts/post-core-coverage-digest.py diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 774e7da62..2de80d22a 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -5,6 +5,7 @@ import os import subprocess import sys +from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path from typing import Any @@ -140,3 +141,69 @@ def test_write_coverage_summary_falls_back_when_missing() -> None: run_text = _step(load_workflow(), "Write coverage summary")["run"] assert "No coverage summary produced" in run_text assert '>> "$GITHUB_STEP_SUMMARY"' in run_text + + +def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> None: + workflow = load_workflow() + assert workflow["on"]["schedule"] == [{"cron": "33 4 * * 1"}] + + report = workflow["jobs"]["report-weekly"] + assert report["if"] == "always() && github.event_name == 'schedule'" + assert report["permissions"] == {"contents": "write"} + report_steps = report["steps"] + mention_step = next( + step + for step in report_steps + if step["name"] == "Read integrations mentions list" + ) + digest_step = next( + step + for step in report_steps + if step["name"] == "Post the weekly coverage digest" + ) + assert mention_step["id"] == "mentions" + assert digest_step["env"]["RECIPIENTS"] == "${{ steps.mentions.outputs.mentions }}" + + +def test_weekly_digest_identifies_low_and_completely_uncovered_files( + tmp_path: Path, +) -> None: + script_path = REPO_ROOT / ".github/scripts/post-core-coverage-digest.py" + spec = spec_from_file_location("post_core_coverage_digest", script_path) + assert spec is not None and spec.loader is not None + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + + lcov = tmp_path / "coverage.lcov" + lcov.write_text( + "\n".join( + [ + "SF:/work/crates/core/src/covered.rs", + "LF:10", + "LH:10", + "end_of_record", + "SF:/work/crates/core/src/low.rs", + "LF:10", + "LH:2", + "end_of_record", + "SF:/work/crates/core/src/none.rs", + "LF:4", + "LH:0", + "end_of_record", + ] + ) + ) + + digest = module.render_digest( + lcov_path=lcov, + label="Core", + recipients="@bandzalkin", + run_url="https://example.test/run", + result="success", + ) + + assert "50.00% lines" in digest + assert "`crates/core/src/none.rs` | 0.00% | 4/4" in digest + assert "`crates/core/src/low.rs` | 20.00% | 8/10" in digest + assert "covered.rs" not in digest From b66931b0f00229225806e5d4f022988986223fc5 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 17:06:55 +0300 Subject: [PATCH 12/18] fix: skip cancelled weekly coverage reports --- .github/workflows/python-core-coverage.yml | 2 +- tests/test_python_core_coverage_workflow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index e16c8496f..3a0cc6240 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -150,7 +150,7 @@ jobs: report-weekly: name: report weekly coverage needs: coverage - if: always() && github.event_name == 'schedule' + if: "!cancelled() && github.event_name == 'schedule'" runs-on: ubuntu-latest permissions: contents: write diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 2de80d22a..b671a2c1e 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -148,7 +148,7 @@ def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> No assert workflow["on"]["schedule"] == [{"cron": "33 4 * * 1"}] report = workflow["jobs"]["report-weekly"] - assert report["if"] == "always() && github.event_name == 'schedule'" + assert report["if"] == "!cancelled() && github.event_name == 'schedule'" assert report["permissions"] == {"contents": "write"} report_steps = report["steps"] mention_step = next( From 50e859dd7a13e9cc0f8e805c792a8a442badaa37 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 17:18:33 +0300 Subject: [PATCH 13/18] ci: report manual Core coverage runs --- .github/workflows/python-core-coverage.yml | 5 +++-- tests/test_python_core_coverage_workflow.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index 3a0cc6240..cec195ba5 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -150,7 +150,7 @@ jobs: report-weekly: name: report weekly coverage needs: coverage - if: "!cancelled() && github.event_name == 'schedule'" + if: "!cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')" runs-on: ubuntu-latest permissions: contents: write @@ -176,9 +176,10 @@ jobs: - name: Read integrations mentions list id: mentions + if: github.event_name == 'schedule' run: .github/scripts/read-integrations-mentions.sh - name: Post the weekly coverage digest env: - RECIPIENTS: ${{ steps.mentions.outputs.mentions }} + RECIPIENTS: ${{ github.event_name == 'schedule' && steps.mentions.outputs.mentions || format('@{0}', github.triggering_actor) }} run: python .github/scripts/post-core-coverage-digest.py diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index b671a2c1e..40897db73 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -148,7 +148,7 @@ def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> No assert workflow["on"]["schedule"] == [{"cron": "33 4 * * 1"}] report = workflow["jobs"]["report-weekly"] - assert report["if"] == "!cancelled() && github.event_name == 'schedule'" + assert report["if"] == "!cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')" assert report["permissions"] == {"contents": "write"} report_steps = report["steps"] mention_step = next( @@ -162,7 +162,8 @@ def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> No if step["name"] == "Post the weekly coverage digest" ) assert mention_step["id"] == "mentions" - assert digest_step["env"]["RECIPIENTS"] == "${{ steps.mentions.outputs.mentions }}" + assert mention_step["if"] == "github.event_name == 'schedule'" + assert digest_step["env"]["RECIPIENTS"] == "${{ github.event_name == 'schedule' && steps.mentions.outputs.mentions || format('@{0}', github.triggering_actor) }}" def test_weekly_digest_identifies_low_and_completely_uncovered_files( From 3ef1a5136053d9c352c05a21a5b75ccbc850f848 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 17:28:41 +0300 Subject: [PATCH 14/18] ci: clarify Core coverage digests --- .github/scripts/post-core-coverage-digest.py | 73 ++++++++++++++++---- tests/test_python_core_coverage_workflow.py | 34 +++++++-- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/.github/scripts/post-core-coverage-digest.py b/.github/scripts/post-core-coverage-digest.py index a2b5a9d45..c3403204f 100644 --- a/.github/scripts/post-core-coverage-digest.py +++ b/.github/scripts/post-core-coverage-digest.py @@ -9,6 +9,7 @@ from pathlib import Path LOW_COVERAGE_PERCENT = 80.0 +MAX_MISSED_LINE_RANGES = 8 @dataclass(frozen=True) @@ -16,6 +17,9 @@ class FileCoverage: path: str found: int hit: int + functions_found: int + functions_hit: int + missed_lines: tuple[int, ...] @property def percent(self) -> float: @@ -31,10 +35,32 @@ def display_path(path: str) -> str: return path[path.index(marker) + 1 :] if marker in path else Path(path).name +def format_line_ranges(numbers: tuple[int, ...]) -> str: + ranges: list[str] = [] + start = previous = None + for number in numbers: + if start is None: + start = previous = number + elif number == previous + 1: + previous = number + else: + ranges.append(str(start) if start == previous else f"{start}-{previous}") + start = previous = number + if start is not None: + ranges.append(str(start) if start == previous else f"{start}-{previous}") + shown = ", ".join(ranges[:MAX_MISSED_LINE_RANGES]) + return ( + shown + if len(ranges) <= MAX_MISSED_LINE_RANGES + else f"{shown}, … ({len(numbers)} missed)" + ) + + def parse_lcov(path: Path) -> list[FileCoverage]: records: list[FileCoverage] = [] source: str | None = None - found = hit = 0 + found = hit = functions_found = functions_hit = 0 + missed_lines: list[int] = [] for line in path.read_text(encoding="utf-8").splitlines(): if line.startswith("SF:"): source = line[3:] @@ -42,25 +68,42 @@ def parse_lcov(path: Path) -> list[FileCoverage]: found = int(line[3:]) elif line.startswith("LH:"): hit = int(line[3:]) + elif line.startswith("FNF:"): + functions_found = int(line[4:]) + elif line.startswith("FNH:"): + functions_hit = int(line[4:]) + elif line.startswith("DA:"): + line_number, count = line[3:].split(",", maxsplit=1) + if count == "0": + missed_lines.append(int(line_number)) elif line == "end_of_record" and source is not None: - records.append(FileCoverage(display_path(source), found, hit)) + records.append( + FileCoverage( + display_path(source), + found, + hit, + functions_found, + functions_hit, + tuple(missed_lines), + ) + ) source = None - found = hit = 0 + found = hit = functions_found = functions_hit = 0 + missed_lines = [] return records def render_digest( *, lcov_path: Path, label: str, recipients: str, run_url: str, result: str ) -> str: - status = "PASS" if result == "success" else result.upper() - header = f"## Weekly Core coverage: {status}" + header = "## Weekly Core coverage report" if not lcov_path.is_file(): return "\n".join( [ header, recipients, "", - "No LCOV report was produced. See the failed run for details.", + f"The coverage run {result}. No LCOV report was produced; see the run for details.", "", f"[Open run]({run_url})", ] @@ -69,7 +112,8 @@ def render_digest( files = parse_lcov(lcov_path) found = sum(item.found for item in files) hit = sum(item.hit for item in files) - percent = 100 * hit / found if found else 0.0 + functions_found = sum(item.functions_found for item in files) + functions_hit = sum(item.functions_hit for item in files) gaps = sorted( (item for item in files if item.percent < LOW_COVERAGE_PERCENT), key=lambda item: (item.percent, -item.found, item.path), @@ -78,20 +122,25 @@ def render_digest( header, recipients, "", - f"**{label}: {percent:.2f}% lines** ({hit}/{found}). Low coverage is below {LOW_COVERAGE_PERCENT:.0f}%.", + f"**{label}**", + "", + "| Measure | Covered | Missed | Coverage |", + "| --- | ---: | ---: | ---: |", + f"| Lines | {hit}/{found} | {found - hit} | {100 * hit / found if found else 0.0:.2f}% |", + f"| Functions | {functions_hit}/{functions_found} | {functions_found - functions_hit} | {100 * functions_hit / functions_found if functions_found else 0.0:.2f}% |", "", ] if gaps: lines.extend( [ - "### Low or uncovered files", + f"### Source files below {LOW_COVERAGE_PERCENT:.0f}% line coverage", "", - "| File | Lines | Missed |", - "| --- | ---: | ---: |", + "| File | Lines | Missed line ranges |", + "| --- | ---: | --- |", ] ) lines.extend( - f"| `{item.path}` | {item.percent:.2f}% | {item.missed}/{item.found} |" + f"| `{item.path}` | {item.hit}/{item.found} ({item.percent:.2f}%) | {format_line_ranges(item.missed_lines)} |" for item in gaps ) else: diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 40897db73..ec2d8d42c 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -148,7 +148,10 @@ def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> No assert workflow["on"]["schedule"] == [{"cron": "33 4 * * 1"}] report = workflow["jobs"]["report-weekly"] - assert report["if"] == "!cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')" + assert ( + report["if"] + == "!cancelled() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')" + ) assert report["permissions"] == {"contents": "write"} report_steps = report["steps"] mention_step = next( @@ -163,7 +166,10 @@ def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> No ) assert mention_step["id"] == "mentions" assert mention_step["if"] == "github.event_name == 'schedule'" - assert digest_step["env"]["RECIPIENTS"] == "${{ github.event_name == 'schedule' && steps.mentions.outputs.mentions || format('@{0}', github.triggering_actor) }}" + assert ( + digest_step["env"]["RECIPIENTS"] + == "${{ github.event_name == 'schedule' && steps.mentions.outputs.mentions || format('@{0}', github.triggering_actor) }}" + ) def test_weekly_digest_identifies_low_and_completely_uncovered_files( @@ -181,14 +187,30 @@ def test_weekly_digest_identifies_low_and_completely_uncovered_files( "\n".join( [ "SF:/work/crates/core/src/covered.rs", + "FNF:2", + "FNH:2", + "DA:1,1", + "DA:2,1", "LF:10", "LH:10", "end_of_record", "SF:/work/crates/core/src/low.rs", + "FNF:2", + "FNH:1", + "DA:10,1", + "DA:11,1", + "DA:12,0", + "DA:13,0", "LF:10", "LH:2", "end_of_record", "SF:/work/crates/core/src/none.rs", + "FNF:1", + "FNH:0", + "DA:20,0", + "DA:21,0", + "DA:22,0", + "DA:23,0", "LF:4", "LH:0", "end_of_record", @@ -204,7 +226,9 @@ def test_weekly_digest_identifies_low_and_completely_uncovered_files( result="success", ) - assert "50.00% lines" in digest - assert "`crates/core/src/none.rs` | 0.00% | 4/4" in digest - assert "`crates/core/src/low.rs` | 20.00% | 8/10" in digest + assert "Weekly Core coverage report" in digest + assert "| Lines | 12/24 | 12 | 50.00% |" in digest + assert "| Functions | 3/5 | 2 | 60.00% |" in digest + assert "`crates/core/src/none.rs` | 0/4 (0.00%) | 20-23" in digest + assert "`crates/core/src/low.rs` | 2/10 (20.00%) | 12-13" in digest assert "covered.rs" not in digest From f9f24d8df5b602fa0b9320531ad28a18474fb875 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 18:05:50 +0300 Subject: [PATCH 15/18] chore: schedule Core coverage for Sunday night --- .github/workflows/python-core-coverage.yml | 2 +- tests/test_python_core_coverage_workflow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index cec195ba5..73047ad63 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -14,7 +14,7 @@ on: # Weekly consumer coverage report. This is separate from the PR gate so it # observes main's current coverage without adding PR notifications. schedule: - - cron: "33 4 * * 1" # Mondays 04:33 UTC + - cron: "33 20 * * 0" # Sunday night: 23:33 Israel summer time, 22:33 winter time pull_request: branches: [main] paths: diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index ec2d8d42c..6faf217f1 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -145,7 +145,7 @@ def test_write_coverage_summary_falls_back_when_missing() -> None: def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> None: workflow = load_workflow() - assert workflow["on"]["schedule"] == [{"cron": "33 4 * * 1"}] + assert workflow["on"]["schedule"] == [{"cron": "33 20 * * 0"}] report = workflow["jobs"]["report-weekly"] assert ( From f778a0a52cae8c81cf2ae58d8379d56ea23b0578 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 18:20:00 +0300 Subject: [PATCH 16/18] ci: improve Core coverage email layout --- .github/scripts/post-core-coverage-digest.py | 48 ++++++++++++++------ tests/test_python_core_coverage_workflow.py | 11 +++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/.github/scripts/post-core-coverage-digest.py b/.github/scripts/post-core-coverage-digest.py index c3403204f..c96e65c4e 100644 --- a/.github/scripts/post-core-coverage-digest.py +++ b/.github/scripts/post-core-coverage-digest.py @@ -10,6 +10,7 @@ LOW_COVERAGE_PERCENT = 80.0 MAX_MISSED_LINE_RANGES = 8 +MAX_LOW_COVERAGE_FILES = 8 @dataclass(frozen=True) @@ -56,6 +57,14 @@ def format_line_ranges(numbers: tuple[int, ...]) -> str: ) +def coverage_marker(percent: float) -> str: + if percent >= LOW_COVERAGE_PERCENT: + return "🟢" + if percent >= 50: + return "🟠" + return "🔴" + + def parse_lcov(path: Path) -> list[FileCoverage]: records: list[FileCoverage] = [] source: str | None = None @@ -96,14 +105,16 @@ def parse_lcov(path: Path) -> list[FileCoverage]: def render_digest( *, lcov_path: Path, label: str, recipients: str, run_url: str, result: str ) -> str: - header = "## Weekly Core coverage report" + header = "## 📊 Weekly Core coverage" if not lcov_path.is_file(): return "\n".join( [ header, recipients, "", - f"The coverage run {result}. No LCOV report was produced; see the run for details.", + f"⚠️ **Coverage unavailable** · workflow `{result}`", + "", + "No LCOV report was produced. Open the run for the failure details.", "", f"[Open run]({run_url})", ] @@ -118,34 +129,45 @@ def render_digest( (item for item in files if item.percent < LOW_COVERAGE_PERCENT), key=lambda item: (item.percent, -item.found, item.path), ) + line_percent = 100 * hit / found if found else 0.0 + function_percent = 100 * functions_hit / functions_found if functions_found else 0.0 + healthy_files = len(files) - len(gaps) lines = [ header, + "", recipients, "", f"**{label}**", "", - "| Measure | Covered | Missed | Coverage |", - "| --- | ---: | ---: | ---: |", - f"| Lines | {hit}/{found} | {found - hit} | {100 * hit / found if found else 0.0:.2f}% |", - f"| Functions | {functions_hit}/{functions_found} | {functions_found - functions_hit} | {100 * functions_hit / functions_found if functions_found else 0.0:.2f}% |", + "### Coverage snapshot", + "", + "| Signal | Result |", + "| --- | --- |", + f"| Lines | {coverage_marker(line_percent)} **{line_percent:.2f}%** · {hit}/{found} covered · {found - hit} missing |", + f"| Functions | {coverage_marker(function_percent)} **{function_percent:.2f}%** · {functions_hit}/{functions_found} covered · {functions_found - functions_hit} missing |", + f"| Files at target | {coverage_marker(100 * healthy_files / len(files) if files else 0)} **{healthy_files}/{len(files)}** at or above {LOW_COVERAGE_PERCENT:.0f}% |", "", ] if gaps: lines.extend( [ - f"### Source files below {LOW_COVERAGE_PERCENT:.0f}% line coverage", + "### 🎯 Where to focus", "", - "| File | Lines | Missed line ranges |", - "| --- | ---: | --- |", + "| Source file | Coverage gap |", + "| --- | --- |", ] ) lines.extend( - f"| `{item.path}` | {item.hit}/{item.found} ({item.percent:.2f}%) | {format_line_ranges(item.missed_lines)} |" - for item in gaps + f"| `{item.path}` | {coverage_marker(item.percent)} **{item.percent:.2f}%** · {item.missed} lines missing
Lines `{format_line_ranges(item.missed_lines)}` |" + for item in gaps[:MAX_LOW_COVERAGE_FILES] ) + if len(gaps) > MAX_LOW_COVERAGE_FILES: + lines.extend( + ["", f"_Plus {len(gaps) - MAX_LOW_COVERAGE_FILES} more low-coverage files in the artifact._"] + ) else: - lines.append("All measured files meet the coverage floor.") - lines.extend(["", f"[Open run and coverage artifact]({run_url}#artifacts)"]) + lines.append("✅ Every measured source file meets the coverage target.") + lines.extend(["", f"[View the run and full coverage artifact →]({run_url}#artifacts)"]) return "\n".join(lines) diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 6faf217f1..392c8741c 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -226,9 +226,10 @@ def test_weekly_digest_identifies_low_and_completely_uncovered_files( result="success", ) - assert "Weekly Core coverage report" in digest - assert "| Lines | 12/24 | 12 | 50.00% |" in digest - assert "| Functions | 3/5 | 2 | 60.00% |" in digest - assert "`crates/core/src/none.rs` | 0/4 (0.00%) | 20-23" in digest - assert "`crates/core/src/low.rs` | 2/10 (20.00%) | 12-13" in digest + assert "📊 Weekly Core coverage" in digest + assert "| Lines | 🟠 **50.00%** · 12/24 covered · 12 missing |" in digest + assert "| Functions | 🟠 **60.00%** · 3/5 covered · 2 missing |" in digest + assert "| Files at target | 🔴 **1/3** at or above 80% |" in digest + assert "`crates/core/src/none.rs` | 🔴 **0.00%** · 4 lines missing
Lines `20-23`" in digest + assert "`crates/core/src/low.rs` | 🔴 **20.00%** · 8 lines missing
Lines `12-13`" in digest assert "covered.rs" not in digest From 5b17eb1270b683db979f0c061f5ad7413c0df917 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Tue, 15 Sep 2026 18:25:56 +0300 Subject: [PATCH 17/18] style: format Core coverage report files --- .github/scripts/post-core-coverage-digest.py | 9 +++++++-- tests/test_python_core_coverage_workflow.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/scripts/post-core-coverage-digest.py b/.github/scripts/post-core-coverage-digest.py index c96e65c4e..c1a81f993 100644 --- a/.github/scripts/post-core-coverage-digest.py +++ b/.github/scripts/post-core-coverage-digest.py @@ -163,11 +163,16 @@ def render_digest( ) if len(gaps) > MAX_LOW_COVERAGE_FILES: lines.extend( - ["", f"_Plus {len(gaps) - MAX_LOW_COVERAGE_FILES} more low-coverage files in the artifact._"] + [ + "", + f"_Plus {len(gaps) - MAX_LOW_COVERAGE_FILES} more low-coverage files in the artifact._", + ] ) else: lines.append("✅ Every measured source file meets the coverage target.") - lines.extend(["", f"[View the run and full coverage artifact →]({run_url}#artifacts)"]) + lines.extend( + ["", f"[View the run and full coverage artifact →]({run_url}#artifacts)"] + ) return "\n".join(lines) diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 392c8741c..9d5de8bd4 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -230,6 +230,12 @@ def test_weekly_digest_identifies_low_and_completely_uncovered_files( assert "| Lines | 🟠 **50.00%** · 12/24 covered · 12 missing |" in digest assert "| Functions | 🟠 **60.00%** · 3/5 covered · 2 missing |" in digest assert "| Files at target | 🔴 **1/3** at or above 80% |" in digest - assert "`crates/core/src/none.rs` | 🔴 **0.00%** · 4 lines missing
Lines `20-23`" in digest - assert "`crates/core/src/low.rs` | 🔴 **20.00%** · 8 lines missing
Lines `12-13`" in digest + assert ( + "`crates/core/src/none.rs` | 🔴 **0.00%** · 4 lines missing
Lines `20-23`" + in digest + ) + assert ( + "`crates/core/src/low.rs` | 🔴 **20.00%** · 8 lines missing
Lines `12-13`" + in digest + ) assert "covered.rs" not in digest From 36641cf5a85824623abb61dfb984dc873b9b0b70 Mon Sep 17 00:00:00 2001 From: Alexander Zaikman Date: Thu, 17 Sep 2026 20:34:34 +0300 Subject: [PATCH 18/18] fix: address Core coverage review findings --- .github/actions/setup-e2e/action.yml | 3 +- .github/scripts/post-core-coverage-digest.py | 2 +- .github/workflows/ci.yml | 10 +++---- .github/workflows/python-core-coverage.yml | 30 +++++++++++++------- tests/test_python_core_coverage_workflow.py | 28 ++++++++++++++++-- 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/.github/actions/setup-e2e/action.yml b/.github/actions/setup-e2e/action.yml index 24f7c4561..a1f6a2fcc 100644 --- a/.github/actions/setup-e2e/action.yml +++ b/.github/actions/setup-e2e/action.yml @@ -10,8 +10,7 @@ runs: using: composite steps: - name: Configure git to use HTTPS for GitHub - shell: bash - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv (with cache) and Python uses: astral-sh/setup-uv@v7 diff --git a/.github/scripts/post-core-coverage-digest.py b/.github/scripts/post-core-coverage-digest.py index c1a81f993..01d262743 100644 --- a/.github/scripts/post-core-coverage-digest.py +++ b/.github/scripts/post-core-coverage-digest.py @@ -53,7 +53,7 @@ def format_line_ranges(numbers: tuple[int, ...]) -> str: return ( shown if len(ranges) <= MAX_MISSED_LINE_RANGES - else f"{shown}, … ({len(numbers)} missed)" + else f"{shown}, … ({len(ranges) - MAX_MISSED_LINE_RANGES} more ranges)" ) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a76cf4d..f46402c6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -76,7 +76,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -124,7 +124,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -187,7 +187,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -233,7 +233,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Configure git to use HTTPS for GitHub - run: git config --global url."https://github.com/".insteadOf "git@github.com:" + uses: ./.github/actions/git-https-auth - name: Install uv uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 diff --git a/.github/workflows/python-core-coverage.yml b/.github/workflows/python-core-coverage.yml index 73047ad63..41c0a8305 100644 --- a/.github/workflows/python-core-coverage.yml +++ b/.github/workflows/python-core-coverage.yml @@ -18,6 +18,9 @@ on: pull_request: branches: [main] paths: + - '.github/actions/git-https-auth/**' + - '.github/scripts/post-core-coverage-digest.py' + - '.github/workflows/python-core-coverage.yml' - 'src/**' - 'tests/**' - 'pyproject.toml' @@ -34,6 +37,9 @@ concurrency: jobs: coverage: + # Dependabot cannot read repository Actions secrets, so it cannot check + # out the private Core repository with this workflow's token. + if: github.actor != 'dependabot[bot]' runs-on: ubuntu-latest timeout-minutes: 30 env: @@ -42,6 +48,7 @@ jobs: REPORT_DIR: target/python-consumer-coverage/report # Single source of truth for band-sdk-core's git tag naming scheme. CORE_TAG_PREFIX: band-sdk-core-core-v + ARTIFACT_DIR: python-core-coverage-artifact steps: - name: Checkout band-sdk-python uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -75,12 +82,8 @@ jobs: run: | version=$(uv run python -c "import importlib.metadata; print(importlib.metadata.version('band-sdk-core'))") - # A failed command substitution inside an assignment does not trip - # bash's `set -e`, so the pre-release guard below runs as its own - # statement -- a non-zero exit there aborts the step for real, - # instead of silently continuing with an empty $version. $version - # is passed via the environment, not spliced into the script text, - # so it can never break out of the embedded Python literal. + # Pass the version through the environment rather than embedding it + # in the Python source string. VERSION="$version" uv run python -c " import os import sys @@ -136,15 +139,21 @@ jobs: echo "No coverage summary produced -- see the coverage step's log above." >> "$GITHUB_STEP_SUMMARY" fi + - name: Stage coverage report + if: always() + run: | + report="band-sdk-core/$REPORT_DIR" + mkdir -p "$ARTIFACT_DIR" + [ ! -f "$report/python-consumer.lcov" ] || cp "$report/python-consumer.lcov" "$ARTIFACT_DIR/" + [ ! -f "$report/summary.txt" ] || cp "$report/summary.txt" "$ARTIFACT_DIR/" + [ ! -d "$report/html" ] || cp -R "$report/html" "$ARTIFACT_DIR/" + - name: Upload coverage report if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: python-core-coverage - path: | - band-sdk-core/${{ env.REPORT_DIR }}/python-consumer.lcov - band-sdk-core/${{ env.REPORT_DIR }}/html/ - band-sdk-core/${{ env.REPORT_DIR }}/summary.txt + path: ${{ env.ARTIFACT_DIR }} if-no-files-found: warn report-weekly: @@ -172,7 +181,6 @@ jobs: with: name: python-core-coverage path: artifacts - if-no-files-found: warn - name: Read integrations mentions list id: mentions diff --git a/tests/test_python_core_coverage_workflow.py b/tests/test_python_core_coverage_workflow.py index 9d5de8bd4..f600df5e8 100644 --- a/tests/test_python_core_coverage_workflow.py +++ b/tests/test_python_core_coverage_workflow.py @@ -95,6 +95,11 @@ def test_core_checkout_uses_the_scoped_read_secret() -> None: assert "Generate GitHub App Token (scoped to band-sdk-core)" not in names +def test_coverage_job_skips_dependabot_without_the_read_secret() -> None: + coverage = load_workflow()["jobs"]["coverage"] + assert coverage["if"] == "github.actor != 'dependabot[bot]'" + + def test_prerelease_guard_runs_before_the_cross_repo_checkout() -> None: steps = load_workflow()["jobs"]["coverage"]["steps"] names = [step.get("name") for step in steps] @@ -121,11 +126,19 @@ def test_report_dir_is_a_single_source_of_truth() -> None: workflow = load_workflow() report_dir = workflow["jobs"]["coverage"]["env"]["REPORT_DIR"] summary_run = _step(workflow, "Write coverage summary")["run"] + stage_run = _step(workflow, "Stage coverage report")["run"] upload_paths = _step(workflow, "Upload coverage report")["with"]["path"] assert "$REPORT_DIR" in summary_run - assert "${{ env.REPORT_DIR }}" in upload_paths + assert "$REPORT_DIR" in stage_run + assert "$ARTIFACT_DIR" in stage_run + assert upload_paths == "${{ env.ARTIFACT_DIR }}" assert report_dir not in summary_run - assert report_dir not in upload_paths + assert report_dir not in stage_run + + +def test_coverage_artifact_has_a_single_staging_root() -> None: + coverage = load_workflow()["jobs"]["coverage"] + assert coverage["env"]["ARTIFACT_DIR"] == "python-core-coverage-artifact" def test_write_coverage_summary_has_no_working_directory() -> None: @@ -143,6 +156,14 @@ def test_write_coverage_summary_falls_back_when_missing() -> None: assert '>> "$GITHUB_STEP_SUMMARY"' in run_text +def test_download_artifact_uses_only_supported_inputs() -> None: + report_steps = load_workflow()["jobs"]["report-weekly"]["steps"] + step = next( + step for step in report_steps if step.get("name") == "Download coverage report" + ) + assert "if-no-files-found" not in step["with"] + + def test_weekly_report_is_scheduled_and_mentions_the_integrations_roster() -> None: workflow = load_workflow() assert workflow["on"]["schedule"] == [{"cron": "33 20 * * 0"}] @@ -239,3 +260,6 @@ def test_weekly_digest_identifies_low_and_completely_uncovered_files( in digest ) assert "covered.rs" not in digest + assert module.format_line_ranges((1, 3, 5, 7, 9, 11, 13, 15, 17)) == ( + "1, 3, 5, 7, 9, 11, 13, 15, … (1 more ranges)" + )