A Cloudflare Worker and GitHub Actions that automate issue triage and PR review for any repository with the aptu GitHub App installed, powered by aptu. The Worker validates GitHub webhook signatures, reads each repository's .github/aptu.yml opt-in config, and dispatches repository_dispatch events to the originating repository to trigger aptu-powered triage, review, security scan, and issue lint workflows via reusable workflows.
GitHub event (issues.opened|edited / pull_request.opened|synchronize|reopened|ready_for_review
/ issue_comment.created / pull_request_review_comment.created)
-> Cloudflare Worker
1. Verify X-Hub-Signature-256 (HMAC)
2. Deduplicate by X-GitHub-Delivery (ReplayGuard Durable Object)
3. Validate source IP against GitHub /meta hook CIDRs (fail-open)
4. Get installation token (scoped to originating repo)
5. Fetch .github/aptu.yml from originating repo
6. Check per-installation quota (enforced for all installations)
7. For PRs: skip draft PRs; evaluate review.paths filters
8. Get dispatch token (scoped to originating repo)
9. POST repository_dispatch to originating repo
-> Caller's .github/workflows/aptu-{review,triage,scan-security,lint-issue}.yml (repository_dispatch handlers)
-> Calls reusable workflow from aptu-github-app repo
-> Checkout caller repo, run aptu CLI with caller's AI API key
-> GitHub Reviews API (inline comments as github-actions[bot])
The Worker uses two Durable Objects: InstallationQuota for per-installation rate limiting (50 events per event type per 24h rolling window, enforced for all installations), and ReplayGuard for webhook deduplication. No external database is required.
Commenting @aptu on an issue or PR review comment triggers triage or review respectively. The commenter must be a collaborator with at least read permission on the repository.
Each repository opts in by adding a .github/aptu.yml file. The Worker fetches this file on every webhook event; if the file is absent or invalid, no dispatch occurs.
version: 1 # required; must be 1
triage:
enabled: true # required if triage block present
review:
enabled: true # required if review block present
instructions-file: .github/instructions/pr-review.md # optional; path in the target repo
skip-labeled: false # optional; aptu skips review if PR has a label
paths: # optional; suppress PR review when no file qualifies
- "src/**" # bare patterns are includes
- "!src/data/**" # '!'-prefixed patterns are excludes
scan:
enabled: true # required if scan block present
fail-on: critical,high # optional: comma-separated severities that fail the scan
path: src/ # optional: root directory to scan (default ".")
lint:
enabled: true # required if lint block present
spec: .github/lint-specs.toml # optional: lint spec path passed to aptu lint-issue --config
ai:
provider: openrouter # required if ai block present
model: google/gemma-4-26b-a4b-it # required if ai block presentThe scan block enables aptu's local pattern-based security scanning. Scan runs without an AI provider -- no ai block is required. Results are uploaded as SARIF code scanning alerts to the originating repository. fail-on takes a comma-separated list of severities (critical, high, medium, low); the scan fails only if a finding matches one of the listed severities. Omitting fail-on reports findings without failing the check.
| Field | Type | Default | Description |
|---|---|---|---|
version |
integer | -- | Config schema version. Must be 1. |
triage.enabled |
boolean | -- | Dispatch aptu-triage on issues.opened events. |
review.enabled |
boolean | -- | Dispatch aptu-review on pull_request opened/synchronize/reopened/ready_for_review events. |
review.instructions-file |
string | .github/instructions/pr-review.md |
Path to PR review instructions file, relative to the target repo root. Passed to aptu as --instructions-file. |
review.skip-labeled |
boolean | false |
Passed to aptu as --skip-labeled. When true, aptu skips review if the PR already carries a label. |
review.paths |
string[] | -- | Glob patterns (picomatch) evaluated against the full PR file list. Bare patterns are includes; !-prefixed patterns are excludes. A PR is dispatched if at least one file qualifies. If no file qualifies, the review dispatch is suppressed. |
ai.provider |
string | -- | AI provider name passed to aptu as --provider. Supported: gemini, anthropic, openrouter. Required if ai block present. Also selects which of the caller's own repository secrets (GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY) the dispatch handler resolves. |
ai.model |
string | -- | AI model name passed to aptu as --model. Required if ai block present. |
scan.enabled |
boolean | false |
Enable aptu scan-security on pull requests. Runs local pattern-based secret scanning and uploads SARIF results to GitHub Code Scanning. No ai block required. |
scan.fail-on |
string | -- | Comma-separated severities that fail the scan (critical, high, medium, low). Omit to report findings without failing the check. |
scan.path |
string | . |
Root directory to scan. |
lint.enabled |
boolean | false |
Dispatch aptu-lint-issue on issues.opened and issues.edited events. Deterministic linting of the issue body; advisory only (never fails the workflow). |
lint.spec |
string | -- | Path to a lint spec file, relative to the target repo root. Passed to aptu as --config. Omit for default auto-discovery. |
telemetry.enabled |
boolean | false |
Opt in to POST anonymized review-context counters (reviews_total, truncation_events_total, files_truncated_total, budget_drop_reason_counts, finish_reasons_counts, model_tier_counts, prompt_budget_pct_histogram) to the hosted Worker for aggregate product analytics. No repo, PR, or actor identifiers are ever included; a failed or unreachable POST never fails the PR review job. Off by default. |
All fields under triage, review, scan, lint, ai, and telemetry are validated strictly: unknown keys are ignored, but a missing enabled boolean causes the entire config to be rejected (no dispatch). Both ai fields (provider and model) are required if the ai block is present; a partial or empty-string block is rejected.
When telemetry.enabled: true, the pr-review.yml workflow passes telemetry-rollup: true to the clouatre-labs/aptu action, which writes anonymized counters to .aptu/telemetry-rollup.jsonl. The workflow then POSTs the last JSONL line to the hosted Worker's POST /telemetry/rollup endpoint, which validates the payload against a strict key allowlist and merges it into a single global aggregate. The endpoint never returns anything but 202 Accepted, and the POST step uses continue-on-error: true plus shell-level || true, so a missing file, malformed payload, or network failure never fails the PR review job. See clouatre-labs/aptu's docs/GITHUB_ACTION.md for the exact per-field counter semantics.
This happy-path example parses with parseConfig and is the same example the welcome issue posted on installation suggests:
version: 1
triage:
enabled: true
ai:
provider: openrouter
model: google/gemma-4-26b-a4b-itThe ai block contains no secret. It selects the provider and model, which determines which of your repository secrets the dispatch handler resolves: gemini -> GEMINI_API_KEY, anthropic -> ANTHROPIC_API_KEY, openrouter -> OPENROUTER_API_KEY. Create the matching secret yourself; the Worker cannot verify that it exists. Security scanning works without the ai block.
Each installation provides its own AI API key via a GitHub repository secret in the caller's repo. The caller installs the dispatch handler workflows (see Installation) which receive repository_dispatch events and call reusable workflows from the clouatre-labs/aptu-github-app repository. The reusable workflow runs in the caller's context, using the caller's GITHUB_TOKEN for repository operations and the caller's AI API key secret for AI provider calls. This is the BYOK (bring your own key) model: the operator's AI keys are never exposed to any installation.
version: 1
triage:
enabled: true
review:
enabled: true
ai:
provider: openrouter
model: google/gemma-4-26b-a4b-itEach repository that uses the aptu GitHub App must have these files:
.github/aptu.yml-- opt-in configuration (see Repository configuration above).github/workflows/aptu-review.yml-- dispatch handler for PR review.github/workflows/aptu-triage.yml-- dispatch handler for issue triage.github/workflows/aptu-scan-security.yml-- dispatch handler for security scans.github/workflows/aptu-lint-issue.yml-- dispatch handler for issue linting (requireslint.enabled: truein.github/aptu.ymland an aptu release containing thelint-issuecommand)
The Worker automatically provisions the four workflow files above when a repository is added to an installation. You only need to add .github/aptu.yml and configure an AI provider API key secret (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY) in your repository. The GitHub App must have the Workflows permission enabled for automatic provisioning. For a step-by-step walkthrough, see docs/ONBOARDING.md.
Each dispatch handler receives its own repository_dispatch event type from the Worker and calls the appropriate reusable workflow hosted in clouatre-labs/aptu-github-app. The Worker mints an operation-scoped installation token and forwards it via client_payload.installation_token, which the dispatch handler passes into the reusable workflow's secrets: block. Installers do not need APP_ID or APP_PRIVATE_KEY secrets in their repository. The caller's AI API key secret is also passed to the reusable workflow via the secrets: block. Each handler resolves the key named after the ai.provider in .github/aptu.yml (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY). Create that secret in your own repository or rely on an org-visible secret of the same name.
Merging to main triggers deploy.yml, which deploys the Worker to Cloudflare via bunx wrangler deploy.
GitHub secrets and variables:
CLOUDFLARE_API_TOKEN-- Cloudflare API token with Workers edit permissionCLOUDFLARE_ACCOUNT_ID-- Cloudflare account ID (repository variable)APP_ID-- GitHub App ID (repository variable, required for Worker to mint installation tokens)APP_PRIVATE_KEY-- GitHub App private key in PKCS#8 PEM format (repository secret, required for Worker)
AI API keys are configured per installation in the caller's repository, not in aptu-github-app. Each caller creates a repository secret named after the provider they select via ai.provider in their own .github/aptu.yml (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY) -- never in the (static, unedited) dispatch handler.
Wrangler secrets (set via bunx wrangler secret put):
WEBHOOK_SECRET-- must match the value configured in the GitHub AppAPP_PRIVATE_KEY-- GitHub App private key in PKCS#8 PEM format
Wrangler variables (in wrangler.toml [vars]):
APP_ID-- GitHub App IDAPTU_BOT_ID-- aptu bot user ID (used to ignore self-mentions)SENTRY_DSN-- Sentry DSN for error tracking (optional; set viabunx wrangler secret put)
The Worker is bound to the route aptu.dev/webhook via wrangler.toml. Cloudflare route bindings require a proxied DNS record for the zone root:
| Type | Name | Content | Proxied |
|---|---|---|---|
| AAAA | aptu.dev | 100:: |
Yes |
100:: is the Cloudflare-documented placeholder for route-based Workers with no real origin.
See clouatre-labs/aptu#94 for GitHub App registration and credential setup.
See RUNBOOK.md for post-deploy steps, secret rotation, and incident response.
See PRIVACY.md for data categories, subprocessors, retention, and data residency.
Releases are cut manually by maintainers from GPG-signed annotated tags on main. See CONTRIBUTING.md for details.
bun install
bun test
bun run biome check
bunx wrangler devApache-2.0. See LICENSE.