refactor: modernize release process with dynamic versioning and CI workflows (#28)

* refactor: modernize release process with dynamic versioning and CI workflows

- Add dynamic version sourcing from pyproject.toml via importlib.metadata
- Create docs/releasing.md with step-by-step release checklist
- Add .github/workflows/verify-package.yml for TestPyPI/PyPI verification
- Simplify docs/development.md releasing section (references releasing.md)
- Update CONTRIBUTING.md doc structure to include releasing.md
- Remove obsolete scripts/pre_release_check.sh (superseded by CI workflows)
- Auto-format example scripts with ruff

The release process now uses CI workflows for verification instead of
local scripts, with a checklist-based approach for Claude Code automation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: separate TestPyPI publish and verify workflows

- Add testpypi-publish.yml for uploading to TestPyPI (one-time per version)
- Simplify verify-package.yml to only verify (can run multiple times)
- Update releasing.md with clearer workflow steps and failure recovery
- Update development.md workflows table

This separation addresses the constraint that TestPyPI doesn't allow
re-uploading the same version - publish once, verify as many times
as needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address code review findings from polish pipeline

1. Replace fragile grep/sed version extraction with Python tomllib parser
2. Fix reference to non-existent basic-usage.py (now quickstart.py)
3. Add shell: bash directive for portability in verify-package.yml
4. Add repository check for E2E tests (fork protection)
5. Add tag validation in publish.yml to ensure tag matches pyproject.toml
6. Move import outside try block in __init__.py (code simplification)
7. Consolidate test dependency installation steps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR review findings

1. Fix PEP 440 version format: 0.0.0.dev → 0.0.0.dev0
2. Fix example script check: use py_compile instead of --help
3. Add logging for version fallback to aid debugging
4. Add fork skip notification with warning in workflow summary
5. Add secrets validation before E2E tests
6. Add force push warnings in troubleshooting section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Teng Lin 2026-01-14 01:01:59 -05:00 committed by GitHub
parent af212b4fc6
commit 5abb134d46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 456 additions and 147 deletions

View file

@ -21,6 +21,16 @@ jobs:
with:
python-version: "3.12"
- name: Validate tag matches version
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
TOML_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
echo "Error: Tag version ($TAG_VERSION) doesn't match pyproject.toml ($TOML_VERSION)"
exit 1
fi
echo "Version validated: $TOML_VERSION"
- name: Install build tools
run: |
python -m pip install --upgrade pip

50
.github/workflows/testpypi-publish.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Publish to TestPyPI
on:
workflow_dispatch:
jobs:
publish:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Get version from pyproject.toml
id: version
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Publishing version: $VERSION"
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Upload to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
- name: Summary
run: |
echo "## Published to TestPyPI" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Package:** https://test.pypi.org/project/notebooklm-py/${{ steps.version.outputs.version }}/" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Next Step:** Run the **Verify Package** workflow with source=testpypi" >> $GITHUB_STEP_SUMMARY

123
.github/workflows/verify-package.yml vendored Normal file
View file

@ -0,0 +1,123 @@
name: Verify Package
on:
workflow_dispatch:
inputs:
source:
description: 'Package source'
required: true
default: 'testpypi'
type: choice
options:
- testpypi
- pypi
jobs:
verify:
name: Verify from ${{ inputs.source }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: 'pip'
- name: Get version from pyproject.toml
id: version
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create verification venv
run: python -m venv verify-venv
- name: Install from TestPyPI
if: inputs.source == 'testpypi'
shell: bash
run: |
source verify-venv/bin/activate
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
notebooklm-py==${{ steps.version.outputs.version }}
- name: Install from PyPI
if: inputs.source == 'pypi'
shell: bash
run: |
source verify-venv/bin/activate
pip install notebooklm-py==${{ steps.version.outputs.version }}
- name: Verify version
shell: bash
run: |
source verify-venv/bin/activate
INSTALLED=$(python -c "from notebooklm import __version__; print(__version__)")
EXPECTED="${{ steps.version.outputs.version }}"
if [ "$INSTALLED" != "$EXPECTED" ]; then
echo "Version mismatch: installed=$INSTALLED expected=$EXPECTED"
exit 1
fi
echo "Version verified: $INSTALLED"
- name: Verify CLI
shell: bash
run: |
source verify-venv/bin/activate
notebooklm --version
notebooklm --help
- name: Verify imports
shell: bash
run: |
source verify-venv/bin/activate
python -c "from notebooklm import NotebookLMClient, Notebook, Source, Artifact"
echo "Core imports verified"
- name: Install test dependencies
shell: bash
run: |
source verify-venv/bin/activate
pip install pytest pytest-asyncio pytest-httpx pytest-cov pytest-rerunfailures python-dotenv vcrpy playwright
playwright install chromium
playwright install-deps
- name: Run unit tests
shell: bash
run: |
source verify-venv/bin/activate
pytest tests/unit -v
- name: Run integration tests
shell: bash
run: |
source verify-venv/bin/activate
pytest tests/integration -v
- name: Skip E2E tests (fork)
if: github.repository != 'teng-lin/notebooklm-py'
run: |
echo "::warning::E2E tests skipped - secrets not available in fork repositories"
echo "## E2E Tests Skipped" >> $GITHUB_STEP_SUMMARY
echo "E2E tests were not run because this workflow is executing on a fork." >> $GITHUB_STEP_SUMMARY
- name: Run E2E tests
if: github.repository == 'teng-lin/notebooklm-py'
shell: bash
env:
NOTEBOOKLM_AUTH_JSON: ${{ secrets.NOTEBOOKLM_AUTH_JSON }}
NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID: ${{ secrets.NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID }}
run: |
source verify-venv/bin/activate
if [ -z "$NOTEBOOKLM_AUTH_JSON" ]; then
echo "::error::NOTEBOOKLM_AUTH_JSON secret is not configured"
exit 1
fi
if [ -z "$NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID" ]; then
echo "::error::NOTEBOOKLM_READ_ONLY_NOTEBOOK_ID secret is not configured"
exit 1
fi
pytest tests/e2e -m "not variants" --reruns 2 -v

View file

@ -143,7 +143,8 @@ docs/
├── configuration.md # Storage and settings
├── troubleshooting.md # Common issues and solutions
├── stability.md # API versioning policy
├── development.md # Architecture, testing, releasing
├── development.md # Architecture and testing
├── releasing.md # Release checklist
├── rpc-development.md # RPC capture and debugging
├── rpc-reference.md # RPC payload structures
└── examples/ # Runnable example scripts

View file

@ -1,7 +1,7 @@
# Contributing Guide
**Status:** Active
**Last Updated:** 2026-01-13
**Last Updated:** 2026-01-14
This guide covers everything you need to contribute to `notebooklm-py`: architecture overview, testing, and releasing.
@ -47,12 +47,12 @@ src/notebooklm/
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Layer │
│ cli/session.py, cli/notebook.py, cli/generate.py, etc. │
│ cli/session.py, cli/notebook.py, cli/generate.py, etc.
└───────────────────────────┬─────────────────────────────────┘
┌───────────────────────────▼─────────────────────────────────┐
│ Client Layer │
│ NotebookLMClient → NotebooksAPI, SourcesAPI, ArtifactsAPI │
│ NotebookLMClient → NotebooksAPI, SourcesAPI, ArtifactsAPI
└───────────────────────────┬─────────────────────────────────┘
┌───────────────────────────▼─────────────────────────────────┐
@ -187,58 +187,22 @@ Need network?
## Releasing
### Pre-release Checklist
See **[releasing.md](releasing.md)** for the complete release checklist.
- [ ] All tests pass: `pytest`
- [ ] E2E readonly tests pass: `pytest tests/e2e -m readonly`
- [ ] No uncommitted changes: `git status`
- [ ] On `main` branch with latest changes
- [ ] Version updated in `src/notebooklm/__init__.py`
- [ ] CHANGELOG.md updated
### Quick Reference
### Release Steps
1. Validate documentation is up to date
2. Update version in `pyproject.toml`
3. Generate changelog entries and update `CHANGELOG.md`
4. Commit and push to main
5. Wait for CI + trigger E2E on main
6. Verify on TestPyPI (verify-package workflow)
7. Tag and push → publishes to PyPI
8. Verify on PyPI (verify-package workflow)
1. **Update version** in `src/notebooklm/__init__.py`:
```python
__version__ = "X.Y.Z"
```
### Version Source of Truth
2. **Update CHANGELOG.md** with release notes
3. **Commit and push:**
```bash
git add -A
git commit -m "chore: release vX.Y.Z"
git push
```
4. **Test on TestPyPI:**
```bash
python -m build
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple notebooklm-py
notebooklm --version
```
5. **Create release tag:**
```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```
This triggers GitHub Actions to publish to PyPI automatically.
### Versioning Policy
| Change Type | Bump | Example |
|-------------|------|---------|
| RPC method ID fixes | PATCH | 0.1.0 → 0.1.1 |
| Bug fixes | PATCH | 0.1.1 → 0.1.2 |
| New features (backward compatible) | MINOR | 0.1.2 → 0.2.0 |
| Breaking API changes | MAJOR | 0.2.0 → 1.0.0 |
See [stability.md](stability.md) for full versioning policy.
Version is defined in `pyproject.toml`. The `__version__` in `__init__.py` is dynamically read using `importlib.metadata`.
---
@ -250,6 +214,8 @@ See [stability.md](stability.md) for full versioning policy.
|----------|---------|---------|
| `test.yml` | Push/PR | Unit tests, linting, type checking |
| `nightly.yml` | Daily 6 AM UTC | E2E tests with real API |
| `testpypi-publish.yml` | Manual dispatch | Publish to TestPyPI |
| `verify-package.yml` | Manual dispatch | Verify TestPyPI or PyPI install + E2E |
| `publish.yml` | Tag push | Publish to PyPI |
### Setting Up Nightly E2E Tests

View file

@ -16,9 +16,8 @@ Usage:
"""
import asyncio
from pathlib import Path
from notebooklm import NotebookLMClient
from notebooklm import NotebookLMClient
# Example sources to import
SOURCES = {
@ -80,9 +79,7 @@ async def main():
print("\nImporting text content...")
for item in SOURCES["text"]:
try:
source = await client.sources.add_text(
nb.id, item["title"], item["content"]
)
source = await client.sources.add_text(nb.id, item["title"], item["content"])
results["success"].append(f"Text: {source.title}")
print(f" + {source.title}")
except Exception as e:
@ -91,7 +88,7 @@ async def main():
# 5. Report results
print("\n" + "=" * 40)
print(f"Import complete!")
print("Import complete!")
print(f" Successful: {len(results['success'])}")
print(f" Failed: {len(results['failed'])}")

View file

@ -12,14 +12,14 @@ Prerequisites:
"""
import asyncio
from notebooklm import NotebookLMClient, ChatMode, ChatGoal, ChatResponseLength
from notebooklm import ChatGoal, ChatMode, ChatResponseLength, NotebookLMClient
async def main():
"""Demonstrate chat and conversation features."""
async with await NotebookLMClient.from_storage() as client:
# Create a notebook with some content
print("Setting up notebook with sources...")
notebook = await client.notebooks.create("Python Learning")
@ -47,7 +47,7 @@ async def main():
"What are the main features of Python?",
)
print(f"Question: What are the main features of Python?")
print("Question: What are the main features of Python?")
print(f"Answer: {result.answer[:500]}...")
print(f"Conversation ID: {result.conversation_id}")
print(f"Turn number: {result.turn_number}")
@ -66,7 +66,7 @@ async def main():
conversation_id=result.conversation_id, # Continue the conversation
)
print(f"Follow-up: How does it compare to other programming languages?")
print("Follow-up: How does it compare to other programming languages?")
print(f"Answer: {followup.answer[:500]}...")
print(f"Is follow-up: {followup.is_follow_up}")
print(f"Turn number: {followup.turn_number}")
@ -78,7 +78,7 @@ async def main():
conversation_id=result.conversation_id,
)
print(f"\nFollow-up 2: What about for data science specifically?")
print("\nFollow-up 2: What about for data science specifically?")
print(f"Answer: {followup2.answer[:400]}...")
# =====================================================================
@ -143,8 +143,8 @@ async def main():
goal=ChatGoal.CUSTOM,
response_length=ChatResponseLength.DEFAULT,
custom_prompt="You are an experienced Python developer. "
"Explain concepts with practical code examples. "
"Focus on best practices and real-world usage.",
"Explain concepts with practical code examples. "
"Focus on best practices and real-world usage.",
)
custom_result = await client.chat.ask(

View file

@ -16,6 +16,7 @@ Prerequisites:
import asyncio
import json
from notebooklm import NotebookLMClient, ReportFormat
@ -23,7 +24,6 @@ async def main():
"""Demonstrate notes and mind map functionality."""
async with await NotebookLMClient.from_storage() as client:
# Create a notebook for our examples
print("Creating notebook...")
notebook = await client.notebooks.create("Study Notes Demo")
@ -51,10 +51,10 @@ async def main():
notebook.id,
title="Key Concepts",
content="# Data Structures Overview\n\n"
"- Arrays: Sequential memory storage\n"
"- Linked Lists: Node-based storage\n"
"- Trees: Hierarchical organization\n"
"- Hash Tables: Key-value mapping",
"- Arrays: Sequential memory storage\n"
"- Linked Lists: Node-based storage\n"
"- Trees: Hierarchical organization\n"
"- Hash Tables: Key-value mapping",
)
print(f"Created note: {note1.title} (ID: {note1.id})")
@ -63,8 +63,8 @@ async def main():
notebook.id,
title="Study Questions",
content="1. What is time complexity?\n"
"2. When to use arrays vs linked lists?\n"
"3. How do hash collisions work?",
"2. When to use arrays vs linked lists?\n"
"3. How do hash collisions work?",
)
print(f"Created note: {note2.title} (ID: {note2.id})")
@ -79,12 +79,12 @@ async def main():
notebook.id,
note1.id,
content="# Data Structures Overview (Updated)\n\n"
"## Linear Structures\n"
"- Arrays: O(1) access, O(n) insertion\n"
"- Linked Lists: O(n) access, O(1) insertion\n\n"
"## Non-Linear Structures\n"
"- Trees: Hierarchical, O(log n) search\n"
"- Graphs: Network relationships",
"## Linear Structures\n"
"- Arrays: O(1) access, O(n) insertion\n"
"- Linked Lists: O(n) access, O(1) insertion\n\n"
"## Non-Linear Structures\n"
"- Trees: Hierarchical, O(log n) search\n"
"- Graphs: Network relationships",
title="Key Concepts (Revised)",
)
print(f"Updated note: {note1.id}")
@ -199,7 +199,7 @@ async def main():
print("\n--- Generating Quiz ---")
from notebooklm import QuizQuantity, QuizDifficulty
from notebooklm import QuizDifficulty, QuizQuantity
quiz_gen = await client.artifacts.generate_quiz(
notebook.id,
@ -243,6 +243,7 @@ async def main():
# Categorize by type
from notebooklm.rpc import StudioContentType
type_counts = {}
for art in all_artifacts:
type_name = StudioContentType(art.artifact_type).name

View file

@ -18,6 +18,7 @@ Usage:
"""
import asyncio
from notebooklm import NotebookLMClient
@ -44,17 +45,13 @@ async def main():
# 4. Generate an audio overview
print("Generating podcast (this may take a few minutes)...")
status = await client.artifacts.generate_audio(
nb.id,
instructions="Focus on the history and key milestones"
nb.id, instructions="Focus on the history and key milestones"
)
print(f" Started generation, task_id: {status.task_id}")
# Wait for completion
final = await client.artifacts.wait_for_completion(
nb.id,
status.task_id,
timeout=300,
poll_interval=10
nb.id, status.task_id, timeout=300, poll_interval=10
)
if final.is_complete:

View file

@ -17,6 +17,7 @@ Usage:
import asyncio
import sys
from notebooklm import NotebookLMClient
@ -41,7 +42,7 @@ async def main(topic: str):
for i in range(max_polls):
status = await client.research.poll(nb.id)
state = status.get("status", "unknown")
print(f" Poll {i+1}/{max_polls}: {state}")
print(f" Poll {i + 1}/{max_polls}: {state}")
if state == "completed":
sources = status.get("sources", [])
@ -62,20 +63,15 @@ async def main(topic: str):
# 5. Generate podcast
print("Generating podcast...")
gen_status = await client.artifacts.generate_audio(
nb.id,
instructions=f"Create an engaging overview of {topic}"
nb.id, instructions=f"Create an engaging overview of {topic}"
)
print("Waiting for audio generation...")
final = await client.artifacts.wait_for_completion(
nb.id,
gen_status.task_id,
timeout=600
)
final = await client.artifacts.wait_for_completion(nb.id, gen_status.task_id, timeout=600)
if final.is_complete:
print(f"\n Success! Audio URL: {final.url}")
print(f"\n Use 'notebooklm download audio' to save the file")
print("\n Use 'notebooklm download audio' to save the file")
else:
print(f"\n Generation ended with status: {final.status}")

View file

@ -15,6 +15,7 @@ Prerequisites:
"""
import asyncio
from notebooklm import NotebookLMClient, VideoFormat, VideoStyle
@ -22,7 +23,6 @@ async def main():
"""Generate a video overview from notebook sources."""
async with await NotebookLMClient.from_storage() as client:
# Step 1: Create a notebook with content
print("Creating notebook...")
notebook = await client.notebooks.create("Video Demo Notebook")
@ -79,8 +79,8 @@ async def main():
notebook.id,
generation.task_id,
initial_interval=10.0, # Check every 10 seconds initially
max_interval=30.0, # Max 30 seconds between checks
timeout=900.0, # 15 minute timeout for videos
max_interval=30.0, # Max 30 seconds between checks
timeout=900.0, # 15 minute timeout for videos
)
if final_status.is_complete:

203
docs/releasing.md Normal file
View file

@ -0,0 +1,203 @@
# Release Checklist
**Status:** Active
**Last Updated:** 2026-01-14
Checklist for releasing a new version of `notebooklm-py`.
> **For Claude Code:** When asked to prepare a release, follow this checklist step by step. Complete each checkbox before moving to the next. Ask the user to confirm before pushing or tagging.
---
## 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:
```bash
python -m py_compile docs/examples/*.py
```
### Version Bump
- [ ] Determine version bump type (see [Version Numbering](#version-numbering) for details)
- [ ] Update version in `pyproject.toml`:
```toml
version = "X.Y.Z"
```
### Changelog
- [ ] Get commits since last release:
```bash
git log $(git describe --tags --abbrev=0)..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:
```markdown
## [Unreleased]
## [X.Y.Z] - YYYY-MM-DD
```
- [ ] Update comparison links at bottom of `CHANGELOG.md`:
```markdown
[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
```
### Commit
- [ ] Verify changes:
```bash
git diff
```
- [ ] Commit:
```bash
git add pyproject.toml CHANGELOG.md
git commit -m "chore: release vX.Y.Z"
```
---
## CI Verification
### Push to Main
- [ ] Push to main:
```bash
git push origin main
```
- [ ] 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 Main
- [ ] Go to **Actions** → **Nightly E2E**
- [ ] Click **Run workflow**, select `main` branch
- [ ] Wait for E2E tests to pass
---
## Package Verification
### Publish to TestPyPI
- [ ] Go to **Actions** → **Publish to TestPyPI**
- [ ] Click **Run workflow**
- [ ] 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.
### 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:
1. Fix issues locally
2. Bump patch version in `pyproject.toml`
3. Update `CHANGELOG.md` with fix
4. Amend or create new commit
5. Push and re-run **Publish to TestPyPI**
---
## Release
### Tag and Publish
- [ ] Create tag:
```bash
git tag vX.Y.Z
```
- [ ] Push tag:
```bash
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`
- [ ] Wait for all tests to pass
### GitHub Release (Optional)
- [ ] Go to **Releases** → **Draft a new release**
- [ ] Select tag `vX.Y.Z`
- [ ] Title: `vX.Y.Z`
- [ ] Copy release notes from `CHANGELOG.md`
- [ ] Publish release
---
## Troubleshooting
### CI fails after push
> **Warning:** Only do this immediately after your own push, before anyone else pulls.
```bash
# Fix locally, then amend
git add -A
git commit --amend --no-edit
git push --force origin main
```
### Need to abort after commit
> **Warning:** Force pushing rewrites history. Only do this if you haven't shared the commit.
```bash
# Undo release commit (local only)
git reset --hard HEAD~1
# If already pushed (use with caution)
git push --force origin main
```
### Tag already exists
```bash
# 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
---
## Version Numbering
| Change Type | Bump | Example |
|-------------|------|---------|
| RPC method ID fixes | PATCH | 0.1.0 → 0.1.1 |
| Bug fixes | PATCH | 0.1.1 → 0.1.2 |
| New features | MINOR | 0.1.2 → 0.2.0 |
| Breaking changes | MAJOR | 0.2.0 → 1.0.0 |
See [stability.md](stability.md) for full versioning policy.

View file

@ -1,49 +0,0 @@
#!/bin/bash
# Pre-release checks for notebooklm-py
set -e
echo "🔍 Running pre-release checks..."
echo ""
# Type checking
echo "📝 Checking types with mypy..."
mypy src/notebooklm || { echo "❌ Type errors found"; exit 1; }
echo "✅ Types OK"
echo ""
# Unit tests
echo "🧪 Running unit tests..."
pytest tests/unit -q || { echo "❌ Unit tests failed"; exit 1; }
echo "✅ Unit tests passed"
echo ""
# Integration tests
echo "🔗 Running integration tests..."
pytest tests/integration -q || { echo "❌ Integration tests failed"; exit 1; }
echo "✅ Integration tests passed"
echo ""
# Coverage check
echo "📊 Checking test coverage..."
coverage run -m pytest tests/unit tests/integration -q
coverage report --fail-under=70 || { echo "❌ Coverage below 70%"; exit 1; }
echo "✅ Coverage OK"
echo ""
# Check GitHub URLs
echo "🔗 Verifying GitHub URLs..."
if grep -q "notebooklm-clinet" pyproject.toml; then
echo "❌ ERROR: Found 'clinet' typo in pyproject.toml"
exit 1
fi
echo "✅ URLs OK"
echo ""
# Build check
echo "📦 Building package..."
hatch build || { echo "❌ Build failed"; exit 1; }
echo "✅ Build OK"
echo ""
echo "🎉 All pre-release checks passed!"
echo "Ready to release. Run: hatch publish"

View file

@ -18,7 +18,21 @@ from ._logging import configure_logging
configure_logging()
__version__ = "0.2.0"
# Version sourced from pyproject.toml via importlib.metadata
import logging
from importlib.metadata import PackageNotFoundError, version
_logger = logging.getLogger(__name__)
try:
__version__ = version("notebooklm-py")
except PackageNotFoundError:
__version__ = "0.0.0.dev0" # Fallback when package is not installed
_logger.debug(
"Package 'notebooklm-py' not found in metadata. "
"Using fallback version '%s'. This is normal during development.",
__version__,
)
# Public API: Authentication
from .auth import DEFAULT_STORAGE_PATH, AuthTokens