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:
- Always use a worktree - never work directly on main for releases
- Use PRs, not direct pushes - all release changes go through a PR
- Explicit confirmation required for: creating PR, publishing to TestPyPI, creating tags, pushing tags
- "ok" is not confirmation - restate what you're about to do and wait for explicit "yes"
- TestPyPI is mandatory - it catches packaging issues that tests cannot detect
- Public API compatibility audit is mandatory - do not publish while
scripts/audit_public_api_compat.pyreports 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 --helpoutput - Verify Python API docs match public exports in
__init__.py - Update
Last Updateddates 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 equalpyproject.toml—tests/unit/test_mcp_desktop_extension.pyenforces this, andpublish-mcpb.ymlaborts 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.Zonly 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.mdanddocs/deprecations.mdwhen the change is a deprecation removal or newly deprecated surfaceCHANGELOG.mdwith the migration pathscripts/api-compat-allowlist.jsonwith the exactcode,object, and a reviewer-readable reason; use itsextra_public_namessection 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:
During a pre-release cycle, base the range on the last stable tag so the aggregate changelog captures the whole cycle (a plain
git log $(git describe --tags --abbrev=0)..HEAD --onelinegit describewould 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]inCHANGELOG.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.locktoo — the version bump changes its workspace-package entry, and for a pre-release theuv syncre-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 Actions → Nightly E2E
- Click Run workflow, set custom_branch to
release/vX.Y.Z - Wait for E2E tests to pass
- If E2E tests fail:
- Fix issues in the release worktree
- Commit and push
- Re-run E2E tests
RPC Health Check on Release Branch
- Go to Actions → RPC 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:
- Fix issues in the release worktree
- Commit and push
- 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 Actions → Publish 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 Actions → Verify Package
- Click Run workflow with source:
testpypi - Wait for all tests to pass (unit, integration, E2E)
- If verification fails:
- Fix issues in the release worktree
- Bump patch version in
pyproject.toml(for a pre-release, bump the pre-release serial, e.g.0.8.0a1 → 0.8.0a2) - Update
CHANGELOG.mdwith fix - 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:
- Dep tree from
uv.lock.uv sync --frozen --extra browser --extra dev --extra markdown --extra mcp --extra serverinstalls the locked dependency tree for the full non-cookies extra set into.venv/: browser automation, developer tooling, Markdown export, MCP, and REST server dependencies.cookiesstays excluded because of the Python 3.13+rookiepyissue. This produces a deterministic dep tree without any TestPyPI lookups. - 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 byuv syncfor the actual published wheel.--no-depsis 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/pythonis required becauseuv syncdoes not seedpipinto the project venv — a baresource .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 Actions → Verify Package
- Click Run workflow with:
- source:
pypi
- source:
- 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 Releases → Draft 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 buildsnotebooklm-mcp.mcpband attaches it to the release as an asset (it re-checks that the manifest, tag, andpyproject.tomlversions 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 avX.Y.ZaNrelease 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 fromscripts/api-compat-allowlist.jsoneveryallowed_breaksentry that described avPREV → vX.Y.Zchange. These are now baked into thevX.Y.Zbaseline. 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 momentvX.Y.Zis 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.Xandnotebooklm.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:
- Version format is canonical PEP 440, byte-identical everywhere. Use
X.Y.ZaN/X.Y.ZbN/X.Y.ZrcN(e.g.0.8.0a1), tagvX.Y.ZaN. Do not use non-canonical forms like0.8.0-alpha.1,0.8.0.rc1, or0.8.0RC1:publish.ymltag-validation andverify-package.ymldo raw string compares, and Verify Package compares against the normalizedimportlib.metadata.version()— a non-canonical spelling passes tag-match but fails Verify Package with a spurious "version mismatch". - Serial progression within one cycle: advance the serial
a1 → a2 → … → b1 → … → rc1 → … → X.Y.0(final). Do not bump the patch between pre-releases. - A pre-release is the final version's real surface. A
X.Y.ZaNtag must already contain every breaking flip for that version with deprecation shims removed. Thetests/_guardrails/test_v080_release_gate.pyversion parser truncates the pre-release suffix, so the v0.8.0 breaking-flip release-gate fires at0.8.0a1. A "soft" alpha that still carries shims is not supported. - Re-lock after the bump. After editing
pyproject.toml's version, runuv syncsouv.lock's workspace-package version matches, else CI--frozeninstalls fail as out-of-date. - Normal users are unaffected.
pip install notebooklm-pyskips pre-releases; onlypip install --preor an exact==0.8.0a1pin selects one. - 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. - Baseline / allowlist: no special handling.
audit_public_api_compat.pykeeps 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 finalX.Y.0tag. - 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.
- 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.