Files
wehub-resource-sync 09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

317 lines
15 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
custom_branch:
description: 'Branch/ref to test (leave empty to use the triggering ref). Use this to run the full gate against a release branch that conflicts with main and so cannot run via pull_request.'
required: false
default: ''
concurrency:
# Include the dispatch input so a manual run against a release branch gets its
# own group instead of sharing (and cancelling) the push/PR run on the
# triggering ref. Empty for push/pull_request, so their behavior is unchanged.
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.custom_branch }}
cancel-in-progress: true
permissions:
contents: read
jobs:
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# Honor the workflow_dispatch custom_branch input; falls back to the
# triggering ref for push/pull_request (input is empty there).
ref: ${{ inputs.custom_branch || github.ref }}
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
shell: bash
run: |
# Retry to ride out transient registry/network blips during dep
# resolution: a single PyPI/GitHub hiccup was hard-failing the leg.
# `uv sync` has no internal retry across the full resolve+download.
# A real lockfile problem fails all 3 attempts identically, so this
# only absorbs flakes — it never masks a genuine resolution error.
attempt=1; max=3
until uv sync --frozen --extra browser --extra dev --extra markdown; do
if [ "$attempt" -ge "$max" ]; then
echo "::error::uv sync failed after $max attempts" >&2
exit 1
fi
echo "::warning::uv sync attempt $attempt failed; retrying in $((attempt * 15))s" >&2
sleep $((attempt * 15))
attempt=$((attempt + 1))
done
- name: Assert core imports without optional adapter extras
# This job uses the canonical lean install (browser+dev+markdown, no
# `mcp`/`server`). Since the test matrix now installs those extras, this
# is the remaining per-PR guard that the core package still imports
# without fastmcp/fastapi present — a stray top-level `import fastmcp` in
# non-adapter code would ImportError here, catching the regression before
# it reaches the release-time smoke test in publish.yml.
#
# `notebooklm_cli` (not `cli.grouped`) is imported because it is the
# console-script entrypoint that builds the full command tree via
# `cli.add_command(...)`, including `cli/mcp_cmd.py` — the most likely
# place a stray `import fastmcp` would land. `cli.grouped` only defines
# the Click Group subclass and traverses none of the command modules.
# `client` + `artifacts` cover the primary public Python-API surface
# (`from notebooklm import NotebookLMClient`). Since the lean install has
# neither fastmcp nor fastapi, a stray top-level import of either from any
# core/`_app` module reachable here fails the step. The `notebooklm-server`
# adapter is intentionally NOT imported: it legitimately requires fastapi
# (so it can't load on the lean install) and is exercised by the `test`
# matrix instead — `server` is not wired into the CLI command tree.
run: uv run python -c "import notebooklm, notebooklm.notebooklm_cli, notebooklm.client, notebooklm.artifacts"
- name: Run pre-commit checks
run: uv run pre-commit run --all-files
- name: Run type checking
run: uv run mypy src/notebooklm --ignore-missing-imports
- name: Assert workflow permissions are scoped
run: uv run python scripts/check_workflow_permissions.py
- name: Assert workflow secret-bearing jobs are gated
# Companion to the permissions check: prevents a new
# ``${{ secrets.NAME }}`` reference from landing without picking up
# a job-level ``environment:`` declaration or a step-level
# ``is_standard`` guard. See docs/development.md →
# "Workflow secret gates".
run: uv run python scripts/check_workflow_secret_gates.py
- name: Assert third-party actions in privileged workflows are SHA-pinned
# Supply-chain gate: every third-party action (non ``actions/*``) in
# publish / testpypi-publish / claude / rpc-health / nightly /
# verify-package must use a 40-char commit SHA, not a floating
# ``@v1`` / ``@release/v1`` / branch ref. Dependabot bumps the
# SHAs weekly via the grouped ``actions`` updates in
# ``.github/dependabot.yml``.
run: uv run python scripts/check_action_pinning.py
- name: Assert no deprecation targets the shipping version
# Release gate: a DeprecationWarning must never name the version in
# pyproject.toml as its removal target. Lapsed shims are allowlisted
# inside the script with a tracking issue.
run: uv run python scripts/check_deprecation_targets.py
- name: Assert coverage thresholds match
run: uv run python scripts/check_coverage_thresholds.py
- name: Assert CI install matches CONTRIBUTING.md
run: uv run python scripts/check_ci_install_parity.py
- name: Assert repo-structure map (docs/architecture.md) is fresh
run: uv run python scripts/check_claude_md_freshness.py
- name: Assert doc module references are fresh
# Companion to the CLAUDE.md freshness gate: guards the rest of the docs
# against stale module paths. Every relative link into src/notebooklm/
# must resolve, and every inline module ref in the live docs must point at
# a real module (rare historical mentions are allowlisted, shrink-only).
run: uv run python scripts/check_docs_module_refs.py
- name: Audit public API compatibility
# ``--check-stale`` also fails when an allowlist entry matches no break
# against the baseline (it is already in the baseline). This forces the
# allowlist to be pruned at each release boundary instead of silently
# accumulating cruft. See docs/releasing.md → prune-allowlist-at-release.
run: uv run python scripts/audit_public_api_compat.py --check-stale
- name: Assert per-method RPC coverage
# Static check: each RPCMethod member must have at least one test
# reference AND at least one cassette body containing its RPC id.
# Pre-existing gaps are grandfathered via PREEXISTING_GAPS inside the
# script — that set is a one-way ratchet and must not grow.
run: uv run python tests/scripts/check_method_coverage.py
- name: Verify e2e test fixtures
run: uv run pytest tests/e2e --collect-only -q
test:
name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
needs: quality
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
env:
# Pin Playwright's browser install dir to a single workspace-relative
# path on every OS. The previous dual-path cache stanza (``~/.cache``
# + ``~/AppData/Local``) intermittently failed the cache restore on
# Windows with ``actions/cache@v5`` — the missing Linux path tripped
# the post-restore validation on a cache hit. Pinning the path here
# lets the cache stanza below carry exactly one entry on every OS.
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
steps:
- uses: actions/checkout@v7
with:
# Honor the workflow_dispatch custom_branch input; falls back to the
# triggering ref for push/pull_request (input is empty there).
ref: ${{ inputs.custom_branch || github.ref }}
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
shell: bash
run: |
# Retry to ride out transient registry/network blips during dep
# resolution: a single PyPI/GitHub hiccup on one matrix entry was
# hard-failing the whole leg while the other 14 passed. `uv sync`
# has no internal retry across the full resolve+download. A real
# lockfile problem fails all 3 attempts identically, so this only
# absorbs flakes — it never masks a genuine resolution error.
#
# `mcp` + `server` + `impersonate` are installed here (on top of the
# canonical browser+dev+markdown set) so the MCP, server, and curl_cffi
# suites (`tests/unit/mcp`, `tests/integration/mcp_vcr`, `tests/server`,
# `tests/unit/test_curl_cffi_transport_poc.py`) run in the main matrix
# instead of being importorskip'd — that gives
# `src/notebooklm/{mcp,server}/**` and `_curl_cffi_transport.py` real
# coverage in coverage.json (no 0% blind spot) across the full
# 3.103.14 × 3-OS matrix, which also proves curl_cffi's native wheels
# resolve on every OS/Python. `cookies` is deliberately excluded:
# rookiepy fails on Python 3.13/3.14 (see check_ci_install_parity.py).
#
# Tradeoff: the deleted MCP/server jobs each enforced a *scoped*
# `--cov=src/notebooklm/{mcp,server} --cov-fail-under=90`. mcp/server (and
# the curl_cffi adapter) are now covered only by the global aggregate gate
# below — no per-subpackage floor backfills them, so a subpackage could
# regress within the aggregate's headroom. Add entries to
# `[tool.notebooklm.per_file_coverage_floors]` if that backstop is
# wanted back.
attempt=1; max=3
until uv sync --frozen --extra browser --extra dev --extra markdown --extra mcp --extra server --extra impersonate; do
if [ "$attempt" -ge "$max" ]; then
echo "::error::uv sync failed after $max attempts" >&2
exit 1
fi
echo "::warning::uv sync attempt $attempt failed; retrying in $((attempt * 15))s" >&2
sleep $((attempt * 15))
attempt=$((attempt + 1))
done
- name: Assert curl_cffi imports (native wheel; no silent skip)
# The matrix installs --extra impersonate, so curl_cffi MUST import on every
# OS/Python. It's a native wheel: if it installs but fails to import, the
# importorskip'd curl_cffi suites would silently skip in the coverage run
# rather than fail. This makes a broken native import a hard error.
shell: bash
run: uv run python -c "import curl_cffi"
- name: Get Playwright version
id: playwright-version
shell: bash
run: |
echo "version=$(uv pip show playwright | grep '^Version:' | cut -d' ' -f2)" >> $GITHUB_OUTPUT
- name: Cache Playwright browsers
uses: actions/cache@v6
# Tolerate transient cache-backend failures: a miss already falls
# through to the install step below, and a backend hiccup should be
# treated the same. Without this, a 29s backend flake on a single
# matrix entry hard-fails the job and skips the entire test run.
continue-on-error: true
with:
path: ${{ github.workspace }}/.playwright-browsers
# ``-ws`` suffix invalidates archives keyed to the prior dual-path
# stanza so first restores after this change land on the new path.
key: playwright-${{ matrix.os }}-${{ steps.playwright-version.outputs.version }}-ws
- name: Install Playwright browsers
run: uv run playwright install chromium
- name: Install Playwright system dependencies (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
if ! timeout 180s uv run playwright install-deps chromium; then
echo "::warning::Playwright system dependency installation timed out or failed."
echo "::warning::Continuing so the non-browser test matrix can run."
fi
- name: Assert cassettes are sanitized
# Runs on all matrix entries (ubuntu, macos, windows × 5 Python versions).
# An earlier bash-only gate was replaced by a portable Python invocation
# so the check runs uniformly across the cross-platform test matrix.
#
# ``--strict`` flips the repair-allowlist from "best-effort suppressor"
# to "must be empty" — the phase-2 cassette cleanup is done, so any
# entry sneaking back in is a CI failure (P1-5).
# ``--recursive`` extends the scan from ``tests/cassettes/*.yaml`` to
# ``tests/cassettes/**/*.yaml`` so a recorder can't smuggle a leak
# into a nested folder like ``tests/cassettes/gzip_coverage/`` (P1-5).
run: uv run python tests/scripts/check_cassettes_clean.py --strict --recursive
- name: Check fixtures for credential leaks
# The cassette guard above is scoped to ``tests/cassettes/*.yaml``. Golden
# RPC fixtures live under ``tests/fixtures/`` as ``.json`` (and one
# captured ``.html`` page), which embed the same WIZ_global_data shapes —
# a Google API key smuggled into a golden HTML fixture would otherwise
# slip past CI entirely (the GET_INTERACTIVE_HTML.json class). ``--secrets
# -only`` scans .json/.html/.yaml for high-severity credential shapes
# (Google auth tokens + API keys) without tripping on the intentional
# placeholder content (``"Scrubbed ..."`` names, test emails) that fills
# those fixtures.
run: uv run python tests/scripts/check_cassettes_clean.py --secrets-only --recursive tests/fixtures
- name: Run tests with coverage
# ``-n auto --dist loadgroup`` parallelizes the ~11k-test suite across the
# runner's cores (pytest-xdist is already a dev dep). ``loadgroup`` honors
# ``@pytest.mark.xdist_group`` — the isolation escape hatch for tests touching
# process-global state (tests/integration/concurrency/conftest.py); unmarked
# tests distribute like ``load``. The suite is parallel-safe via per-test
# ``NOTEBOOKLM_HOME`` tmp isolation, and pytest-cov merges per-worker coverage
# automatically, so the coverage.json + per-file floors below are unaffected.
#
# Emit coverage.json so the per-file floor check below can read it without
# re-running the suite. ``term-missing`` is kept so build logs still show the
# missing-lines summary.
run: uv run pytest -n auto --dist loadgroup --cov=src/notebooklm --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=90
- name: Assert per-file coverage floors
# Linux-only because ``coverage.json`` on Windows records backslash
# paths (e.g. ``src\notebooklm\cli\doctor.py``) that won't match the
# forward-slash keys in ``[tool.notebooklm.per_file_coverage_floors]``
# in pyproject.toml. macOS uses forward slashes and would also work,
# but a single OS is enough — the floors are platform-independent.
if: runner.os == 'Linux'
run: uv run python scripts/check_coverage_thresholds.py --coverage-json coverage.json