pr-generator watches for branches matching configurable regex patterns and automatically opens Pull Requests. Built for ArgoCD Image Updater and GitOps workflows.
$ docker run \ -e GITHUB_APP_PRIVATE_KEY="$(cat github-app.pem)" \ -v ./config.yaml:/etc/pr-generator/config.yaml \ ghcr.io/devops-ia/pr-generator:latest
Features
A lightweight Python daemon with no persistent storage requirements. Runs anywhere: bare metal, Docker, or Kubernetes.
GitHub App (PEM private key) or Personal Access Token. Configure per provider with auth_method.
Full Bitbucket Cloud support via access token. Run GitHub and Bitbucket providers in a single process.
Multiple named providers of the same type. Different orgs, workspaces, or repos — all watched in one daemon.
Match any branch naming convention with full Python regex. Each rule can target multiple destination branches.
Duplicate PR detection prevents opening a second PR for the same branch. dry_run mode for safe testing in production.
Built-in HTTP server exposes /livez and /healthz (liveness) and /readyz (ready after first scan). No extra dependencies.
Set scan_frequency to control how often branches are polled. Defaults to 300 seconds.
Official Helm chart with ServiceAccount, PDB, startup probe, and security-hardened pod spec.
Let each ArgoCD Application declare its own PR rules via annotations — no central config change needed. Three modes: config_only, annotations_only, hybrid.
Built-in /metrics endpoint with counters, histograms, and gauges for PRs, scan cycles, errors, and annotation rules. Pod annotations and ServiceMonitor included.
Installation
Run via Docker (recommended), install from PyPI, or deploy on Kubernetes with the Helm chart.
services: pr-generator: image: ghcr.io/devops-ia/pr-generator:latest restart: unless-stopped volumes: - ./config.yaml:/etc/pr-generator/config.yaml:ro environment: GITHUB_APP_PRIVATE_KEY: "$(cat github-app.pem)" healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8080/livez"] interval: 30s retries: 3
$ pip install pr-generator $ pr-generator --config /etc/pr-generator/config.yaml
$ helm repo add helm-pr-generator https://devops-ia.github.io/helm-pr-generator $ helm repo update $ helm install pr-generator helm-pr-generator/pr-generator \ --namespace pr-generator --create-namespace \ --set config.providers.github.owner=my-org \ --set config.providers.github.repo=my-repo \ --set config.providers.github.appId="123456" \ --set secrets.github.privateKey="$(cat github-app.pem)"
Configuration
Mount your config at /etc/pr-generator/config.yaml.
Credentials are passed via environment variables — never in the config file.
scan_frequency: 300 # seconds between scan cycles dry_run: false # set true to simulate without creating PRs providers: github: # name is free-form; type inferred from key enabled: true owner: my-org repo: my-app auth_method: app # "app" (PEM) or "pat" (token) app_id: "123456" installation_id: "78901234" private_key_path: /secrets/github.pem # or use GITHUB_APP_PRIVATE_KEY env bitbucket: enabled: true workspace: my-workspace repo_slug: my-app token_env: BITBUCKET_TOKEN # env var holding the access token rules: - pattern: "^image-updater/.*" destinations: github: main bitbucket: main
token_env value.
The daemon raises a ValueError at startup if duplicates are detected — preventing silent credential overwrite.
ArgoCD Image Updater
ArgoCD Image Updater pushes image version bumps to a new branch. pr-generator watches that branch pattern and opens the PR automatically — closing the GitOps loop without manual intervention.
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: annotations: # Image Updater pushes to a branch, pr-generator opens the PR argocd-image-updater.argoproj.io/write-back-method: git:repobranch argocd-image-updater.argoproj.io/git-branch: main:image-updater/{{.SHA}}
rules: - pattern: "^image-updater/.*" destinations: github: main
Annotation-based discovery
Instead of a central rules list, each ArgoCD Application can declare its own PR rules
via annotations. No config reload or restart needed — rules are discovered on every scan cycle.
Three modes: config_only (default, existing behavior), annotations_only,
and hybrid (static rules + annotations merged, annotation wins on collision).
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: my-app annotations: pr-generator.io/enabled: "true" pr-generator.io/pattern: "^image-updater/.*" pr-generator.io/destination.github: "main" pr-generator.io/destination.bitbucket: "develop"
annotation_discovery: mode: hybrid # config_only | annotations_only | hybrid annotation_prefix: pr-generator.io # default prefix # rules: still required when mode is config_only or hybrid rules: - pattern: "^hotfix/.*" destinations: github: main
get and list
applications.argoproj.io cluster-wide. The Helm chart creates this automatically when
annotationDiscovery.enabled: true.
Observability
GET /metrics on the health port (8080) exposes Prometheus text format.
Works with plain Prometheus (pod annotations) and Prometheus Operator (ServiceMonitor).
$ curl http://localhost:8080/metrics # HELP pr_generator_scan_cycles_total Total number of scan cycles completed. # TYPE pr_generator_scan_cycles_total counter pr_generator_scan_cycles_total 42.0 # HELP pr_generator_prs_created_total Total PRs opened, labelled by provider. # TYPE pr_generator_prs_created_total counter pr_generator_prs_created_total{provider="github"} 7.0
| Metric | Type | Labels | Description |
|---|---|---|---|
pr_generator_scan_cycles_total | counter | — | Scan cycles completed |
pr_generator_scan_duration_seconds | histogram | — | Duration per cycle |
pr_generator_last_scan_timestamp_seconds | gauge | — | Unix timestamp of last cycle |
pr_generator_prs_created_total | counter | provider | PRs opened |
pr_generator_prs_skipped_total | counter | provider | PRs skipped (already open) |
pr_generator_prs_simulated_total | counter | provider | PRs simulated in dry-run |
pr_generator_scan_errors_total | counter | provider | Errors during scan |
pr_generator_rules_active | gauge | — | Rules active in current cycle |
pr_generator_annotation_rules_discovered | gauge | — | Rules from ArgoCD annotations |
metrics: enabled: true serviceMonitor: enabled: true interval: 30s labels: release: kube-prometheus-stack # match your Operator's selector
Reference
Common fields and environment variables. Full reference in README.md and llms.txt.
| Field / Env var | Description | Type |
|---|---|---|
scan_frequency |
Seconds between branch scan cycles. | int |
dry_run |
Simulate PR creation without modifying repositories. | bool |
providers.*.auth_method |
app (GitHub App PEM) or pat (Personal Access Token). |
str |
providers.*.private_key_path |
Path to PEM file. Raises FileNotFoundError if set but missing. |
str |
providers.*.token_env |
Env var holding the access token. Must be unique per provider type. | str |
rules[].pattern |
Python regex matched against each branch name. | regex |
rules[].destinations |
Map of provider name to base branch (e.g. github: main). |
map |
annotation_discovery.mode |
config_only (default), annotations_only, or hybrid. |
str |
annotation_discovery.annotation_prefix |
Annotation key prefix to watch on ArgoCD Applications. Default: pr-generator.io. |
str |
GITHUB_APP_PRIVATE_KEY |
GitHub App PEM key as a string (alternative to private_key_path). |
env |
Troubleshooting
Most issues come down to auth, branch regex, or credential uniqueness.
Check the daemon output first. Look for lines prefixed with [SCAN] to see which branches were evaluated and why they were skipped.
Common causes:
import re; re.match(r'^image-updater/.*', 'image-updater/my-branch')dry_run: true — PRs are simulated only.enabled: false).If private_key_path is set, the file must exist. The daemon raises FileNotFoundError at startup rather than silently falling back to the env var.
FileNotFoundError: Private key file not found: /secrets/github.pem
Either correct the path or remove private_key_path and set GITHUB_APP_PRIVATE_KEY as an environment variable instead.
Each provider of the same type must map to a distinct environment variable. The daemon refuses to start if two providers share the same token_env:
ValueError: Duplicate token_env 'BITBUCKET_TOKEN' across bitbucket providers: bitbucket, bitbucket-org2
Fix by assigning unique env var names:
providers:
bitbucket:
token_env: BITBUCKET_TOKEN
bitbucket-org2:
type: bitbucket
token_env: BITBUCKET_ORG2_TOKEN # must differ
app_id is shown on the GitHub App settings page (Settings → Developer settings → GitHub Apps → your app). The installation_id is found at https://github.com/organizations/<org>/settings/installations — it appears in the URL when you click on the installed app.
Both must be strings (quoted) in config.yaml.