Automated Pull Request creation daemon for GitHub and Bitbucket Cloud.
pr-generator runs as a long-lived service that periodically scans your repository branches, matches them against configurable regex patterns, and automatically opens Pull Requests toward the configured destination branches — skipping any PR that already exists.
┌─────────────────────────────────────────────────────────────┐
│ Scan cycle │
│ │
│ 1. Fetch all branches ──▶ GitHub / Bitbucket │
│ 2. For every rule │
│ match branches against regex pattern │
│ for each match │
│ skip if open PR already exists │
│ create PR source ──▶ destination │
│ 3. Sleep scan_frequency seconds │
│ 4. Repeat │
└─────────────────────────────────────────────────────────────┘
Key design points:
- Concurrent: branches are fetched from all providers in parallel; rule×provider pairs are also processed concurrently (up to 10 workers).
- Idempotent: an existing open PR for the same source→destination pair is detected and skipped.
- Dry-run mode: log what would be created without actually calling the API.
- Graceful shutdown: handles
SIGTERM/SIGINTand drains in-progress work.
# Install
pip install -e .
# Point to your config file and run
CONFIG_PATH=./config.yaml pr-generatorOr with Docker:
docker run --rm \
-v "$(pwd)/config.yaml:/etc/pr-generator/config.yaml:ro" \
ghcr.io/devops-ia/pr-generator:latestThe default config path is /etc/pr-generator/config.yaml. Override with the CONFIG_PATH environment variable. The application exits with an error at startup if the file is not found.
# config.yaml
# How often (seconds) to scan for new branches.
scan_frequency: 300 # default: 300
# Logging level: DEBUG | INFO | WARNING | ERROR
log_level: INFO # default: INFO
# Log format: "text" (human-readable) or "json" (structured, for log aggregators)
log_format: text # default: text
# When true, PRs are logged but never actually created.
dry_run: false # default: false
# Port for the built-in health server.
health_port: 8080 # default: 8080
providers:
github:
enabled: true
owner: my-org
repo: my-repo
app_id: "123456"
installation_id: "78901234" # optional — auto-resolved if omitted
private_key_path: /secrets/github-app.pem # path to PEM file
# Alternative: set GITHUB_APP_PRIVATE_KEY env var (plain PEM or base64-encoded)
timeout: 30 # HTTP timeout in seconds
bitbucket:
enabled: true
workspace: my-workspace
repo_slug: my-repo
token_env: BITBUCKET_TOKEN # name of the env var that holds the token
close_source_branch: true # delete source branch after merge (default: true)
timeout: 30
rules:
- pattern: "feature/.*" # Python regex matched against branch names
destinations:
github: main
bitbucket: develop
- pattern: "release/.*"
destinations:
github: main
- pattern: ".*-hotfix-.*"
destinations:
bitbucket: masterUse any name as the provider key and set type: github (or type: bitbucket) to identify the implementation. Rules reference providers by their name.
providers:
github-acme:
type: github # required for non-standard key names
enabled: true
owner: acme-org
repo: backend
app_id: "111"
private_key_path: /secrets/acme-app.pem
github-skunkworks:
type: github
enabled: true
owner: skunkworks-org
repo: platform
auth_method: pat
token_env: SKUNKWORKS_GITHUB_TOKEN
bitbucket: # "github" / "bitbucket" keys default type automatically
enabled: true
workspace: my-workspace
repo_slug: my-repo
token_env: BITBUCKET_TOKEN
rules:
- pattern: "feature/.*"
destinations:
github-acme: main
github-skunkworks: develop
bitbucket: developConfig fields reference
| Field | Type | Default | Description |
|---|---|---|---|
scan_frequency |
int | 300 |
Seconds between scan cycles |
log_level |
string | "INFO" |
Python logging level |
dry_run |
bool | false |
Simulate PR creation without API calls |
health_port |
int | 8080 |
Port for health HTTP server |
providers.<name>.type |
string | (key name) | Provider implementation: github or bitbucket. Required when the key name is not github or bitbucket |
providers.<name>.enabled |
bool | false |
Activate this provider instance. If no providers are enabled the application starts in idle mode — it logs a warning and keeps running without performing any scans |
providers.<name>.owner |
string | — | GitHub organisation or user (GitHub only) |
providers.<name>.repo |
string | — | Repository name (GitHub only) |
providers.<name>.app_id |
string | — | GitHub App ID (GitHub App auth) |
providers.<name>.installation_id |
string | (auto) | Installation ID; resolved automatically if omitted (GitHub App auth) |
providers.<name>.private_key_path |
string | — | Path to GitHub App private key PEM file (GitHub App auth) |
providers.<name>.auth_method |
string | "app" |
app (GitHub App) or pat (Personal Access Token) (GitHub only) |
providers.<name>.token_env |
string | "GITHUB_TOKEN" / "BITBUCKET_TOKEN" |
Env var name containing the token (PAT / Bitbucket) |
providers.<name>.workspace |
string | — | Bitbucket workspace slug (Bitbucket only) |
providers.<name>.repo_slug |
string | — | Bitbucket repository slug (Bitbucket only) |
providers.<name>.close_source_branch |
bool | true |
Delete source branch after PR merges (Bitbucket only) |
providers.<name>.timeout |
float | 30 |
HTTP timeout (seconds) |
rules[].pattern |
string | — | Python regex applied to branch names |
rules[].destinations |
map | — | provider_name: destination_branch pairs |
Authentication uses a GitHub App. Two modes are available:
GitHub App (recommended) — the provider:
- Signs a short-lived JWT with the App's RSA private key.
- Exchanges it for an installation access token (cached up to ~55 minutes).
- Uses the installation token for all API calls.
- Caches per-cycle PR-existence and branch-existence lookups to reduce API usage.
Personal Access Token (PAT) — set auth_method: pat and point token_env at an env var holding the PAT.
Required GitHub App permissions: Contents (read), Pull requests (read & write).
Authentication uses a project/repository Bearer token (HTTP access token).
The provider fetches default reviewers at PR creation time and automatically includes them in the payload.
Required Bitbucket permissions: Repositories (read), Pull requests (read & write).
Each rule has:
pattern— a Python regex (re.compile) matched against branch names usingre.match(anchored at the start). The destination branch is excluded from matching.destinations— a map ofprovider_name → destination_branch. Only providers that are both listed here and active inprovidersare processed.
rules:
- pattern: "feature/.*"
destinations:
github: main # create PRs toward "main" on GitHub
bitbucket: develop # create PRs toward "develop" on BitbucketMultiple rules are supported.
A lightweight HTTP server starts on health_port (default 8080):
| Endpoint | Behaviour |
|---|---|
GET /livez |
200 live while running; 503 shutting down during shutdown |
GET /healthz |
Same as /livez (alias) |
GET /readyz |
200 ready after the first scan cycle completes; 503 not ready before that |
Suitable for Kubernetes liveness, readiness, and startup probes:
livenessProbe:
httpGet:
path: /livez
port: 8080
readinessProbe:
httpGet:
path: /readyz
port: 8080The image is built from a two-stage Dockerfile:
- Stage 1 – installs Python dependencies into
/install. - Stage 2 – minimal
python:3.14-slimruntime; runs as a non-root user (prgen).
# Build
docker build -t pr-generator .
# Run with YAML config
docker run --rm \
-v "$(pwd)/config.yaml:/etc/pr-generator/config.yaml:ro" \
-v "$(pwd)/github-app.pem:/secrets/github-app.pem:ro" \
-e BITBUCKET_TOKEN=<token> \
-p 8080:8080 \
pr-generatorPrerequisites: Python ≥ 3.11
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate
# Install the package in editable mode with dev extras
pip install -e .
pip install pytest
# Run tests
pytest
# Run with a local config
CONFIG_PATH=./config.yaml python -m pr_generatorProject layout
src/pr_generator/
├── __main__.py # Entry point: startup, provider init, scan loop
├── config.py # Config loading from YAML file
├── models.py # Dataclasses: AppConfig, ProviderConfig, ScanRule, …
├── scanner.py # Concurrent scan cycle orchestrator
├── health.py # HTTP health server (/livez, /readyz, /healthz)
├── http_client.py # Shared HTTP client with retry/backoff
├── logging_config.py # Logging setup (plain text or structured JSON)
└── providers/
├── base.py # ProviderInterface Protocol
├── github.py # GitHub App provider
└── bitbucket.py # Bitbucket Cloud provider
tests/
├── conftest.py # Shared pytest fixtures
├── test_config.py # Config loading tests
├── test_health.py # Health server tests
├── test_models.py # Model tests
└── test_scanner.py # Scan cycle tests