GitHub App Integration
wreck-it can be driven by a GitHub App webhook in addition to (or instead of) the cron-based headless runner. The webhook approach uses a Cloudflare Worker that reacts to GitHub events in real time, advancing the task state machine without waiting for the next scheduled run.
When configured with full GitHub App credentials (private key + app ID), the worker can also trigger cloud agents (create issues + assign Copilot) and merge pull requests — making it a fully autonomous alternative to the cron-based headless runner.
This document covers the full integration surface: which events trigger iterations, how state updates propagate, how labels are used, and how the webhook and cron modes interact.
Overview
GitHub Event (issues / push / pull_request)
│
▼
┌─────────────────┐
│ Cloudflare │
│ Worker (WASM) │
│ │
│ 1. Verify sig │
│ 2. Parse event │
│ 3. Vend token │◄──── GitHub App (JWT → installation token)
│ 4. Read config │◄──── GitHub API (Contents)
│ 5. Read state │◄──── .wreck-it/config.toml + state branch
│ 6. Process iter │
│ 7. Trigger agent│────► GitHub API (Issues + GraphQL)
│ 8. Commit state │────► GitHub API (Contents) ─── only when a task is started
│ 9. Merge PRs │────► GitHub API (REST + GraphQL)
└─────────────────┘
The worker authenticates using the GitHub App's private key and app ID to generate a JWT, which is exchanged for an installation access token scoped to the specific repository from the webhook payload. This token has the permissions granted to the app, enabling the worker to manage the complete task lifecycle.
Events That Trigger Iterations
The worker subscribes to three GitHub webhook event types. All other events are ignored with an event ignored response.
| Event | Action filter | When it fires |
|---|---|---|
issues | opened or labeled | An issue is created or labeled and carries the wreck-it label |
push | (any) | A push to any branch (typically the state branch after a state commit) |
pull_request | closed with merged: true | A pull request is merged (signals task completion) |
pull_request | opened, ready_for_review, synchronize | Approves pending workflow runs and enables auto-merge for trusted PRs |
ping | — | GitHub's setup verification ping; the worker responds with pong |
Issue events
The worker only processes issue events when the issue has the wreck-it label. Both the opened and labeled actions are handled:
opened+ has label: Fires when wreck-it (or a user) creates an issue with thewreck-itlabel already applied.labeled+ has label: Fires when thewreck-itlabel is added to an existing issue.
This means creating an issue with the wreck-it label — which is exactly what the worker does when triggering an agent (see Agent Triggering below) — automatically triggers a webhook iteration.
Push events
Any push to the repository triggers an iteration. This is the primary mechanism for chaining: when the worker commits updated state to the state branch, the resulting push event fires another iteration, allowing the system to advance through multiple tasks in quick succession without waiting for the next cron tick.
Pull request events
Merged PRs trigger a dedicated handler that marks the corresponding task as complete, updates the state, and prepares the system to pick up the next task.
Non-merged PR events (opened, ready_for_review, synchronize) from trusted authors trigger workflow run approval: the worker approves any pending workflow runs so required checks can execute, then enables auto-merge when the base branch has required status checks. This handles cases where workflow runs need explicit approval before they can run (e.g. first-time contributors, outside collaborators, or fork PRs).
Agent Triggering
When the worker selects a new task (via the shared iteration logic in wreck-it-core), it also triggers a cloud coding agent:
- Creates a GitHub issue with the task description and the
wreck-it+copilotlabels. - Discovers a coding agent via the
suggestedActorsGraphQL query (searches forcopilot-swe-agent,copilot,claude,codex). - Assigns the agent to the issue via the
addAssigneesToAssignableGraphQL mutation.
This mirrors the same flow used by the headless CLI runner, so both modes produce identical issue + assignment patterns.
If agent triggering fails (e.g. no agent found, or insufficient permissions), the state still advances and the task is marked as in-progress. The next iteration or a cron run can retry the trigger.
PR Merging
When the worker receives a pull_request.closed event with merged: true:
- Marks the task as complete — finds the task associated with the merged PR and sets its status to
Completed. - Updates state — transitions to
Completedphase and removes the PR from the tracked list. - Commits state — writes updated task and state files to the state branch.
The worker also supports the full PR lifecycle management through its GitHub client:
- Draft detection — identifies draft PRs and can mark them ready for review.
- Merge status checks — determines whether a PR is mergeable, a draft, or has conflicts.
- Direct merge — merges PRs using squash merge when no required checks exist.
- Auto-merge — enables auto-merge via GraphQL for PRs with required status checks.
- Branch protection — detects required status checks on the base branch.
Label Behavior
Labels added on issue creation
When the worker (or headless runner) triggers a cloud coding agent, it creates a GitHub issue via the REST API with two labels:
{
"title": "[wreck-it] <task-id>",
"body": "<task description + memory context>",
"labels": ["wreck-it", "copilot"]
}
| Label | Purpose |
|---|---|
wreck-it | Identifies the issue as wreck-it managed. Required for the webhook worker to process the corresponding issues event. |
copilot | Conventional label indicating the issue is intended for a coding agent. |
Both labels are applied at creation time (not added after the fact), so the issues.opened event already contains the labels. The webhook worker sees the wreck-it label and processes the event.
Labels on pull requests
wreck-it does not add labels to pull requests. PRs are created by the cloud coding agent (e.g. GitHub Copilot), not by wreck-it itself. The worker identifies relevant PRs by the closed + merged action, not by labels.
Labels on tasks
Tasks in tasks.json have an optional labels field for organizational metadata. These labels are local to the task file and are not automatically synced to GitHub issues or PRs.
State Commit Behavior
A key design concern is whether state commits can cause infinite event loops. The short answer: no — the worker only commits when there is real work to do.
When the worker commits
The worker writes updated task and state files to the state branch only when a task is started (IterationOutcome::TaskStarted). In the other two outcomes:
| Outcome | Files written | Commits | Triggers push event |
|---|---|---|---|
AllComplete | None | No | No |
NoPendingTasks | None | No | No |
TaskStarted | Task file + state file | Yes | Yes |
This means the event chain self-terminates: once all eligible tasks have been started (or all tasks are complete), the worker stops committing, which stops generating push events.
Event chain lifecycle
A typical chain looks like this:
1. Issue created with "wreck-it" label
└─► issues webhook fires
└─► Worker selects task A, creates agent issue, commits state
└─► push webhook fires
└─► Worker selects task B, creates agent issue, commits state
└─► push webhook fires
└─► Worker finds no pending tasks → no commit → chain ends
2. Agent completes task A, PR is merged
└─► pull_request webhook fires
└─► Worker marks task A complete, commits updated state
Headless CLI (cron mode) behavior
The cron-based headless runner (wreck-it run --headless) also avoids unnecessary commits:
- After the state machine loop, the state is serialized to disk via
save_headless_state. commit_and_push_statechecks for actual git changes (git diff --quiet+ untracked file check).- If the serialized state is byte-for-byte identical to what was already on disk, git detects no changes and no commit is made.
- Only when the state actually changed (e.g. a phase transition, a new task started) does a commit and push occur.
This ensures that a cron run that finds nothing to do does not produce a spurious commit that would trigger the webhook worker.
Worker vs. Cron: Two Modes of Operation
wreck-it supports two complementary ways to advance the state machine:
| Aspect | Webhook Worker | Cron Headless Runner |
|---|---|---|
| Trigger | GitHub events (real-time) | Scheduled cron (e.g. every 10 min) |
| Runtime | Cloudflare Worker (WASM) | GitHub Actions runner (native binary) |
| State access | GitHub Contents API | Git worktree on local filesystem |
| Authentication | GitHub App JWT → repo-scoped installation token | GITHUB_TOKEN (PAT) |
| Can trigger agents | Yes (creates issues, assigns Copilot) | Yes (creates issues, assigns Copilot) |
| Can merge PRs | Yes (via GitHub REST + GraphQL API) | Yes (via GitHub REST + GraphQL API) |
Both modes are now functionally equivalent. The webhook worker provides faster state advancement by reacting to events immediately, while the headless runner can serve as a fallback or complement for scenarios where webhook delivery is delayed.
Both modes are safe to run simultaneously. They operate on the same state branch but through different mechanisms (API vs. git). The concurrency group in the GitHub Actions workflow (cancel-in-progress: false) prevents overlapping cron runs.
Authentication
The worker generates a short-lived JWT from the app's private key and numeric ID, then exchanges it for an installation access token scoped to the specific repository from the webhook payload.
- No static tokens to rotate — JWTs are generated on the fly and expire in 10 minutes.
- Full permissions — the installation token inherits all permissions from the app configuration.
- Repository-scoped — each token is limited to the single repository that triggered the webhook.
Setup
Prerequisites
- A GitHub App with the required permissions (see below)
- A Cloudflare Workers account
- Rust with the
wasm32-unknown-unknowntarget
1. Create a GitHub App
Create a GitHub App with the following permissions:
| Permission | Access | Purpose |
|---|---|---|
| Contents | Read & write | Read config/state files, commit state updates |
| Issues | Read & write | Create issues for agent triggers, read issue status |
| Pull requests | Read & write | Check PR status, merge PRs, enable auto-merge |
| Actions | Read & write | Approve pending workflow runs |
Subscribe to the following webhook events:
- Issues — to trigger on issue creation / labeling
- Push — to react to state branch changes
- Pull requests — to detect merged PRs
Set the webhook URL to your deployed worker URL.
Generate a private key from the app settings page and note the App ID.
2. Configure and deploy the worker
# Install the WASM target
rustup target add wasm32-unknown-unknown
# Create the KV namespace
cd worker
wrangler kv namespace create WRECK_IT_STORE
Copy the id from the output and replace REPLACE_WITH_KV_NAMESPACE_ID in wrangler.toml:
[[kv_namespaces]]
binding = "WRECK_IT_STORE"
id = "<your-namespace-id>"
# Set secrets
wrangler secret put GITHUB_WEBHOOK_SECRET # Webhook secret from GitHub App settings
wrangler secret put GITHUB_APP_ID # Numeric App ID from the GitHub App settings
wrangler secret put GITHUB_APP_PRIVATE_KEY # PEM-encoded private key (paste the full key)
wrangler secret put API_TOKEN # Bearer token for the REST API endpoints
# Deploy
wrangler deploy
3. Required secrets
| Secret | Required | Purpose |
|---|---|---|
GITHUB_WEBHOOK_SECRET | Yes | HMAC-SHA256 secret for verifying webhook payload signatures |
GITHUB_APP_ID | Yes | Numeric GitHub App ID |
GITHUB_APP_PRIVATE_KEY | Yes | PEM-encoded RSA private key from the GitHub App |
API_TOKEN | Yes | Bearer token for authenticating REST API requests |
Source Code Reference
| File | Purpose |
|---|---|
worker/src/lib.rs | Worker entry point — receives webhooks, verifies signatures, vends tokens, routes events |
worker/src/github_app.rs | GitHub App authentication — JWT generation and installation token vending |
worker/src/webhook.rs | HMAC-SHA256 signature verification, event type parsing |
worker/src/github.rs | GitHub REST + GraphQL API client (file read/write, issue creation, agent assignment, PR merging) |
worker/src/processor.rs | Iteration logic — reads config/state, advances task machine, triggers agents, commits results |
worker/src/types.rs | Domain types (re-exports from wreck-it-core) and webhook payload types |
cli/src/cloud_agent.rs | Cloud agent client — creates issues with labels, assigns Copilot |
cli/src/headless.rs | Headless state machine — drives the full agent lifecycle in cron mode |
cli/src/state_worktree.rs | Git worktree management — commit/push with no-change detection |
core/src/iteration.rs | Shared iteration logic used by both the worker and CLI |
Next Steps
- CI & Headless Mode — Cron-based headless operation in GitHub Actions
- Architecture — Ralph Wiggum loop internals
- Getting Started — Local installation and TUI usage