This file provides guidance for AI agents (Claude Code, ChatGPT, Copilot, etc.) working on this repository.
Stampbot is a GitHub App that automatically approves pull requests based on labels and ChatOps commands. Built with Python 3.11+, FastAPI, and deployed on Kubernetes.
- Language: Python 3.11+
- Web Framework: FastAPI
- GitHub Integration: PyGithub
- Configuration: Dynaconf, Pydantic Settings
- Logging: Structlog
- Metrics: Prometheus Client
- Tracing: OpenTelemetry
- Containerization: Docker
- Orchestration: Kubernetes via Helm
- CI/CD: GitHub Actions
IMPORTANT: All local development MUST use the virtual environment in .venv/. The Makefile handles this automatically for all targets. Never run Python tools (pytest, ruff, mypy, etc.) directly from system Python or via global installations like uv - they won't have the project dependencies. Always use make targets or explicitly activate the venv first.
# Development (venv auto-created by Makefile)
make install-dev # Install dependencies (dev mode)
make dev # Run with hot-reload (uvicorn --reload)
make lint # Run ruff + mypy
make format # Format with ruff format + ruff --fix
make test # Run pytest with coverage
# Run single test (note: uses venv explicitly)
.venv/bin/pytest tests/test_webhook_handler.py::test_function_name -v
# Or activate the venv first
source .venv/bin/activate
pytest tests/test_webhook_handler.py -v
# Pre-commit hooks
make pre-commit-install # Install git hooks (run once)
make pre-commit # Run all pre-commit checks manually
# Local GitHub Actions testing (requires: docker, act)
make act-lint # Run lint job locally
make act-test # Run test job locally
make act-ci # Run full CI workflow locally
# Docker/Kubernetes (no venv needed)
make docker-build # Build multi-arch image
make helm-lint # Lint Helm chart
make helm-install # Install chart locally
# Cleanup (removes venv)
make cleanRequest flow:
GitHub Webhook -> POST /webhook -> WebhookHandler.handle_event()
|
+---------------+---------------+
| | |
v v v
pull_request issue_comment ping
(label-based) (ChatOps) (health)
| |
v v
GitHubAppClient (JWT + installation token auth)
|
v
approve_pr() / dismiss_approval()
Core modules in stampbot/:
main.py- FastAPI app, webhook, health, readiness, and setup endpointswebhook_handler.py- Event routing, label/chatops processinggithub_client.py- GitHub API with app authentication (JWT)config.py- Dynaconf-based configuration (env vars, settings.toml, per-repo stampbot.toml)manifest.py- GitHub App manifest creation for easy setupmetrics.py- Prometheus metric definitions and the optional separate listenertelemetry.py- OpenTelemetry tracing setuplogger.py- Structlog configuration
Configuration hierarchy:
- Environment variables (STAMPBOT_*) - highest priority
- .secrets.toml - for sensitive values (gitignored)
- settings.toml - app defaults
- Per-repo stampbot.toml - repo-specific overrides (merged with defaults)
IMPORTANT: Always run pre-commit checks before pushing:
make pre-commit
# or
.venv/bin/pre-commit run --all-filesThis prevents CI failures and avoids branch divergence when pre-commit.ci auto-fixes formatting issues.
All local development MUST use the virtual environment. The Makefile handles this automatically - all make targets that run locally (install, test, lint, format, run, dev) will create and use .venv/.
Do NOT:
- Install packages globally
- Run tools like
pytest,ruff,mypydirectly from system Python - Use
uv run,python -m pytest, or similar without the venv active - Assume any Python tooling is available outside the venv
Always either:
- Use
maketargets (preferred):make test,make lint, etc. - Explicitly use venv binaries:
.venv/bin/pytest,.venv/bin/ruff - Activate the venv first:
source .venv/bin/activate && pytest
This project follows the Google Python Style Guide with the following exception:
- Line length: 100 characters (instead of Google's 80)
Tooling:
- Ruff: All-in-one linter and formatter enforcing Google style
- Format: 100-char lines, double quotes, spaces, 4-space indentation
- Lint rules: E, F, I, N, W, B, C4, UP, D (pydocstyle with Google convention)
- MyPy: Strict type checking (required per Google style guide section 2.21)
- Docstrings: Google style with Args, Returns, Raises sections
Key Google style conventions:
- Use type annotations for all public APIs
- Prefer small, focused functions
- Use explicit exception handling (no bare
except:) - Imports: one per line, grouped (stdlib, third-party, local)
- Naming:
module_name,ClassName,function_name,CONSTANT_NAME
Commits and PR titles: Both must use conventional commit format. This is enforced by commitlint on individual commits, but with squash-merge the PR title becomes the merge commit message on main. The release workflow reads commit subjects on main to decide whether to release and what version bump to apply — so a non-conventional PR title means no release, even if the branch commits were properly formatted.
Format: <type>[optional scope]: <description>
| Type | Triggers release? | Use for |
|---|---|---|
feat |
Yes — minor bump | New features |
fix |
Yes — patch bump | Bug fixes |
docs |
No | Documentation only |
test |
No | Tests only |
refactor |
No | Code restructuring |
chore |
No | Maintenance, deps |
ci |
No | CI/CD changes |
style |
No | Formatting |
perf |
No | Performance |
build |
No | Build system |
revert |
No | Revert a commit |
Breaking changes: append ! after the type (e.g., feat!:) or include BREAKING CHANGE: in the commit body — triggers a major version bump.
Branches: feat/*, fix/*, docs/*, refactor/*, chore/*
- Framework: pytest with pytest-asyncio
- Coverage: Aim for >80% coverage
- Test Location:
tests/directory mirroring source structure
Important: Tests run in the virtual environment. Always use make test or run pytest from the venv:
# Run all tests (preferred)
make test
# Run specific test file
.venv/bin/pytest tests/test_webhook_handler.py -v
# Run specific test
.venv/bin/pytest tests/test_webhook_handler.py::TestClassName::test_name -v
# Never run pytest directly from system Python - it won't have dependencies- Webhook signature verification uses HMAC-SHA256 constant-time comparison
- GitHub App auth: generate JWT from private key -> exchange for installation token
- Metrics use labels with low cardinality (<100 unique combinations)
- Structured logging outputs JSON in production, console in debug mode
- Create a feature branch:
feat/feature-name - Update relevant modules in
stampbot/ - Add tests in
tests/ - Update documentation
- Run linting and tests:
make lint test - Run pre-commit before pushing:
make pre-commit(or.venv/bin/pre-commit run --all-files) - Commit with conventional commit format
- Push and create PR
Important: Always run pre-commit before pushing to avoid CI failures from formatting issues. Pre-commit.ci will auto-fix some issues, but this causes branch divergence that requires rebasing.
- Define metric in
stampbot/metrics.py - Use metric in relevant module
- Document metric in README.md
- Test metric collection
- Update
stampbot/webhook_handler.py - Consider impact on
stampbot/github_client.py - Update tests
- Test with sample webhook payloads
When modifying any Helm chart component (values, templates, helpers, or documentation under charts/):
- Modify
charts/stampbot/values.yamlor templates - Required: Run
helm lint charts/stampbotbefore sending changes - Required: Run
helm template test charts/stampbotto verify templates render correctly - Test deployment in test cluster if possible
- Do NOT manually update
versionorappVersioninChart.yaml- these use0.0.0-placeholderand are set automatically at release time - Document changes in README.md
Helm testing layers:
| Layer | Tool | Speed | What it catches |
|---|---|---|---|
| Lint | helm lint |
Fast | Syntax, structure |
| Template | helm template |
Fast | Rendering errors |
| Validate | kubeconform |
Fast | K8s schema issues |
| Unit | helm-unittest |
Fast | Logic, conditionals |
| Install | kind cluster |
Slow | Real deployment issues |
| Post-install | helm test |
Slow | Live endpoint reachability, readiness, and the webhook signature path in-cluster |
| Upgrade | helm upgrade in kind |
Slow | Upgrade-only breakage (immutable fields, removed values, hook ordering) when moving from a released chart to the working tree |
Running Helm tests locally:
# Run all Helm tests (lint + validate + unittest)
make helm-test
# Or run individual steps:
make helm-lint # Lint the chart
make helm-template # Render templates
make helm-validate # Validate with kubeconform (requires kubeconform installed)
make helm-unittest # Run unit tests via Docker (no plugin install needed)Unit tests: Located in charts/stampbot/tests/. Each *_test.yaml file tests a specific template. When adding new templates or modifying existing ones, update/add corresponding tests.
Post-install tests (helm test): Chart-shipped test hooks live in charts/stampbot/templates/tests/ and run against a deployed release with helm test <release>. test-connection checks /health, /ready, and / on the main Service. It also checks /metrics on the internal metrics Service when metrics are enabled. test-webhook verifies the /webhook HMAC signature path (valid ping → 200, tampered → 401). The CI install job runs these after the pod is Ready. They reuse the application image, so no extra image needs pinning.
Pre-commit hooks: The helm-kubeconform hook validates templates on commit. Requires kubeconform installed locally (go install github.com/yannh/kubeconform/cmd/kubeconform@latest).
CI testing: GitHub Actions runs lint, kubeconform validation, unit tests, and integration tests in kind clusters. Integration tests are auto-discovered from charts/stampbot/ci/*-values.yaml files, and each installed release is verified with helm test (the chart-shipped hooks above).
Adding a new integration test case:
- Create
charts/stampbot/ci/<name>-values.yaml - Set required values (see
charts/stampbot/ci/README.md) - (Optional) Add
charts/stampbot/ci/<name>-setup.shto install prerequisites (e.g. CRDs) beforehelm install— auto-discovered like the values file - CI will automatically discover and run the new test case
Cases beyond default exercise feature toggles: ingress, autoscaling,
networkpolicy, and servicemonitor (the last installs the ServiceMonitor CRD
via its setup hook). External Secrets is validated by helm-unittest rather than
an install case, since it needs a live operator + backend to become Ready.
Upgrade testing: the helm-upgrade-test job installs a previously released
chart, helm upgrades it to the working-tree chart, and re-runs helm test.
The upgrade-from set is computed dynamically from chart-v* tags — the latest
patch of the current chart minor line plus the previous up-to-two minor lines —
so it tracks new releases without edits. The released chart is run on the
locally built image so the test isolates chart upgrade behavior (immutable
fields, hook ordering) rather than image availability.
Chart versioning: The chart uses placeholder versions (0.0.0-placeholder) in source control. The release workflow automatically replaces these with the actual version when publishing. Never commit a real version number to Chart.yaml.
Values schema: Use @schema comment blocks in values.yaml to document value types. Example:
# @schema
# type: integer
# minimum: 1
# @schema
replicaCount: 2- Add to
pyproject.tomlunder[tool.poetry.dependencies] - Run
poetry lockto update the lock file - Run
make install-dev(uses venv automatically) - The pre-commit hook will auto-generate
requirements.txton commit - Test build:
make docker-build
Note: Do NOT edit requirements.txt manually - it is auto-generated from pyproject.toml by the poetry-export pre-commit hook.
The following files are auto-generated and should never be edited manually:
| File | Generated By | Source of Truth |
|---|---|---|
requirements.txt |
poetry-export pre-commit hook |
pyproject.toml |
poetry.lock |
poetry lock |
pyproject.toml |
These files are marked in .gitattributes with linguist-generated=true. If you need to change dependencies, edit pyproject.toml instead.
All dependencies must be pinned to exact versions for reproducibility and security. Renovate manages updates automatically.
GitHub Actions: Pin to full SHA with version comment (Renovate-compatible format):
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2- Always use the 40-character commit SHA, never just a tag
- Add a
# vX.Y.Zcomment with the full semantic version for readability - Renovate will update both the SHA and the version comment automatically
Docker images: Pin to digest with tag comment:
FROM python:3.11-slim@sha256:5be45dbade29bebd6886af6b438fd7e0b4eb7b611f39ba62b430263f82de36d2- Use
image:tag@sha256:digestformat - Renovate will update digests automatically
Python packages: Pin to exact versions:
fastapi==0.128.0
Pre-commit hooks: Pin to version tags (Renovate manages these):
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0Secret Detection Baseline: When adding intentional test secrets (e.g., fake keys for CI):
make secrets-baseline # Update .secrets.baseline
git add .secrets.baselineTool Installation: CI workflows use jdx/mise-action to install tools from .tool-versions. This ensures CI uses the same tool versions as local development.
- name: Set up tools
uses: jdx/mise-action@<SHA> # v3.x.x
with:
cache: trueDo NOT use separate setup actions (e.g., azure/setup-helm) for tools defined in .tool-versions. Adding a tool to .tool-versions automatically makes it available in CI.
Python Package Caching: For jobs that install Python packages, add setup-python after mise-action to enable poetry package caching:
- name: Set up tools
uses: jdx/mise-action@<SHA> # v3.x.x
with:
cache: true
- name: Set up Python package cache
uses: actions/setup-python@<SHA> # v6.x.x
with:
python-version-file: '.python-version'
cache: 'poetry'
- name: Install dependencies
run: poetry install --only main,devThe mise-action caches tool binaries (python, poetry, helm), while setup-python caches installed Python packages.
Docker Builds: The Dockerfile uses pip with requirements.txt (not Poetry) to enable layer caching. The requirements.txt is auto-generated from pyproject.toml by the poetry-export pre-commit hook. This separation allows dependency layers to be cached independently of source code changes.
When making changes, consider the impact on:
- Configuration:
stampbot/config.py,charts/stampbot/values.yaml - API Surface:
stampbot/main.py,stampbot/webhook_handler.py - GitHub Integration:
stampbot/github_client.py - Deployment:
Dockerfile,charts/stampbot/templates/deployment.yaml - CI/CD:
.github/workflows/
export STAMPBOT_LOG_LEVEL=DEBUGngrok http 8000
# Update GitHub App webhook URLexport STAMPBOT_METRICS_ENABLED=true
export STAMPBOT_METRICS_HOST=127.0.0.1
export STAMPBOT_METRICS_PORT=9090
.venv/bin/python -m stampbotIn another terminal:
curl http://127.0.0.1:9090/metrics/health is the liveness signal (always 200 while the process is up). /ready
is the readiness signal: it returns 200 when the pod can serve its current
purpose and 503 otherwise, with a checks breakdown. The pod is ready when it
is configured (can serve webhooks) or setup mode is enabled (can
serve /setup). It is "not ready" (503) only when unconfigured and setup is
disabled — returning 503 in setup mode would pull the pod from the Service and
make /setup unreachable. The chart's liveness probe targets /health and the
readiness probe targets /ready.
curl http://localhost:8000/health # liveness
curl http://localhost:8000/ready # readiness (503 only if unconfigured AND setup disabled)- Verify GitHub App ID and private key
- Check private key format (PEM)
- Ensure app is installed on target repo
- Verify webhook secret matches
- Check ingress/load balancer configuration
- Review GitHub App webhook delivery
- Monitor
github_api_rate_limit_remainingmetric - Consider implementing caching
- Use conditional requests where possible
- Lint code (ruff format, ruff check, mypy)
- Run tests with coverage
- Build Docker image (PR-tagged)
- Lint Helm chart
- Run full CI
- Build multi-arch Docker image
- Push to container registry
- Build multi-arch Docker image
- Push to
ghcr.io/dannysauer/stampbot - Create GitHub release with changelog
- Trigger chart release workflow
- Calculate next chart version from conventional commits
- Replace
0.0.0-placeholderin Chart.yaml with actual version - Package Helm chart
- Push to OCI registry:
oci://ghcr.io/dannysauer/charts/stampbot - Create GitHub release for chart
You can run GitHub Actions workflows locally using nektos/act:
# Prerequisites: Docker and act installed
# Install act: brew install act (macOS) or see https://github.com/nektos/act
# Run individual jobs
make act-lint # Run lint job
make act-test # Run test job
make act-helm # Run helm-lint job
# Run multiple jobs
make act-ci # Run lint + test + helm-lint
# Run with custom event
act -j lint --eventpath .github/act/pull_request.jsonNote: Docker build/push jobs require registry authentication and are skipped locally.
Pre-commit hooks run automatically before each commit to catch issues early:
# One-time setup
make pre-commit-install
# What runs on each commit:
# - Trailing whitespace removal
# - End-of-file fixer
# - YAML/TOML/JSON validation
# - Large file check
# - Private key detection
# - Python AST validation
# - Case conflict detection
# - Branch protection (blocks commits to main/develop)
# - Conventional commit validation (commit-msg stage)
# - Ruff format check
# - Ruff linter
# - MyPy type checking
# - Renovate config validation
# - Helm chart linting
# Run manually on all files
make pre-commitConfiguration: .pre-commit-config.yaml, .commitlintrc.yaml