| 1 | # siGit Code GitHub App — automatic PR reviews |
| 2 | |
| 3 | The hosted GitHub App behind siGit Code's pull-request reviews. Users install |
| 4 | the App on their github.com repos; every reviewable pull-request event hits |
| 5 | `POST /github/webhooks`, and `GithubPrReviewJob` posts a walkthrough summary |
| 6 | plus line-level comments on the PR, powered by Onde Cloud inference. |
| 7 | |
| 8 | ## How it fits together |
| 9 | |
| 10 | ``` |
| 11 | github.com ──webhook──▶ GithubWebhooksController ──enqueue──▶ GithubPrReviewJob (Solid Queue) |
| 12 | │ verifies X-Hub-Signature-256 │ fetch PR + diff (GithubAppService) |
| 13 | │ syncs github_app_installations │ budget/annotate (GithubReviewPrompt) |
| 14 | └ <10s, never does slow work │ inference (OndeCloudService) |
| 15 | │ validate lines (GithubPatchIndex) |
| 16 | └ POST one review back to GitHub |
| 17 | ``` |
| 18 | |
| 19 | State lives in two tables: `github_app_installations` (mirror of App |
| 20 | installations, plus the cached 1-hour installation token) and |
| 21 | `github_pr_reviews` (one row per repo + PR + head SHA — the idempotency |
| 22 | guard, and the audit trail: status, skip reason, error, comment count). |
| 23 | |
| 24 | ## The registered App |
| 25 | |
| 26 | The App already exists — do not create a second one. It lives under the |
| 27 | `getsigit` org: |
| 28 | |
| 29 | - Name: **siGit Code**, App ID **3492899** |
| 30 | - Public page: <https://github.com/apps/sigit-code> |
| 31 | - Settings: `github.com/organizations/getsigit/settings/apps/sigit-code` |
| 32 | - Homepage URL: `https://sigit.si/code` |
| 33 | - Webhook URL: `https://sigit.si/github/webhooks` |
| 34 | - Permissions: **Pull requests: Read and write**, **Contents: Read-only**, |
| 35 | **Metadata: Read-only** (mandatory) |
| 36 | - Subscribed events: **Pull request**. (`installation` and |
| 37 | `installation_repositories` are always delivered to App webhooks; there is |
| 38 | no checkbox for them.) |
| 39 | |
| 40 | If you ever register a replacement from scratch, mirror those settings, set |
| 41 | "Where can this App be installed" to **Any account**, generate a webhook |
| 42 | secret (`bin/rails secret | head -c 48`), and **Generate a private key** |
| 43 | (downloads a `.pem`) for the config below. |
| 44 | |
| 45 | ## Configuration |
| 46 | |
| 47 | Preferred in production: Rails credentials, because the private key is |
| 48 | multi-line and prod env vars live in the `git` user's `~/.profile`, which is |
| 49 | single-line only: |
| 50 | |
| 51 | ```yaml |
| 52 | github_app: |
| 53 | app_id: "3492899" |
| 54 | webhook_secret: "..." |
| 55 | private_key: | |
| 56 | -----BEGIN RSA PRIVATE KEY----- |
| 57 | ... |
| 58 | -----END RSA PRIVATE KEY----- |
| 59 | ``` |
| 60 | |
| 61 | **Edit credentials locally and commit — never on the server.** |
| 62 | `config/credentials.yml.enc` is git-tracked, and the deploy hook's |
| 63 | `git checkout -f main` overwrites the server copy, silently deleting any |
| 64 | server-side edit on the next deploy (this happened: the App credentials were |
| 65 | added on the server on 2026-07-05 and wiped by the v1.1.0 deploy the next |
| 66 | day). Run `EDITOR=nano bin/rails credentials:edit` in a local checkout that |
| 67 | has `config/master.key`, paste the block, commit the `.enc`, and deploy. |
| 68 | |
| 69 | When pasting the key, indent every PEM line to match `private_key: |`. |
| 70 | Verify after deploy without booting the app: |
| 71 | `bin/rails runner 'puts GithubAppService.configured?'` on the server — and |
| 72 | expect `POST /github/webhooks` to return 401 (not 503) to an unsigned probe. |
| 73 | Note the `webhook_secret` here is separate from `stripe.webhook_secret`; they |
| 74 | share a key name but live under different top-level parents. |
| 75 | |
| 76 | ENV fallback (dev, or single-line values): `GITHUB_APP_ID`, |
| 77 | `GITHUB_APP_WEBHOOK_SECRET`, and `GITHUB_APP_PRIVATE_KEY` with literal `\n` |
| 78 | escapes in place of newlines. See `.env.example`. |
| 79 | |
| 80 | Switches: |
| 81 | |
| 82 | - `SIGIT_GITHUB_REVIEWS_ENABLED=false` — the kill switch. Stops new enqueues |
| 83 | *and* drains already-queued jobs as no-ops (the job re-checks). Reviews are |
| 84 | on by default whenever the App credentials are configured. |
| 85 | - `SIGIT_GITHUB_REVIEWS_MODEL` — Onde tier for reviews (default `onde-large`). |
| 86 | |
| 87 | ## Production rollout |
| 88 | |
| 89 | The deploy is the usual `git push` to the bare-repo hook (see |
| 90 | `.agents/skills/deployment/SKILL.md`), but this feature is the first user of |
| 91 | Solid Queue, so on the first deploy: |
| 92 | |
| 93 | 1. Verify the queue schema loaded: the hook's `db:prepare` should load |
| 94 | `db/queue_schema.rb` into `sigitsi_production_queue` (that DB exists but |
| 95 | was empty — a known issue in the deployment skill). Check with |
| 96 | `psql sigitsi_production_queue -c '\dt'` — expect `solid_queue_*` tables. |
| 97 | 2. **Amend the post-receive hook before relying on `bin/jobs start`**: the |
| 98 | hook starts a jobs supervisor on every deploy but never stops the previous |
| 99 | one, so supervisors stack. Add something like |
| 100 | `pkill -f 'solid_queue' || true` before the `bin/jobs start` line. |
| 101 | 3. App credentials are already in production credentials (see Configuration); |
| 102 | restart Puma after the deploy so the new code picks them up. |
| 103 | 4. Install the App on a test repo, open a PR, and watch: |
| 104 | - the jobs log (`output-jobs.log`) for the review run, |
| 105 | - the `github_pr_reviews` row (`status`, `skip_reason`, `error_message`), |
| 106 | - the PR itself for the summary + line comments. |
| 107 | |
| 108 | ## Operational notes |
| 109 | |
| 110 | - **GitHub never retries webhook deliveries** (10-second timeout, fire-once). |
| 111 | A missed delivery is recovered by the next push (`synchronize`) or manually: |
| 112 | App settings → Advanced → Recent Deliveries → Redeliver (kept 30 days). |
| 113 | - **Skipped, not failed**: drafts, closed/superseded PRs, PRs over the size |
| 114 | budget (> `GithubReviewPrompt::MAX_FILES` files or nothing reviewable) are |
| 115 | recorded as `skipped` with a `skip_reason`; over-budget PRs get a short |
| 116 | explanatory comment instead of a review. |
| 117 | - **422 from GitHub on review creation** means a comment targeted a line |
| 118 | outside the diff. The job degrades automatically: full review → |
| 119 | summary-only review → plain issue comment. If these show up in the logs, |
| 120 | look at `GithubPatchIndex` first. |
| 121 | - **One review per head SHA**: redelivered webhooks and rapid pushes collapse |
| 122 | via the unique `(repo_full_name, pr_number, head_sha)` index; a newer push |
| 123 | supersedes queued reviews of older SHAs. |
| 124 | - Reviews are **ungated** in v1 — any installation gets them. Billing or |
| 125 | account linking is future work. |