GitOps · Python · Automation

Automated PRs for
GitHub & Bitbucket

pr-generator watches for branches matching configurable regex patterns and automatically opens Pull Requests. Built for ArgoCD Image Updater and GitOps workflows.

Quick start — Docker
$ 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

Everything you need, out of the box

A lightweight Python daemon with no persistent storage requirements. Runs anywhere: bare metal, Docker, or Kubernetes.

GitHub App & PAT

GitHub App (PEM private key) or Personal Access Token. Configure per provider with auth_method.

Bitbucket Cloud

Full Bitbucket Cloud support via access token. Run GitHub and Bitbucket providers in a single process.

Multi-provider

Multiple named providers of the same type. Different orgs, workspaces, or repos — all watched in one daemon.

Regex branch patterns

Match any branch naming convention with full Python regex. Each rule can target multiple destination branches.

Safety by default

Duplicate PR detection prevents opening a second PR for the same branch. dry_run mode for safe testing in production.

Health endpoint

Built-in HTTP server exposes /livez and /healthz (liveness) and /readyz (ready after first scan). No extra dependencies.

Configurable scan frequency

Set scan_frequency to control how often branches are polled. Defaults to 300 seconds.

Kubernetes-native

Official Helm chart with ServiceAccount, PDB, startup probe, and security-hardened pod spec.

Annotation-based discovery

Let each ArgoCD Application declare its own PR rules via annotations — no central config change needed. Three modes: config_only, annotations_only, hybrid.

Prometheus metrics

Built-in /metrics endpoint with counters, histograms, and gauges for PRs, scan cycles, errors, and annotation rules. Pod annotations and ServiceMonitor included.

Multiple deployment options

Run via Docker (recommended), install from PyPI, or deploy on Kubernetes with the Helm chart.

docker-compose.yml
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
bash — PyPI
$ pip install pr-generator
$ pr-generator --config /etc/pr-generator/config.yaml
bash — Kubernetes (Helm)
$ 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)"

config.yaml reference

Mount your config at /etc/pr-generator/config.yaml. Credentials are passed via environment variables — never in the config file.

config.yaml — GitHub App + Bitbucket
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
Security: Each provider of the same type must use a unique token_env value. The daemon raises a ValueError at startup if duplicates are detected — preventing silent credential overwrite.

GitOps integration

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.

argocd Application annotations
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}}
config.yaml — matching rule
rules:
  - pattern: "^image-updater/.*"
    destinations:
      github: main

Per-app rules via annotations

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).

argocd Application — annotation schema
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"
config.yaml — enable annotation discovery
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
RBAC: Annotation discovery requires a ClusterRole to get and list applications.argoproj.io cluster-wide. The Helm chart creates this automatically when annotationDiscovery.enabled: true.

Prometheus metrics

GET /metrics on the health port (8080) exposes Prometheus text format. Works with plain Prometheus (pod annotations) and Prometheus Operator (ServiceMonitor).

bash — scrape manually
$ 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
MetricTypeLabelsDescription
pr_generator_scan_cycles_totalcounterScan cycles completed
pr_generator_scan_duration_secondshistogramDuration per cycle
pr_generator_last_scan_timestamp_secondsgaugeUnix timestamp of last cycle
pr_generator_prs_created_totalcounterproviderPRs opened
pr_generator_prs_skipped_totalcounterproviderPRs skipped (already open)
pr_generator_prs_simulated_totalcounterproviderPRs simulated in dry-run
pr_generator_scan_errors_totalcounterproviderErrors during scan
pr_generator_rules_activegaugeRules active in current cycle
pr_generator_annotation_rules_discoveredgaugeRules from ArgoCD annotations
Helm values — Prometheus Operator
metrics:
  enabled: true
  serviceMonitor:
    enabled: true
    interval: 30s
    labels:
      release: kube-prometheus-stack  # match your Operator's selector

Key configuration fields

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

Common issues

Most issues come down to auth, branch regex, or credential uniqueness.

No PRs created — branches exist but daemon is silent

Check the daemon output first. Look for lines prefixed with [SCAN] to see which branches were evaluated and why they were skipped.

Common causes:

  • Branch pattern does not match. Test your regex: import re; re.match(r'^image-updater/.*', 'image-updater/my-branch')
  • dry_run: true — PRs are simulated only.
  • Provider is disabled (enabled: false).
  • A PR already exists for that branch — duplicates are skipped.
GitHub App: FileNotFoundError on private key

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.

ValueError: duplicate token_env across providers

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
GitHub installation_id vs app_id confusion

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.