{"id":51778342,"url":"https://github.com/bborbe/github-pr-watcher","last_synced_at":"2026-07-20T08:32:15.924Z","repository":{"id":369367008,"uuid":"1289528338","full_name":"bborbe/github-pr-watcher","owner":"bborbe","description":"GitHub PR watcher — extracted from bborbe/maintainer. Watches PR events via the agent framework; image docker.io/bborbe/github-pr-watcher.","archived":false,"fork":false,"pushed_at":"2026-07-10T19:27:58.000Z","size":175,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-20T08:32:15.386Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bborbe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-04T21:45:02.000Z","updated_at":"2026-07-10T19:28:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bborbe/github-pr-watcher","commit_stats":null,"previous_names":["bborbe/github-pr-watcher"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/bborbe/github-pr-watcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-pr-watcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-pr-watcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-pr-watcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-pr-watcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bborbe","download_url":"https://codeload.github.com/bborbe/github-pr-watcher/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bborbe%2Fgithub-pr-watcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35681006,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"ssl_error","status_checked_at":"2026-07-20T02:08:09.736Z","response_time":111,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-20T08:32:15.276Z","updated_at":"2026-07-20T08:32:15.912Z","avatar_url":"https://github.com/bborbe.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# github-pr-watcher\n\nPolls the GitHub Search API for open pull requests and publishes a `CreateTaskCommand` to Kafka\nfor each new or force-pushed PR so the [`github-pr-review-agent`](https://github.com/bborbe/github-pr-review-agent) picks it up automatically.\n\n## Links\n\nDev:\nhttps://dev.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/setloglevel/3\nhttps://dev.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/check\nhttps://dev.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/trigger?url=https://github.com/owner/repo/pull/123\n\nProd:\nhttps://prod.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/setloglevel/3\nhttps://prod.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/check\nhttps://prod.quant.benjamin-borbe.de/admin/maintainer-watcher-github-pr/trigger?url=https://github.com/owner/repo/pull/123\n\n## How It Works\n\nThe watcher runs a `user:\u003cscope\u003e` GitHub Search query on a configurable interval. On each poll\nit compares the PR's current head SHA against the value stored in the cursor; if the SHA has\nchanged (force-push) the PR is re-submitted as a new task. The cursor is persisted to\n`/data/cursor.json` between polls so a restart does not re-trigger every known PR.\n\nTwo independent decision chains run per PR — see [`docs/watcher-decision-chains.md`](https://github.com/bborbe/maintainer/blob/master/docs/watcher-decision-chains.md):\n\n- **`TaskCreationFilter`** — should we create a task at all? (drafts, bots, WIP titles, age, allowlist)\n- **`TrustGate`** — given a task is created, auto-process or route to `human_review`? (trusted authors)\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `GH_TOKEN` | yes | — | GitHub personal access token (read scope sufficient) |\n| `KAFKA_BROKERS` | yes | — | Comma-separated Kafka broker list |\n| `STAGE` | yes | — | Deployment stage (`dev` or `prod`) |\n| `TRUSTED_AUTHORS` | yes | — | Comma-separated trusted GitHub logins; empty list refuses startup |\n| `LISTEN` | no | `:9090` | HTTP listen address (`/healthz`, `/readiness`, `/metrics`, `/check`, `/trigger`) |\n| `POLL_INTERVAL` | no | `5m` | Poll interval (Go duration string) |\n| `REPO_SCOPE` | no | `bborbe` | GitHub user or org to search for PRs |\n| `REPO_ALLOWLIST` | no | — | Comma-separated host-qualified repo allowlist (`host/owner/repo`); empty means allow-all |\n| `BOT_ALLOWLIST` | no | `dependabot[bot],renovate[bot]` | Comma-separated bot author logins to skip |\n| `MAX_PR_AGE` | no | `2160h` (90d) | Skip PRs older than this; empty disables |\n| `BACKFILL_DURATION` | no | `720h` (30d) | On cold start, backdate the initial cursor by this; empty disables |\n| `SENTRY_DSN` | no | — | Sentry DSN for error tracking |\n| `SENTRY_PROXY` | no | — | HTTP proxy URL for Sentry transport |\n\n### `REPO_ALLOWLIST` syntax\n\nEntries are comma-separated. A leading `!` marks an exclusion. A target is allowed iff `(includes is empty OR any include matches) AND (no exclude matches)`; excludes always override includes.\n\n| Entry shape | Example | Meaning |\n|---|---|---|\n| Literal include | `github.com/bborbe/maintainer` | Allow exactly this repo |\n| Wildcard include | `github.com/bborbe/*` | Allow every repo under this owner |\n| Literal exclude | `!github.com/bborbe/go-skeleton` | Reject exactly this repo (overrides any matching include) |\n| Wildcard exclude | `!github.com/bborbe/*` | Reject every repo under this owner |\n\nAn allowlist consisting of only exclude entries is treated as allow-all-except: every target passes the include gate, and only the exclude gate filters. Example: `REPO_ALLOWLIST=!github.com/bborbe/go-skeleton` rejects go-skeleton and allows every other repo (including all other bborbe repos). To allow every bborbe repo except go-skeleton, write `github.com/bborbe/*,!github.com/bborbe/go-skeleton`.\n\n## HTTP Endpoints\n\n| Path | Method | Purpose |\n|---|---|---|\n| `/healthz` | GET | Liveness probe (always returns 200 OK) |\n| `/readiness` | GET | Readiness probe (always returns 200 OK) |\n| `/metrics` | GET | Prometheus metrics |\n| `/check` | POST | Run a poll cycle in the background; returns 200 immediately |\n| `/trigger` | POST | Fire a single-PR review by URL (`?url=\u003cpr_url\u003e`); reuses the filter chain and trust evaluation |\n\n### Single-PR Trigger Known Limit\n\nIf a vault task already exists for the same `(PR, SHA)`, the controller's `create-if-not-exists` is idempotent and no fresh agent Job spawns. To force a re-run in that case, reset the vault task's frontmatter (`phase`, `status`, `trigger_count`) manually OR push a new commit so the SHA changes.\n\n## Development\n\n```bash\nmake test          # run unit tests\nmake generate      # regenerate counterfeiter mocks\nmake precommit     # format + lint + test + security checks\n```\n\n## Cursor Mechanism\n\nThe cursor at `/data/cursor.json` records the timestamp of the most-recently-seen PR update plus\na map of `task_id → head_sha`. On cold start (missing file) the cursor is initialised to the\nprocess start time minus `BACKFILL_DURATION`, so only PRs updated within that window are\nsubmitted. Force-push detection compares the stored head SHA for a known PR against the value\nreturned by the current poll; a mismatch publishes a new `CreateTaskCommand` with the new SHA.\n\nA corrupt cursor refuses startup — see `pkg/cursor.go`.\n\n## Relationship to pr-reviewer\n\nThis service feeds tasks into the [`github-pr-review-agent`](https://github.com/bborbe/github-pr-review-agent) Pattern B Job via Kafka: for every new or\nforce-pushed PR it publishes a `CreateTaskCommand` that the agent task controller picks up and\nspawns into a per-task K8s Job. The agent runs `/coding:pr-review` and posts the verdict back to\nthe PR.\n\nSee [`docs/architecture.md`](https://github.com/bborbe/maintainer/blob/master/docs/architecture.md) for the full pipeline.\n\n## License\n\nBSD 2-Clause License. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbborbe%2Fgithub-pr-watcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbborbe%2Fgithub-pr-watcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbborbe%2Fgithub-pr-watcher/lists"}