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

23 KiB

Release Checklist

Status: Active Last Updated: 2026-05-23

Checklist for releasing a new version of notebooklm-py.

For Claude Code: Follow this checklist step by step. NO STEPS ARE OPTIONAL. "Quick release" means efficient execution, NOT skipping steps.

Critical rules:

  1. Always use a worktree - never work directly on main for releases
  2. Use PRs, not direct pushes - all release changes go through a PR
  3. Explicit confirmation required for: creating PR, publishing to TestPyPI, creating tags, pushing tags
  4. "ok" is not confirmation - restate what you're about to do and wait for explicit "yes"
  5. TestPyPI is mandatory - it catches packaging issues that tests cannot detect
  6. Public API compatibility audit is mandatory - do not publish while scripts/audit_public_api_compat.py reports unapproved breaks

Pre-Flight Summary

Before starting, present this summary to the user:

Release Plan for vX.Y.Z:
1. Create release worktree (`release/vX.Y.Z` branch)
2. Update pyproject.toml and CHANGELOG.md
3. Run public API compatibility audit
4. Run pre-commit checks (ruff, mypy, pytest)
5. Commit changes
6. ⏸️ CONFIRM: Create PR to main?
7. Wait for CI to pass on PR
8. Run E2E and RPC health checks on release branch
9. ⏸️ CONFIRM: Publish to TestPyPI?
10. Verify TestPyPI package
11. Merge PR to main
12. ⏸️ CONFIRM: Create and push tag vX.Y.Z?
13. Wait for PyPI publish
14. Create GitHub release (add `--prerelease` for a pre-release — see [Pre-releases](#pre-releases-alpha--beta--rc))
15. Clean up worktree

Proceed with release preparation?

Setup

Create Release Worktree

  • Create a dedicated worktree for the release:
    git worktree add .worktrees/vX.Y.Z -b release/vX.Y.Z main
    cd .worktrees/vX.Y.Z
    
  • Set up the development environment:
    # Canonical contributor install; add --extra mcp/--extra server when
    # validating those adapters locally. `[all]` also includes mcp+server
    # and deliberately excludes cookies (Python 3.13+ rookiepy issue).
    uv sync --frozen --extra browser --extra dev --extra markdown
    uv run playwright install chromium
    

Pre-Release

Documentation

  • Verify README.md reflects current features
  • Check CLI reference matches notebooklm --help output
  • Verify Python API docs match public exports in __init__.py
  • Update Last Updated dates in modified docs
  • Verify example scripts have valid syntax:
    uv run python -m py_compile examples/*.py
    

Related docs to check/update if relevant:

Doc Update when...
README.md New features, changed capabilities, Beyond the Web UI section
SKILL.md New CLI commands, changed flags, new workflows
cli-reference.md Any CLI changes
python-api.md New/changed Python API
troubleshooting.md New known issues, fixed issues to remove
development.md Architecture changes, new test patterns
configuration.md New env vars, config options
stability.md Public API changes, deprecations

Version Bump

  • Determine version bump type using this decision tree:

    Did you add new items to `__all__` in `__init__.py`?
    ├── YES → MINOR (new public API)
    └── NO → PATCH (fixes, logging, UX, internal improvements)
    
    When in doubt, it's PATCH.
    

    For a pre-release, target the final version plus a pre-release serial (e.g. 0.8.0a1) — see Pre-releases.

    See Version Numbering for full details.

  • Update version in pyproject.toml:

    version = "X.Y.Z"
    
  • Update the matching version in desktop-extension/manifest.json ("version": "X.Y.Z"). It must equal pyproject.tomltests/unit/test_mcp_desktop_extension.py enforces this, and publish-mcpb.yml aborts the release-asset build on a mismatch.

Public API Compatibility Gate

  • Run the cross-release public API audit before committing release changes:
    uv run python scripts/audit_public_api_compat.py
    
  • Read every reported break. The audit compares this checkout with the latest reachable stable release tag (pre-releases are skipped; override with --baseline-ref vX.Y.Z only when auditing against a specific previous release).
  • If the audit reports an unapproved break, prefer a compatibility shim or restored alias. Do not proceed to packaging while unapproved breaks remain.
  • If a break is intentional and allowed by the stability policy, update all of these in the same PR:
    • docs/stability.md and docs/deprecations.md when the change is a deprecation removal or newly deprecated surface
    • CHANGELOG.md with the migration path
    • scripts/api-compat-allowlist.json with the exact code, object, and a reviewer-readable reason; use its extra_public_names section only for documented names that are intentionally public but not listed in __all__
  • Re-run the audit. The acceptable release state is either no breaks or only reviewed allowlisted breaks printed by the script.
  • If CLI commands, flags, arguments, help text, or env-var bindings changed, also run the CLI contract baseline:
    uv run pytest tests/unit/cli/test_cli_contract.py
    

The allowlist is not a bypass for accidental breakage. It is a paper trail for intentional removals already permitted by docs/stability.md, such as a completed deprecation cycle. Public enum members exposed through documented imports, including notebooklm.rpc.RPCMethod, count as API surface. So do documented client namespace methods under NotebookLMClient.notebooks, sources, artifacts, chat, research, notes, settings, and sharing. Function signatures include positional/keyword compatibility and default values; changing a default is a public behavior change.

The allowlist is release-scoped: each entry records a break pending the next stable release, not a permanent exemption. A pre-release tag does not advance the baseline (see the Pre-releases section). Once a stable vX.Y.Z ships, its entries are in the baseline and must be pruned — see Prune the API-Compat Allowlist. The Code Quality job runs the audit with --check-stale, so a stale entry (one matching no break against the baseline) is a CI failure, not silent cruft.

Changelog

  • Get commits since last release:
    git log $(git describe --tags --abbrev=0)..HEAD --oneline
    
    During a pre-release cycle, base the range on the last stable tag so the aggregate changelog captures the whole cycle (a plain git describe would start at the alpha):
    git log $(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' \
      --exclude '*a[0-9]*' --exclude '*b[0-9]*' --exclude '*rc[0-9]*')..HEAD --oneline
    
  • Generate changelog entries in Keep a Changelog format:
    • Added - New features
    • Fixed - Bug fixes
    • Changed - Changes in existing functionality
    • Deprecated - Soon-to-be removed features
    • Removed - Removed features
    • Security - Security fixes
  • Add entries under ## [Unreleased] in CHANGELOG.md
  • Move [Unreleased] content to new version section:
    ## [Unreleased]
    
    ## [X.Y.Z] - YYYY-MM-DD
    
  • Update comparison links at bottom of CHANGELOG.md:
    [Unreleased]: https://github.com/teng-lin/notebooklm-py/compare/vX.Y.Z...HEAD
    [X.Y.Z]: https://github.com/teng-lin/notebooklm-py/compare/vPREV...vX.Y.Z
    

Pre-Commit Checks

  • Run all checks before committing:
    uv run pre-commit run --all-files && uv run mypy src/notebooklm --ignore-missing-imports && uv run pytest
    
  • Ensure CI runs the same lint gate (pre-commit run --all-files) as local release prep
  • Run documentation drift checks (mirror the CI gates in .github/workflows/test.yml):
    uv run python scripts/check_ci_install_parity.py
    uv run python scripts/check_claude_md_freshness.py
    uv run python scripts/check_docs_module_refs.py
    # second run confirms release edits did not introduce new API drift
    uv run python scripts/audit_public_api_compat.py --check-stale
    
  • Fix any issues before proceeding

Commit

  • Verify changes:
    git diff
    
  • Commit (stage uv.lock too — the version bump changes its workspace-package entry, and for a pre-release the uv sync re-lock in the Pre-releases section requires it; staging it is a no-op on releases where it did not change):
    git add pyproject.toml desktop-extension/manifest.json CHANGELOG.md uv.lock docs/
    git commit -m "chore: release vX.Y.Z"
    
  • Show commit to user:
    git show --stat
    

CI Verification

Create Pull Request

  • ⏸️ CONFIRM: Ask user "Ready to create PR for release vX.Y.Z?"
  • Push branch and create PR:
    git push -u origin release/vX.Y.Z
    gh pr create --title "chore: release vX.Y.Z" --body "Release vX.Y.Z
    
    See CHANGELOG.md for details."
    
  • Wait for test.yml to pass:
    • Linting and formatting
    • Type checking
    • Unit and integration tests (Python 3.10-3.14, all platforms)

E2E Tests on Release Branch

  • Go to ActionsNightly E2E
  • Click Run workflow, set custom_branch to release/vX.Y.Z
  • Wait for E2E tests to pass
  • If E2E tests fail:
    1. Fix issues in the release worktree
    2. Commit and push
    3. Re-run E2E tests

RPC Health Check on Release Branch

  • Go to ActionsRPC Health Check
  • Click Run workflow, set custom_branch to release/vX.Y.Z
  • Wait for RPC health check to pass
  • If RPC health check fails:
    1. Fix issues in the release worktree
    2. Commit and push
    3. Re-run RPC health check

MCP connector smoke (manual, per release)

The nightly E2E run now installs --extra mcp and exercises the MCP/CLI layers against the live API (tests/e2e/test_mcp*.py, test_cli_live.py) — so the on-demand Nightly run above already covers Layer A (tools ⇄ live API) and Layer B (HTTP transport + signed-URL routes, in-process). What it cannot cover is claude.ai actually driving the remote connector (OAuth login + the browser upload/download pages). Verify that by hand, once per release (~10 min):

  • cd deploy && make dev (or point at your deployed tunnel) and connect the connector in claude.ai.
  • OAuth login page renders and the password gate accepts your password.
  • List notebooks through the connector.
  • Add a URL source.
  • Ask a question and get a grounded answer.
  • Generate one artifact (e.g. a report).
  • Upload a local file via the signed link (open it in a browser, pick a file, confirm the source lands).
  • Download the generated artifact via the signed link.

Bootstrap / sanity helper for the upload+download halves (drives a RUNNING server's file routes, prints PASS/FAIL). Requires the mcp extra (e.g. uv sync --extra mcp):

python scripts/mcp_live_smoke.py \
    --base-url https://your-tunnel.example.com \
    --bearer "$NOTEBOOKLM_MCP_TOKEN" \
    --notebook <notebook-id>

Package Verification

⚠️ REQUIRED: Do NOT skip TestPyPI verification. Always test on TestPyPI before publishing to PyPI. This catches packaging issues that unit tests cannot detect (missing files, broken imports, dependency problems).

Publish to TestPyPI

  • ⏸️ CONFIRM: Ask user "Ready to publish to TestPyPI?"
  • Go to ActionsPublish to TestPyPI
  • Click Run workflow, select the release/vX.Y.Z branch
  • Wait for upload to complete
  • Verify package appears: https://test.pypi.org/project/notebooklm-py/

Note: TestPyPI does not allow re-uploading the same version. If you need to fix issues after publishing, bump the patch version and start over. For a pre-release, bump the pre-release serial (a1 → a2), not the patch.

Verify TestPyPI Package

  • Go to ActionsVerify Package
  • Click Run workflow with source: testpypi
  • Wait for all tests to pass (unit, integration, E2E)
  • If verification fails:
    1. Fix issues in the release worktree
    2. Bump patch version in pyproject.toml (for a pre-release, bump the pre-release serial, e.g. 0.8.0a1 → 0.8.0a2)
    3. Update CHANGELOG.md with fix
    4. Commit, push, and re-run Publish to TestPyPI

How the verify chain works

The Verify Package workflow (.github/workflows/verify-package.yml) exercises a published wheel in two phases so packaging bugs cannot silently fall through to a stale PyPI mirror:

  1. Dep tree from uv.lock. uv sync --frozen --extra browser --extra dev --extra markdown --extra mcp --extra server installs the locked dependency tree for the full non-cookies extra set into .venv/: browser automation, developer tooling, Markdown export, MCP, and REST server dependencies. cookies stays excluded because of the Python 3.13+ rookiepy issue. This produces a deterministic dep tree without any TestPyPI lookups.
  2. Wheel from the chosen index, --no-deps. uv pip install --python .venv/bin/python --no-deps --reinstall --no-cache --only-binary=:all: --index-url <testpypi|pypi> "notebooklm-py==<version>" swaps the editable install left behind by uv sync for the actual published wheel. --no-deps is load-bearing: without it the previous --extra-index-url https://pypi.org/simple/ fallback would mask a broken/missing TestPyPI upload by resolving an older version from PyPI. --reinstall --no-cache --only-binary=:all: guarantee we test the freshly-uploaded wheel and never a cached sdist. The explicit --python .venv/bin/python is required because uv sync does not seed pip into the project venv — a bare source .venv/bin/activate && pip install … would silently fall back to the runner's system pip and leave the editable install in place.

The same chain runs for source: pypi (post-publish verification) — only the wheel index changes; the locked dep tree is identical.

The Publish to PyPI step in publish.yml also opts into PEP 740 attestations (attestations: true). pypa/gh-action-pypi-publish generates an in-toto attestation per uploaded artifact and signs it under Trusted Publishing (OIDC, no API token); PyPI accepts and stores the attestation alongside the wheel, giving downstream consumers cryptographic proof the wheel was built by this GitHub workflow on this tagged commit. The PyPA action enables attestations by default for Trusted Publishing flows, so the explicit attestations: true is documentation-as-code rather than a feature flag.


Merge to Main

  • Once TestPyPI verification passes, merge the PR:
    gh pr merge --squash --delete-branch
    
  • Pull latest main (in main repo):
    cd /path/to/notebooklm-py
    git pull origin main
    

Release

Tag and Publish

  • ⏸️ CONFIRM: Ask user "TestPyPI verified. Ready to create tag vX.Y.Z and publish to PyPI? This is irreversible."
  • Create tag (on main branch):
    git tag vX.Y.Z
    
  • Push tag:
    git push origin vX.Y.Z
    
  • Wait for publish.yml to complete
  • Verify on PyPI: https://pypi.org/project/notebooklm-py/

PyPI Verification

  • Go to ActionsVerify Package
  • Click Run workflow with:
    • source: pypi
  • Wait for all tests to pass

GitHub Release

  • Create release from tag:

    gh release create vX.Y.Z --title "vX.Y.Z" --notes "$(cat CHANGELOG.md | sed -n '/## \[X.Y.Z\]/,/## \[/p' | sed '$d')"
    

    Or manually:

    • Go to ReleasesDraft a new release
    • Select tag vX.Y.Z
    • Title: vX.Y.Z
    • Copy release notes from CHANGELOG.md
    • Publish release
  • Publishing a stable release fires publish-mcpb.yml, which builds notebooklm-mcp.mcpb and attaches it to the release as an asset (it re-checks that the manifest, tag, and pyproject.toml versions all agree). Confirm the asset appears under the release once the workflow finishes — that is the one-click Claude Desktop bundle users download. Pre-releases skip this step by design (the thin launcher resolves the latest stable server, not the pre-release), so a vX.Y.ZaN release carries no .mcpb.

Prune the API-Compat Allowlist

Pushing a stable tag advances the audit baseline — audit_public_api_compat.py resolves the latest reachable stable release tag (pre-releases are skipped), so the breaks you just shipped are now part of the vX.Y.Z baseline and are no longer breaks against it. (A pre-release tag such as v0.8.0a1 does not advance the baseline, so the allowlist survives the whole pre-release cycle and prunes once, here, at the final vX.Y.Z.) The baseline advances automatically; the allowlist is the manual half that must reset, or it accumulates dead entries that describe nothing.

The lifecycle to keep in mind:

  • baseline = the last stable released version (automatic — the latest stable release tag; pre-releases are skipped).
  • allowlist = the intentional breaks pending the next release. It should reset to (near) empty at each release boundary.

Concretely, after the tag is pushed, prune the entries that just shipped:

  • In a follow-up PR (on main, after the tag exists), remove from scripts/api-compat-allowlist.json every allowed_breaks entry that described a vPREV → vX.Y.Z change. These are now baked into the vX.Y.Z baseline. List the stale entries with:
    uv run python scripts/audit_public_api_compat.py --json \
      | python -c "import json,sys; print('\n'.join(f\"{e['code']}  {e['object']}\" for e in json.load(sys.stdin)['stale_allowances']))"
    
  • Re-run the gate in strict mode — it must report no stale entries:
    uv run python scripts/audit_public_api_compat.py --check-stale
    

Forcing function: the Code Quality job runs the audit with --check-stale, which fails on any allowlist entry that matches no break against the baseline. So the moment vX.Y.Z is tagged, the just-shipped entries become stale and CI goes red until this prune PR lands — the prune is mandatory, not a checklist nicety. The pair-aware rule keeps the two path-views of a callable (notebooklm.X and notebooklm.client.X) together: a unit is pruned only when neither view still matches a break.


Cleanup

Remove Release Worktree

  • Return to main repo:
    cd /path/to/notebooklm-py
    
  • Remove the release worktree:
    git worktree remove .worktrees/vX.Y.Z
    
  • Delete the local branch (if not already deleted by PR merge):
    git branch -d release/vX.Y.Z
    

Troubleshooting

CI fails on PR

Fix issues in the release worktree and push again:

# In release worktree
git add -A
git commit -m "fix: address CI failures"
git push

Need to abort release

# Close the PR without merging
gh pr close

# Remove worktree
git worktree remove .worktrees/vX.Y.Z

# Delete local branch
git branch -D release/vX.Y.Z

# Delete remote branch (if pushed)
git push origin --delete release/vX.Y.Z

Tag already exists

# Delete local tag
git tag -d vX.Y.Z

# Delete remote tag (if pushed)
git push origin :refs/tags/vX.Y.Z

TestPyPI upload fails

  • Check if version already exists on TestPyPI
  • TestPyPI doesn't allow re-uploading same version
  • Bump to next patch version if needed (for a pre-release, bump the pre-release serial, e.g. 0.8.0a1 → 0.8.0a2)

Pre-releases (alpha / beta / rc)

Pre-releases let you stage a release for early adopters without affecting normal users. PEP 440 pre-release versions flow through the existing build, tag, publish, and verify machinery unchanged, and pip install notebooklm-py will not serve them to normal users. Follow the normal checklist above, with these differences:

  1. Version format is canonical PEP 440, byte-identical everywhere. Use X.Y.ZaN / X.Y.ZbN / X.Y.ZrcN (e.g. 0.8.0a1), tag vX.Y.ZaN. Do not use non-canonical forms like 0.8.0-alpha.1, 0.8.0.rc1, or 0.8.0RC1: publish.yml tag-validation and verify-package.yml do raw string compares, and Verify Package compares against the normalized importlib.metadata.version() — a non-canonical spelling passes tag-match but fails Verify Package with a spurious "version mismatch".
  2. Serial progression within one cycle: advance the serial a1 → a2 → … → b1 → … → rc1 → … → X.Y.0 (final). Do not bump the patch between pre-releases.
  3. A pre-release is the final version's real surface. A X.Y.ZaN tag must already contain every breaking flip for that version with deprecation shims removed. The tests/_guardrails/test_v080_release_gate.py version parser truncates the pre-release suffix, so the v0.8.0 breaking-flip release-gate fires at 0.8.0a1. A "soft" alpha that still carries shims is not supported.
  4. Re-lock after the bump. After editing pyproject.toml's version, run uv sync so uv.lock's workspace-package version matches, else CI --frozen installs fail as out-of-date.
  5. Normal users are unaffected. pip install notebooklm-py skips pre-releases; only pip install --pre or an exact ==0.8.0a1 pin selects one.
  6. Changelog. Keep pre-release changes accumulating under ## [Unreleased] (or a single in-progress ## [0.8.0] heading). Do not cut a dated [X.Y.ZaN] section per pre-release.
  7. Baseline / allowlist: no special handling. audit_public_api_compat.py keeps the baseline on the last stable tag through the whole pre-release cycle (pre-release tags are skipped), so the allowlist behaves exactly as for a normal release — one prune after the final X.Y.0 tag.
  8. Gates still apply; none are optional. The public-API audit, pre-commit/mypy/pytest, CI on PR, TestPyPI, and Verify Package all run unchanged. Verify Package reads the version from the checked-out ref, so dispatch it on the pre-release branch. Because Verify Package runs E2E, there is no "skip E2E for a pre-release" shortcut.
  9. GitHub release must be flagged as a pre-release, and the notes must extract the aggregate heading (not a per-pre-release one, which would yield empty notes):
    gh release create v0.8.0a1 --prerelease --title "v0.8.0a1" \
      --notes "$(sed -n '/## \[0.8.0\]/,/## \[/p' CHANGELOG.md | sed '$d')"
    

Version Numbering

IMPORTANT: Read stability.md before deciding version bump.

Change Type Bump Example
RPC method ID fixes PATCH 0.1.0 → 0.1.1
Bug fixes PATCH 0.1.1 → 0.1.2
Internal improvements (logging, auth UX, CI) PATCH 0.1.2 → 0.1.3
New public API (new classes, methods in __all__) MINOR 0.1.3 → 0.2.0
Breaking changes to public API MAJOR 0.2.0 → 1.0.0

Key distinction: "New features" means new public API surface (additions to __all__ in __init__.py). Internal improvements, better error messages, logging enhancements, and UX improvements are PATCH releases.