A GitHub App that automatically pre-audits pull requests opened during Drips Waves so maintainers can review and pay bounty hunters quickly.
When a PR is opened (or updated) during a Wave, the app:
- Checks out the PR branch into an isolated workspace.
- Runs AST linting over the changed files (a curated ESLint rule set).
- Measures cyclomatic complexity of every changed function via a real AST parse.
- Executes the repository's unit test suite.
- Compares the diff's size/complexity against the issue's assigned point level (
Trivial,Medium,High). - Posts a preliminary audit scorecard as a PR comment and creates/updates a Check Run with a verdict for the maintainer.
The scorecard is a heuristic signal to speed up human review — it is not a replacement for it.
- Bounty issues are labelled (or described) with a point level.
- Hunters submit PRs that reference the issue (e.g.
Closes #12). - The app finds the linked issue, resolves its point level, audits the diff against that level, and posts the scorecard.
- Maintainers get a fast pass/fail signal before the Wave ends, so contributors get paid sooner.
- Automatic audit on
pull_requestevents:opened,synchronize,reopened,ready_for_review(drafts are skipped). - Re-run on demand with a
/reauditcomment on the PR. - Point level detection from issue labels or body keywords (
trivial,small,1 pt,medium,high,5 pts, numeric point values, …), with a configurable default fallback. - AST linting — ESLint flat-config with a curated rule set (style + correctness + a
complexitywarning), run only on the changed files. - Cyclomatic complexity analysis — per-function complexity for functions touched by the diff, surfaced as a hotspot table.
- Test execution — runs the repo's
testscript (npm/yarn/pnpm/bundetected from lockfiles), parses common runner output (Jest, Vitest, Mocha, node:test, AVA) and reports pass/fail counts. - Diff-vs-point-level check — files changed, lines added/removed and worst changed-function complexity are compared against limits per point level.
- Single, self-updating scorecard comment (keyed by an HTML marker) — no spam on every push.
- Check Run with
success/neutral/failureconclusion. - Safety guard — audits are skipped (with a note) when a diff exceeds
MAX_TOTAL_CHANGED_LINES.
GitHub ──webhook──▶ Express (src/index.js)
│ @octokit/webhooks middleware
├─▶ pull_request / issue_comment handlers (src/webhooks/)
│ └─▶ runAuditFlow (create check run → audit → scorecard)
└─▶ src/audit/
├─ index.js pipeline orchestration
├─ workspace.js clone/checkout PR ref (or local dir)
├─ diff.js numstat + hunk line-range parsing
├─ complexity.js AST cyclomatic complexity (espree)
├─ lint.js ESLint programmatic API
├─ tests.js test runner + output parsing
├─ points.js point-level detection + verdict logic
└─ scorecard.js markdown renderer
- No database. State lives in the PR thread and check runs.
- No frontend. The interface is the GitHub comment + check run.
- No smart contract (per specification — everything runs off-chain as a GitHub App).
| Concern | Choice | Why |
|---|---|---|
| HTTP server | Express.js | required by the specification |
| Webhook handling | @octokit/webhooks |
HMAC verification, event routing, node middleware |
| GitHub API | octokit + @octokit/auth-app |
app JWT + short-lived installation tokens |
| AST parsing | espree (ESTree) |
the parser ESLint uses; full AST walk for complexity |
| Linting | eslint programmatic API |
runs against a curated flat config, independent of the PR repo's config |
| Tests (auditor's own) | vitest |
fast, ESM-native, zero-config |
| Runtime | Node.js ≥ 20 | modern, ESM, stable fetch |
wave-sprint-auditor/
├── src/
│ ├── index.js # Express app + webhook wiring
│ ├── server.js # entrypoint (loads config, listens)
│ ├── config.js # env → frozen config (validated)
│ ├── logger.js
│ ├── github/
│ │ ├── client.js # app octokit + installation-token factory
│ │ └── issues.js # linked-issue resolution + point-level detection
│ ├── webhooks/
│ │ ├── pullRequest.js # PR event handler
│ │ ├── comment.js # /reaudit command handler
│ │ ├── audit-flow.js # shared audit flow
│ │ └── publish.js # check runs + scorecard comment (upsert)
│ └── audit/ # see architecture diagram
├── scripts/
│ └── audit-local.mjs # run the audit on a local repo (no GitHub needed)
├── test/
│ ├── fixtures/ # real mini-repos used by integration tests
│ └── *.test.js # unit + integration tests
├── Dockerfile
├── docker-compose.yml
└── .env.example
- Go to Settings → Developer settings → GitHub Apps → New GitHub App.
- Set a name (e.g.
wave-sprint-ai-auditor). - Webhook URL:
https://your-host.example.com/api/github/webhooks. - Webhook secret: a long random string (this becomes
WEBHOOK_SECRET). - Permissions (Repository):
- Checks: Read & write
- Issues: Read & write
- Pull requests: Read & write
- Events to subscribe:
Pull requestsIssue commentPing
- Download the generated private key (PEM) and note the App ID.
cp .env.example .envFill in at minimum:
| Variable | Description |
|---|---|
GITHUB_APP_ID |
the app ID from step 7 |
GITHUB_APP_PRIVATE_KEY_PATH |
path to the PEM file (or inline GITHUB_APP_PRIVATE_KEY) |
WEBHOOK_SECRET |
the webhook secret you set when creating the app |
Optional tuning (see .env.example):
DEFAULT_POINT_LEVEL—trivial|medium|high(defaultmedium).MAX_TOTAL_CHANGED_LINES— safety guard (default 1500).INSTALL_TIMEOUT_MS,TEST_TIMEOUT_MS,LINT_TIMEOUT_MS.LOG_LEVEL—debug|info|warn|error.GITHUB_BASE_URL— set for GitHub Enterprise Server.
Install the app on the org/repo that hosts the Wave's bounty repos. Point-level detection works best when the linked issue carries a label like point: medium or a body containing points: 5.
npm install
npm run dev # starts the server with --watchThe app ships with a local CLI that audits a folder directly (no clone, no credentials):
node scripts/audit-local.mjs --repo ../some-repo --points medium
# exit code 0 → PASS, 1 → FAIL/NEEDS_REVIEW (useful for CI gates)Or try it on the bundled fixtures:
node scripts/audit-local.mjs --repo test/fixtures/sample-repo --points medium # PASS
node scripts/audit-local.mjs --repo test/fixtures/sample-repo-issues --points trivial # FAILSign a payload and POST it to the running server (the signature uses the webhook secret):
node -e '
const crypto = require("crypto");
const body = JSON.stringify(require("./test/webhook.fixture.json"));
const sig = "sha256=" + crypto.createHmac("sha256", process.env.WEBHOOK_SECRET).update(body).digest("hex");
fetch("http://localhost:3000/api/github/webhooks", {
method: "POST",
headers: { "content-type": "application/json", "x-github-event": "ping",
"x-github-delivery": "dev", "x-hub-signature-256": sig },
body,
}).then((r) => console.log(r.status));
'npm test # vitest run
npm run lint # eslint on src/scripts/testThe suite covers:
- Unit: complexity counting, diff parsing, point-level detection, verdict/score logic, scorecard rendering, test-output parsing, webhook signature verification.
- Integration: the full audit pipeline runs against real fixture repos — a clean repo (PASS) and a messy repo with lint errors, a failing test, and an over-complex function (FAIL).
- Webhook/E2E: signed-payload routing, draft skipping,
/reauditcommand, failure reporting through check runs — all with an injected fake GitHub client, so no network access is needed.
# place your PEM at ./github-app.private-key.pem, fill .env, then:
docker compose up -d --buildExposes port 3000; /health is available for liveness checks.
- Run
node src/server.jsbehind TLS (e.g. nginx/caddy) since GitHub webhooks require HTTPS. - The only external dependencies are
gitandnpm(or the package manager used by the audited repos) in the runtime image.
| Level | Files | Added lines | Removed lines | Max function complexity |
|---|---|---|---|---|
| Trivial | ≤ 2 | ≤ 60 | ≤ 40 | ≤ 5 |
| Medium | ≤ 5 | ≤ 200 | ≤ 120 | ≤ 10 |
| High | ≤ 10 | ≤ 500 | ≤ 300 | ≤ 16 |
These live in src/audit/points.js (POINT_THRESHOLDS) and are applied against the changed lines/functions only.
Linterrors → fail; warnings → minor penalty.- Failing tests → fail; missing test script → informational (partial credit).
- Complexity over the level's limits → fail when a changed function exceeds the per-function ceiling, otherwise a warning.
PASSwhen there are no blockers and the score is ≥ 85,FAILwhen any blocker exists (lint errors, failing tests, over-complex changed functions), otherwiseNEEDS_REVIEW.
- Webhook requests are HMAC-verified (
sha256). - The app authenticates per-installation with short-lived installation tokens (never the app's private key).
- Git cloning uses a
GIT_ASKPASShelper so the token never appears in process arguments. - Audited code runs in an isolated temp workspace that is deleted afterwards (
WORKSPACE_DIR). - Private key is loaded from the environment or a file mount — never committed.
- A diff-size guard prevents resource exhaustion from huge PRs.
- The auditor uses its own ESLint config instead of the PR repo's, so a repo's config can't crash the run or pull in arbitrary plugins.
- Linting currently targets JavaScript/JSX (
.js,.mjs,.cjs,.jsx). TypeScript files are skipped for lint/complexity (listed as non-parsed in the scorecard). Extension is straightforward via@typescript-eslint. - Test detection relies on the repo's
testscript and common runner output formats; exotic runners may fall back to the exit-code signal. - Point levels are mapped from labels/bodies; if the convention differs in your Wave, adjust
detectPointLevelinsrc/audit/points.js.