A cookiecutter template for Python libraries with modern CI/CD setup.
- Automated testing on PR using GitHub Actions
- Pre-commit hooks for code quality (ruff, isort, trailing whitespace, etc.)
- Semantic release using GitHub Actions
- Automatic code coverage report in README
- Automatic wheel build and GitHub Release publishing
- Modern Python packaging with pyproject.toml
Notes Workflows trigger when a branch is merged into main! To install, please follow all the instructions in this readme. The workflows require a PAT set as secret (see GitHub Repository Setup section for instructions). See the notes on how to create semantic releases at the bottom of the README.
If you followed all the steps, whenever a PR is merged into main, the workflows are triggered and should:
- Run pre-commit checks (fail fast on code quality issues)
- Ensure that tests pass (before merge)
- Create a code coverage report and commit that to the bottom of the README
- Create a semantic release (if you follow the semantic release pattern) and automatically update the version number of your code
- Build a wheel and publish it as a GitHub Release asset
Cookiecutter template:
- Cd to your new library location
cd /your/new/library/path/
- Install cookiecutter using pip
pip install cookiecutter
- Run the cookiecutter template from this GitHub repo
cookiecutter https://github.com/jonathanvanleeuwen/lib_template
- Fill in your new library values (including your GitHub username for CODEOWNERS)
- Create new virtual environment
python -m venv .venv
- Activate the environment and install library with dev dependencies
pip install -e ".[dev]"
- Install pre-commit hooks
pip install pre-commitpre-commit install
- Run pre-commit on all files (important for initial commit!)
pre-commit run --all-files
- Check proper install by running tests
pytest
Open git bash
cd C:/your/code/directoryTo init the repository, add all files and commit
git init
git add *
git add .github
git add .gitignore
git add .pre-commit-config.yaml
git commit -m "fix: Initial commit"To add the new git repository to your GitHub:
- Go to github.
- Log in to your account.
- Click the new repository button in the top-right. You'll have an option there to initialize the repository with a README file, but don't. Leave the repo empty
- Give the new repo the same name you gave your repo with the cookiecutter
- Click the "Create repository" button.
Now we want to make sure we are using main as main branch name and push the code to GitHub
git remote add origin https://github.com/username/new_repo_name.git
git branch -M main
git push -u origin mainComplete these steps in order to enable the CI/CD pipeline.
The workflow needs a Personal Access Token to push to the protected main branch.
- Go to GitHub Settings β Developer settings β Personal access tokens β Fine-grained tokens
- Click "Generate new token"
- Configure the token:
- Token name:
RELEASE_TOKEN_YOUR_REPO_NAME - Expiration: Choose an appropriate duration (recommend 90 days, set a reminder to rotate)
- Repository access: Select "Only select repositories" β choose your repository
- Permissions:
- Contents: Read and write (for pushing commits and tags)
- Metadata: Read-only (automatically selected)
- Token name:
- Click "Generate token"
- Copy the token immediately - you won't see it again!
If fine-grained tokens don't work for your use case:
- Go to GitHub Settings β Developer settings β Personal access tokens β Tokens (classic)
- Click "Generate new token (classic)"
- Configure:
- Note:
RELEASE_TOKEN - Expiration: Set an appropriate duration
- Scopes: Select
repo(full control of private repositories)
- Note:
- Click "Generate token" and copy it
-
Go to your repository on GitHub
-
Navigate to Settings β Secrets and variables β Actions
-
Click "New repository secret"
-
Configure:
- Name:
RELEASE_TOKEN - Secret: Paste your copied PAT
- Name:
-
Click "Add secret"
GitHub Rulesets provide modern, flexible branch protection. The PAT allows the workflow to bypass these rules while humans must go through PRs.
-
Go to your repository β Settings β Rules β Rulesets
-
Click "New ruleset" β "New branch ruleset"
-
Configure the ruleset:
- Ruleset name:
Protect main - Enforcement status: Active
- Target branches: Click "Add target" β "Include by pattern" β enter
main
- Ruleset name:
-
Enable these rules:
- β Restrict deletions - Prevent branch deletion
- β
Require a pull request before merging
- Required approvals:
1(or more) - β Dismiss stale pull request approvals when new commits are pushed
- β Require conversation resolution before merging
- Required approvals:
- β
Require status checks to pass
- β Require branches to be up to date before merging
- Add status checks:
ci / Run Pre-commit Checksci / Lint with Ruffci / Run Tests with Pytest
- β Block force pushes
-
Click "Create"
Note: Status checks appear as
ci / <Job Name>because they run inside the reusable workflow. You may need to run a PR first before these checks appear in the dropdown.
This setup provides security through multiple layers:
| Protection | What it prevents |
|---|---|
| CODEOWNERS | Requires your approval for any workflow changes |
| Required PRs | No direct pushes to main (humans must use PRs) |
| Required reviews | At least one approval needed for every change |
| Status checks | Tests must pass before merge |
| PAT as secret | Token only accessible to workflows, not forks |
Why is the PAT safe?
- The PAT is stored as a secret, never exposed in logs (GitHub auto-masks it)
- Forks cannot access repository secrets
- Any attempt to modify workflows to steal the PAT requires your explicit approval via CODEOWNERS
- The PAT can only push; it cannot change branch protection rules
https://python-semantic-release.readthedocs.io/en/latest/
The workflows are triggered when you merge into main!
When committing use the following format for your commit message:
- patch (0.0.X):
fix: commit message - minor (0.X.0):
feat: commit message - major/breaking (X.0.0) - add the breaking change on the third line:
feat: commit message BREAKING CHANGE: description of breaking change
Libraries created with this template support multiple installation methods: Note that the @v1.0.0 can be updated for a specific version, or removed for the newest version
This method doesn't expose your token in command history:
# Store credentials in git (one-time setup)
git config --global credential.helper store
# Then install normally - git will prompt for credentials once
pip install "git+https://github.com/username/repo_name.git@v1.1.0"
# When prompted: username = your GitHub username, password = your PAT# Using pip with a personal access token
pip install "git+https://YOUR_TOKEN@github.com/username/repo_name.git@v1.0.0"
# Using uv
uv pip install "git+https://YOUR_TOKEN@github.com/username/repo_name.git@v1.0.0"After cloning, install the wheel file from the dist/ directory:
pip install repo_name/dist/repo_name-1.0.0-py3-none-any.whlgit clone https://github.com/username/repo_name.git
cd repo_name
pip install -e ".[dev]"pip install build
python -m build --wheel
# The wheel will be created in the dist/ directoryIn requirements.txt:
# Using git+https (requires GH_TOKEN environment variable to be set)
repo_name @ git+https://github.com/username/repo_name.git@v1.1.0In pyproject.toml (for projects using PEP 621):
[project]
dependencies = [
"repo_name @ git+https://github.com/username/repo_name.git@v1.1.0",
]Note: When installing from requirements.txt with a private repo, ensure your git credentials are configured (see Option 1, Step 2, Option B above).