# github-webhook-server

> Automate GitHub repository management — PR workflows, CI checks, cherry-picks, and merge gates — from a single webhook server

---

Source: quickstart.md

Let me explore the repository thoroughly before writing the documentation.

Now let me check for GitHub App setup details and how tokens work:

Now let me check how GitHub tokens are required:

Now let me check the `.github-webhook-server.yaml` in repo root for more context:

Now let me check what Python version is required:

Let me check the docker-compose more carefully - the `build:` should be `image:`:

Now let me look at how the GitHub App and tokens are used in practice, and verify key configuration concepts:

Now I have enough information. Let me write the Getting Started guide.

# Getting Started

Get the webhook server running, connect it to a GitHub repository, and process your first webhook — all in under 10 minutes. Once complete, the server will automatically manage labels, check runs, and PR workflows for your repositories.

## Prerequisites

- **Python 3.13** installed (required by the project)
- [**uv**](https://docs.astral.sh/uv/) package manager installed
- A **GitHub personal access token** with `repo` and `admin:repo_hook` permissions
- A publicly accessible URL (or a tunnel like [smee.io](https://smee.io)) where GitHub can deliver webhooks

## Quick start

```bash
git clone https://github.com/myk-org/github-webhook-server.git
cd github-webhook-server
uv sync

export WEBHOOK_SERVER_DATA_DIR=/tmp/webhook-data
mkdir -p "$WEBHOOK_SERVER_DATA_DIR"
```

Create a minimal `config.yaml` in your data directory:

```yaml
github-tokens:
  - ghp_yourGitHubPersonalAccessToken

webhook-ip: https://your-domain.com/webhook_server

repositories:
  my-repo:
    name: my-org/my-repo
```

Start the server:

```bash
WEBHOOK_SERVER_DATA_DIR=/tmp/webhook-data uv run entrypoint.py
```

The server starts on `http://0.0.0.0:5000` by default. GitHub webhooks are received at `/webhook_server`.

## Step-by-step setup

### 1. Generate a GitHub token

1. Go to **Settings → Developer settings → Personal access tokens → Tokens (classic)** on GitHub.
2. Create a token with these scopes: `repo`, `admin:repo_hook`, `read:org`.
3. Copy the token — you'll need it for `config.yaml`.

> **Tip:** You can configure multiple tokens for automatic failover when one hits GitHub's rate limit. List them under `github-tokens`.

### 2. Get a webhook URL

GitHub needs a public URL to deliver events to your server. Choose one of these options:

| Method | Best for | URL format |
|--------|----------|------------|
| Public server / reverse proxy | Production | `https://your-domain.com/webhook_server` |
| [smee.io](https://smee.io) | Local development | `https://smee.io/your-channel` |
| ngrok or similar tunnel | Testing | `https://abc123.ngrok.io/webhook_server` |

Set this URL as the `webhook-ip` value in your config.

### 3. Create your configuration file

Create `config.yaml` in your data directory (`WEBHOOK_SERVER_DATA_DIR`). Here's a working example with common options:

```yaml
github-tokens:
  - ghp_yourGitHubToken1
  - ghp_yourGitHubToken2

webhook-ip: https://your-domain.com/webhook_server

default-status-checks:
  - "WIP"
  - "can-be-merged"

repositories:
  my-repo:
    name: my-org/my-repo
    verified-job: true
    pre-commit: true
    protected-branches:
      main: []
```

Key fields:

- **`github-tokens`** — one or more GitHub personal access tokens (the server picks the one with the highest remaining rate limit)
- **`webhook-ip`** — the full public URL where GitHub delivers webhook events (must include `/webhook_server` path unless using smee.io)
- **`repositories`** — each entry maps an identifier to a repository with `name` in `org/repo` format

> **Note:** On startup, the server automatically creates webhooks on each configured repository. You do not need to set up webhooks manually in GitHub.

### 4. Set the data directory

The server reads `config.yaml` from the path specified by `WEBHOOK_SERVER_DATA_DIR`. The default is `/home/podman/data` (used inside the Docker container). For local development, point it to your own directory:

```bash
export WEBHOOK_SERVER_DATA_DIR=/path/to/your/data
```

Your data directory should contain:

```
/path/to/your/data/
├── config.yaml
└── webhook-server.private-key.pem   # Only needed if using a GitHub App
```

### 5. Start the server

```bash
WEBHOOK_SERVER_DATA_DIR=/path/to/your/data uv run entrypoint.py
```

You should see log output indicating:
- Tokens are validated and rate limits checked
- Repository settings are configured
- Webhooks are created on your repositories
- The server is listening on port 5000

### 6. Verify it works

Check the health endpoint:

```bash
curl http://localhost:5000/webhook_server/healthcheck
```

You should get:

```json
{"status": 200, "message": "Alive"}
```

Now open a pull request on your configured repository. The webhook server will:

1. Receive the `pull_request` event from GitHub
2. Add size labels (e.g., `size/S`, `size/M`) based on lines changed
3. Add a branch label (e.g., `branch/main`)
4. Create WIP and can-be-merged check runs
5. Post a welcome comment on the PR

## Adding per-repository configuration

Beyond the global `config.yaml`, you can add a `.github-webhook-server.yaml` file to any repository's root. Settings in this file override the global config for that specific repository.

```yaml
# .github-webhook-server.yaml (in your repo root)
verified-job: true
pre-commit: true
conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
```

This is useful when different repositories need different CI checks, label configurations, or merge policies without changing the central `config.yaml`.

See [Configuring Repositories](configuring-repositories.html) for the full list of per-repository options.

## Advanced Usage

### Using a GitHub App instead of personal tokens

For organizations managing many repositories, a GitHub App provides better rate limits and fine-grained permissions. To use one:

1. Create a GitHub App with the required permissions (repository administration, pull requests, checks, contents).
2. Install the app on your organization or repositories.
3. Download the private key and save it as `webhook-server.private-key.pem` in your data directory.
4. Add the app ID to your `config.yaml`:

```yaml
github-app-id: 123456
github-tokens:
  - ghp_yourTokenForFallback
```

> **Note:** GitHub tokens are still required alongside the App — they serve as fallback and are used for operations the App cannot perform.

### Running with Docker

For production deployments, use the container image:

```bash
mkdir -p ./webhook_server_data_dir
cp config.yaml ./webhook_server_data_dir/
```

```yaml
# docker-compose.yaml
services:
  github-webhook-server:
    container_name: github-webhook-server
    image: ghcr.io/myk-org/github-webhook-server:latest
    volumes:
      - "./webhook_server_data_dir:/home/podman/data:Z"
    environment:
      - WEBHOOK_SERVER_PORT=5000
    ports:
      - "5000:5000"
    privileged: true
    restart: unless-stopped
```

```bash
docker compose up -d
```

See [Deploying with Docker](deploying-with-docker.html) for the complete Docker Compose reference including health checks, environment variables, and persistent storage.

### Securing webhooks

Add a webhook secret to verify that incoming requests are genuinely from GitHub:

```yaml
# In config.yaml
webhook-secret: your-random-secret-string
```

The server will reject any webhook that doesn't include a valid HMAC signature matching this secret. The secret is automatically configured on your GitHub repositories when webhooks are created at startup.

### Filtering events per repository

By default, the server subscribes to all GitHub events. To listen only to specific events for a repository:

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    events:
      - push
      - pull_request
      - pull_request_review
      - issue_comment
      - check_run
      - status
```

### Enabling the log viewer

Set the `ENABLE_LOG_SERVER` environment variable to view webhook processing logs in your browser:

```bash
ENABLE_LOG_SERVER=true WEBHOOK_SERVER_DATA_DIR=/path/to/data uv run entrypoint.py
```

Then open `http://localhost:5000/logs` in your browser.

> **Warning:** The log viewer has no authentication. Only enable it on trusted networks.

See [Using the Log Viewer](using-the-log-viewer.html) for full details.

### Multi-token failover

List multiple tokens to automatically survive GitHub API rate limits:

```yaml
github-tokens:
  - ghp_primaryToken
  - ghp_secondaryToken
  - ghp_tertiaryToken
```

The server selects the token with the highest remaining rate limit on each startup and API initialization. You can also set per-repository tokens:

```yaml
repositories:
  high-traffic-repo:
    name: my-org/high-traffic-repo
    github-tokens:
      - ghp_dedicatedToken1
      - ghp_dedicatedToken2
```

## Troubleshooting

**Server fails with "Config file not found"**
- Ensure `WEBHOOK_SERVER_DATA_DIR` points to a directory containing `config.yaml`.
- The default path is `/home/podman/data` — override it for local development.

**Server fails with "does not have `repositories`"**
- Your `config.yaml` must have at least one entry under `repositories`.

**Webhooks not arriving**
- Verify `webhook-ip` is reachable from the internet (or from GitHub's network).
- Check that the URL includes the full path (e.g., `https://example.com/webhook_server`).
- For local development, confirm your smee.io or ngrok tunnel is active.

**Token errors or "rate limit set to 60"**
- A rate limit of 60 indicates an invalid or expired token. Regenerate it in GitHub settings.
- Make sure the token has `repo` and `admin:repo_hook` scopes.

**Port already in use**
- Change the port in `config.yaml`:

```yaml
port: 8080
```

## Next steps

- [Configuring Repositories](configuring-repositories.html) — full config.yaml and per-repo YAML reference
- [Managing Pull Requests](managing-pull-requests.html) — learn PR commands like `/retest`, `/cherry-pick`, and `/approve`
- [Setting Up CI Checks](setting-up-ci-checks.html) — configure tox, pre-commit, and custom check runs
- [Deploying with Docker](deploying-with-docker.html) — production container deployment guide
- [Environment Variables](environment-variables.html) — all supported environment variables

## Related Pages

- [Configuring Repositories](configuring-repositories.html)
- [Deploying with Docker](deploying-with-docker.html)
- [Environment Variables](environment-variables.html)
- [Managing Pull Requests](managing-pull-requests.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)

---

Source: deploying-with-docker.md

Let me explore the repository to understand the Docker deployment setup.

Now let me check how config.yaml maps environment variables to config keys:

I see — these settings are actually in `config.yaml`, not env vars. Let me check the docker-compose env vars more closely to understand the mapping:

Now let me check how the docker-compose environment variables get mapped to config. It appears they may be handled through config.yaml rather than being direct env var to code mappings:

Now let me check for additional details about ports and the GitHub App PEM key:

Now let me check the e2e test setup for docker compose details:

Let me look at the image reference more closely since the docker-compose.yaml uses `build:` instead of `image:`:

I notice the `build:` field is being used with a registry URL, which is unusual for docker-compose. This appears to be `image:` in intent. Let me verify by checking if there's a different docker-compose format being used:

Now I have enough information. Let me also verify how the docker-compose uses `image:` vs `build:`:

The example docker-compose uses `build: ghcr.io/myk-org/github-webhook-server:latest` which is technically an error - this should be `image:`. I'll document this correctly using `image:` since that's the intended semantics (pulling a pre-built image from a registry).

Now I have everything I need. Let me write the documentation:

# Deploying with Docker

Run the GitHub webhook server as a Docker container with persistent configuration, health monitoring, and automatic restarts — the recommended way to deploy for production use.

## Prerequisites

- Docker Engine 20.10+ and Docker Compose V2 installed
- A GitHub personal access token (classic or fine-grained) with repository access
- A `config.yaml` file with at least one repository configured (see [Configuring Repositories](configuring-repositories.html))
- Network access from GitHub to your server on port 5000 (direct or via reverse proxy)

## Quick Start

1. Create a data directory and add your configuration:

```bash
mkdir -p webhook_server_data_dir
```

2. Create a minimal `webhook_server_data_dir/config.yaml`:

```yaml
github-tokens:
  - ghp_your_token_here

webhook-ip: https://your-domain.com/webhook_server

repositories:
  my-repo:
    name: my-org/my-repository
```

3. Create a `docker-compose.yaml`:

```yaml
services:
  github-webhook-server:
    container_name: github-webhook-server
    image: ghcr.io/myk-org/github-webhook-server:latest
    volumes:
      - "./webhook_server_data_dir:/home/podman/data:Z"
      - "/tmp/podman-storage-${USER:-1000}:/tmp/storage-run-1000"
    ports:
      - "5000:5000"
    privileged: true
    restart: unless-stopped
```

4. Start the server:

```bash
docker compose up -d
```

5. Verify it's running:

```bash
curl http://localhost:5000/webhook_server/healthcheck
```

You should see `{"status": 200, "message": "Alive"}`.

## Step-by-Step Setup

### 1. Prepare the Data Directory

The container expects your configuration files at `/home/podman/data` inside the container. Mount a local directory to this path.

Your data directory should contain:

- **`config.yaml`** (required) — server and repository configuration
- **`webhook-server.private-key.pem`** (optional) — GitHub App private key, only needed if using a GitHub App instead of personal access tokens

```
webhook_server_data_dir/
├── config.yaml
└── webhook-server.private-key.pem   # optional
```

> **Note:** Log files are also written to this directory. Make sure the directory is writable by the container user (UID 1000 by default).

### 2. Configure docker-compose.yaml

Here is the full production-ready `docker-compose.yaml` with all available options:

```yaml
services:
  github-webhook-server:
    container_name: github-webhook-server
    image: ghcr.io/myk-org/github-webhook-server:latest
    volumes:
      - "./webhook_server_data_dir:/home/podman/data:Z"
      # Mount temporary directories to prevent boot ID mismatch issues
      - "/tmp/podman-storage-${USER:-1000}:/tmp/storage-run-1000"
      # Mount Google Cloud credentials for Vertex AI (optional)
      # - $HOME/.config/gcloud:/home/podman/.config/gcloud:ro
    environment:
      - TZ=UTC
      - ENABLE_LOG_SERVER=true
      - ENABLE_MCP_SERVER=false
    ports:
      - "5000:5000"
    privileged: true
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:5000/webhook_server/healthcheck && curl -f http://localhost:${SIDECAR_PORT:-9100}/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    restart: unless-stopped
```

> **Warning:** The container requires `privileged: true` because it uses Podman inside the container to build and push container images for your repositories. If you don't use container build features, the server still requires this flag for Podman runtime initialization.

### 3. Configure Volumes

Two volume mounts are important:

| Volume | Container Path | Purpose |
|--------|---------------|---------|
| Data directory | `/home/podman/data` | Config, logs, and GitHub App private key |
| Podman temp storage | `/tmp/storage-run-1000` | Prevents Podman boot ID mismatch issues on container restart |

The `:Z` suffix on the data volume sets the correct SELinux context. Omit it if you're not using SELinux.

### 4. Set Environment Variables

Configure the server behavior through environment variables in the `environment` section:

| Variable | Default | Description |
|----------|---------|-------------|
| `TZ` | System default | Timezone for log timestamps (e.g., `UTC`, `America/New_York`) |
| `ENABLE_LOG_SERVER` | `false` | Enable the built-in log viewer web UI and API |
| `ENABLE_MCP_SERVER` | `false` | Enable the MCP server for AI agent integration |
| `SIDECAR_PORT` | `9100` | Port for the AI sidecar service |

> **Tip:** Server settings like `port`, `ip-bind`, `max-workers`, `webhook-secret`, `verify-github-ips`, and `verify-cloudflare-ips` are configured in `config.yaml`, not as environment variables. See [Configuration Reference](configuration-reference.html) for all options.

For a complete reference of all environment variables, see [Environment Variables](environment-variables.html).

### 5. Expose Ports

The container exposes three ports:

| Port | Service | Expose Externally? |
|------|---------|-------------------|
| 5000 | Webhook server (main API) | Yes — GitHub sends webhooks here |
| 5001 | Internal tool server | No — binds to 127.0.0.1 inside the container |
| 9100 | AI sidecar | No — internal only |

Only port 5000 needs to be published. The other services are internal to the container.

```yaml
ports:
  - "5000:5000"
```

To use a different host port:

```yaml
ports:
  - "8080:5000"
```

### 6. Configure Health Checks

The built-in health check verifies both the main webhook server and the AI sidecar are responding:

```yaml
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:5000/webhook_server/healthcheck && curl -f http://localhost:${SIDECAR_PORT:-9100}/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 30s
```

The `start_period` gives the server 30 seconds to initialize before health checks begin failing. Increase this if your server manages many repositories and takes longer to start.

Check container health status:

```bash
docker inspect --format='{{.State.Health.Status}}' github-webhook-server
```

### 7. Start and Verify

```bash
# Start in the background
docker compose up -d

# Watch the logs
docker compose logs -f

# Check health status
docker compose ps
```

The `HEALTHY` status in `docker compose ps` confirms both services are running.

## Advanced Usage

### Configuring Webhook Security

Add these settings to your `config.yaml` to verify that incoming webhooks are genuinely from GitHub:

```yaml
webhook-secret: your-secret-here
verify-github-ips: true
verify-cloudflare-ips: true  # if behind Cloudflare
```

The `webhook-secret` must match the secret configured in your GitHub webhook settings. IP verification fetches GitHub's published IP ranges at startup and rejects requests from other sources.

> **Warning:** If IP verification is enabled but the server cannot reach the GitHub or Cloudflare API at startup, it will refuse to start rather than run in an insecure state.

### Tuning Worker Count

Control concurrency by setting `max-workers` in `config.yaml`:

```yaml
max-workers: 50
```

The default is 10 workers. Increase this for servers handling many repositories or high webhook volume.

### Enabling the Log Viewer

Set `ENABLE_LOG_SERVER=true` in your environment to activate the built-in web UI for browsing webhook processing logs:

```yaml
environment:
  - ENABLE_LOG_SERVER=true
```

> **Warning:** The log viewer endpoints are unauthenticated. Only deploy with log server enabled on trusted networks (VPN, internal network). Access is restricted to private/loopback IP ranges by default.

See [Using the Log Viewer](using-the-log-viewer.html) for details.

### Enabling AI Features

To use AI-powered features (conventional title suggestions, cherry-pick conflict resolution), provide API keys as environment variables:

```yaml
environment:
  - ANTHROPIC_API_KEY=sk-ant-xxx       # For Claude Code
  - GEMINI_API_KEY=xxx                  # For Gemini CLI
  # - CURSOR_API_KEY=xxx                # For Cursor Agent
  # - SIDECAR_PORT=9100                 # AI sidecar port (default: 9100)
```

You also need to configure the `ai-features` section in your `config.yaml`. See [Enabling AI Features](enabling-ai-features.html) for setup details.

### Mounting Google Cloud Credentials

If using Vertex AI for AI features, mount your Google Cloud credentials read-only:

```yaml
volumes:
  - "./webhook_server_data_dir:/home/podman/data:Z"
  - "/tmp/podman-storage-${USER:-1000}:/tmp/storage-run-1000"
  - "$HOME/.config/gcloud:/home/podman/.config/gcloud:ro"
```

### Updating the Container

```bash
# Pull the latest image
docker compose pull

# Recreate the container with the new image
docker compose up -d
```

Your configuration and logs persist in the mounted data directory, so updates are safe.

## Troubleshooting

### Container exits immediately

Check the logs for configuration errors:

```bash
docker compose logs
```

The most common cause is a missing or invalid `config.yaml`. The server requires at least a `repositories` section with one entry.

### Health check fails

If `docker compose ps` shows `unhealthy`:

```bash
# Check which health check is failing
docker exec github-webhook-server curl -f http://localhost:5000/webhook_server/healthcheck
docker exec github-webhook-server curl -f http://localhost:9100/health
```

The main server health check failing usually means a startup error. The sidecar health check failing means AI features won't work but doesn't affect core webhook processing.

### Podman boot ID mismatch

If you see errors about Podman boot ID mismatches after container restarts, ensure the temp storage volume is mounted:

```yaml
volumes:
  - "/tmp/podman-storage-${USER:-1000}:/tmp/storage-run-1000"
```

The container runs an automatic cleanup script on startup to clear stale Podman runtime state.

### Permission denied on data directory

The container runs as UID 1000. Ensure your data directory is owned by this user:

```bash
chown -R 1000:1000 webhook_server_data_dir/
```

## Related Pages

- [Getting Started](quickstart.html)
- [Environment Variables](environment-variables.html)
- [Configuring Repositories](configuring-repositories.html)
- [Using the Log Viewer](using-the-log-viewer.html)
- [Enabling AI Features](enabling-ai-features.html)

---

Source: configuring-repositories.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me look at what settings can go in the `.github-webhook-server.yaml` file (the per-repo override file), and the config precedence:

Now let me look at how the config file is structured and what a minimal config looks like:

Now I have a comprehensive understanding of the configuration. Let me also check what settings can go into the per-repo `.github-webhook-server.yaml` file:

Now I have all the information I need. Let me write the documentation page.

# Configuring Repositories

Set up your webhook server to manage one or more GitHub repositories by defining global defaults and per-repository overrides in `config.yaml`, with optional in-repo settings in `.github-webhook-server.yaml`.

## Prerequisites

- A running webhook server instance (see [Getting Started](quickstart.html))
- At least one GitHub personal access token or GitHub App
- The `WEBHOOK_SERVER_DATA_DIR` environment variable pointing to your data directory (default: `/home/podman/data`)

## Quick Example

Create a `config.yaml` in your data directory with one repository:

```yaml
github-app-id: 123456
github-tokens:
  - ghp_your_token_here

webhook-ip: https://your-domain.com/webhook_server

repositories:
  my-repo:
    name: my-org/my-repo
```

That's it — the server will start processing webhooks for `my-org/my-repo` using all default settings.

## Step 1: Set Up Global Settings

Global settings in `config.yaml` apply to all repositories unless overridden. Place these at the top level of the file.

```yaml
log-level: INFO
log-file: webhook-server.log
mask-sensitive-data: true

github-app-id: 123456
github-tokens:
  - ghp_token_one
  - ghp_token_two

webhook-ip: https://your-domain.com/webhook_server
webhook-secret: your_webhook_secret

default-status-checks:
  - "WIP"
  - "dpulls"
  - "can-be-merged"

auto-verified-and-merged-users:
  - "renovate[bot]"
  - "pre-commit-ci[bot]"
```

> **Tip:** Provide multiple tokens in `github-tokens` for automatic failover — the server picks the token with the highest remaining API rate limit.

## Step 2: Add Repositories

Each repository lives under the `repositories` key. The key is a short name you choose; the `name` field must be the full `org/repo` format.

```yaml
repositories:
  my-app:
    name: my-org/my-app

  my-library:
    name: my-org/my-library
```

> **Warning:** A repository **must** have a `name` field in `org/repo` format. Without it, the server cannot locate the repository on GitHub.

## Step 3: Configure Repository-Specific Settings

Override any global setting at the repository level. Repository settings take precedence over global defaults.

```yaml
repositories:
  my-app:
    name: my-org/my-app
    log-level: DEBUG
    log-file: my-app.log
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url

    github-tokens:
      - ghp_repo_specific_token

    events:
      - push
      - pull_request
      - pull_request_review
      - issue_comment
      - check_run
      - status

    verified-job: true
    pre-commit: true
    create-issue-for-new-pr: true
    minimum-lgtm: 1
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"

    auto-verified-and-merged-users:
      - "renovate[bot]"

    default-status-checks:
      - "WIP"
      - "can-be-merged"
      - "ci/my-external-check"

    can-be-merged-required-labels:
      - qa-approved
```

### Filtering Webhook Events

By default, the server listens to all events (`*`). Use the `events` key to listen only to specific events:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    events:
      - push
      - pull_request
      - issue_comment
```

Omit the `events` key entirely to receive all events.

## Step 4: Use Per-Repository In-Repo Config (Optional)

For settings that repository maintainers should control themselves, add a `.github-webhook-server.yaml` file to the root of the GitHub repository. This file uses the same keys as the repository section in `config.yaml`.

```yaml
# .github-webhook-server.yaml (in the root of your GitHub repo)
pre-commit: true
conventional-title: "feat,fix,docs"
minimum-lgtm: 2
create-issue-for-new-pr: false
```

> **Note:** The `.github-webhook-server.yaml` file is read from the repository's default branch on every webhook event. Changes take effect immediately without restarting the server.

### Config Resolution Order

Settings are resolved in this order, with earlier sources taking priority:

| Priority | Source | Location |
|----------|--------|----------|
| 1 (highest) | `.github-webhook-server.yaml` | In the GitHub repository |
| 2 | Repository section in `config.yaml` | `repositories.<name>.*` |
| 3 (lowest) | Global section in `config.yaml` | Top-level keys |

For example, if `minimum-lgtm` is set to `2` in `.github-webhook-server.yaml`, `1` in the repository config, and `0` globally — the value `2` is used.

## Step 5: Customize the PR Welcome Message (Optional)

Add custom information to the bottom of the welcome message posted on new PRs. You can set this at any config level:

```yaml
# In config.yaml (global or per-repository)
welcome-extra-info: |
  **Note:** Please review the contribution guide before merging.
  - Ensure tests pass
  - Update documentation if needed
```

Alternatively, create a `.github-webhook-server-welcome-message.md` file in the repository root. This file takes the highest priority for welcome message content and supports full Markdown.

> **Note:** The welcome message file and `welcome-extra-info` value are each limited to 10 KB.

## Advanced Usage

### Multiple Token Failover

Supply multiple GitHub tokens for automatic failover. The server selects the token with the highest remaining API rate limit on each webhook event:

```yaml
# Global tokens (used by all repositories)
github-tokens:
  - ghp_primary_token
  - ghp_backup_token

repositories:
  critical-repo:
    name: my-org/critical-repo
    # Override with repo-specific tokens
    github-tokens:
      - ghp_dedicated_token_1
      - ghp_dedicated_token_2
```

### Setting Up CI: Tox and Pre-Commit

Configure tox test environments per branch and enable pre-commit checks:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    pre-commit: true
    tox:
      python-version: "3.12"
      args: "-p -v"
      main: all
      dev: testenv1,testenv2
```

See [Setting Up CI Checks](setting-up-ci-checks.html) for full details on tox, pre-commit, container builds, and custom check runs.

### Protected Branches

Define which status checks are required for protected branches:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    protected-branches:
      main:
        include-runs:
          - "pre-commit.ci - pr"
          - "WIP"
        exclude-runs:
          - "SonarCloud Code Analysis"
      dev: []   # all default checks
```

Use an empty array (`[]`) to apply all default status checks to a branch. Use `include-runs` and `exclude-runs` for fine-grained control.

See [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html) for more on branch protection rules and OWNERS files.

### Branch Protection Rules

Configure GitHub branch protection settings that the server manages:

```yaml
branch-protection:
  strict: true
  require_code_owner_reviews: true
  dismiss_stale_reviews: false
  required_approving_review_count: 1
  required_linear_history: true
  required_conversation_resolution: true
```

These can be set globally or per-repository.

### Auto-Merge Configuration

Automatically merge PRs on specific branches when all checks pass:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    set-auto-merge-prs:
      - main
    auto-verified-and-merged-users:
      - "renovate[bot]"
    auto-verify-cherry-picked-prs: true
```

### Commands on Draft PRs

By default, PR comment commands are blocked on draft PRs. Configure exceptions:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    # Allow only specific commands on drafts
    allow-commands-on-draft-prs:
      - build-and-push-container
      - retest
```

Set to an empty list (`[]`) to allow all commands on draft PRs. Omit the key entirely to block all commands on drafts.

### Container Builds

Configure container image builds triggered by PR events or releases:

```yaml
repositories:
  my-app:
    name: my-org/my-app
    container:
      username: registry_user
      password: registry_password
      repository: registry.example.com/my-org/my-app
      tag: latest
      release: true
      context: src
      build-args:
        - MY_ARG=value
      args:
        - --format docker
```

See [Setting Up CI Checks](setting-up-ci-checks.html) for container build details and OCI annotations.

### Docker Registry Credentials

For pulling base images from Docker Hub during builds, set global Docker credentials:

```yaml
docker:
  username: your_docker_username
  password: your_docker_password
```

### Labels and PR Size Thresholds

Customize which label categories are active and define custom PR size categories:

```yaml
labels:
  enabled-labels:
    - verified
    - size
    - can-be-merged
  colors:
    verified: green
    hold: red

pr-size-thresholds:
  Tiny:
    threshold: 10
    color: lightgray
  Small:
    threshold: 50
    color: green
  Large:
    threshold: 300
    color: red
  Massive:
    threshold: inf
    color: darkred
```

Both can be set globally or per-repository. See [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html) for full details.

### Security Checks

Enable detection of suspicious file paths and committer identity mismatches:

```yaml
security-checks:
  mandatory: true
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
    - ".vscode/"
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"
```

See [Enabling Security Checks](enabling-security-checks.html) for details.

### AI Features

Enable AI-powered conventional title suggestions, cherry-pick conflict resolution, and test oracle integration:

```yaml
ai-features:
  ai-provider: claude
  ai-model: sonnet
  conventional-title:
    enabled: true
    mode: suggest
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
```

See [Enabling AI Features](enabling-ai-features.html) for setup instructions.

### PyPI Publishing

Configure automatic PyPI publishing on release:

```yaml
repositories:
  my-library:
    name: my-org/my-library
    pypi:
      token: pypi-your-token-here
```

### Schema Validation

Use the YAML language server schema reference at the top of your `config.yaml` for editor autocompletion and validation:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/myk-org/github-webhook-server/refs/heads/main/webhook_server/config/schema.yaml
```

See [Configuration Reference](configuration-reference.html) for a complete list of every available option.

## Troubleshooting

**Server won't start — "Config file not found"**
- Ensure `config.yaml` exists in the directory specified by `WEBHOOK_SERVER_DATA_DIR` (default: `/home/podman/data`). See [Environment Variables](environment-variables.html).

**Server won't start — "does not have `repositories`"**
- Your `config.yaml` must include a `repositories` section with at least one repository entry.

**Repository not processing webhooks**
- Verify the `name` field uses `org/repo` format (e.g., `my-org/my-repo`, not just `my-repo`).
- Check that your GitHub token has access to the repository.
- If using `events`, confirm the relevant event type is listed.

**Changes to `config.yaml` not taking effect**
- The `config.yaml` is re-read on every webhook event — changes take effect on the next event without restarting the server. Settings like `log-level` and `log-file` also apply immediately.

**`.github-webhook-server.yaml` not being picked up**
- The file must be in the repository root on the default branch.
- Ensure it contains valid YAML. Invalid YAML will cause the server to fall back to `config.yaml` settings.

**"Invalid label categories in enabled-labels"**
- Only these categories are valid: `verified`, `hold`, `wip`, `needs-rebase`, `has-conflicts`, `can-be-merged`, `size`, `branch`, `cherry-pick`, `automerge`. Reviewed-by labels are always enabled.

## Related Pages

- [Configuration Reference](configuration-reference.html)
- [Getting Started](quickstart.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html)
- [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html)

---

Source: managing-pull-requests.md

Let me explore the repository structure and knowledge graph first.

Now let me look at the constants to understand the command strings:

Now let me look at the OWNERS file handling, the welcome message, and how permissions work:

Let me look at the rebase and welcome message functionality:

Now let me look at the welcome message to understand what users see:

Now let me check the cherry-pick and automerge sections:

Now let me look at how the `allow-commands-on-draft-prs` is configured:

Now let me look at the examples config to see PR-related settings:

Now let me check the process_command_reprocess and check_if_can_be_merged:

Let me also check the `run_retests` method to understand how retests work:

Now let me check the OWNERS file format example:

Now let me also check how `lgtm` and `approve` label commands work — the label_by_user_comment method:

Let me also check what the `current_pull_request_supported_retest` includes:

Now I have all the information I need to write the documentation. Let me compile everything into the page:

# Managing Pull Requests

Control your entire PR lifecycle — from requesting reviews and running tests to cherry-picking and merging — by posting slash commands as comments on your pull requests.

## Prerequisites

- The webhook server is running and receiving events from your repository. See [Getting Started](quickstart.html).
- Your repository is configured in `config.yaml`. See [Configuring Repositories](configuring-repositories.html).
- An `OWNERS` file exists in your repository root with approvers and reviewers defined.

## Quick Example

Post any of these as a comment on a pull request:

```
/retest tox
/cherry-pick v1.0
/approve
/rebase
/automerge
/assign-reviewers
```

The server reacts to your comment with a 👍 and processes the command immediately.

## How Commands Work

When you open a PR, the webhook server posts a **welcome comment** listing all available commands and their configuration for that repository. Use it as a quick reference.

Commands are entered as PR comments. Each line starting with `/` is treated as a separate command — you can run multiple commands in a single comment:

```
/approve
/cherry-pick release-1.0 release-2.0
```

### Who Can Run Commands

Commands require appropriate permissions. The server checks whether you are a:

- **Repository collaborator or contributor** — can run most commands
- **Approver** (listed in `OWNERS` file) — required for `/approve`, `/hold`, `/automerge`
- **Maintainer** (admin or maintain permission on the repo) — required for `/security-override`, `/rebase` on other users' PRs

If you lack permission, the server posts a comment explaining who can grant access. A maintainer or approver can authorize you by commenting:

```
/add-allowed-user @your-username
```

## Command Reference

### Review and Approval

| Command | Description | Who can use it |
|---------|-------------|----------------|
| `/lgtm` | Mark the PR as "looks good to me" — adds an `lgtm-<username>` label | Any authorized user (except the PR owner) |
| `/lgtm cancel` | Remove your LGTM | The user who gave the LGTM |
| `/approve` | Approve the PR — adds an `approved-<username>` label | Approvers only (from OWNERS file) |
| `/approve cancel` | Remove your approval | The approver who approved |

> **Note:** `/lgtm` and `/approve` are different. A PR may require a minimum number of LGTMs *and* at least one approval before it can merge. See [Configuring Repositories](configuring-repositories.html) for the `minimum-lgtm` setting.

### Testing and Retesting

Rerun checks that have failed or need refreshing:

```
/retest tox
```

| Command | Description |
|---------|-------------|
| `/retest tox` | Rerun the tox test suite |
| `/retest pre-commit` | Rerun pre-commit hooks |
| `/retest build-container` | Rebuild the container image |
| `/retest python-module-install` | Retest Python package installation |
| `/retest conventional-title` | Revalidate the PR title format |
| `/retest security-suspicious-paths` | Rerun the suspicious paths check |
| `/retest security-committer-identity` | Rerun the committer identity check |
| `/retest <custom-check-name>` | Rerun a custom check run |
| `/retest all` | Rerun all configured checks |

> **Tip:** Only checks that are configured for your repository will appear in the welcome comment. The server tells you if a requested test is not configured.

You can retest multiple specific checks at once:

```
/retest tox pre-commit
```

> **Warning:** `/retest all` cannot be combined with individual test names. Use one or the other.

For details on configuring which checks are available, see [Setting Up CI Checks](setting-up-ci-checks.html).

### Cherry-Picking

Schedule or execute cherry-picks to other branches:

```
/cherry-pick v1.0
```

**On an open (unmerged) PR:**
- Adds `cherry-pick-v1.0` labels to the PR
- When the PR merges, the server automatically cherry-picks to those branches

**On a merged PR:**
- Executes the cherry-pick immediately
- Creates a new PR targeting the specified branch

Cherry-pick to multiple branches at once:

```
/cherry-pick release-1.0 release-2.0 release-3.0
```

If a cherry-pick fails (e.g., due to conflicts), the server posts a comment with manual cherry-pick instructions. If AI conflict resolution is configured, it attempts to resolve conflicts automatically. See [Enabling AI Features](enabling-ai-features.html).

#### Retrying a Failed Cherry-Pick

If a cherry-pick failed or the resulting PR has issues, use:

```
/cherry-pick-retry release-1.0
```

This command:
1. Validates the PR is merged and has the `cherry-pick-release-1.0` label
2. Closes the existing failed cherry-pick PR (if one exists)
3. Reruns the cherry-pick to create a new PR

> **Note:** `/cherry-pick-retry` accepts exactly one branch name and only works on merged PRs. Use `/cherry-pick` for new cherry-pick requests.

For more on cherry-pick configuration and branch protection, see [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html).

### Rebasing

Rebase your PR branch onto its base branch:

```
/rebase
```

The server fetches the latest base branch, rebases your PR's head branch, and force-pushes the result.

**Permission rules for rebase:**

| PR type | Who can rebase |
|---------|---------------|
| Your own PR | You or any maintainer |
| Another user's PR | Maintainers only |
| Bot-created PR (e.g., cherry-pick) | The PR assignee or maintainers |

> **Warning:** `/rebase` is not supported for fork PRs — the head branch must be in the same repository.

### Automerge

Enable automatic merging once all requirements are met:

```
/automerge
```

Only maintainers and approvers can set a PR to automerge. The PR merges automatically when:

1. At least one `/approve` from an approver
2. The required minimum number of `/lgtm` reviews (if configured)
3. All required status checks pass
4. No blocking labels (`wip`, `hold`, `has-conflicts`)
5. The `verified` label is present (if verification is required)

### PR Status Labels

| Command | Effect |
|---------|--------|
| `/wip` | Marks the PR as work-in-progress — adds the `wip` label and prepends `WIP:` to the title |
| `/wip cancel` | Removes WIP status and the title prefix |
| `/hold` | Blocks merging — approvers only |
| `/hold cancel` | Unblocks merging |
| `/verified` | Marks the PR as verified — also updates the `verified` check run |
| `/verified cancel` | Removes verification status and resets the check run |

> **Tip:** The `verified` label is automatically removed when new commits are pushed, unless the server detects the push was a clean rebase (same diff, just rebased onto the latest base).

### Reviewer Assignment

| Command | Description |
|---------|-------------|
| `/assign-reviewers` | Assigns all reviewers defined in the OWNERS file for the changed files |
| `/assign-reviewer @username` | Assigns a specific user as reviewer (must be a repository collaborator) |

### Container Builds

If container builds are configured for your repository:

```
/build-and-push-container
```

Build and push a container image tagged with the PR number. You can pass additional build arguments:

```
/build-and-push-container --build-arg KEY=value
```

See [Setting Up CI Checks](setting-up-ci-checks.html) for container build configuration.

### Other Commands

| Command | Description |
|---------|-------------|
| `/reprocess` | Reruns the entire PR workflow from scratch (useful if a webhook was missed or configuration changed) |
| `/regenerate-welcome` | Regenerates the welcome comment (useful after config changes) |
| `/check-can-merge` | Manually checks whether the PR meets all merge requirements |
| `/test-oracle` | Triggers AI-powered test recommendation analysis (see [Enabling AI Features](enabling-ai-features.html)) |

### Security Override

When security checks are configured as mandatory and are blocking your PR:

```
/security-override
```

This sets all security check runs to pass. Only **maintainers** can use this command.

To re-enable security checks after an override:

```
/security-override cancel
```

See [Enabling Security Checks](enabling-security-checks.html) for full details on security configuration.

## Advanced Usage

### Canceling Any Label Command

Append `cancel` to any label command to remove it:

```
/hold cancel
/wip cancel
/verified cancel
```

### Commands on Draft PRs

By default, all commands are blocked on draft PRs (except `/test-oracle`). You can configure which commands are allowed:

```yaml
# Allow all commands on draft PRs
allow-commands-on-draft-prs: []

# Allow only specific commands
allow-commands-on-draft-prs:
  - build-and-push-container
  - retest
```

This setting can be configured globally or per repository. If a blocked command is used on a draft PR, the server posts a comment listing which commands are allowed.

### Running Multiple Commands

You can combine multiple commands in a single comment — each `/` line is processed in parallel:

```
/approve
/cherry-pick release-1.0
/automerge
```

### Cherry-Pick Duplicate Prevention

The server tracks cherry-picks with labels. If a `cherry-pick-<branch>` label already exists on the PR, that branch is skipped. To re-trigger a cherry-pick:

1. Remove the `cherry-pick-<branch>` label from the PR
2. Run `/cherry-pick <branch>` again

Or use `/cherry-pick-retry <branch>` on merged PRs, which handles cleanup automatically.

## Troubleshooting

**Command is ignored (no reaction emoji)**
- The comment must start with `/` at the beginning of a line
- The server only processes `created` comment actions — edited or deleted comments are ignored
- Check that the webhook server is receiving `issue_comment` events. See [Configuring Repositories](configuring-repositories.html).

**"Not allowed to run commands" response**
- You need to be a collaborator, contributor, or listed in the OWNERS file
- Ask a maintainer to comment `/add-allowed-user @your-username` on the PR

**"/retest <name>" says the test is not configured**
- Only checks enabled in your repository configuration can be retested. Check the welcome comment for the list of available retests.
- See [Setting Up CI Checks](setting-up-ci-checks.html) for how to configure checks.

**Cherry-pick fails with conflicts**
- The server posts manual cherry-pick instructions in a comment
- If AI features are configured, the server attempts automatic conflict resolution first. See [Enabling AI Features](enabling-ai-features.html).
- Use `/cherry-pick-retry <branch>` after fixing the issue

**"/rebase" rejected on a fork PR**
- Rebase only works when the PR's head branch is in the same repository. Fork PRs must be rebased locally.

For copy-paste command examples covering common workflows, see [PR Comment Command Recipes](pr-command-recipes.html).

## Related Pages

- [PR Comment Command Recipes](pr-command-recipes.html)
- [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Enabling AI Features](enabling-ai-features.html)
- [Configuring Repositories](configuring-repositories.html)

---

Source: setting-up-ci-checks.md

Let me explore the repository to understand the CI checks configuration.

Now let me look at the custom check runs feature more closely and see how the pull request handler integrates with the runner:

Now let me check the constants file to understand the check run names:

Now let me check the `.github-webhook-server.yaml` per-repo file capability:

Now I have enough information. Let me compile the documentation page.

# Setting Up CI Checks

Automate code quality and compliance checks on every pull request by configuring tox tests, pre-commit hooks, container builds, Python module installs, conventional commit validation, and custom check runs in your webhook server configuration.

## Prerequisites

- A running github-webhook-server instance (see [Getting Started](quickstart.html))
- At least one repository configured in `config.yaml` (see [Configuring Repositories](configuring-repositories.html))
- For container builds: `podman` installed on the server and registry credentials configured

## Quick Example

Add CI checks to a repository by editing the `repositories` section in `config.yaml`:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    tox:
      main: all
    pre-commit: true
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
```

With this config, every PR targeting `main` will automatically run tox tests, pre-commit hooks, and conventional commit title validation. Results appear as GitHub check runs on the PR.

## Configuring Tox

Tox runs your project's test suite using `uvx tox`. Configure it per branch:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    tox:
      main: all                      # Run all tox envs for PRs targeting main
      dev: testenv1,testenv2         # Run specific envs for PRs targeting dev
      args: "-p -v"                  # Extra CLI arguments (optional)
      python-version: "3.11"         # Python version for tox (optional)
```

- Set a branch name to `all` to run all environments defined in `tox.ini`.
- Specify a comma-separated list to run only those environments (e.g., `testenv1,testenv2`).
- The `args` key passes extra CLI arguments directly to tox (e.g., `"-p -v"` for parallel verbose runs).

> **Note:** The `tox-python-version` key at the repository level is deprecated. Use `python-version` nested under `tox` instead.

## Configuring Pre-commit

Enable pre-commit to run all hooks defined in your repository's `.pre-commit-config.yaml`:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    pre-commit: true
```

When enabled, the server runs `pre-commit run --all-files` in the PR worktree. A `.pre-commit-config.yaml` file must exist in the repository root.

## Configuring Container Builds

Build and optionally push container images on every PR:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    container:
      username: myuser
      password: my-registry-password
      repository: quay.io/myorg/myapp
      tag: latest
      release: true                  # Push on new release with release tag
      build-args:
        - my-build-arg1=value1
        - my-build-arg2=value2
      args:                          # Additional podman build arguments
        - --format docker
      context: src                   # Subdirectory as build context (default: repo root)
```

- During a PR, the server builds the container but does **not** push it. The check run reports build success or failure.
- On merge or release (when `release: true`), the image is built and pushed to the registry.
- Use the `/build-and-push-container` comment command to manually trigger a build and push. See [Managing Pull Requests](managing-pull-requests.html) for details.

> **Warning:** Container credentials are stored in `config.yaml`. Protect this file and consider using a secrets manager. Never commit credentials to version control.

## Configuring Python Module Installs

If your repository publishes to PyPI, enable the Python module install check to verify your package builds correctly:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    pypi:
      token: pypi-your-token-here
```

When a `pypi` configuration is present, the server runs `pip wheel` against the PR worktree to validate the package builds. The PyPI token is used for publishing on release — the install check itself does not upload anything.

## Configuring Conventional Commit Validation

Enforce [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) formatting on PR titles:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
```

This validates that every PR title follows the format `<type>[optional scope]: <description>`.

**Common configurations:**

| Config value | Behavior |
|---|---|
| `"feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"` | Allow only standard types |
| `"feat,fix,hotfix,release"` | Allow standard + custom types |
| `"*"` | Accept any type while enforcing the overall format |

Valid PR title examples:
- `feat: add user authentication`
- `fix(api): handle edge case`
- `feat!: breaking change`
- `docs: update installation guide`

> **Tip:** When combined with AI features, the server can suggest or auto-fix invalid titles. See [Enabling AI Features](enabling-ai-features.html) for setup instructions.

## Configuring Custom Check Runs

Define your own checks that run arbitrary commands on every PR:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    custom-check-runs:
      - name: lint
        command: uv tool run --from ruff ruff check
        mandatory: true

      - name: security-scan
        command: uv tool run --from bandit bandit -r .
        mandatory: false

      - name: complex-check
        command: |
          uv run python -c "
          import sys
          print('Running complex check')
          sys.exit(0)
          "
```

Each custom check requires:

- **`name`** — Unique name displayed in the GitHub check run UI. Must contain only alphanumeric characters, dots, underscores, or hyphens (max 64 characters).
- **`command`** — Shell command to execute. Runs in the repository worktree directory. Environment variables can be included inline (e.g., `TOKEN=xyz command args`).
- **`mandatory`** (optional, default: `true`) — When `true`, the check must pass for the PR to be marked `can-be-merged`. Set to `false` for advisory checks.

**Validation rules:**

- The command's executable must be available on the server. If not found, the check is silently skipped with a log warning.
- Custom check names cannot collide with built-in check names (`tox`, `pre-commit`, `build-container`, `python-module-install`, `conventional-title`, `can-be-merged`, `security-suspicious-paths`, `security-committer-identity`).
- Duplicate names are rejected — only the first occurrence is used.

## Retesting Failed Checks

When a check fails, you can re-run it from a PR comment:

```
/retest tox
/retest pre-commit
/retest build-container
/retest python-module-install
/retest conventional-title
/retest lint
/retest all
```

The `/retest <name>` command works for all built-in and custom checks. Use `/retest all` to re-run every configured check. See [PR Comment Command Recipes](pr-command-recipes.html) for more examples.

## How Checks Affect Mergeability

The `can-be-merged` label and check run are determined by whether all **mandatory** checks pass:

| Check type | Mandatory by default? |
|---|---|
| tox | Yes |
| pre-commit | No (runs but doesn't block) |
| build-container | Yes |
| python-module-install | Yes |
| conventional-title | Yes |
| Custom check (`mandatory: true`) | Yes |
| Custom check (`mandatory: false`) | No |

You can also add external status checks to the required list using `default-status-checks`:

```yaml
# Global level
default-status-checks:
  - "WIP"
  - "can-be-merged"
  - "ci/my-external-check"

# Or per repository
repositories:
  my-repository:
    name: my-org/my-repository
    default-status-checks:
      - "WIP"
      - "can-be-merged"
      - "ci/my-external-check"
```

## Advanced Usage

### Per-Repo Configuration via `.github-webhook-server.yaml`

Instead of editing the server's `config.yaml`, repository maintainers can add a `.github-webhook-server.yaml` file to the repository root. Settings in this file override the corresponding values in `config.yaml`.

```yaml
# .github-webhook-server.yaml (in repository root)
tox:
  main: all
  args: "--parallel"
  python-version: "3.12"
pre-commit: true
conventional-title: "feat,fix,docs"
custom-check-runs:
  - name: typecheck
    command: uv tool run --from mypy mypy src/
```

See [Configuring Repositories](configuring-repositories.html) for the full precedence rules.

### Container Build Context and OCI Annotations

For monorepos or projects where the Dockerfile is not at the root, set a subdirectory as the build context:

```yaml
container:
  username: myuser
  password: my-registry-password
  repository: quay.io/myorg/myapp
  tag: latest
  context: src    # Build from <repo>/src/ instead of repo root
```

> **Note:** The `context` value must be a relative path within the repository. It cannot escape the repository root — attempts to traverse above it are rejected for security.

Add OCI-standard metadata annotations to built images:

```yaml
container:
  username: myuser
  password: my-registry-password
  repository: quay.io/myorg/myapp
  tag: latest
  oci-annotations:
    enabled: true
    static:
      org.opencontainers.image.vendor: "My Organization"
      org.opencontainers.image.licenses: "Apache-2.0"
    auto:
      created: true     # Build timestamp
      source: true      # Repository URL
      revision: true    # Commit SHA
      version: true     # Tag on release builds
      title: true       # Repository name
```

### Tox with Branch-Specific Test Environments

Run different test environments depending on the PR's target branch:

```yaml
tox:
  main: all                          # Full suite for main
  dev: unit,integration              # Only unit + integration for dev
  release-1.0: unit                  # Minimal tests for release branch
  args: "--parallel"                 # Shared across all branches
  python-version: "3.12"
```

### Combining Multiple Checks

All checks run concurrently. A typical full configuration looks like:

```yaml
repositories:
  my-repository:
    name: my-org/my-repository
    tox:
      main: all
      python-version: "3.12"
    pre-commit: true
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
    pypi:
      token: pypi-your-token
    container:
      username: myuser
      password: my-registry-password
      repository: quay.io/myorg/myapp
      tag: latest
    custom-check-runs:
      - name: lint
        command: uv tool run --from ruff ruff check
      - name: type-check
        command: uv tool run --from mypy mypy src/
      - name: security-audit
        command: uv tool run --from bandit bandit -r .
        mandatory: false
```

## Troubleshooting

**Check shows "queued" but never starts**
- Verify the server has network access to clone the repository. Check the server logs for clone or worktree errors.

**Custom check skipped with no error**
- The command executable must exist on the server. Check server logs for a warning like `executable 'xxx' not found on server`. Install the missing tool or use `uvx`/`uv tool run` to run it without pre-installing.

**Tox tests pass locally but fail on the server**
- Confirm the `python-version` in your tox config matches what's available on the server. The server uses `uvx tox` to run tests.

**Container build fails with "current system boot ID differs"**
- This is a known podman issue after server restarts. The server automatically retries the build after clearing the podman cache. If it persists, restart the podman service.

**Conventional title check fails unexpectedly**
- Ensure the PR title exactly follows `<type>[optional scope]: <description>`. A common mistake is missing the space after the colon. Check that the type is in your configured list.

## Related Pages

- [Configuring Repositories](configuring-repositories.html)
- [Managing Pull Requests](managing-pull-requests.html)
- [Configuration Reference](configuration-reference.html)
- [PR Comment Command Recipes](pr-command-recipes.html)
- [Enabling AI Features](enabling-ai-features.html)

---

Source: configuring-labels-and-size.md

Let me explore the codebase to understand how labels and PR size thresholds are configured.

Now let me check how label colors are merged (global vs repo-level):

Now let me look at the `.github-webhook-server.yaml` for per-repo override support:

Now let me check the default size thresholds and how they map:

Now I have a complete understanding of the label and PR size configuration system. Let me write the documentation.

# Configuring Labels and PR Size Thresholds

Control which labels appear on your pull requests, customize their colors, and define how PR size categories are calculated based on total lines changed.

## Prerequisites

- A working `config.yaml` file — see [Configuring Repositories](configuring-repositories.html) for initial setup
- The webhook server receiving pull request events from GitHub

## Quick Example

Add this to your `config.yaml` to customize labels and PR size categories:

```yaml
labels:
  enabled-labels:
    - verified
    - hold
    - size
    - can-be-merged
  colors:
    hold: red
    verified: green

pr-size-thresholds:
  Tiny:
    threshold: 10
    color: lightgray
  Small:
    threshold: 50
    color: green
  Medium:
    threshold: 150
    color: orange
  Large:
    threshold: 300
    color: red
  Massive:
    threshold: inf
    color: darkred
```

This configuration enables only four label categories, sets custom colors for `hold` and `verified`, and creates five PR size buckets instead of the built-in defaults.

## Choosing Which Labels to Enable

By default, all label categories are active. To limit which labels the server manages, list only the categories you want under `enabled-labels`:

```yaml
labels:
  enabled-labels:
    - verified
    - hold
    - size
```

With this configuration, only `verified`, `hold`, and `size` labels will be added to PRs. Labels from other categories (like `wip`, `needs-rebase`, `branch`, etc.) will not be created.

### Available Label Categories

| Category | Labels Created | Description |
|---|---|---|
| `verified` | `verified` | PR has been verified/approved for merge |
| `hold` | `hold` | PR is on hold (blocks merge) |
| `wip` | `wip` | Work in progress (blocks merge) |
| `needs-rebase` | `needs-rebase` | PR branch needs rebasing |
| `has-conflicts` | `has-conflicts` | PR has merge conflicts |
| `can-be-merged` | `can-be-merged` | PR passes all checks and can be merged |
| `size` | `size/XS`, `size/S`, `size/M`, etc. | PR size based on lines changed |
| `branch` | `branch-main`, `branch-dev`, etc. | Target branch of the PR |
| `cherry-pick` | `cherry-pick-*`, `CherryPicked` | Cherry-pick tracking labels |
| `automerge` | `automerge` | PR will auto-merge when ready |

> **Note:** Review labels (`approved-*`, `lgtm-*`, `changes-requested-*`, `commented-*`) are **always enabled** and cannot be disabled. These are essential for the review workflow.

Setting `enabled-labels` to an empty list disables all configurable labels while keeping the review labels active:

```yaml
labels:
  enabled-labels: []
```

## Setting Label Colors

Customize label colors using CSS3 color names (like `red`, `green`, `blue`, `coral`, `royalblue`):

```yaml
labels:
  colors:
    hold: red
    verified: green
    wip: orange
    needs-rebase: darkred
    has-conflicts: red
    can-be-merged: limegreen
    automerge: green
```

For dynamic labels that include a username or branch name, use the prefix with a trailing hyphen:

```yaml
labels:
  colors:
    approved-: green
    lgtm-: yellowgreen
    changes-requested-: orange
    commented-: gold
    cherry-pick-: coral
    branch-: royalblue
```

This sets the color for all labels matching that prefix — for example, `approved-: green` applies to `approved-alice`, `approved-bob`, and so on.

> **Tip:** You can combine `enabled-labels` and `colors` in the same `labels` block. Colors apply to any label that gets created, whether or not you explicitly filter categories.

## Defining PR Size Thresholds

PR size is calculated as the total number of lines changed (additions + deletions). Without custom thresholds, the server uses these built-in defaults:

| Label | Lines Changed |
|---|---|
| `size/XS` | 0–19 |
| `size/S` | 20–49 |
| `size/M` | 50–99 |
| `size/L` | 100–299 |
| `size/XL` | 300–499 |
| `size/XXL` | 500+ |

To define your own categories, add a `pr-size-thresholds` block. Each entry needs a `color` (CSS3 color name) and a `threshold` — the number of changed lines at which the *next* category starts:

```yaml
pr-size-thresholds:
  Tiny:
    threshold: 10
    color: lightgray
  Small:
    threshold: 50
    color: green
  Medium:
    threshold: 150
    color: orange
  Large:
    threshold: 300
    color: red
  Massive:
    threshold: inf
    color: darkred
```

This creates the following size buckets:

| Label | Lines Changed |
|---|---|
| `size/Tiny` | 0–9 |
| `size/Small` | 10–49 |
| `size/Medium` | 50–149 |
| `size/Large` | 150–299 |
| `size/Massive` | 300+ |

Each threshold defines the upper boundary of the *previous* category. A PR with 49 lines gets `size/Small` (below the `Medium` threshold of 50), while a PR with 50 lines gets `size/Medium`.

> **Tip:** Use `inf` as the threshold for your largest category to ensure it captures all PRs beyond the previous boundary. Without `inf`, PRs larger than your highest finite threshold still get the last category, but `inf` makes the intent explicit.

You can name categories whatever you want — `Express`, `Standard`, `Premium`, or anything else meaningful to your team. The names become the label suffixes (e.g., `size/Express`).

### How Thresholds Are Sorted

You can define thresholds in any order in YAML. The server always sorts them by threshold value before applying them, so this:

```yaml
pr-size-thresholds:
  Large:
    threshold: 300
    color: red
  Small:
    threshold: 50
    color: green
  Medium:
    threshold: 150
    color: orange
```

produces the same result as listing them in ascending order.

## Advanced Usage

### Repository-Level Overrides

Both `labels` and `pr-size-thresholds` can be set globally or per-repository. Repository-level settings override global ones:

```yaml
# Global defaults
labels:
  enabled-labels:
    - verified
    - hold
    - wip
    - size
    - can-be-merged
  colors:
    hold: red

pr-size-thresholds:
  Small:
    threshold: 50
    color: green
  Large:
    threshold: 300
    color: red

repositories:
  my-repository:
    name: my-org/my-repository
    # Override labels for this repo only
    labels:
      enabled-labels:
        - verified
        - hold
        - size
      colors:
        hold: purple
    # Override size thresholds for this repo only
    pr-size-thresholds:
      Express:
        threshold: 25
        color: lightblue
      Standard:
        threshold: 100
        color: green
      Premium:
        threshold: 500
        color: orange
```

In this example, `my-repository` uses only three label categories with a purple `hold` label and three custom size buckets, while all other repositories use the global settings.

Label colors are deep-merged: repository-level colors override global colors for the same key, but global colors not overridden by the repository still apply.

### In-Repository Configuration

You can also set `labels` and `pr-size-thresholds` in the `.github-webhook-server.yaml` file inside your repository. This file takes the highest precedence — it overrides both global and repository-level `config.yaml` settings. See [Configuring Repositories](configuring-repositories.html) for details on this file.

### Omitting the Color Field

If you omit the `color` field from a size threshold entry, it defaults to `lightgray`:

```yaml
pr-size-thresholds:
  Small:
    threshold: 100
  Large:
    threshold: 500
    color: red
```

Here, `size/Small` labels will be light gray, while `size/Large` labels will be red.

### Using a Single Threshold

You can define as few categories as you like. A single threshold means all PRs get that one size label:

```yaml
pr-size-thresholds:
  Standard:
    threshold: 100
    color: green
```

PRs of any size receive the `size/Standard` label.

## Troubleshooting

**Labels aren't appearing on PRs**
- Verify that `size` (or the relevant category) is listed in `enabled-labels`. If `enabled-labels` is set, only listed categories are active.
- Ensure the webhook server is receiving `pull_request` events. See [Configuring Repositories](configuring-repositories.html) for event configuration.

**Custom size categories aren't working**
- Check that each entry under `pr-size-thresholds` has a valid `threshold` value — it must be a positive integer or `inf`. Zero, negative values, and non-numeric strings are ignored.
- Verify YAML indentation. Each category name should be a key under `pr-size-thresholds`, with `threshold` and `color` nested beneath it.

**Label color looks wrong**
- Colors must be valid CSS3 color names (e.g., `red`, `green`, `darkorange`, `royalblue`). Hex codes and RGB values are not accepted. If an invalid color name is provided, the label defaults to light gray.

**Changes not taking effect**
- The server reloads configuration without requiring a restart. Verify your `config.yaml` changes are saved and the file path is correct. See [Environment Variables](environment-variables.html) for `WEBHOOK_SERVER_DATA_DIR` configuration.

## Related Pages

- [Configuring Repositories](configuring-repositories.html)
- [Configuration Reference](configuration-reference.html)
- [Managing Pull Requests](managing-pull-requests.html)
- [Configuration Recipes](config-recipes.html)

---

Source: cherry-picking-and-branching.md

Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files.

Now let me look at the config schema for branch protection, cherry-pick, and OWNERS-related settings:

Now let me look at the issue_comment handler for cherry-pick commands and the PR handler's cherry-pick sections:

Now let me look at the cherry_pick method and the auto-verify cherry-pick logic:

Now let me look at the cherry-pick retry command and the welcome message cherry-pick section:

Now let me look at the protected-branches configuration and how it connects to status checks:

Let me also check the `_prepare_merge_requirements` for OWNERS file info:

Now let me look at the ROOT_APPROVERS_KEY constant and the merge requirements with OWNERS file checking:

Let me check the approve flow to understand how OWNERS file approvers control merge:

Now I have all the information I need. Let me write the documentation page.

# Cherry-Picking and Branch Protection

Automate the process of backporting merged PRs to release branches and enforce consistent branch protection rules across your repositories, using OWNERS files to control who can approve changes.

## Prerequisites

- A running webhook server instance (see [Getting Started](quickstart.html))
- At least one repository configured in `config.yaml` (see [Configuring Repositories](configuring-repositories.html))
- A GitHub App installed on your repositories with branch protection permissions

## Quick Example

Add cherry-pick targets and branch protection to your repository configuration:

```yaml
# config.yaml
cherry-pick-assign-to-pr-author: true
auto-verify-cherry-picked-prs: true

branch-protection:
  strict: true
  required_approving_review_count: 1
  required_conversation_resolution: true

repositories:
  my-repo:
    name: my-org/my-repo
    protected-branches:
      main: []
      release-1.0: []
```

Then create an `OWNERS` file in your repository root:

```yaml
approvers:
  - alice
  - bob
reviewers:
  - charlie
  - dana
```

When a PR is merged, comment `/cherry-pick release-1.0` to backport it — or add the label before merging so it happens automatically.

## Cherry-Picking PRs to Target Branches

### Scheduling a Cherry-Pick Before Merge

Comment on an open PR to queue cherry-picks for when it merges:

```
/cherry-pick release-1.0
```

This adds a `cherry-pick-release-1.0` label to the PR. When the PR merges, the server automatically cherry-picks the merge commit to the `release-1.0` branch and opens a new PR.

You can specify multiple branches at once:

```
/cherry-pick release-1.0 release-2.0 hotfix
```

Each branch gets its own `cherry-pick-<branch>` label, and each is processed independently on merge.

### Cherry-Picking an Already-Merged PR

Comment on a merged PR to cherry-pick immediately:

```
/cherry-pick release-1.0
```

For merged PRs, the cherry-pick executes right away instead of waiting. A `cherry-pick-release-1.0` label is added to track that the operation was performed.

### What Happens During a Cherry-Pick

When a cherry-pick is triggered, the server:

1. Validates the target branch exists
2. Creates a worktree and checks out the target branch
3. Runs `git cherry-pick <merge-commit-sha>` (automatically retries with `-m 1` for merge commits)
4. Restores the original PR author on the cherry-pick commit (for DCO/sign-off compliance)
5. Runs pre-commit hooks if enabled for the repository
6. Pushes the new branch and opens a PR against the target branch
7. Labels the new PR with `CherryPicked-from-<source-branch>`
8. Assigns the original PR author and requests their review

If the cherry-pick fails due to conflicts and AI conflict resolution is not enabled (or fails), the server posts a comment with manual cherry-pick instructions:

```
**Manual cherry-pick is needed**
Cherry pick failed for abc1234 to release-1.0:
To cherry-pick run:
  git remote update
  git checkout release-1.0
  git pull origin release-1.0
  git checkout -b my-feature-release-1.0
  git cherry-pick abc1234
  # If the above fails with 'is a merge but no -m option', run:
  # git cherry-pick -m 1 abc1234
  git push origin my-feature-release-1.0
```

### Retrying a Failed Cherry-Pick

If a cherry-pick fails or the resulting PR has issues, use the retry command on the original merged PR:

```
/cherry-pick-retry release-1.0
```

This command:

- Validates the PR is merged and has the `cherry-pick-release-1.0` label
- Closes any existing failed cherry-pick PR created by the bot for that branch
- Re-runs the cherry-pick operation

> **Note:** `/cherry-pick-retry` accepts exactly one branch name. If the cherry-pick label doesn't exist on the PR, use `/cherry-pick <branch>` instead.

### Duplicate Prevention

If a `cherry-pick-<branch>` label already exists on the PR, that branch is skipped. To re-trigger a cherry-pick for a branch that was already processed, remove the label and run the command again.

### Auto-Verification of Cherry-Picked PRs

By default, cherry-picked PRs from auto-verified users are automatically marked as verified. Control this behavior globally or per-repository:

```yaml
# Global setting (default: true)
auto-verify-cherry-picked-prs: true

repositories:
  my-repo:
    name: my-org/my-repo
    # Override per repository
    auto-verify-cherry-picked-prs: false
```

When set to `false`, cherry-picked PRs require manual verification even if the original author is in the `auto-verified-and-merged-users` list.

> **Warning:** Cherry-picked PRs with AI-resolved conflicts are **never** auto-verified, regardless of this setting. The `ai-resolved-conflicts` label forces manual review.

### Cherry-Pick PR Assignment

By default, cherry-pick PRs are assigned to the original PR author. Disable this globally or per-repository:

```yaml
# Global setting (default: true)
cherry-pick-assign-to-pr-author: true

repositories:
  my-repo:
    name: my-org/my-repo
    cherry-pick-assign-to-pr-author: false
```

When the original author cannot be assigned (e.g., they don't have repository access), the server falls back to assigning the first root approver from the `OWNERS` file.

## Configuring Branch Protection

Branch protection rules are applied to every branch listed under `protected-branches` for a repository. Configure the rules at the global level, the repository level, or both (repository overrides global).

### Branch Protection Settings

```yaml
branch-protection:
  strict: true
  require_code_owner_reviews: false
  dismiss_stale_reviews: true
  required_approving_review_count: 0
  required_linear_history: true
  required_conversation_resolution: true
```

| Setting | Default | Description |
|---------|---------|-------------|
| `strict` | `true` | Require branches to be up-to-date before merging |
| `require_code_owner_reviews` | `false` | Require review from code owners |
| `dismiss_stale_reviews` | `true` | Dismiss approvals when new commits are pushed |
| `required_approving_review_count` | `0` | Minimum number of GitHub review approvals |
| `required_linear_history` | `true` | Require linear commit history (no merge commits) |
| `required_conversation_resolution` | `true` | Require all review conversations to be resolved |

> **Tip:** The `required_conversation_resolution` setting also controls whether the server checks for unresolved review threads when evaluating the `can-be-merged` check run.

### Defining Protected Branches

List the branches to protect under `protected-branches` in your repository config. Each branch can specify which status checks are required:

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    protected-branches:
      main:
        include-runs:
          - "pre-commit.ci - pr"
          - "WIP"
        exclude-runs:
          - "SonarCloud Code Analysis"
      dev: []
      release-1.0: []
```

**Three formats are supported:**

| Format | Example | Behavior |
|--------|---------|----------|
| Empty list | `dev: []` | Auto-detects status checks from repo config (tox, pre-commit, container builds, etc.) |
| Include/exclude object | `main: { include-runs: [...], exclude-runs: [...] }` | Explicitly controls which checks are required |
| Simple array | `feature: ["check1", "check2"]` | Uses only the listed checks |

When you use the empty list `[]`, the server automatically builds the required status checks from your repository configuration — if `tox` is enabled it adds `tox`, if `container` is configured it adds `build-container`, and so on. The `can-be-merged` and `verified` checks are always included by default.

### Per-Repository Branch Protection Overrides

Override global branch protection at the repository level:

```yaml
branch-protection:
  strict: true
  required_approving_review_count: 0

repositories:
  strict-repo:
    name: my-org/strict-repo
    branch-protection:
      required_approving_review_count: 2
      dismiss_stale_reviews: true
    protected-branches:
      main: []
```

Repository-level settings override global settings for the same key. Any keys not specified at the repository level fall back to the global value, then to the built-in defaults.

## Using OWNERS Files for Approval Workflows

OWNERS files define who can approve and review PRs for specific parts of your codebase. The server reads these files from the repository's base branch and uses them to enforce approval requirements.

### OWNERS File Format

Create an `OWNERS` file (YAML format) in any directory:

```yaml
approvers:
  - alice
  - bob
reviewers:
  - charlie
  - dana
```

- **Approvers** can use the `/approve` command to approve PRs touching files in that directory
- **Reviewers** are automatically assigned to PRs and can use `/lgtm`

### Directory-Scoped Ownership

Place `OWNERS` files in subdirectories to define granular ownership:

```
repo-root/
├── OWNERS              # Root approvers/reviewers (apply to all PRs)
├── api/
│   └── OWNERS          # Approvers for API changes
├── frontend/
│   └── OWNERS          # Approvers for frontend changes
└── docs/
    └── OWNERS          # Approvers for documentation
```

When a PR changes files in `api/`, the server requires approval from an approver listed in `api/OWNERS`. Root approvers (from the repository root `OWNERS`) can always approve any PR.

### Controlling Root Approver Requirements

By default, root approvers are always required in addition to directory-specific approvers. Override this by adding `root-approvers: false` to a subdirectory's `OWNERS` file:

```yaml
# api/OWNERS
approvers:
  - api-lead
reviewers:
  - api-dev
root-approvers: false
```

With `root-approvers: false`, only the approvers listed in `api/OWNERS` need to approve changes under `api/` — root approvers are not required for those files.

> **Note:** If a PR changes files in both `api/` (with `root-approvers: false`) and an unmatched directory, root approvers are still required for the unmatched files.

### How Approval Checking Works

The server evaluates approvals when determining if a PR can be merged:

1. For each changed file, the server finds the most specific `OWNERS` file
2. At least one approver from each relevant `OWNERS` file must use `/approve`
3. A root approver's `/approve` satisfies all directory requirements
4. If a reviewer uses `/lgtm`, it counts toward the `minimum-lgtm` threshold but does not count as an approval
5. Change requests from approvers block the `can-be-merged` check

### Automatic Reviewer Assignment

When a PR is opened, reviewers from all relevant `OWNERS` files are automatically assigned. You can also trigger this manually:

```
/assign-reviewers
```

Or assign a specific reviewer:

```
/assign-reviewer @username
```

### Allowed Users

The `OWNERS` file in the repository root can include an `allowed-users` list to grant command execution permissions:

```yaml
# Root OWNERS file
approvers:
  - alice
reviewers:
  - bob
allowed-users:
  - external-contributor
```

Users not in the approvers, reviewers, collaborators, or contributors lists need explicit permission via `allowed-users` or a maintainer commenting `/add-allowed-user @username` on the PR.

## Advanced Usage

### AI-Powered Cherry-Pick Conflict Resolution

When a cherry-pick encounters merge conflicts, the server can use AI to attempt automatic resolution. See [Enabling AI Features](enabling-ai-features.html) for setup details.

```yaml
ai-features:
  ai-provider: claude
  ai-model: sonnet
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
    timeout-minutes: 10
```

When AI resolves conflicts:

- The cherry-pick PR is labeled with `ai-resolved-conflicts`
- The PR is **never** auto-verified — manual review is always required
- The server logs a scope verification comparing original vs. cherry-picked file counts
- If AI resolution fails, the server falls back to posting manual cherry-pick instructions

### Combining Cherry-Pick Labels with Enabled Labels

Cherry-pick labels (`cherry-pick-<branch>` and `CherryPicked`) belong to the `cherry-pick` label category. If you use `enabled-labels` to restrict which labels are active, include `cherry-pick` to keep cherry-pick functionality working:

```yaml
labels:
  enabled-labels:
    - verified
    - cherry-pick
    - can-be-merged
```

See [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html) for full label configuration options.

### Default Status Checks

Override which status checks are added by default to all protected branches:

```yaml
# Global defaults
default-status-checks:
  - "WIP"
  - "dpulls"
  - "can-be-merged"

repositories:
  my-repo:
    name: my-org/my-repo
    # Override for this repo only
    default-status-checks:
      - "WIP"
      - "can-be-merged"
      - "ci/my-external-check"
```

These checks are combined with auto-detected checks (tox, pre-commit, etc.) when a protected branch uses the empty list `[]` format.

## Troubleshooting

**Cherry-pick label exists but cherry-pick didn't run**
Cherry-picks only execute when a PR is merged (for pre-merge labels) or immediately (for post-merge `/cherry-pick` commands). If the PR was closed without merging, cherry-pick labels are ignored. Re-open and merge the PR, or use `/cherry-pick <branch>` on the merged PR.

**"Target branch does not exist" error**
The server validates that each target branch exists before adding cherry-pick labels. Check that the branch name matches exactly (branch names are case-sensitive).

**Cherry-pick PR is not auto-verified**
Check whether `auto-verify-cherry-picked-prs` is set to `false` for the repository. Also check if the cherry-pick had AI-resolved conflicts — PRs with the `ai-resolved-conflicts` label are never auto-verified.

**Branch protection not applied to a branch**
Only branches listed under `protected-branches` in the repository config receive protection rules. The repository must also be public — private repositories skip branch protection settings.

**"/approve not accepted" from a user**
The user must be listed as an `approver` in a relevant `OWNERS` file. Reviewers can use `/lgtm` but cannot `/approve`. Check which `OWNERS` file covers the changed files and verify the username spelling.

## Related Pages

- [Managing Pull Requests](managing-pull-requests.html)
- [Enabling AI Features](enabling-ai-features.html)
- [Configuring Repositories](configuring-repositories.html)
- [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html)
- [Configuration Reference](configuration-reference.html)

---

Source: enabling-security-checks.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me explore the security-related source code:

Now let me look at the security override command and the issue comment handler:

Now let me look at the security tests to understand edge cases and behaviors:

Now let me check the retest command to see how security checks can be retested:

Now let me check the per-repository security-checks override pattern:

I now have a comprehensive understanding of the security checks system. Let me write the documentation.

# Enabling Security Checks

Protect your repositories from supply-chain attacks and unauthorized commits by enabling security checks on pull requests. These checks detect modifications to sensitive file paths and verify that committer identities match PR authors.

## Prerequisites

- A running webhook server instance (see [Getting Started](quickstart.html))
- At least one repository configured in `config.yaml` (see [Configuring Repositories](configuring-repositories.html))

## Quick Example

Add this to your `config.yaml` to enable both security checks with default settings:

```yaml
security-checks:
  mandatory: true
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
    - ".claude/"
    - ".vscode/"
    - ".cursor/"
    - ".devcontainer/"
    - ".pi/"
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"
```

This creates two GitHub check runs on every PR:
- **`security-suspicious-paths`** — fails if any changed file starts with a monitored path prefix
- **`security-committer-identity`** — fails if the last commit's committer doesn't match the PR author

Both checks block merging by default (`mandatory: true`).

## Step-by-Step Setup

### 1. Enable Suspicious Path Detection

The suspicious path check compares every changed file in a PR against a list of path prefixes. If any file matches, the check run fails and lists all flagged files.

```yaml
security-checks:
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
```

If you omit `suspicious-paths`, the server uses these defaults:

| Default Path Prefix | What It Protects |
|---|---|
| `.claude/` | Claude AI configuration |
| `.vscode/` | VS Code settings and extensions |
| `.cursor/` | Cursor editor rules |
| `.devcontainer/` | Dev container definitions |
| `.pi/` | Pi sidecar configuration |
| `.github/workflows/` | CI/CD workflow definitions |
| `.github/actions/` | Custom GitHub Actions |

> **Tip:** Set `suspicious-paths` to an empty list (`[]`) to disable this check entirely while keeping other security checks active.

### 2. Enable Committer Identity Verification

The committer identity check compares the PR author against the last commit's committer. It catches cases where someone pushes a commit to another user's PR branch.

```yaml
security-checks:
  committer-identity-check: true
```

This check is enabled by default. The check run will:
- **Pass** when the last committer matches the PR author
- **Pass** when the last committer is in the trusted committers list
- **Fail** when the last committer is a different, untrusted user
- **Fail** when the committer identity is unknown (no linked GitHub account)

> **Note:** The committer identity check also detects web-flow impersonation. If someone creates a GitHub account named `web-flow`, the check verifies the account's immutable user ID against GitHub's real web-flow system account.

### 3. Configure Trusted Committers

Bots and automation tools frequently commit to PR branches with a different identity than the PR author. Add them to `trusted-committers` to prevent false positives:

```yaml
security-checks:
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"
    - "renovate[bot]"
    - "my-org-bot"
```

You only need to list **external** committers here. The following are automatically trusted:
- The GitHub App bot used by the webhook server
- GitHub's `web-flow` account (used for web UI merges and edits)
- All API users from your configured `github-tokens`

> **Tip:** Trusted committer matching is case-insensitive. `Pre-Commit-CI[bot]` and `pre-commit-ci[bot]` are treated as the same identity.

### 4. Choose Mandatory or Advisory Mode

By default, security checks block the `can-be-merged` status. Set `mandatory: false` to make them advisory — the checks still run and report results, but they won't prevent merging:

| Setting | Check Runs Execute | Blocks Merge | Blocks Auto-Merge |
|---|---|---|---|
| `mandatory: true` (default) | ✅ | ✅ | ✅ |
| `mandatory: false` | ✅ | ❌ | ✅* |

\* Suspicious path detection always blocks auto-merge for flagged files, regardless of the `mandatory` setting.

```yaml
security-checks:
  mandatory: false  # Advisory only — checks run but don't block merge
  suspicious-paths:
    - ".github/workflows/"
  committer-identity-check: true
```

## Overriding Security Checks

When a maintainer has reviewed a flagged PR and determined the changes are safe, they can override the security checks using a PR comment:

```
/security-override
```

This sets both security check runs to pass. Only repository maintainers (defined in OWNERS files) can use this command.

To reverse an override and re-run the security checks:

```
/security-override cancel
```

> **Warning:** Non-maintainers who attempt `/security-override` will receive a rejection comment. The check runs remain in their original state.

You can also re-run individual security checks using the retest command:

```
/retest security-suspicious-paths
/retest security-committer-identity
```

See [Managing Pull Requests](managing-pull-requests.html) for the full list of PR comment commands.

## Advanced Usage

### Per-Repository Configuration

Override global security settings for a specific repository in `config.yaml`:

```yaml
security-checks:
  mandatory: true
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
  committer-identity-check: true

repositories:
  my-repository:
    name: my-org/my-repository
    security-checks:
      suspicious-paths:
        - ".github/workflows/"  # Only monitor CI workflows for this repo
      committer-identity-check: false  # Disable identity check for this repo
```

You can also configure security checks in a repository's `.github-webhook-server.yaml` file, which takes priority over `config.yaml`. See [Configuring Repositories](configuring-repositories.html) for the override hierarchy.

### Custom Suspicious Paths

Monitor project-specific sensitive locations by adding custom path prefixes:

```yaml
security-checks:
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
    - "deploy/"
    - "infra/terraform/"
    - "scripts/ci/"
    - ".npmrc"
```

Path matching is prefix-based: a prefix of `deploy/` matches `deploy/production.yml`, `deploy/scripts/rollback.sh`, and any other file under that directory.

### Auto-Merge Interaction

When suspicious path detection is enabled, the server automatically blocks auto-merge for any PR that modifies flagged files — even if auto-merge was already enabled on the PR:

1. The PR is checked for changed files matching suspicious path prefixes
2. If matches are found, auto-merge is blocked and a comment is posted listing the flagged files
3. If auto-merge was already enabled on the PR, it is automatically disabled
4. A maintainer can use `/security-override` to allow auto-merge to proceed

> **Note:** This protection applies regardless of whether `mandatory` is `true` or `false`. Suspicious paths always block auto-merge as a defense-in-depth measure.

### Welcome Message Integration

When security checks are enabled, the PR welcome message automatically includes a **Security Checks** section showing:
- Which checks are active and what they monitor
- Whether checks are mandatory or advisory
- Available override commands (for maintainers)

## Troubleshooting

**Security check shows "unknown" committer**
The last commit was made with a local Git identity that isn't linked to a GitHub account. The committer needs to add their Git email to their GitHub profile, or push a new commit with a verified email.

**Trusted committer still fails the identity check**
Verify the login name matches exactly (case doesn't matter, but special characters like `[bot]` do). Check the check run output — it shows both the PR author and last committer for comparison.

**Security checks don't appear on PRs**
Ensure `security-checks` is configured either globally or for the specific repository. If `suspicious-paths` is set to `[]` *and* `committer-identity-check` is `false`, no checks will run.

**Auto-merge blocked but security check passed**
This can happen if the security check was overridden by a maintainer but the check run result was later invalidated by a new push. Use `/security-override` again after reviewing the updated changes.

## Related Pages

- [Managing Pull Requests](managing-pull-requests.html)
- [Configuring Repositories](configuring-repositories.html)
- [Configuration Reference](configuration-reference.html)
- [PR Comment Command Recipes](pr-command-recipes.html)
- [Configuration Recipes](config-recipes.html)

---

Source: enabling-ai-features.md

Let me start by reading the knowledge graph and understanding the repository structure.

Now let me explore the AI-related source files:

Now let me look at the runner handler for how AI features are actually used in cherry-picks and conventional titles:

Now let me look at how the welcome message displays AI features and understand the `auto-verify-cherry-picked-prs` interaction:

Now let me check the SIDECAR_PORT environment variable usage and the issue comment handler for `/test-oracle`:

Now let me check how the review handler triggers test oracle on approval:

Let me also check the cherry-pick flow in the runner handler to understand where AI conflict resolution fits:

Now let me look at what environment variables control sidecar/AI provider credentials:

Now let me look at the per-repo `.github-webhook-server.yaml` to understand how AI settings can be configured at repo level:

Now let me look at what the tool server does:

Let me check how Cursor interactive login works, since it was mentioned in docker-compose:

Let me also check what happens when test oracle posts its results (i.e., how the results are communicated to the user):

Now I have enough information to write the documentation. Let me compile everything:

# Enabling AI Features

Set up AI-powered automation for your webhook server to get conventional commit title suggestions on failing PR checks, automatically resolve cherry-pick merge conflicts, and analyze PRs for test coverage recommendations.

## Prerequisites

- A running webhook server instance (see [Getting Started](quickstart.html))
- **Docker deployment** (recommended): The container image ships with the AI sidecar pre-built
- An API key for at least one supported AI provider:

| Provider | Environment Variable | Notes |
|----------|---------------------|-------|
| Claude | `ANTHROPIC_API_KEY` | Direct API access |
| Claude (Vertex AI) | Google Cloud credentials + `VERTEX_CLAUDE_1M=true` | For 1M context window models |
| Gemini | `GEMINI_API_KEY` | Google Gemini CLI |
| Cursor | `CURSOR_API_KEY` or interactive login | API key or `docker exec -it github-webhook-server agent` |

## Quick Example

Add AI features to your `config.yaml` to start using AI-powered conventional title suggestions and cherry-pick conflict resolution:

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
```

Then pass your API key as an environment variable when starting the server:

```yaml
# docker-compose.yaml
environment:
  - ANTHROPIC_API_KEY=sk-ant-xxx
```

That's it — PRs that fail conventional title validation now show AI-suggested titles, and cherry-picks with merge conflicts are automatically resolved.

## Step 1: Configure the AI Provider

The `ai-features` block in `config.yaml` sets the AI provider and model used by the conventional title and cherry-pick conflict resolution features. Add it at the top level for all repositories, or inside a specific repository to override:

```yaml
# Global (applies to all repositories)
ai-features:
  ai-provider: "claude"       # claude | gemini | cursor
  ai-model: "sonnet"          # Model identifier (e.g., sonnet, gemini-2.5-pro)
```

```yaml
# Per-repository override
repositories:
  my-repo:
    name: my-org/my-repo
    ai-features:
      ai-provider: "gemini"
      ai-model: "gemini-2.5-pro"
      conventional-title:
        enabled: true
        mode: fix
```

> **Note:** The `ai-provider` and `ai-model` fields are required whenever `ai-features` is present. The `conventional-title` and `resolve-cherry-pick-conflicts-with-ai` sub-keys are optional.

## Step 2: Set Up API Credentials

Pass the appropriate environment variable for your chosen provider. In Docker Compose:

```yaml
services:
  github-webhook-server:
    environment:
      # Pick one (or more if using different providers per repo):
      - ANTHROPIC_API_KEY=sk-ant-xxx         # Claude
      - GEMINI_API_KEY=xxx                    # Gemini
      - CURSOR_API_KEY=xxx                    # Cursor (API key method)
      # Optional: Enable Cursor model discovery
      # - ACPX_AGENTS=cursor
      # Optional: Enable Claude 1M context window via Vertex AI
      # - VERTEX_CLAUDE_1M=true
```

For Vertex AI (Claude via Google Cloud), mount your credentials into the container:

```yaml
volumes:
  - $HOME/.config/gcloud:/home/podman/.config/gcloud:ro
```

> **Note:** For Cursor interactive login (instead of API key), exec into the running container: `docker exec -it github-webhook-server agent`


> **Warning:** Never commit API keys to your repository. Use environment variables or a secrets manager. See [Environment Variables](environment-variables.html) for all available settings.

## Step 3: Enable Conventional Title Suggestions

When your repository enforces conventional commit PR titles, AI can suggest or auto-fix titles that fail validation. This requires both `conventional-title` under `ai-features` **and** the `conventional-title` setting on the repository. See [Setting Up CI Checks](setting-up-ci-checks.html) for configuring conventional commit validation.

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest            # Show suggestion in check run output
    timeout-minutes: 10      # Optional (default: 10)

repositories:
  my-repo:
    name: my-org/my-repo
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
```

There are two modes:

| Mode | Behavior |
|------|----------|
| `suggest` | When the PR title fails validation, the check run output includes an AI-suggested title. The author copies it manually. |
| `fix` | The PR title is automatically updated to the AI suggestion. The check run re-evaluates and passes. |

In **suggest** mode, the check run output includes a section like:

```
### AI-Suggested Title

> feat(auth): add OAuth2 login support
```

In **fix** mode, the PR title is updated silently and a success message confirms the change.

> **Tip:** Start with `suggest` mode to review AI suggestions before trusting `fix` to auto-update titles.

## Step 4: Enable AI Cherry-Pick Conflict Resolution

When a PR is merged and cherry-picked to another branch, merge conflicts sometimes occur. With this feature enabled, the AI automatically resolves conflicts — preserving the intent of the original commit on the target branch.

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
    timeout-minutes: 10      # Optional (default: 10)
```

When a cherry-pick encounters a `CONFLICT`:

1. The AI inspects the original commit, its diff, and the conflicted files
2. It edits the conflicted files to resolve the merge
3. The resolved files are staged and the cherry-pick is finalized
4. The cherry-pick PR is created with an `ai-resolved-conflicts` label
5. A comment is posted on both the original PR and the cherry-pick PR requesting manual review

If AI resolution fails, the server falls back to posting manual cherry-pick instructions (the same behavior as when AI is disabled).

> **Warning:** AI-resolved cherry-picks are **never auto-verified and never auto-merged**, even when `auto-verify-cherry-picked-prs` is `true`. The `ai-resolved-conflicts` label ensures a human reviews the changes. See [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html) for more on cherry-pick workflows.

## Step 5: Set Up the PR Test Oracle

The Test Oracle is a separate feature from `ai-features` — it has its own configuration block. It integrates with the [pr-test-oracle](https://github.com/myk-org/pr-test-oracle) server to analyze PR diffs and recommend which tests to run.

```yaml
test-oracle:
  server-url: "http://localhost:8000"    # URL of your pr-test-oracle instance
  ai-provider: "claude"                  # claude | gemini | cursor
  ai-model: "sonnet"
  test-patterns:                         # Optional — oracle has defaults
    - "tests/**/*.py"
  triggers:                              # Optional (default: [approved])
    - approved                           # Run when /approve command is used
    # - pr-opened                        # Run when a new PR is opened
    # - pr-synchronized                  # Run when new commits are pushed
```

The Test Oracle can be configured globally or per repository:

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    test-oracle:
      server-url: "http://localhost:8000"
      ai-provider: "claude"
      ai-model: "sonnet"
      triggers:
        - approved
        - pr-opened
```

### Test Oracle Triggers

| Trigger | When It Fires |
|---------|---------------|
| `approved` | When a maintainer uses the `/approve` command on a PR |
| `pr-opened` | When a new PR is opened |
| `pr-synchronized` | When new commits are pushed to an existing PR |

> **Tip:** The `/test-oracle` comment command works anytime on any PR, regardless of configured triggers. Triggers only control *automatic* analysis.

### Deploying the Test Oracle Server

The Test Oracle requires a running instance of [pr-test-oracle](https://github.com/myk-org/pr-test-oracle). Follow its setup instructions, then point `server-url` to your instance. The webhook server performs a health check before each analysis request and posts a comment if the oracle is unreachable.

## Verifying the Setup

After configuring AI features, verify they're working:

1. **Check sidecar health** — The container health check includes the sidecar:
   ```
   curl -f http://localhost:9100/health
   ```

2. **Open a PR with a bad title** — If you have conventional title enforcement enabled with AI in `suggest` or `fix` mode, the check run output should include an AI suggestion or auto-fix.

3. **Trigger a cherry-pick with conflicts** — Merge a PR with cherry-pick targets where you know conflicts exist. The AI should attempt resolution and the resulting cherry-pick PR should carry the `ai-resolved-conflicts` label.

4. **Run `/test-oracle`** — Comment `/test-oracle` on any PR to trigger an on-demand analysis.

## Advanced Usage

### Combining Features

All three AI features are independent and can be enabled in any combination:

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: fix
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
    timeout-minutes: 15

test-oracle:
  server-url: "http://oracle.internal:8000"
  ai-provider: "gemini"
  ai-model: "gemini-2.5-pro"
  triggers:
    - approved
    - pr-opened
    - pr-synchronized
```

> **Note:** The Test Oracle can use a different AI provider and model than `ai-features`. Each is configured independently.

### Using Different Providers Per Repository

Override the global AI configuration for specific repositories:

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest

repositories:
  critical-repo:
    name: my-org/critical-repo
    ai-features:
      ai-provider: "claude"
      ai-model: "claude-opus-4-6-1m"
      conventional-title:
        enabled: true
        mode: fix
        timeout-minutes: 15
      resolve-cherry-pick-conflicts-with-ai:
        enabled: true
        timeout-minutes: 20
```

### Adjusting Timeouts

Both conventional title and cherry-pick resolution support `timeout-minutes` (default: 10). Increase this for large repositories or complex conflicts:

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest
    timeout-minutes: 5       # Title suggestions are quick
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
    timeout-minutes: 20      # Conflict resolution may take longer
```

### Sidecar Port Configuration

The AI sidecar runs on port 9100 by default. Change it with the `SIDECAR_PORT` environment variable:

```yaml
environment:
  - SIDECAR_PORT=9200
```

The container health check automatically uses the configured port.

### Welcome Message Integration

When AI features are configured, the PR welcome comment includes an **AI Features** section summarizing what's active:

- **Conventional Title**: Mode and provider/model
- **Cherry-Pick Conflict Resolution**: Whether enabled and provider/model
- **Test Oracle**: Configured triggers and the `/test-oracle` command availability

This helps PR authors understand what AI automation is in play. See [Managing Pull Requests](managing-pull-requests.html) for more on welcome messages.

## Troubleshooting

**Sidecar health check fails on startup:**
The entrypoint script waits up to 15 seconds for the sidecar to become healthy. If it fails, you'll see `ERROR: sidecar failed to become healthy within 15s — AI features will not work`. Check that:
- The AI provider API key environment variable is set correctly
- The `SIDECAR_PORT` isn't conflicting with another service
- Container logs show the sidecar started without errors

**AI title suggestion returns nothing:**
- Verify `conventional-title` has `enabled: true` under `ai-features`
- Ensure the repository also has `conventional-title` configured with allowed commit types
- Check server logs for timeout or API errors

**Cherry-pick AI resolution falls back to manual:**
- Look for log messages containing "AI conflict resolution failed" for details
- The AI only attempts resolution for actual `CONFLICT` markers — other cherry-pick failures skip AI
- If the sidecar is unavailable, it returns immediately with a fallback

**Test Oracle says "server is not responding":**
- Verify the `server-url` is reachable from the webhook server container
- The oracle server must respond to `GET /health` within 5 seconds
- Check that the [pr-test-oracle](https://github.com/myk-org/pr-test-oracle) service is running

**AI-resolved cherry-pick won't auto-merge:**
This is by design. Cherry-picks with the `ai-resolved-conflicts` label are never auto-merged or auto-verified, regardless of other settings. A human must review and manually verify the PR.

## Related Pages

- [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Environment Variables](environment-variables.html)
- [Configuring Repositories](configuring-repositories.html)
- [Managing Pull Requests](managing-pull-requests.html)

---

Source: using-the-log-viewer.md

# Using the Log Viewer

Browse, search, filter, and export webhook processing logs through a built-in web UI so you can debug failed webhooks, monitor PR workflows, and audit event history without SSH-ing into your server.

## Prerequisites

- A running github-webhook-server instance (see [Getting Started](quickstart.html))
- The `ENABLE_LOG_SERVER` environment variable set to `true`

## Quick Start

Add `ENABLE_LOG_SERVER=true` to your environment and open `/logs` in a browser:

```yaml
# docker-compose.yaml (environment section)
environment:
  - ENABLE_LOG_SERVER=true
```

For a non-Docker setup:

```bash
ENABLE_LOG_SERVER=true WEBHOOK_SERVER_DATA_DIR=/path/to/data uv run entrypoint.py
```

Then navigate to `http://your-server:5000/logs`.

> **Warning:** The log viewer endpoints are **unauthenticated**. Deploy only on trusted networks (VPN, internal) or behind a reverse proxy with authentication. Never expose `/logs` to the public internet.

## Enabling the Log Viewer

Set `ENABLE_LOG_SERVER=true` as an environment variable before starting the server. The value must be the literal string `true` — any other value (including `True`, `1`, or `yes`) leaves the log viewer disabled.

When the log viewer is disabled, all log viewer paths (`/logs`, `/logs/api/*`, `/logs/ws`) return a 404 response.

The viewer reads log files from `${WEBHOOK_SERVER_DATA_DIR}/logs`. If you have not set `WEBHOOK_SERVER_DATA_DIR`, the default is `/home/podman/data`, so logs are read from `/home/podman/data/logs`.

See [Environment Variables](environment-variables.html) for the full list of environment variables.

## Configuring Log Files and Masking

The log viewer uses the same logging configuration as the rest of the server. Add these keys to your `config.yaml`:

```yaml
log-level: INFO
log-file: webhook-server.log
logs-server-log-file: logs_server.log
mask-sensitive-data: true
```

| Key | What it controls | Default |
|---|---|---|
| `log-level` | Verbosity of logs (`INFO` or `DEBUG`) | `INFO` |
| `log-file` | Main log file name | `webhook-server.log` |
| `logs-server-log-file` | Separate log file for the log viewer itself | `logs_server.log` |
| `mask-sensitive-data` | Redact tokens, passwords, and secrets from log output | `true` |

> **Tip:** You can override `mask-sensitive-data` per repository in `config.yaml` for debugging a specific repo without exposing secrets across all repositories.

See [Configuration Reference](configuration-reference.html) for all available options.

## Browsing Logs in the Web UI

Open `http://your-server:5000/logs` to see the log viewer interface. The page loads the most recent log entries automatically.

### Filtering

Use the filter bar at the top of the page to narrow down results:

| Filter | Description | Example |
|---|---|---|
| **Search** | Free-text search across log messages (case-insensitive) | `container build failed` |
| **Hook ID** | GitHub webhook delivery ID (`X-GitHub-Delivery` header) | `f4b3c2d1-a9b8-...` |
| **PR #** | Pull request number | `42` |
| **Repository** | Repository in `owner/repo` format | `myorg/myrepo` |
| **User** | GitHub username who triggered the event | `octocat` |
| **Level** | Log severity level | `ERROR`, `WARNING`, `INFO`, `DEBUG` |
| **Start Time / End Time** | Restrict results to a time range | Datetime picker |
| **Results Limit** | Maximum entries to return | `100`, `500`, `1000`, `5000`, `10000` |

Filters are applied as you type (with a 300ms debounce). The server re-queries with each filter change to provide accurate, backend-filtered results.

Click **Clear Filters** to reset all filters at once.

### Reading Log Entries

Each log entry shows three columns:

1. **Timestamp** — when the event occurred (displayed in your local timezone)
2. **Level** — severity badge (`INFO`, `WARNING`, `ERROR`, `SUCCESS`, `STEP`, `DEBUG`, `COMPLETED`)
3. **Message** — the log message followed by clickable metadata tags for Hook ID, PR number, repository, and user

### Statistics Bar

Below the filters, three counters help you understand the dataset:

- **Shown** — number of entries currently displayed
- **Total** — estimated total log entries across all log files
- **Scanned** — entries the server examined for the last query (a `+` suffix and "(partial scan)" label indicate more logs exist beyond what was scanned)

## Real-Time Log Streaming

Click **Start Real-time** to open a WebSocket connection that streams new log entries as they arrive. The current filter settings are applied to the stream — you only see entries matching your active filters.

Click **Stop Real-time** to disconnect. The connection status indicator at the top of the page shows whether streaming is active.

> **Note:** Enable **Auto-scroll** (toggle in the controls) to keep the newest entries visible as they arrive. Disable it when you need to read through older entries without being scrolled away.

## Viewing Webhook Flow Timelines

Click any **Hook ID** link in a log entry to open the **Webhook Flow Timeline** modal. This shows:

- **Flow Overview** — hook ID, total steps, processing duration, token spend (API calls made), and repository
- **Step-by-step timeline** — each workflow step with its status icon (✓ success, ✗ error, ◷ in-progress), relative timing, and duration
- **Final status** — whether the flow completed successfully, with errors, or is still running

Steps are grouped by task ID. Click a group header to expand and see individual steps. Click any step to view its detailed execution metadata and associated log entries.

### Viewing All Events for a PR

Click any **PR number** link in a log entry to open the **PR Workflow** modal. This lists every unique webhook delivery ID associated with that PR. Click any event in the list to jump to its flow timeline.

## Exporting Logs

Click **Export JSON** to download the currently filtered logs as a JSON file. The export respects all active filters and the results limit.

The downloaded file includes:

- **Export metadata** — timestamp, applied filters, and entry count
- **Log entries** — the full array of matching log entries

The file is named `webhook_logs_YYYYMMDD_HHMMSS.json`.

> **Tip:** Increase the **Results Limit** before exporting if you need more than the default number of entries.

## Advanced Usage

### Switching Themes

Click the theme toggle button (🌙/☀️) in the top-right corner to switch between light and dark mode. Your preference is saved in the browser.

### Collapsing the Filter Panel

Click the **▼** button next to "Filters & Controls" to collapse the filter panel and give more screen space to log entries. The collapsed state is remembered across page loads.

### Drilling Into Step Logs

In the flow timeline modal, clicking a step fetches the actual log entries that occurred during that step's execution window. This is useful for understanding exactly what happened during a specific operation — for example, why a container build failed or which GitHub API call hit a rate limit.

Each step detail view shows:

- **Status badge** and **duration**
- **Error details** (if the step failed)
- **Execution metadata** from the structured log
- **Time-correlated log entries** from the text logs

### Using the REST API Directly

The log viewer is backed by a REST API that you can call programmatically. See [Log Viewer API Reference](log-viewer-api.html) for the complete endpoint documentation, including:

- `GET /logs/api/entries` — query and filter log entries
- `GET /logs/api/export` — export filtered logs as JSON
- `GET /logs/api/workflow-steps/{hook_id}` — get the step timeline for a webhook delivery
- `GET /logs/api/step-logs/{hook_id}/{step_name}` — get log entries for a specific step
- `GET /logs/api/pr-flow/{hook_id}` — get PR flow visualization data
- `WebSocket /logs/ws` — real-time log streaming with server-side filtering

## Troubleshooting

**Log viewer page returns 404**
- Verify that `ENABLE_LOG_SERVER=true` is set (literal string `true`). Restart the server after changing environment variables.

**No log entries appear**
- Check that `WEBHOOK_SERVER_DATA_DIR` points to a directory containing a `logs/` subdirectory with `.log` or `webhooks_*.json` files.
- Ensure the server process has read permissions on the log files.

**WebSocket disconnects immediately**
- The log viewer checks `ENABLE_LOG_SERVER` on WebSocket connect. If the environment variable was changed after startup, restart the server.

**Sensitive data visible in logs**
- Set `mask-sensitive-data: true` in your `config.yaml` (this is the default). See [Configuring Repositories](configuring-repositories.html) for per-repository overrides.

**"(partial scan)" shown in statistics**
- The server caps how many entries it scans per query for performance. Add more specific filters (hook ID, repository, time range) to narrow the scan, or increase the results limit.

## Related Pages

- [Log Viewer API Reference](log-viewer-api.html)
- [Environment Variables](environment-variables.html)
- [Deploying with Docker](deploying-with-docker.html)
- [Configuration Reference](configuration-reference.html)
- [MCP Server for AI Agents](mcp-server-integration.html)

---

Source: setting-up-notifications.md

Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly.

Now let me check the examples/config.yaml for the full context of how slack-webhook-url sits in the config structure:

Now let me look at the schema definition more carefully for the `slack-webhook-url` field and check if it can be set globally:

Now let me check if slack-webhook-url is also defined at the global level or only at the repository level:

So `slack-webhook-url` is defined only within repository config (`additionalProperties` under `repositories`), not at global level. Let me confirm by checking the schema structure:

Let me check if there's a global-level `slack-webhook-url` in the schema:

Good. The `slack-webhook-url` field is only available at the per-repository level. Let me also look at how the per-repo config in `.github-webhook-server.yaml` can set it:

Now let me check what specific events trigger Slack notifications - the runner_handler container push section more broadly:

Let me also look at the test for notification_utils to understand error scenarios:

Now let me check how `mask-sensitive-data` handles slack URLs:

Now let me check if there's a `.github-webhook-server.yaml` schema definition that includes `slack-webhook-url`:

Now I have a thorough understanding of the Slack notification feature. Let me write the documentation.

# Setting Up Slack Notifications

Get notified in Slack when your PyPI packages are published and container images are pushed (or fail to push). This guide walks you through connecting a Slack incoming webhook to your repositories.

## Prerequisites

- A running github-webhook-server instance with at least one repository configured
- A Slack workspace where you can create incoming webhooks
- PyPI publishing or container builds configured for the repository (notifications only fire for these events)

## Quick Example

Add `slack-webhook-url` to your repository configuration in either `config.yaml` or `.github-webhook-server.yaml`:

```yaml
slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
```

That's it. Once configured, you'll receive Slack messages when:

- A Python package version is published to PyPI
- A container image is successfully pushed to a registry
- A container image push fails

## Step-by-Step Setup

### 1. Create a Slack Incoming Webhook

1. Go to [https://api.slack.com/apps](https://api.slack.com/apps) and create a new app (or use an existing one).
2. Under **Incoming Webhooks**, toggle the feature on.
3. Click **Add New Webhook to Workspace** and select the channel where you want notifications.
4. Copy the webhook URL. It looks like:
   ```
   https://slack-webhook-url/replace-with-your-webhook-url
   ```

### 2. Add the Webhook URL to Your Repository

You have two options for where to place the configuration.

**Option A: Server-side in `config.yaml`**

Add `slack-webhook-url` under the repository entry:

```yaml
repositories:
  my-repo:
    name: my-org/my-repository
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    pypi:
      token: <PYPI TOKEN>
    container:
      username: my-user
      password: my-password
      repository: quay.io/my-org/my-repo
      tag: latest
      release: true
```

**Option B: In-repository `.github-webhook-server.yaml`**

Place a `.github-webhook-server.yaml` file in the root of your GitHub repository:

```yaml
slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
```

> **Tip:** Values in `.github-webhook-server.yaml` take precedence over `config.yaml`. Use the in-repository file when different teams manage their own notification channels.

### 3. Verify It Works

Push a tag to a repository that has PyPI publishing configured. For example:

```bash
git tag v1.0.0
git push origin v1.0.0
```

If the PyPI upload succeeds, you'll see a message in your Slack channel like:

```
my-org/my-repository Version v1.0.0 published to PYPI.
```

## What Triggers Notifications

Slack notifications are not a general-purpose event stream — they fire only for specific release-related operations:

| Event | Notification sent? | Message content |
|---|---|---|
| PyPI package published successfully | ✅ Yes | `<repo> Version <tag> published to PYPI.` |
| Container image pushed successfully | ✅ Yes | `<repo> New container for <image:tag> published.` |
| Container image push failed | ✅ Yes | `<repo> Failed to build and push <image:tag>.` |
| PR opened/closed/merged | ❌ No | — |
| Check runs (tox, pre-commit) | ❌ No | — |
| Cherry-picks | ❌ No | — |
| Label changes | ❌ No | — |

> **Note:** If `slack-webhook-url` is not set for a repository, notifications are silently skipped. No errors are logged.

## Advanced Usage

### Different Webhook URLs Per Repository

Each repository can have its own Slack webhook URL pointing to a different channel. There is no global `slack-webhook-url` setting — you configure it per repository:

```yaml
repositories:
  frontend:
    name: my-org/frontend
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    container:
      # ...

  backend:
    name: my-org/backend
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    pypi:
      token: <TOKEN>
```

### Sensitive Data Masking

The webhook URL is treated as sensitive data. When `mask-sensitive-data` is enabled (the default), Slack webhook URLs are automatically redacted in log output. This prevents accidental exposure of the URL in logs.

> **Warning:** Do not set `mask-sensitive-data: false` in production. Your Slack webhook URL will appear in plaintext in logs. See [Configuration Reference](configuration-reference.html) for details on the masking behavior.

### Combining with Container and PyPI Workflows

Slack notifications work alongside container builds and PyPI publishing — not independently. You must configure at least one of these for the repository to receive any notifications:

- **PyPI publishing** requires a `pypi.token` in your config. See [Setting Up CI Checks](setting-up-ci-checks.html) for details.
- **Container push on release** requires a `container` block with `release: true`. See [Setting Up CI Checks](setting-up-ci-checks.html) for container build configuration.

A minimal config that enables both container push notifications and PyPI publish notifications:

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    pypi:
      token: <PYPI TOKEN>
    container:
      username: my-user
      password: my-password
      repository: quay.io/my-org/my-repo
      tag: latest
      release: true
```

## Troubleshooting

**No notifications appearing in Slack**

- Confirm `slack-webhook-url` is set at the repository level, not at the root of `config.yaml`. It is a per-repository setting only.
- Verify the event is one that triggers notifications (PyPI publish or container push). PR merges and check runs do not send Slack messages.
- Check that the triggering workflow completed — a PyPI upload that fails before finishing does not send a notification.

**Error: "Request to slack returned an error 401"**

The webhook URL is invalid or has been revoked. Generate a new incoming webhook in your Slack app settings and update the configuration.

**Error: "Request to slack returned an error 404"**

The webhook URL endpoint no longer exists. This typically happens when the Slack app or the webhook has been deleted. Recreate it in [Slack's API dashboard](https://api.slack.com/apps).

**Connection timeout errors**

The server uses a 10-second timeout for Slack webhook requests. If your network is slow or Slack is experiencing an outage, the notification will fail but the underlying operation (PyPI publish, container push) will still complete normally.

> **Tip:** Slack is a notification layer, not the source of truth. Your actual release state lives in GitHub, your container registry, and PyPI. A failed Slack notification does not mean the release failed.

## Related Pages

- [Configuring Repositories](configuring-repositories.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Publishing Packages to PyPI](publishing-to-pypi.html)
- [Configuration Reference](configuration-reference.html)
- [Configuration Recipes](config-recipes.html)

---

Source: pr-command-recipes.md

Let me explore the repository structure and understand the codebase first.

Now let me look at the constants to understand the exact command strings:

Now let me look at the runner handler for understanding retest, rebase, cherry-pick, and container build:

Now let me check the `current_pull_request_supported_retest` to understand what tests are retestable:

Now let me look at the welcome message builder to see what commands it displays:

Now let me look at the merge requirements and automerge sections in the welcome message:

Let me check the retest welcome comment and the add-allowed-user/test-oracle commands:

Let me see how the retest welcome comment is built:

Now let me check the `is_user_valid_to_run_commands` to understand command permissions:

Let me now check the security-override section and the merge-requirements section:

Now let me also check if there are any `allow-commands-on-draft-prs` details and check the test-oracle command:

Now let me look at the build-and-push-container command args to understand what extra args are supported:

Let me also check for multi-command support (multiple commands in a single comment):

Good - multiple commands in a single comment are parsed line by line. Now let me look at the examples config to understand what's available:

Now I have a thorough understanding of all PR comment commands. Let me also check the custom check runs configuration:

Now I have all the information I need. Let me write the complete documentation page:

# PR Comment Command Recipes

Copy-paste PR comment commands for common workflows. Post these as comments on any pull request to trigger server actions.

> **Note:** Commands must start with `/` at the beginning of a line. You can combine multiple commands in a single comment — one per line. The server reacts with 👍 to acknowledge each command.

## Re-run a Single Failing Check

Re-trigger a specific check that failed without pushing a new commit.

```
/retest tox
```

Replace `tox` with the exact check name. Built-in check names are: `tox`, `build-container`, `pre-commit`, `python-module-install`, `conventional-title`, `security-suspicious-paths`, `security-committer-identity`. Custom check names match the `name` field in your `custom-check-runs` config.

> **Tip:** The welcome message on your PR lists every retestable check for that repository.

## Re-run All Checks

Re-trigger every configured check for the PR at once.

```
/retest all
```

This runs all checks in parallel — tox, pre-commit, container build, custom checks, and security checks (whichever are configured for the repository).

> **Warning:** `/retest all` cannot be combined with individual check names. Use either `all` or specific names — not both.

## Re-run Multiple Specific Checks

Re-trigger only the checks you need.

```
/retest tox pre-commit
```

List check names separated by spaces. Any unrecognized name will be reported back as a comment on the PR.

## Cherry-Pick to a Release Branch

Schedule an automatic cherry-pick to a target branch when the PR merges.

```
/cherry-pick v1.0
```

This adds a `cherry-pick-v1.0` label to the PR. When the PR is merged, the server automatically cherry-picks the merge commit to the `v1.0` branch and opens a new PR.

- If the target branch does not exist, the server posts an error comment.
- If the PR is already merged, the cherry-pick executes immediately.
- If AI conflict resolution is configured, merge conflicts are resolved automatically. See [Enabling AI Features](enabling-ai-features.html) for setup.

## Cherry-Pick to Multiple Branches

Cherry-pick to several release branches in one command.

```
/cherry-pick v1.0 v2.0 release-3.x
```

Each branch gets its own `cherry-pick-<branch>` label and its own cherry-pick PR after merge.

## Retry a Failed Cherry-Pick

Re-run a cherry-pick that previously failed (closes the old cherry-pick PR and creates a new one).

```
/cherry-pick-retry v1.0
```

This only works on **merged PRs** where the `cherry-pick-v1.0` label already exists. It closes any existing failed cherry-pick PR created by the bot and retries the operation.

- Only accepts one branch name at a time.
- To cherry-pick to a new branch (no existing label), use `/cherry-pick <branch>` instead.

## Rebase a PR onto Its Base Branch

Rebase the PR branch onto the latest base branch and force-push.

```
/rebase
```

The server checks out the PR branch, rebases it onto `origin/<base-branch>`, and force-pushes with `--force-with-lease`. If conflicts arise, the rebase is aborted and the server posts the error output.

- Only the **PR owner** or **maintainers** can rebase user-owned PRs.
- For bot-owned PRs (e.g., cherry-pick PRs), only the **PR assignee** or **maintainers** can rebase.
- Fork PRs cannot be rebased (the head branch is in a different repository).

## Override Security Checks

Force security check runs to pass when you've reviewed the flagged changes (maintainers only).

```
/security-override
```

This sets both `security-suspicious-paths` and `security-committer-identity` check runs to success. Only repository maintainers can use this command.

> **Warning:** This bypasses security gates. Only use after manually verifying the flagged file changes or committer identity are legitimate.

## Re-enable Security Checks After Override

Remove a previous security override and re-run security checks.

```
/security-override cancel
```

This re-evaluates the PR against the configured suspicious paths and committer identity rules, restoring the original check results.

## Trigger a Container Build and Push

Build a container image from the PR and push it to the configured registry.

```
/build-and-push-container
```

The image is tagged with the PR number. The server posts a comment with the published image tag on success.

- Requires `container` to be configured for the repository. See [Setting Up CI Checks](setting-up-ci-checks.html) for configuration details.
- You can pass additional podman build arguments:

```
/build-and-push-container --no-cache
```

## Approve a PR

Mark the PR as approved (approvers and maintainers only).

```
/approve
```

This adds the `approved-<username>` label and triggers the can-be-merged evaluation. If a test oracle is configured, it also runs automatically on approval. See [Enabling AI Features](enabling-ai-features.html) for test oracle setup.

## LGTM — Looks Good to Me

Add a lightweight review signal without full approval.

```
/lgtm
```

Adds the `lgtm-<username>` label. Some repositories require a minimum number of LGTMs before a PR can be merged (configured via `minimum-lgtm`).

## Enable Auto-Merge

Automatically merge the PR once all requirements are met (maintainers and approvers only).

```
/automerge
```

The server continuously evaluates merge requirements (approval, status checks, no blockers) and merges the PR when everything passes. See [Managing Pull Requests](managing-pull-requests.html) for details on merge requirements.

## Mark PR as Work in Progress

Block the PR from being merged and prefix the title with `WIP:`.

```
/wip
```

To remove WIP status and restore the original title:

```
/wip cancel
```

## Put a PR on Hold

Block merging without changing the title (approvers only).

```
/hold
```

To release the hold:

```
/hold cancel
```

## Mark PR as Verified

Add the `verified` label and set the verified check run to success.

```
/verified
```

To remove verification (resets the check run to queued):

```
/verified cancel
```

> **Note:** The `verified` label is automatically removed when new commits are pushed, unless the server detects a clean rebase.

## Check Merge Readiness

Ask the server to evaluate whether the PR meets all merge requirements.

```
/check-can-merge
```

The server checks approval status, required checks, labels, and conflicts, then updates the `can-be-merged` check run accordingly.

## Assign Reviewers from OWNERS File

Assign reviewers automatically based on the repository's OWNERS file.

```
/assign-reviewers
```

## Assign a Specific Reviewer

Request a review from a specific collaborator.

```
/assign-reviewer @alice
```

The `@` prefix is optional — `/assign-reviewer alice` works too. The user must be a repository collaborator.

## Grant Command Access to a Non-Collaborator

Allow an external contributor to run commands on this PR.

```
/add-allowed-user @contributor-name
```

This must be posted by a maintainer or approver. After this, the named user can run commands like `/retest` on the PR.

## Re-trigger PR Processing

Force the server to reprocess the entire PR workflow from scratch.

```
/reprocess
```

Useful when a webhook delivery failed or when the server configuration changed after the PR was opened.

> **Note:** This only works on **open** PRs.

## Regenerate the Welcome Message

Update the automated welcome comment to reflect current configuration.

```
/regenerate-welcome
```

Use this after changing OWNERS files, label configuration, or enabled features so the welcome comment shows accurate information.

## Run the Test Oracle

Trigger an AI-powered analysis of PR changes to recommend which tests to run.

```
/test-oracle
```

Requires test oracle configuration. See [Enabling AI Features](enabling-ai-features.html) for setup. This is the only command allowed on draft PRs by default.

## Combine Multiple Commands

Execute several actions in a single PR comment — one command per line.

```
/retest tox
/retest pre-commit
/verified
/cherry-pick v1.0 v2.0
```

All commands run in parallel. Each command gets its own 👍 reaction.

## Allow Specific Commands on Draft PRs

By default, all commands except `/test-oracle` are blocked on draft PRs. Configure allowed commands in your `config.yaml` to change this behavior.

To allow all commands on draft PRs, add to your repository config:

```yaml
allow-commands-on-draft-prs: []
```

To allow only specific commands:

```yaml
allow-commands-on-draft-prs:
  - build-and-push-container
  - retest
```

See [Configuring Repositories](configuring-repositories.html) for full configuration options.

## Quick Reference

| Command | Arguments | Who Can Run | Works on Draft? |
|---|---|---|---|
| `/retest` | `<check> [check2...]` or `all` | Collaborators, contributors, approvers | No* |
| `/cherry-pick` | `<branch> [branch2...]` | Approvers, maintainers | No* |
| `/cherry-pick-retry` | `<branch>` | Approvers, maintainers | No* |
| `/rebase` | — | PR owner, maintainers | No* |
| `/build-and-push-container` | Optional build args | Collaborators with permission | No* |
| `/approve` | — | Approvers only | No* |
| `/lgtm` | — | Anyone with access | No* |
| `/automerge` | — | Maintainers, approvers | No* |
| `/wip` | Optional: `cancel` | Collaborators, contributors, approvers | No* |
| `/hold` | Optional: `cancel` | Approvers only | No* |
| `/verified` | Optional: `cancel` | Collaborators, contributors, approvers | No* |
| `/check-can-merge` | — | Collaborators, contributors, approvers | No* |
| `/assign-reviewers` | — | Collaborators, contributors, approvers | No* |
| `/assign-reviewer` | `@username` | Collaborators, contributors, approvers | No* |
| `/add-allowed-user` | `@username` | Maintainers, approvers | No* |
| `/security-override` | Optional: `cancel` | Maintainers only | No* |
| `/reprocess` | — | Collaborators, contributors, approvers | No* |
| `/regenerate-welcome` | — | Collaborators, contributors, approvers | No* |
| `/test-oracle` | — | Collaborators, contributors, approvers | **Yes** |

*\* Blocked on draft PRs unless configured via `allow-commands-on-draft-prs`. See [Configuring Repositories](configuring-repositories.html).*

## Related Pages

- [Managing Pull Requests](managing-pull-requests.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html)
- [Enabling Security Checks](enabling-security-checks.html)
- [Configuring Repositories](configuring-repositories.html)

---

Source: config-recipes.md

Let me explore the repository structure and key files to write accurate documentation.

Now let me look at how the config resolution works for repo-specific overrides and more details on specific features:

Now let me look at specific handler implementations for custom check runs and protected branches:

Let me look at the runner handler for how custom check runs are executed and the `.github-webhook-server.yaml` format:

Now let me look at the `_validate_custom_check_runs` method and the auto-merge flow more closely:

Now I have enough information to write the comprehensive Configuration Recipes page. Let me check a couple more things:

Now I have comprehensive knowledge of the codebase. Let me write the Configuration Recipes page:

# Configuration Recipes

Ready-to-use `config.yaml` snippets for the most common github-webhook-server setups. Each recipe is self-contained and copy-paste ready.

For full option documentation, see the [Configuration Reference](configuration-reference.html). For per-repository file overrides, see [Configuring Repositories](configuring-repositories.html).

---

## Minimal Working Configuration

**The smallest config that will accept and process webhooks.**

```yaml
github-tokens:
  - ghp_your_github_token_here

webhook-ip: https://your-domain.com/webhook_server

repositories:
  my-repo:
    name: my-org/my-repo
```

This is the bare minimum: one GitHub token, a webhook URL, and one repository. All label categories are enabled by default, and the server listens on `0.0.0.0:5000`.

---

## Bot Auto-Merge (Renovate, Dependabot, pre-commit-ci)

**Automatically verify and merge PRs from trusted bots.**

```yaml
auto-verified-and-merged-users:
  - "renovate[bot]"
  - "pre-commit-ci[bot]"
  - "dependabot[bot]"

github-tokens:
  - ghp_token_one
  - ghp_token_two

webhook-ip: https://your-domain.com/webhook_server

repositories:
  my-repo:
    name: my-org/my-repo
    set-auto-merge-prs:
      - main
```

When a user listed in `auto-verified-and-merged-users` opens a PR, it is automatically verified and GitHub's auto-merge is enabled. The `set-auto-merge-prs` setting enables auto-merge for *all* PRs targeting the listed branches (not just bot PRs). Both settings work independently.

- `auto-verified-and-merged-users` is global — override per-repo by adding the same key under a repository.
- AI-resolved cherry-picks are **never** auto-merged regardless of these settings.
- PRs modifying [security-sensitive paths](enabling-security-checks.html) have auto-merge blocked automatically.

---

## Multi-Token Failover

**Use multiple GitHub tokens for automatic rate-limit failover.**

```yaml
github-tokens:
  - ghp_primary_token
  - ghp_secondary_token
  - ghp_tertiary_token

repositories:
  my-repo:
    name: my-org/my-repo
```

On every webhook, the server checks the rate limit of each token and selects the one with the highest remaining quota. If a single token is configured, it's used directly without rate-limit comparison. Invalid tokens (rate limit = 60) are automatically skipped.

- Override tokens per-repository with `github-tokens` under the repository block.
- Token order doesn't matter — selection is based on remaining rate limit.

### Per-Repository Token Override

```yaml
github-tokens:
  - ghp_org_wide_token

repositories:
  private-repo:
    name: my-org/private-repo
    github-tokens:
      - ghp_private_repo_token_1
      - ghp_private_repo_token_2
```

> **Tip:** Use repository-scoped tokens for private repos that require different access credentials.

---

## Conventional Commits Enforcement

**Require PR titles to follow the Conventional Commits specification.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
```

A `conventional-title` check run is created for each PR. Titles must match the format `<type>[optional scope]: <description>` where the type is one of the listed values. Invalid titles fail the check.

- Use `"*"` as a wildcard to accept any type while still enforcing the format structure.
- Breaking changes (`feat!: description`) and scopes (`fix(api): description`) are supported.

### With AI-Powered Auto-Fix

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"

ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: fix            # auto-fix invalid titles (use "suggest" for suggestions only)
    timeout-minutes: 10
```

When `mode: fix` is set, the server automatically updates the PR title using AI if it doesn't match the conventional format. Use `mode: suggest` to show a suggestion in the check run output without modifying the title. See [Enabling AI Features](enabling-ai-features.html) for provider setup.

---

## Custom Check Runs

**Run your own commands as GitHub check runs on every PR.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    custom-check-runs:
      - name: lint
        command: uv tool run --from ruff ruff check
        mandatory: true
      - name: security-scan
        command: uv tool run --from bandit bandit -r .
        mandatory: false
```

Each custom check runs the specified command in the repository worktree. Mandatory checks (`mandatory: true`, the default) block merging if they fail. Non-mandatory checks run but don't affect the `can-be-merged` status.

- Commands support environment variables and shell syntax: `TOKEN=xyz uv tool run --from bandit bandit -r .`
- Check names must not collide with built-in names (`tox`, `pre-commit`, `build-container`, `python-module-install`, `conventional-title`, `can-be-merged`, `security-suspicious-paths`, `security-committer-identity`).
- Duplicate check names are detected and the second occurrence is skipped.
- Custom checks can be retested with `/retest lint` in a PR comment.

### Multi-Line Command

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    custom-check-runs:
      - name: integration-test
        command: |
          uv run python -c "
          import sys
          print('Running integration tests')
          sys.exit(0)
          "
```

> **Warning:** The command executable must be installed on the webhook server. The server validates executables at startup with `shutil.which()` and skips checks with missing executables.

---

## Repository-Specific Overrides

**Override global settings for individual repositories.**

```yaml
# Global defaults
log-level: INFO
auto-verified-and-merged-users:
  - "renovate[bot]"
default-status-checks:
  - "WIP"
  - "can-be-merged"
create-issue-for-new-pr: true
labels:
  enabled-labels:
    - verified
    - hold
    - size
    - can-be-merged

repositories:
  strict-repo:
    name: my-org/strict-repo
    log-level: DEBUG                          # Override log level
    default-status-checks:                    # Override status checks
      - "WIP"
      - "can-be-merged"
      - "ci/integration"
    create-issue-for-new-pr: false            # Disable tracking issues
    auto-verified-and-merged-users:           # Override auto-verified users
      - "my-bot[bot]"
    labels:                                   # Override labels
      enabled-labels:
        - verified
        - hold
        - wip
        - size
        - can-be-merged
      colors:
        hold: purple

  relaxed-repo:
    name: my-org/relaxed-repo
    # Inherits all global defaults
```

Config values are resolved in priority order: (1) `.github-webhook-server.yaml` in the repository, (2) repository section in `config.yaml`, (3) root level in `config.yaml`. Repository-level settings completely replace (not merge with) their global counterparts.

> **Tip:** Place a `.github-webhook-server.yaml` file in a repository's root to let repository maintainers control their own settings without access to the server's `config.yaml`. See [Configuring Repositories](configuring-repositories.html).

---

## Tox CI Per Branch

**Run different tox test environments depending on the PR's target branch.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    tox:
      main: all                       # Run all tox envs for PRs targeting main
      dev: "testenv1,testenv2"        # Run specific envs for PRs targeting dev
      args: "-p -v"                   # Extra CLI args passed to tox
      python-version: "3.12"          # Python version for tox execution
```

The `tox` key maps branch names to tox environments. Use `all` to run every environment in `tox.ini`, or a comma-separated string for specific ones. The `args` and `python-version` sub-keys apply to all branches.

---

## Protected Branches with Required Checks

**Configure which status checks are required for specific branches.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    protected-branches:
      main:
        include-runs:
          - "pre-commit.ci - pr"
          - "WIP"
        exclude-runs:
          - "SonarCloud Code Analysis"
      dev: []                         # All default checks, no customization
```

The `include-runs` list specifies external checks that must pass. The `exclude-runs` list removes checks from requirements. Use an empty array `[]` to accept all default checks without modification.

---

## Branch Protection Rules

**Set GitHub branch protection settings managed by the webhook server.**

```yaml
branch-protection:
  strict: true
  require_code_owner_reviews: true
  dismiss_stale_reviews: false
  required_approving_review_count: 1
  required_linear_history: true
  required_conversation_resolution: true

repositories:
  my-repo:
    name: my-org/my-repo
    branch-protection:
      strict: true
      require_code_owner_reviews: true
      dismiss_stale_reviews: true              # Override: dismiss stale reviews
      required_approving_review_count: 2       # Override: require 2 approvals
      required_linear_history: true
      required_conversation_resolution: true
```

Global `branch-protection` applies to all repositories. Override any field per-repository. The `required_conversation_resolution` setting also controls whether the server processes `pull_request_review_thread` webhook events. See [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html) for details.

---

## Custom PR Size Labels

**Define custom size categories and thresholds for PR size labels.**

```yaml
pr-size-thresholds:
  Tiny:
    threshold: 10
    color: lightgray
  Small:
    threshold: 50
    color: green
  Medium:
    threshold: 150
    color: orange
  Large:
    threshold: 300
    color: red
  Massive:
    threshold: inf
    color: darkred
```

Thresholds define the *minimum* number of total changed lines (additions + deletions) for each category. Use `inf` for the unbounded largest category — it always sorts last regardless of definition order. Override per-repository under the repository block.

### Repository-Specific Size Thresholds

```yaml
repositories:
  docs-repo:
    name: my-org/docs-repo
    pr-size-thresholds:
      Express:
        threshold: 25
        color: lightblue
      Standard:
        threshold: 100
        color: green
      Premium:
        threshold: 500
        color: orange
```

See [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html) for full details on label customization.

---

## Label Customization

**Control which label categories are enabled and set custom colors.**

```yaml
labels:
  enabled-labels:
    - verified
    - hold
    - size
    - can-be-merged
    - cherry-pick
  colors:
    hold: red
    verified: green
    can-be-merged: limegreen
    approved-: green           # Prefix for dynamic labels (approved-username)
    lgtm-: yellowgreen
    cherry-pick-: coral
    branch-: royalblue
```

If `enabled-labels` is not set, all categories are enabled. Reviewed-by labels (`approved-*`, `lgtm-*`, `changes-requested-*`, `commented-*`) are always enabled and cannot be disabled. Colors use CSS3 color names.

> **Note:** Available categories: `verified`, `hold`, `wip`, `needs-rebase`, `has-conflicts`, `can-be-merged`, `size`, `branch`, `cherry-pick`, `automerge`.

---

## Security Checks

**Detect suspicious file modifications and committer identity mismatches.**

```yaml
security-checks:
  mandatory: true
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
    - ".claude/"
    - ".vscode/"
    - ".cursor/"
    - ".devcontainer/"
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"
```

When `mandatory: true` (default), failed security checks block the `can-be-merged` status. Set to `false` for advisory-only mode. The GitHub App bot, `web-flow`, and API token users are automatically trusted — only add additional external committers to `trusted-committers`. See [Enabling Security Checks](enabling-security-checks.html).

### Advisory-Only Security (Non-Blocking)

```yaml
security-checks:
  mandatory: false
  committer-identity-check: true
  suspicious-paths:
    - ".github/workflows/"
```

---

## Container Build and Push

**Build and push container images on PR events and releases.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    container:
      username: myuser
      password: registry-secret-token
      repository: ghcr.io/my-org/my-repo
      tag: latest
      release: true
      build-args:
        - BUILD_ENV=production
        - VERSION=1.0
      args:
        - --format docker
      context: ""              # Repo root (default). Use "src" for subdirectory.
      oci-annotations:
        enabled: true
        static:
          org.opencontainers.image.vendor: "My Organization"
          org.opencontainers.image.licenses: "Apache-2.0"
        auto:
          created: true
          source: true
          revision: true
          version: true
          title: true
```

The `release: true` flag pushes the image with the release tag on new tag pushes. Container builds can be triggered manually with `/build-and-push-container` in a PR comment. See [Setting Up CI Checks](setting-up-ci-checks.html) for details.

---

## Slack Notifications

**Send webhook processing notifications to Slack.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
```

Notifications are sent for PR merges, container builds, PyPI uploads, and other processing events. See [Setting Up Slack Notifications](setting-up-notifications.html) for setup instructions.

---

## Cherry-Pick Auto-Verification

**Control whether cherry-picked PRs are automatically verified.**

```yaml
# Global: auto-verify all cherry-picks (default)
auto-verify-cherry-picked-prs: true

repositories:
  critical-repo:
    name: my-org/critical-repo
    auto-verify-cherry-picked-prs: false   # Require manual verification
```

When enabled (default), cherry-picked PRs receive the `verified` label automatically. Disable per-repository for critical repos that require manual review of every cherry-pick. AI-resolved cherry-picks are **never** auto-verified regardless of this setting.

---

## Required Labels for Merge

**Require specific labels before a PR can be marked as mergeable.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    can-be-merged-required-labels:
      - qa-approved
      - docs-reviewed
```

The `can-be-merged` check run will not pass until all listed labels are present on the PR, in addition to all other merge requirements (approvals, passing checks, etc.).

---

## Draft PR Command Allowlist

**Allow specific commands on draft PRs.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    allow-commands-on-draft-prs:
      - build-and-push-container
      - retest
```

By default, all commands are blocked on draft PRs. Set an empty list `[]` to allow all commands. Set a list of specific command names to allow only those. See [Managing Pull Requests](managing-pull-requests.html) for available commands.

---

## LGTM Requirements

**Require multiple LGTM approvals before a PR can be merged.**

```yaml
repositories:
  my-repo:
    name: my-org/my-repo
    minimum-lgtm: 2
```

The PR must receive at least the specified number of `/lgtm` commands from different users before it is approved. The default is `0` (no minimum).

---

## PR Welcome Message Customization

**Add custom information to the automated PR welcome comment.**

```yaml
welcome-extra-info: |
  **Note:** Please review the contribution guide before merging.
  - Ensure tests pass
  - Update documentation if needed

repositories:
  my-repo:
    name: my-org/my-repo
    welcome-extra-info: |
      **Project-specific notes:**
      - Run `make docs` if you changed API endpoints
      - Tag @platform-team for infrastructure changes
```

The `welcome-extra-info` content is appended to the end of the PR welcome comment as raw markdown. Repository-level settings override the global value. Set an empty string `""` to explicitly clear an inherited value. Maximum size is 10 KB.

> **Tip:** You can also place a `.github-webhook-server-welcome-message.md` file in the repository root — it takes priority over all config-based settings.

---

## Full Production Setup

**A complete production-ready configuration combining multiple features.**

```yaml
log-level: INFO
log-file: webhook-server.log
mask-sensitive-data: true
disable-ssl-warnings: true

github-app-id: 123456
github-tokens:
  - ghp_primary_token
  - ghp_secondary_token

webhook-ip: https://webhooks.example.com/webhook_server
webhook-secret: your-webhook-secret-here
verify-github-ips: true
ip-bind: "0.0.0.0"
port: 5000

default-status-checks:
  - "WIP"
  - "can-be-merged"

auto-verified-and-merged-users:
  - "renovate[bot]"
  - "pre-commit-ci[bot]"

branch-protection:
  strict: true
  require_code_owner_reviews: true
  dismiss_stale_reviews: false
  required_approving_review_count: 1
  required_linear_history: true
  required_conversation_resolution: true

security-checks:
  mandatory: true
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"

ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true

repositories:
  backend-api:
    name: my-org/backend-api
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    verified-job: true
    pre-commit: true
    conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"
    minimum-lgtm: 1
    set-auto-merge-prs:
      - main
    tox:
      main: all
      dev: "unit,integration"
      python-version: "3.12"
    container:
      username: myuser
      password: registry-token
      repository: ghcr.io/my-org/backend-api
      tag: latest
      release: true
    custom-check-runs:
      - name: lint
        command: uv tool run --from ruff ruff check
      - name: type-check
        command: uv tool run --from mypy mypy src/
    protected-branches:
      main:
        include-runs:
          - "pre-commit.ci - pr"
      dev: []
```

> **Warning:** Never commit tokens or secrets directly in `config.yaml`. Use environment variables or a secret management system. See [Environment Variables](environment-variables.html).

## Related Pages

- [Configuration Reference](configuration-reference.html)
- [Configuring Repositories](configuring-repositories.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Enabling Security Checks](enabling-security-checks.html)
- [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html)

---

Source: configuration-reference.md

Let me start by reading the knowledge graph and understanding the project structure.

Now let me look at the repository-local config (`.github-webhook-server.yaml`) handling and more details:

Now let me check what's available in `.github-webhook-server.yaml` (what keys it can contain):

Let me check the `verify-github-ips` and `verify-cloudflare-ips` usage:

Now let me look at the `.github-webhook-server.yaml` more closely - what keys does it support:

Let me look at the `welcome-message.md` file pattern and remaining config usage:

Now let me look at tox config more closely and check events config:

Now let me check the OWNERS file support:

Let me also check the `verify-github-ips` and IP allowlist implementation:

Now let me check the `disable-ssl-warnings` key and look for all the config keys used at the app level:

Now let me check the `.github-webhook-server.yaml` in-repo config more carefully - specifically what keys it can accept. It should be basically the same as the repository-level config:

Now I have a comprehensive understanding. Let me verify the `dockerfile` key usage:

Now I have enough information. Let me write the comprehensive Configuration Reference page.

# Configuration Reference

Complete reference for all configuration options in `config.yaml` (global server config) and `.github-webhook-server.yaml` (per-repository in-repo config).

## Configuration Files Overview

| File | Location | Purpose |
|---|---|---|
| `config.yaml` | `$WEBHOOK_SERVER_DATA_DIR/config.yaml` | Main server config: global settings + per-repository settings |
| `.github-webhook-server.yaml` | Repository root (committed to repo) | Per-repository overrides (highest priority) |
| `.github-webhook-server-welcome-message.md` | Repository root (committed to repo) | Custom PR welcome message content (overrides `welcome-extra-info`) |
| `OWNERS` | Any directory in repo | Approver/reviewer definitions for OWNERS-based approval workflow |

### Config Resolution Order

Values are resolved in this order (first match wins):

1. `.github-webhook-server.yaml` (in-repo file)
2. Repository-level settings in `config.yaml` (under `repositories.<name>`)
3. Global (root-level) settings in `config.yaml`

> **Note:** Dot notation is supported for nested lookups (e.g., `docker.username`, `pypi.token`).

---

## Global Server Settings

These settings are defined at the root level of `config.yaml` only. They cannot be set in `.github-webhook-server.yaml`.

### `log-level`

| Property | Value |
|---|---|
| Type | `string` |
| Allowed values | `INFO`, `DEBUG` |
| Default | — |

Global log level. Changes take effect immediately without server restart.

```yaml
log-level: INFO
```

### `log-file`

| Property | Value |
|---|---|
| Type | `string` |
| Default | — |

File path for the main log file. Changes take effect immediately without server restart.

```yaml
log-file: webhook-server.log
```

### `mcp-log-file`

| Property | Value |
|---|---|
| Type | `string` |
| Default | `mcp_server.log` |

File path for the MCP server log file.

```yaml
mcp-log-file: mcp_server.log
```

### `logs-server-log-file`

| Property | Value |
|---|---|
| Type | `string` |
| Default | `logs_server.log` |

File path for the Logs Server log file.

```yaml
logs-server-log-file: logs_server.log
```

### `mask-sensitive-data`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Mask sensitive data (tokens, passwords, secrets) in logs. Can be overridden per repository.

```yaml
mask-sensitive-data: true
```

> **Warning:** Setting to `false` in production will expose secrets in log files.

### `github-app-id`

| Property | Value |
|---|---|
| Type | `integer` |
| Default | — |

The GitHub App ID used by the webhook server for repository management.

```yaml
github-app-id: 123456
```

### `github-tokens`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | — |

Global GitHub personal access tokens. Multiple tokens enable automatic failover — the server selects the token with the highest remaining rate limit. Can be overridden per repository.

```yaml
github-tokens:
  - ghp_token1abc123
  - ghp_token2def456
```

### `webhook-ip`

| Property | Value |
|---|---|
| Type | `string` (URI) |
| Default | — |

Full webhook URL including path. This is registered on each managed repository as the webhook endpoint.

```yaml
webhook-ip: https://your-domain.com/webhook_server
```

> **Tip:** For local development, use a [smee.io](https://smee.io) channel: `https://smee.io/your-channel`.

### `ip-bind`

| Property | Value |
|---|---|
| Type | `string` |
| Default | `0.0.0.0` |

IP address to bind the HTTP server to.

```yaml
ip-bind: 0.0.0.0
```

### `port`

| Property | Value |
|---|---|
| Type | `integer` |
| Default | `5000` |

Port to bind the HTTP server to.

```yaml
port: 5000
```

### `max-workers`

| Property | Value |
|---|---|
| Type | `integer` |
| Default | `10` |

Maximum number of uvicorn worker processes. Only used in production mode (not in dev mode with `WEBHOOK_SERVER_DEV_MODE=true`).

```yaml
max-workers: 10
```

### `webhook-secret`

| Property | Value |
|---|---|
| Type | `string` |
| Default | — |

Shared secret for validating GitHub webhook payloads via HMAC-SHA256 signature. When set, the server verifies the `x-hub-signature-256` header on every incoming request.

```yaml
webhook-secret: my-super-secret-value
```

### `verify-github-ips`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `false` |

Restrict incoming webhooks to GitHub's published IP ranges. At startup, the server fetches the GitHub meta API to build an IP allowlist.

```yaml
verify-github-ips: true
```

### `verify-cloudflare-ips`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `false` |

Restrict incoming webhooks to Cloudflare's published IP ranges. Use when the server sits behind a Cloudflare proxy.

```yaml
verify-cloudflare-ips: true
```

> **Note:** `verify-github-ips` and `verify-cloudflare-ips` can be combined. If both are enabled, requests from either range are accepted. If enabled but IP lists fail to load, the server refuses to start.

### `disable-ssl-warnings`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `false` |

Disable urllib3 SSL warnings. Useful in production environments with internal CAs to reduce log noise.

```yaml
disable-ssl-warnings: true
```

### `docker`

Docker Hub credentials for pulling base images during container builds.

| Key | Type | Description |
|---|---|---|
| `username` | `string` | Docker Hub username |
| `password` | `string` | Docker Hub password |

```yaml
docker:
  username: my-docker-user
  password: my-docker-password
```

---

## Global Defaults

These settings are defined at the root level of `config.yaml` and serve as defaults. They can be overridden at the repository level in `config.yaml` or in `.github-webhook-server.yaml`.

### `default-status-checks`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | — |

Status checks included when configuring branch protection required status checks. The `can-be-merged` check is always appended automatically.

```yaml
default-status-checks:
  - "WIP"
  - "dpulls"
  - "can-be-merged"
```

### `auto-verified-and-merged-users`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | `[]` |

Users whose PRs are automatically verified and merged. Typically used for bots like Renovate or pre-commit CI.

```yaml
auto-verified-and-merged-users:
  - "renovate[bot]"
  - "pre-commit-ci[bot]"
```

### `auto-verify-cherry-picked-prs`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Automatically add the `verified` label to cherry-picked PRs.

```yaml
auto-verify-cherry-picked-prs: true
```

### `create-issue-for-new-pr`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Create a tracking issue for each new pull request.

```yaml
create-issue-for-new-pr: true
```

### `cherry-pick-assign-to-pr-author`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Assign cherry-pick PRs to the original PR author.

```yaml
cherry-pick-assign-to-pr-author: true
```

### `allow-commands-on-draft-prs`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | not set (commands blocked on drafts) |

Controls which PR comment commands are allowed on draft PRs.

| Configuration | Behavior |
|---|---|
| Not set (default) | All commands blocked on draft PRs |
| Empty list `[]` | All commands allowed on draft PRs |
| List with values | Only listed commands allowed on draft PRs |

```yaml
# Allow all commands on draft PRs
allow-commands-on-draft-prs: []

# Allow only specific commands
allow-commands-on-draft-prs:
  - build-and-push-container
  - retest
```

### `welcome-extra-info`

| Property | Value |
|---|---|
| Type | `string` |
| Max length | 10,240 bytes |
| Default | `""` |

Additional markdown content appended to the PR welcome message. An empty string explicitly clears any inherited value.

```yaml
welcome-extra-info: |
  **Note:** Please review the contribution guide before merging.
  - Ensure tests pass
  - Update documentation if needed
```

> **Tip:** For larger welcome message content, commit a `.github-webhook-server-welcome-message.md` file to the repository root. It takes priority over all config-based `welcome-extra-info` settings.

---

## Labels Configuration

Controls which labels the server manages and their colors. Can be set globally or per repository. See [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html) for usage details.

### `labels`

#### `labels.enabled-labels`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | all categories enabled |

List of label categories to enable. If not set, all categories are active.

| Category | Labels managed |
|---|---|
| `verified` | `verified` |
| `hold` | `hold` |
| `wip` | `wip` |
| `needs-rebase` | `needs-rebase` |
| `has-conflicts` | `has-conflicts` |
| `can-be-merged` | `can-be-merged` |
| `size` | `size/XS`, `size/S`, `size/M`, `size/L`, `size/XL`, `size/XXL` (or custom thresholds) |
| `branch` | `branch-<name>` |
| `cherry-pick` | `cherry-pick-<branch>`, `CherryPicked`, `ai-resolved-conflicts` |
| `automerge` | `automerge` |

> **Note:** Reviewed-by labels (`approved-<user>`, `lgtm-<user>`, `changes-requested-<user>`, `commented-<user>`) are always enabled and cannot be disabled.

```yaml
labels:
  enabled-labels:
    - verified
    - hold
    - size
    - can-be-merged
```

#### `labels.colors`

| Property | Value |
|---|---|
| Type | `object` (label name → CSS3 color) |
| Default | built-in color scheme |

Custom colors for labels. Use the exact label name for static labels, or the prefix for dynamic labels.

| Default label / prefix | Default color |
|---|---|
| `hold` | `B60205` (red) |
| `verified` | `0E8A16` (green) |
| `wip` | `B60205` (red) |
| `automerge` | `0E8A16` (green) |
| `needs-rebase` | `B60205` (red) |
| `can-be-merged` | `0E8A17` (green) |
| `has-conflicts` | `B60205` (red) |
| `approved-` | `0E8A16` (green) |
| `lgtm-` | `DCED6F` (yellow-green) |
| `changes-requested-` | `F5621C` (orange) |
| `commented-` | `D93F0B` (dark orange) |
| `cherry-pick-` | `F09C74` (salmon) |
| `branch-` | `1D76DB` (blue) |

```yaml
labels:
  colors:
    hold: red
    verified: green
    approved-: green
    lgtm-: yellowgreen
    cherry-pick-: coral
    branch-: royalblue
```

### `pr-size-thresholds`

| Property | Value |
|---|---|
| Type | `object` (category name → `{threshold, color}`) |
| Default | built-in XS/S/M/L/XL/XXL categories |

Custom PR size categories based on total lines changed (additions + deletions).

| Sub-key | Type | Required | Description |
|---|---|---|---|
| `threshold` | `integer` or `"inf"` | Yes | Minimum number of changed lines for this category |
| `color` | `string` | No | CSS3 color name for the label |

Categories are sorted by threshold. Each PR gets the label whose threshold it meets but whose next-higher threshold it does not. Use `"inf"` for the unbounded largest category.

```yaml
pr-size-thresholds:
  Tiny:
    threshold: 10    # 0–9 lines changed
    color: lightgray
  Small:
    threshold: 50    # 10–49 lines changed
    color: green
  Medium:
    threshold: 150   # 50–149 lines changed
    color: orange
  Large:
    threshold: 300   # 150–299 lines changed
    color: red
  Massive:
    threshold: inf   # 300+ lines changed
    color: darkred
```

---

## Branch Protection

Configures GitHub branch protection rules applied at server startup. Can be set globally or per repository. Repository-level settings override global.

### `branch-protection`

| Key | Type | Default | Description |
|---|---|---|---|
| `strict` | `boolean` | `true` | Require branches to be up to date before merging |
| `require_code_owner_reviews` | `boolean` | `false` | Require review from code owners |
| `dismiss_stale_reviews` | `boolean` | `true` | Dismiss approvals when new commits are pushed |
| `required_approving_review_count` | `integer` | `0` | Number of required approving reviews |
| `required_linear_history` | `boolean` | `true` | Require linear commit history |
| `required_conversation_resolution` | `boolean` | `true` | Require all PR review conversations to be resolved before merge |

```yaml
branch-protection:
  strict: true
  require_code_owner_reviews: true
  dismiss_stale_reviews: false
  required_approving_review_count: 1
  required_linear_history: true
  required_conversation_resolution: true
```

---

## Security Checks

Detects potentially malicious PR patterns. Can be set globally or per repository. See [Enabling Security Checks](enabling-security-checks.html) for usage details.

### `security-checks`

| Key | Type | Default | Description |
|---|---|---|---|
| `mandatory` | `boolean` | `true` | When `true`, security check failures block `can-be-merged`. When `false`, checks are advisory only. |
| `suspicious-paths` | `array` of `string` | See below | Path prefixes considered security-sensitive. PRs modifying files under these paths fail the `security-suspicious-paths` check run. |
| `committer-identity-check` | `boolean` | `true` | Compare PR author against the last commit's committer. Fails if they differ. |
| `trusted-committers` | `array` of `string` | `[]` | Committer logins always trusted for the identity check (case-insensitive). |

**Default suspicious paths:**
- `.claude/`
- `.vscode/`
- `.cursor/`
- `.devcontainer/`
- `.pi/`
- `.github/workflows/`
- `.github/actions/`

> **Note:** The GitHub App bot, `web-flow`, and API users from `github-tokens` are automatically added to the trusted committers list. Only list additional external committers.

```yaml
security-checks:
  mandatory: true
  suspicious-paths:
    - ".github/workflows/"
    - ".github/actions/"
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"
```

---

## AI Features

AI-powered enhancements using external AI CLI providers. Can be set globally or per repository. See [Enabling AI Features](enabling-ai-features.html) for usage details.

### `ai-features`

| Key | Type | Required | Description |
|---|---|---|---|
| `ai-provider` | `string` | Yes | AI CLI provider: `claude`, `gemini`, or `cursor` |
| `ai-model` | `string` | Yes | Model identifier (e.g., `claude-opus-4-6-1m`, `sonnet`, `gemini-2.5-pro`) |
| `conventional-title` | `object` | No | AI-powered conventional title suggestions |
| `resolve-cherry-pick-conflicts-with-ai` | `object` | No | AI-powered cherry-pick conflict resolution |

#### `ai-features.conventional-title`

| Key | Type | Default | Description |
|---|---|---|---|
| `enabled` | `boolean` | — (required) | Enable AI conventional title suggestions |
| `mode` | `string` | `suggest` | `suggest`: show suggestion in check run output. `fix`: auto-update the PR title. |
| `timeout-minutes` | `integer` | `10` | Timeout for the AI CLI process (minimum: 1) |

#### `ai-features.resolve-cherry-pick-conflicts-with-ai`

| Key | Type | Default | Description |
|---|---|---|---|
| `enabled` | `boolean` | — (required) | Enable AI conflict resolution for cherry-picks |
| `timeout-minutes` | `integer` | `10` | Timeout for the AI CLI process (minimum: 1) |

> **Note:** AI-resolved cherry-picks are never auto-verified — manual review is always required.

```yaml
ai-features:
  ai-provider: "claude"
  ai-model: "sonnet"
  conventional-title:
    enabled: true
    mode: suggest
    timeout-minutes: 10
  resolve-cherry-pick-conflicts-with-ai:
    enabled: true
    timeout-minutes: 10
```

---

## Test Oracle

PR Test Oracle integration that analyzes diffs with AI and recommends which tests to run. Can be set globally or per repository.

### `test-oracle`

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `server-url` | `string` (URI) | Yes | — | URL of the pr-test-oracle server |
| `ai-provider` | `string` | Yes | — | AI provider: `claude`, `gemini`, or `cursor` |
| `ai-model` | `string` | Yes | — | AI model identifier |
| `test-patterns` | `array` of `string` | No | oracle defaults | Glob patterns for test files |
| `triggers` | `array` of `string` | No | `[approved]` | When to automatically run analysis |

**Trigger values:**

| Trigger | Description |
|---|---|
| `approved` | Run when a PR review is approved |
| `pr-opened` | Run when a new PR is opened |
| `pr-synchronized` | Run when new commits are pushed to a PR |

> **Tip:** The `/test-oracle` comment command always works when configured, regardless of triggers.

```yaml
test-oracle:
  server-url: "http://localhost:8000"
  ai-provider: "claude"
  ai-model: "sonnet"
  test-patterns:
    - "tests/**/*.py"
  triggers:
    - approved
    - pr-opened
```

---

## Repository Settings

Each repository is defined under the `repositories` key in `config.yaml`. The repository key is an alias; the actual GitHub repository is identified by the `name` field.

```yaml
repositories:
  my-repo-alias:
    name: my-org/my-repository
    # ... repository-specific settings
```

### `name`

| Property | Value |
|---|---|
| Type | `string` |
| Required | Yes |

Full repository name in `org/repo` format.

```yaml
name: my-org/my-repository
```

### `log-level`

| Property | Value |
|---|---|
| Type | `string` |
| Allowed values | `INFO`, `DEBUG` |
| Default | inherits global |

Override the global log level for this repository.

```yaml
log-level: DEBUG
```

### `log-file`

| Property | Value |
|---|---|
| Type | `string` |
| Default | inherits global |

Override the global log file for this repository.

```yaml
log-file: my-repository.log
```

### `mask-sensitive-data`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Override the global sensitive data masking for this repository.

```yaml
mask-sensitive-data: false
```

### `slack-webhook-url`

| Property | Value |
|---|---|
| Type | `string` |
| Default | — |

Slack webhook URL for notifications on PR merges, container builds, PyPI uploads, and other events. See [Setting Up Slack Notifications](setting-up-notifications.html) for details.

```yaml
slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
```

### `verified-job`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `true` |

Enable the verified job check run functionality.

```yaml
verified-job: true
```

### `events`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | `["*"]` (all events) |

GitHub webhook events to listen to. If omitted, all events are subscribed. See [Webhook Events and Handlers](webhook-events-reference.html) for supported events.

```yaml
events:
  - push
  - pull_request
  - pull_request_review
  - pull_request_review_thread
  - issue_comment
  - check_run
  - status
```

### `github-tokens`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | inherits global |

Override global GitHub tokens for this repository. Supports multi-token failover.

```yaml
github-tokens:
  - ghp_repo_specific_token1
  - ghp_repo_specific_token2
```

### `default-status-checks`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | inherits global |

Override global default status checks for this repository.

```yaml
default-status-checks:
  - "WIP"
  - "can-be-merged"
  - "ci/my-external-check"
```

### `minimum-lgtm`

| Property | Value |
|---|---|
| Type | `integer` |
| Default | `0` |

Minimum number of LGTM approvals required before a PR can be approved.

```yaml
minimum-lgtm: 2
```

### `can-be-merged-required-labels`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | `[]` |

Additional labels required for a PR to receive the `can-be-merged` label.

```yaml
can-be-merged-required-labels:
  - qa-approved
  - docs-reviewed
```

### `set-auto-merge-prs`

| Property | Value |
|---|---|
| Type | `array` of `string` |
| Default | `[]` |

Branches for which auto-merge is automatically enabled on new PRs.

```yaml
set-auto-merge-prs:
  - main
  - release
```

### `conventional-title`

| Property | Value |
|---|---|
| Type | `string` |
| Default | — (disabled) |

Comma-separated list of allowed [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) types for PR title validation. Use `"*"` to accept any type while still enforcing the format `<type>[optional scope]: <description>`.

**Standard types:** `feat`, `fix`, `build`, `chore`, `ci`, `docs`, `style`, `refactor`, `perf`, `test`, `revert`

```yaml
# Specific types
conventional-title: "feat,fix,build,chore,ci,docs,style,refactor,perf,test,revert"

# Any type (wildcard) — enforces format only
conventional-title: "*"
```

See [Setting Up CI Checks](setting-up-ci-checks.html) for details on how validation integrates with check runs.

### `pypi`

PyPI publishing configuration. When set, the server runs a Python module install check on PRs.

| Key | Type | Description |
|---|---|---|
| `token` | `string` | PyPI API token for package publishing |

```yaml
pypi:
  token: pypi-AgEIcHlwaS5vcmc...
```

---

## Tox Configuration

Configures tox test execution per branch. Defined under the repository in `config.yaml`. See [Setting Up CI Checks](setting-up-ci-checks.html) for details.

### `tox`

The `tox` key maps branch names to tox environments, with optional sub-keys for extra configuration.

| Key | Type | Description |
|---|---|---|
| `<branch-name>` | `string` | Comma-separated tox environments, or `all` for all environments |
| `args` | `string` | Additional CLI arguments passed to tox (e.g., `-p -v`) |
| `python-version` | `string` | Python version for tox execution (e.g., `3.11`) |

```yaml
tox:
  args: "-p -v"
  python-version: "3.12"
  main: all
  dev: testenv1,testenv2
  feature: lint,test
```

### `tox-python-version` (deprecated)

| Property | Value |
|---|---|
| Type | `string` |
| Default | — |

> **Warning:** Deprecated. Use `tox.python-version` instead. This key still works but logs a warning.

```yaml
# Deprecated
tox-python-version: "3.11"

# Use instead
tox:
  python-version: "3.11"
```

---

## Pre-commit

### `pre-commit`

| Property | Value |
|---|---|
| Type | `boolean` |
| Default | `false` |

Enable pre-commit checks on pull requests. When enabled, the server runs `pre-commit run --all-files` in the PR worktree.

```yaml
pre-commit: true
```

---

## Container Build Configuration

Configures container image builds using Podman. See [Setting Up CI Checks](setting-up-ci-checks.html) for details.

### `container`

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `username` | `string` | Yes | — | Container registry username |
| `password` | `string` | Yes | — | Container registry password |
| `repository` | `string` | Yes | — | Full registry repository path (e.g., `quay.io/org/image`) |
| `tag` | `string` | No | `latest` | Image tag |
| `dockerfile` | `string` | No | `Dockerfile` | Path to Dockerfile (not in schema but supported in code) |
| `release` | `boolean` | No | `false` | Push image with release tag on new GitHub release |
| `build-args` | `array` of `string` | No | `[]` | Build arguments (e.g., `my-arg=value`) |
| `args` | `array` of `string` | No | `[]` | Additional podman build command arguments |
| `context` | `string` | No | `""` (repo root) | Subdirectory for Docker build context (alphanumeric, dots, hyphens, underscores, slashes only) |
| `oci-annotations` | `object` | No | disabled | OCI image annotation configuration |

```yaml
container:
  username: my-user
  password: my-password
  repository: quay.io/myorg/myimage
  tag: latest
  release: true
  context: src
  build-args:
    - BUILD_VERSION=1.0
  args:
    - --format docker
```

#### `container.oci-annotations`

| Key | Type | Default | Description |
|---|---|---|---|
| `enabled` | `boolean` | `false` | Enable OCI annotations on built images |
| `static` | `object` | `{}` | Static key-value annotation pairs (reverse domain notation recommended) |
| `auto` | `object` | all `true` when enabled | Auto-populated annotations from webhook context |

**Auto annotations (all default to `true` when `enabled` is `true`):**

| Key | OCI Annotation | Description |
|---|---|---|
| `created` | `org.opencontainers.image.created` | Build timestamp |
| `source` | `org.opencontainers.image.source` | Repository URL |
| `revision` | `org.opencontainers.image.revision` | Commit SHA |
| `version` | `org.opencontainers.image.version` | Tag on release builds |
| `title` | `org.opencontainers.image.title` | Repository name |

```yaml
container:
  # ...
  oci-annotations:
    enabled: true
    static:
      org.opencontainers.image.vendor: "My Organization"
      org.opencontainers.image.licenses: "Apache-2.0"
    auto:
      created: true
      source: true
      revision: true
      version: true
      title: true
```

---

## Protected Branches

Configures required status checks for branch protection per branch.

### `protected-branches`

Each key is a branch name. The value is either:
- An empty array `[]` — uses all default status checks
- An array of strings — uses those exact status checks
- An object with `include-runs` and/or `exclude-runs`

| Sub-key | Type | Description |
|---|---|---|
| `include-runs` | `array` of `string` | Explicit list of required status checks (overrides auto-detection) |
| `exclude-runs` | `array` of `string` | Status checks to exclude from auto-detected list |

```yaml
protected-branches:
  dev: []  # All default checks
  main:
    include-runs:
      - "pre-commit.ci - pr"
      - "WIP"
    exclude-runs:
      - "SonarCloud Code Analysis"
  feature:
    - "lint"
    - "test"
```

---

## Custom Check Runs

User-defined check runs that execute commands on PR events. See [Setting Up CI Checks](setting-up-ci-checks.html) for details.

### `custom-check-runs`

| Property | Value |
|---|---|
| Type | `array` of objects |
| Default | `[]` |

Each custom check run object:

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `name` | `string` | Yes | — | Unique name displayed in the GitHub UI |
| `command` | `string` | Yes | — | Command to execute in the repository worktree. Environment variables can be inlined. |
| `mandatory` | `boolean` | No | `true` | Whether this check must pass for the PR to be mergeable |

> **Warning:** Custom check names cannot conflict with built-in check names: `tox`, `pre-commit`, `build-container`, `python-module-install`, `conventional-title`, `can-be-merged`, `security-suspicious-paths`, `security-committer-identity`.

```yaml
custom-check-runs:
  - name: lint
    command: uv tool run --from ruff ruff check
    mandatory: true
  - name: security-scan
    command: TOKEN=xyz uv tool run --from bandit bandit -r .
    mandatory: false
  - name: complex-check
    command: |
      uv run python -c "
      import sys
      print('Running complex check')
      sys.exit(0)
      "
```

---

## Per-Repository In-Repo Config

The `.github-webhook-server.yaml` file is committed to a repository's root and provides repository-specific overrides at the highest priority.

### Supported Keys

This file accepts any key that is valid under `repositories.<name>` in `config.yaml`. The value resolution order is: `.github-webhook-server.yaml` → repository config in `config.yaml` → global config in `config.yaml`.

Common keys used in `.github-webhook-server.yaml`:

- `tox`
- `pre-commit`
- `conventional-title`
- `container`
- `auto-verified-and-merged-users`
- `auto-verify-cherry-picked-prs`
- `can-be-merged-required-labels`
- `labels`
- `pr-size-thresholds`
- `ai-features`
- `security-checks`
- `custom-check-runs`
- `welcome-extra-info`
- `test-oracle`
- `set-auto-merge-prs`
- `minimum-lgtm`
- `create-issue-for-new-pr`
- `cherry-pick-assign-to-pr-author`
- `allow-commands-on-draft-prs`
- `branch-protection`

```yaml
# .github-webhook-server.yaml (in repository root)
conventional-title: "feat,fix,docs,chore"
pre-commit: true
tox:
  main: all
labels:
  enabled-labels:
    - verified
    - size
```

---

## Welcome Message File

### `.github-webhook-server-welcome-message.md`

A markdown file committed to the repository root. Its content replaces the `welcome-extra-info` config value for the Additional Information section of the PR welcome comment.

| Property | Value |
|---|---|
| Location | Repository root |
| Encoding | UTF-8 |
| Max size | 10,240 bytes (10 KB) |
| Priority | Overrides all `welcome-extra-info` config settings |

```markdown
<!-- .github-webhook-server-welcome-message.md -->
**Contribution Guidelines:**
- All PRs must include tests
- Update CHANGELOG.md for user-facing changes
- Squash commits before merging
```

---

## Full Example

```yaml
# config.yaml
log-level: INFO
log-file: webhook-server.log
mask-sensitive-data: true

github-app-id: 123456
github-tokens:
  - ghp_globaltoken1
  - ghp_globaltoken2

webhook-ip: https://webhooks.example.com/webhook_server
webhook-secret: my-secret
ip-bind: 0.0.0.0
port: 5000
max-workers: 10

verify-github-ips: true
disable-ssl-warnings: false

docker:
  username: dockeruser
  password: dockerpass

default-status-checks:
  - "WIP"
  - "can-be-merged"

auto-verified-and-merged-users:
  - "renovate[bot]"

auto-verify-cherry-picked-prs: true
create-issue-for-new-pr: true
cherry-pick-assign-to-pr-author: true

labels:
  enabled-labels:
    - verified
    - hold
    - size
    - can-be-merged
  colors:
    hold: red
    verified: green

pr-size-thresholds:
  Small:
    threshold: 50
    color: green
  Medium:
    threshold: 200
    color: orange
  Large:
    threshold: inf
    color: red

branch-protection:
  strict: true
  required_approving_review_count: 1
  required_conversation_resolution: true

security-checks:
  mandatory: true
  committer-identity-check: true
  trusted-committers:
    - "pre-commit-ci[bot]"

ai-features:
  ai-provider: claude
  ai-model: sonnet
  conventional-title:
    enabled: true
    mode: suggest

test-oracle:
  server-url: "http://localhost:8000"
  ai-provider: claude
  ai-model: sonnet
  triggers:
    - approved

repositories:
  my-service:
    name: my-org/my-service
    log-level: DEBUG
    slack-webhook-url: https://slack-webhook-url/replace-with-your-webhook-url
    events:
      - push
      - pull_request
      - issue_comment
      - check_run
    conventional-title: "feat,fix,chore,docs,ci"
    pre-commit: true
    tox:
      python-version: "3.12"
      main: all
      dev: lint,test
    container:
      username: quayuser
      password: quaypass
      repository: quay.io/myorg/my-service
      tag: latest
      release: true
    protected-branches:
      main:
        include-runs:
          - "tox"
          - "pre-commit"
    custom-check-runs:
      - name: lint
        command: uv tool run --from ruff ruff check
        mandatory: true
```

## Related Pages

- [Configuring Repositories](configuring-repositories.html)
- [Configuration Recipes](config-recipes.html)
- [Environment Variables](environment-variables.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Enabling Security Checks](enabling-security-checks.html)

---

Source: webhook-events-reference.md

Let me start by reading the knowledge graph and the pages manifest, then explore the codebase systematically.

Now let me look at the remaining handlers:

Now let me look at the `process()` method in `github_api.py` to understand the event routing:

Now let me look at the runner handler and constants:

Now let me look at the `_recheck_merge_eligibility` for status events, and let me also check what `get_pull_request` does:

Now let me look at the runner handler's key methods to understand what CI checks do:

Now let me look at the labels handler and owners handler briefly:

Now let me check for the `ping` event and look at the `get_pull_request` method to understand which events resolve to PRs:

Now I have enough information. Let me write the comprehensive reference page.

# Webhook Events and Handlers

This page documents every GitHub webhook event that the server processes, the handler responsible, and the actions performed for each event and action combination.

> **Note:** The server returns `200 OK` immediately upon receiving a valid webhook payload. All processing happens asynchronously in a background task. See [Getting Started](quickstart.html) for initial setup.

---

## Event Routing Overview

When a webhook arrives, the server validates the payload and routes it to the appropriate handler based on the `X-GitHub-Event` header.

| Event Type | Handler Class | Source File |
|---|---|---|
| `ping` | *(inline in `GithubWebhook.process()`)* | `webhook_server/libs/github_api.py` |
| `push` | `PushHandler` | `webhook_server/libs/handlers/push_handler.py` |
| `pull_request` | `PullRequestHandler` | `webhook_server/libs/handlers/pull_request_handler.py` |
| `issue_comment` | `IssueCommentHandler` | `webhook_server/libs/handlers/issue_comment_handler.py` |
| `check_run` | `CheckRunHandler` | `webhook_server/libs/handlers/check_run_handler.py` |
| `status` | *(inline — re-evaluates merge eligibility)* | `webhook_server/libs/github_api.py` |
| `pull_request_review` | `PullRequestReviewHandler` | `webhook_server/libs/handlers/pull_request_review_handler.py` |
| `pull_request_review_thread` | *(inline — re-evaluates merge eligibility)* | `webhook_server/libs/github_api.py` |

> **Tip:** Draft PRs are skipped entirely unless `allow-commands-on-draft-prs` is configured. When configured, only `issue_comment` events are processed on drafts. See [Configuration Reference](configuration-reference.html) for details.

---

## `ping`

Acknowledges the webhook connection. No processing is performed.

**Trigger:** GitHub sends this event when a webhook is first created or its configuration is updated.

**Actions performed:** Logs the ping event and returns.

---

## `push`

Handles tag pushes for release workflows. Branch pushes are logged but not processed.

**Handler:** `PushHandler`

### Routing Logic

| Condition | Behavior |
|---|---|
| `deleted` is `true` in payload | Skipped — branch/tag deletion |
| `ref` starts with `refs/tags/` | Clones repo, processes tag push |
| `ref` starts with `refs/heads/` | Skipped — branch push |

### Tag Push Actions

When a tag is pushed, the following actions run based on repository configuration:

#### PyPI Upload

**Condition:** `pypi` is configured for the repository.

Checks out the tag, builds a source distribution with `uv build --sdist`, validates with `twine check`, and uploads with `twine upload`. On failure, a GitHub issue is created with the error details. On success, a Slack notification is sent (if configured).

#### Container Build and Push

**Condition:** `container` is configured with `release: true`.

Builds and pushes a container image tagged with the release version.

> **Note:** See [Setting Up CI Checks](setting-up-ci-checks.html) for PyPI and container build configuration.

---

## `pull_request`

The primary event handler that manages the full PR lifecycle.

**Handler:** `PullRequestHandler`

### Actions

#### `opened`

Triggered when a new PR is created.

| Step | Description |
|---|---|
| Welcome comment | Posts a detailed welcome message with available commands, merge requirements, and reviewer info |
| Tracking issue | Creates a GitHub issue linked to the PR (if `create-issue-for-new-pr` is enabled) |
| WIP detection | Adds `wip` label if title starts with `WIP:` |
| Reviewer assignment | Assigns reviewers from OWNERS files |
| Branch label | Adds `branch-<base-ref>` label |
| Merge state check | Labels PR with `needs-rebase` or `has-conflicts` if applicable |
| Size label | Adds `size/XS` through `size/XXL` based on lines changed |
| PR owner assignee | Adds PR author as assignee |
| Verified processing | Auto-verifies if author is in `auto-verified-and-merged-users`; otherwise sets `verified` check to queued |
| CI checks queued | Queues all configured check runs (tox, pre-commit, build-container, etc.) |
| CI execution | Runs tox, pre-commit, python-module-install, container build, conventional title, security checks, and custom checks in parallel |
| Auto-merge | Enables GitHub auto-merge (squash) if author is in `auto-verified-and-merged-users` or base branch is in `set-auto-merge-prs` |
| Test oracle | Triggers AI test oracle in background (if configured) |

#### `reopened`

Same as `opened` except: no welcome comment is posted and no tracking issue is created.

#### `ready_for_review`

Same as `opened`. Triggered when a draft PR is marked as ready for review.

#### `edited`

Triggered when a PR's title or body is changed.

| Step | Description |
|---|---|
| WIP detection | Re-evaluates `wip` label based on updated title |
| Conventional title | Re-runs conventional title check if title was changed and `conventional-title` is configured |

#### `synchronize`

Triggered when new commits are pushed to the PR branch.

**Clean rebase detection:** The handler computes SHA-256 hashes of the diff between the merge-base and head for both the old and new commits. If the hashes match, the push is treated as a clean rebase (no code changes).

| Condition | Behavior |
|---|---|
| Clean rebase | Posts a comment noting preserved labels; runs CI but **preserves** review labels (`approved-*`, `lgtm-*`, `commented-*`, `changes-requested-*`, `verified`) |
| Non-clean rebase / new commits | Removes all review labels; re-runs full CI; resets verified status |

In both cases, the test oracle is triggered in background (if configured).

#### `closed`

Triggered when a PR is closed (merged or not).

| Step | Condition | Description |
|---|---|---|
| Close tracking issue | Always | Closes the linked tracking issue with a comment |
| Delete remote tag | `container` configured | Deletes the PR-specific container image tag from the registry (GHCR via API, others via `regctl`) |
| Cherry-pick | PR is merged + `cherry-pick-<branch>` labels present | Executes cherry-picks to each labeled target branch |
| Container build | PR is merged + `container` configured | Builds and pushes container image |
| Re-label open PRs | PR is merged | After 30s delay, checks all open PRs for `needs-rebase` / `has-conflicts` status |

#### `labeled` / `unlabeled`

Triggered when a label is added to or removed from a PR. Re-evaluates merge eligibility when relevant labels change.

| Label | Effect |
|---|---|
| `approved-<user>` / `lgtm-<user>` / `changes-requested-<user>` | Re-checks merge eligibility if user is an approver/reviewer |
| `verified` | Sets the `verified` check run to success (labeled) or queued (unlabeled); re-checks merge eligibility |
| `wip` / `hold` / `automerge` | Re-checks merge eligibility |
| `can-be-merged` | Ignored (prevents recursive processing) |

---

## `issue_comment`

Processes slash commands posted as comments on PRs.

**Handler:** `IssueCommentHandler`

### Routing Logic

- Only `created` action is processed; `edited` and `deleted` are ignored.
- Comments containing the welcome message marker are ignored.
- Commands must start with `/` and be on their own line.
- Multiple commands per comment are executed in parallel.

### Available Commands

#### PR Status Management

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/wip` | — | Collaborator | Adds `wip` label and prepends `WIP:` to title |
| `/wip cancel` | — | Collaborator | Removes `wip` label and `WIP:` prefix from title |
| `/hold` | — | Approver | Adds `hold` label to block merging |
| `/hold cancel` | — | Approver | Removes `hold` label |
| `/verified` | — | Collaborator | Adds `verified` label and sets check to success |
| `/verified cancel` | — | Collaborator | Removes `verified` label and sets check to queued |
| `/reprocess` | — | Collaborator | Triggers full PR workflow from scratch (skips if PR is merged) |
| `/regenerate-welcome` | — | Collaborator | Updates or creates the welcome comment |

#### Review & Approval

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/lgtm` | — | Anyone | Adds `lgtm-<user>` label |
| `/approve` | — | Approver | Adds `approved-<user>` label; triggers test oracle if configured |
| `/automerge` | — | Maintainer or Approver | Adds `automerge` label for automatic squash-merge when all requirements are met |
| `/assign-reviewers` | — | Collaborator | Re-assigns reviewers from OWNERS files |
| `/assign-reviewer` | `@username` | Collaborator | Assigns a specific collaborator as reviewer |
| `/check-can-merge` | — | Collaborator | Manually triggers the merge eligibility check |

#### Testing & Validation

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/retest tox` | check name(s) | Collaborator | Re-runs the tox test suite |
| `/retest pre-commit` | check name(s) | Collaborator | Re-runs pre-commit hooks |
| `/retest build-container` | check name(s) | Collaborator | Re-runs container build |
| `/retest python-module-install` | check name(s) | Collaborator | Re-runs Python module install check |
| `/retest conventional-title` | check name(s) | Collaborator | Re-runs conventional title validation |
| `/retest <custom-check-name>` | check name(s) | Collaborator | Re-runs a custom check |
| `/retest all` | — | Collaborator | Re-runs all configured checks |
| `/test-oracle` | — | Collaborator | Manually triggers the AI test oracle |

> **Note:** `/retest` requires at least one argument. Multiple checks can be retested in one command: `/retest tox pre-commit`. See [Managing Pull Requests](managing-pull-requests.html) for more examples.

#### Cherry-pick & Branch Operations

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/cherry-pick` | `branch1 [branch2 ...]` | Collaborator | On unmerged PRs: adds `cherry-pick-<branch>` labels for auto cherry-pick on merge. On merged PRs: executes cherry-pick immediately. |
| `/cherry-pick-retry` | `branch` | Collaborator | Retries a failed cherry-pick on a merged PR. Closes the existing failed cherry-pick PR (if found) and re-runs the cherry-pick. Requires the `cherry-pick-<branch>` label to exist. |
| `/rebase` | — | Collaborator | Rebases the PR branch onto its base branch |

#### Container Operations

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/build-and-push-container` | `[--build-arg KEY=value ...]` | Collaborator | Builds and pushes a container image tagged with the PR number |

#### Security

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/security-override` | — | Maintainer | Sets all configured security checks (suspicious paths, committer identity) to pass |
| `/security-override cancel` | — | Maintainer | Re-runs all configured security checks |

> **Note:** See [Enabling Security Checks](enabling-security-checks.html) for security check configuration.

#### Label Management

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/<label-name>` | — | Varies | Adds the specified label to the PR |
| `/<label-name> cancel` | — | Varies | Removes the specified label from the PR |

Available label names: `hold`, `verified`, `wip`, `lgtm`, `approve`, `automerge`.

#### Other

| Command | Arguments | Permission | Description |
|---|---|---|---|
| `/add-allowed-user` | `username` | Anyone | Posts a comment confirming the user is allowed to run commands |

### Draft PR Command Filtering

When `allow-commands-on-draft-prs` is configured as a list:
- An empty list (`[]`) allows all commands on draft PRs.
- A non-empty list allows only the listed commands. Other commands are rejected with a comment explaining which commands are allowed.
- `/test-oracle` is always allowed on draft PRs regardless of configuration.

See [Configuration Reference](configuration-reference.html) for the `allow-commands-on-draft-prs` option.

---

## `check_run`

Monitors GitHub check run completions and triggers merge evaluation.

**Handler:** `CheckRunHandler`

### Routing Logic

| Condition | Behavior |
|---|---|
| `action` ≠ `completed` | Skipped |
| Check run name is `can-be-merged` with non-success conclusion | Skipped |
| Check run name is `can-be-merged` with `success` conclusion + `automerge` label | Executes squash merge |
| Any other completed check run | Re-evaluates merge eligibility via `check_if_can_be_merged()` |

### Auto-merge Flow

When a `can-be-merged` check run completes with `success`:

1. Checks if the PR has the `automerge` label.
2. If present, performs a squash merge via the GitHub API.
3. If the merge fails, falls back to re-evaluating merge eligibility.

### Check Run States

The server creates and manages these check runs on PRs:

| Check Run Name | Source | Description |
|---|---|---|
| `can-be-merged` | Built-in | Aggregated merge eligibility status |
| `tox` | Config: `tox` | Python test suite result |
| `pre-commit` | Config: `pre-commit` | Pre-commit hooks result |
| `build-container` | Config: `container` | Container build result |
| `python-module-install` | Config: `pypi` | Python package install test result |
| `conventional-title` | Config: `conventional-title` | Commit message format validation |
| `verified` | Config: `verified-job` | Manual verification status |
| `security-suspicious-paths` | Config: `security-checks.suspicious-paths` | Suspicious file path detection |
| `security-committer-identity` | Config: `security-checks.committer-identity-check` | Committer/author mismatch detection |
| *(custom name)* | Config: `custom-check-runs[].name` | User-defined check command |

Each check run transitions through these states:

| State | Meaning |
|---|---|
| `queued` | Check is registered but not yet running |
| `in_progress` | Check is currently executing |
| `success` | Check completed successfully |
| `failure` | Check completed with errors |

> **Note:** See [Setting Up CI Checks](setting-up-ci-checks.html) for configuring each check type.

---

## `status`

Monitors GitHub commit status updates (legacy Status API) and re-evaluates merge eligibility.

**Handler:** Inline in `GithubWebhook.process()`

### Routing Logic

| Condition | Behavior |
|---|---|
| `state` = `pending` | Skipped (early exit before API user initialization) |
| `state` = `success`, `failure`, or `error` | Clones repository, re-evaluates `can-be-merged` |

### Effect

When a commit status reaches a terminal state, the server:

1. Clones the repository (needed for OWNERS file processing).
2. Initializes the OWNERS file handler.
3. Calls `check_if_can_be_merged()` to re-evaluate all merge conditions.

> **Tip:** This enables the server to react to external CI systems that report via the Status API (e.g., Jenkins, CircleCI) rather than the Check Runs API.

---

## `pull_request_review`

Processes PR review submissions and manages review labels.

**Handler:** `PullRequestReviewHandler`

### Routing Logic

| Condition | Behavior |
|---|---|
| `action` ≠ `submitted` | Skipped |
| `action` = `submitted` | Processes the review |

### Actions on Submitted Review

1. **Adds a review label** based on the review state:

   | Review State | Label Added |
   |---|---|
   | `approved` | `approved-<username>` (if user is an approver) or `lgtm-<username>` (if reviewer, non-PR-owner) |
   | `changes_requested` | `changes-requested-<username>` |
   | `commented` | `commented-<username>` |

2. **Processes `/approve` command** in review body: If the review body contains a line with exactly `/approve`, the handler adds the `approved-<user>` label and triggers the test oracle (if configured).

---

## `pull_request_review_thread`

Monitors review thread resolution/unresolvement for conversation resolution requirements.

**Handler:** Inline in `GithubWebhook.process()`

### Routing Logic

| Condition | Behavior |
|---|---|
| `action` not in (`resolved`, `unresolved`) | Skipped |
| `required_conversation_resolution` is disabled | Skipped |
| `action` = `resolved` or `unresolved` | Re-evaluates `can-be-merged` |

### Effect

When a review thread is resolved or unresolved and `required_conversation_resolution` is enabled in branch protection settings, the server re-evaluates all merge conditions. Unresolved threads block the `can-be-merged` check.

See [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html) for configuring `required_conversation_resolution`.

---

## Merge Eligibility Check (`can-be-merged`)

The `check_if_can_be_merged()` method is called by multiple event handlers. It evaluates all merge conditions and sets the `can-be-merged` check run to success or failure.

### Conditions Evaluated

| # | Condition | Failure Message |
|---|---|---|
| 1 | PR is mergeable (no conflicts) | `PR is not mergeable: False` |
| 2 | No required check runs in progress | `Some required check runs in progress <names>` |
| 3 | No `wip` or `hold` labels | `PR has wip/hold label` |
| 4 | All required check runs passed | `Some check runs failed: <names>` or `Some check runs not started: <names>` |
| 5 | No `changes-requested-<approver>` labels | `PR has changed requests from approvers` |
| 6 | All `can-be-merged-required-labels` present | `Missing required labels: <names>` |
| 7 | No unresolved review threads (if `required_conversation_resolution` enabled) | `PR has N unresolved review conversation(s)` |
| 8 | Approved by required approvers | `Missing approved from approvers: <names>` |
| 9 | Minimum LGTM count met | `Missing lgtm from reviewers. Minimum N required, (M given)` |

When all conditions pass, the `can-be-merged` label is added and the check run is set to success.

### Required Status Checks

The list of required checks is built from:

1. **Branch protection rules** (from GitHub API, public repos only)
2. **Enabled features:** `tox`, `verified`, `build-container`, `python-module-install`, `conventional-title`
3. **Mandatory custom checks** (custom checks with `mandatory: true`, which is the default)
4. **Mandatory security checks** (when `security-checks.mandatory` is `true`)

> **Note:** Non-mandatory custom checks and non-mandatory security checks still run but do not block merging. See [Configuration Reference](configuration-reference.html) for the `mandatory` option.

---

## Labels Reference

Labels are automatically created and managed by the server. See [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html) for color customization and enabling/disabling label categories.

### Static Labels

| Label | Default Color | Description |
|---|---|---|
| `hold` | `#B60205` (red) | Blocks merging |
| `verified` | `#0E8A16` (green) | PR has been verified |
| `wip` | `#B60205` (red) | Work in progress |
| `lgtm` | `#0E8A16` (green) | Looks good to me |
| `approve` | `#0E8A16` (green) | Approved |
| `automerge` | `#0E8A16` (green) | Auto-merge enabled |
| `CherryPicked` | `#1D76DB` (blue) | PR was cherry-picked |
| `ai-resolved-conflicts` | `#FFA500` (orange) | Cherry-pick conflicts resolved by AI |
| `can-be-merged` | `#0E8A17` (green) | All merge requirements met |
| `needs-rebase` | `#B60205` (red) | PR is behind base branch |
| `has-conflicts` | `#B60205` (red) | PR has merge conflicts |
| `size/XS` | `#ededed` | ≤ 20 lines changed |
| `size/S` | `#0E8A16` | ≤ 50 lines changed |
| `size/M` | `#F09C74` | ≤ 100 lines changed |
| `size/L` | `#F5621C` | ≤ 300 lines changed |
| `size/XL` | `#D93F0B` | ≤ 500 lines changed |
| `size/XXL` | `#B60205` | > 500 lines changed |

### Dynamic Labels (prefixed)

| Label Prefix | Default Color | Example | Description |
|---|---|---|---|
| `approved-` | `#0E8A16` | `approved-alice` | PR approved by user |
| `lgtm-` | `#DCED6F` | `lgtm-bob` | LGTM from user |
| `commented-` | `#D93F0B` | `commented-carol` | Review comment from user |
| `changes-requested-` | `#F5621C` | `changes-requested-dave` | Changes requested by user |
| `cherry-pick-` | `#F09C74` | `cherry-pick-release-1.0` | Cherry-pick target branch |
| `branch-` | `#1D76DB` | `branch-main` | PR target branch |

---

## Event Processing Flowchart

The following describes the processing order for PR-related events:

1. **Webhook arrives** → validate payload, verify signature (if configured), return `200 OK`
2. **Background task starts** → create structured logging context
3. **Initialize `GithubWebhook`** → load config, validate repository
4. **Early exit checks** → filter draft PRs, skip non-actionable events
5. **Initialize API users** → fetch token user logins for auto-verify list
6. **Event routing** → dispatch to appropriate handler
7. **Handler processing** → execute event-specific logic
8. **Context finalization** → log summary, write structured log entry

> **Warning:** All processing after step 1 happens asynchronously. A `200 OK` response does **not** mean the webhook was processed successfully. Use the [Log Viewer](using-the-log-viewer.html) or the `delivery_id` in the response to track processing results.

## Related Pages

- [Managing Pull Requests](managing-pull-requests.html)
- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Configuration Reference](configuration-reference.html)
- [Cherry-Picking and Branch Protection](cherry-picking-and-branching.html)
- [Configuring Labels and PR Size Thresholds](configuring-labels-and-size.html)

---

Source: environment-variables.md

Let me explore the codebase to understand all environment variables used in the project.

Let me check how those Docker Compose environment variables map to config.yaml:

Now let me check how the Docker Compose environment variables map to config.yaml settings:

These Docker Compose environment variables appear in the example `docker-compose.yaml` but are not actually read by the Python code via `os.environ`. Let me check if there's any mapping logic:

These are only in the Docker Compose example and docs, not actually consumed by the Python app. Let me verify by checking config.yaml examples:

Now let me check the `scripts/podman-cleanup.sh` for PUID/PGID usage:

Now let me check the exposed ports and any additional env vars:

Now let me also verify the MCP server specifics:

Now I have a thorough understanding. Let me also verify the `TZ` usage:

Good — `TZ` is only in the Docker Compose example and isn't read by the application code directly (it's a standard Linux/container env var).

Let me also verify if `FORCE_COLOR` is just documented or actively used:

Now let me check the exclusion file again to make sure I handle it properly:

These are pages I should not reference. Let me check the pages list again:

The deny-list contains page slugs that I should NOT mention or reference. These are:
- `using-the-log-viewer.html`
- `enabling-ai-features.html`
- `deploying-with-docker.html`

Now I have everything I need. Let me write the documentation.

# Environment Variables

All environment variables recognized by the webhook server, grouped by function.

## Server Core

### `WEBHOOK_SERVER_DATA_DIR`

| Property | Value |
|----------|-------|
| **Type** | String (directory path) |
| **Default** | `/home/podman/data` |
| **Required** | No |
| **Read by** | `webhook_server/libs/config.py` |

Base directory containing `config.yaml` and the `logs/` subdirectory. The server reads its configuration from `$WEBHOOK_SERVER_DATA_DIR/config.yaml` and writes structured logs to `$WEBHOOK_SERVER_DATA_DIR/logs/`.

```bash
# Local development
export WEBHOOK_SERVER_DATA_DIR=/path/to/my/data
uv run entrypoint.py

# Docker Compose
environment:
  - WEBHOOK_SERVER_DATA_DIR=/home/podman/data  # matches the container default
```

> **Note:** When running outside Docker, you must set this variable to a directory that contains a valid `config.yaml`. See [Configuration Reference](configuration-reference.html) for the full config schema.

---

### `WEBHOOK_SERVER_DEV_MODE`

| Property | Value |
|----------|-------|
| **Type** | Boolean string (`1`, `true`, `yes` — case-insensitive) |
| **Default** | Disabled (empty / unset) |
| **Required** | No |
| **Read by** | `entrypoint.py` |

Enables Uvicorn's auto-reload mode for development. When enabled, the server watches for file changes and restarts automatically. When disabled, the server starts with the configured number of workers (`max-workers` in `config.yaml`, default `10`).

```bash
WEBHOOK_SERVER_DEV_MODE=true uv run entrypoint.py
```

> **Warning:** Do not enable in production. Dev mode disables multi-worker support and adds filesystem polling overhead.

---

### `ENABLE_LOG_SERVER`

| Property | Value |
|----------|-------|
| **Type** | String (exact match: `true`) |
| **Default** | Disabled (any value other than `true`) |
| **Required** | No |
| **Read by** | `webhook_server/app.py` |

Registers the log viewer HTTP and WebSocket endpoints under `/logs`. When not set to exactly `true`, all `/logs/*` endpoints return HTTP 404.

```bash
# Enable
ENABLE_LOG_SERVER=true uv run entrypoint.py

# Docker Compose
environment:
  - ENABLE_LOG_SERVER=true
```

> **Warning:** Log viewer endpoints are unauthenticated. Only enable on trusted networks (VPN, internal). Access is restricted to private/loopback IP ranges, but this can be bypassed behind a misconfigured reverse proxy.

Affected endpoints when enabled:

| Endpoint | Description |
|----------|-------------|
| `GET /logs` | Log viewer web UI |
| `GET /logs/api/entries` | Query log entries |
| `GET /logs/api/export` | Export logs as JSON |
| `GET /logs/api/pr-flow/{hook_id}` | PR workflow visualization |
| `GET /logs/api/workflow-steps/{hook_id}` | Workflow step timeline |
| `GET /logs/api/step-logs/{hook_id}/{step_name}` | Logs for a specific step |
| `WS /logs/ws` | Real-time log streaming |

See [Log Viewer API Reference](log-viewer-api.html) for endpoint details.

---

### `ENABLE_MCP_SERVER`

| Property | Value |
|----------|-------|
| **Type** | String (exact match: `true`) |
| **Default** | Disabled (any value other than `true`) |
| **Required** | No |
| **Read by** | `webhook_server/app.py` |

Registers the Model Context Protocol (MCP) endpoint at `/mcp` for AI agent integration. When enabled, the server exposes its API operations as MCP tools that AI agents can discover and invoke.

```bash
# Enable
ENABLE_MCP_SERVER=true uv run entrypoint.py

# Docker Compose
environment:
  - ENABLE_MCP_SERVER=true
```

MCP logging is separated from the main application log. The log file is configured via the `mcp-log-file` key in `config.yaml` (default: `mcp_server.log`).

> **Warning:** The MCP endpoint has no authentication. Deploy only on trusted networks. Use a reverse proxy with authentication for any external access.


> **Tip:** You must restart the server after changing `ENABLE_MCP_SERVER`. The endpoint registration happens at import time, not at runtime.

---

## AI Sidecar

### `SIDECAR_PORT`

| Property | Value |
|----------|-------|
| **Type** | Integer (port number) |
| **Default** | `9100` |
| **Required** | No |
| **Read by** | `entrypoint.sh` |

Port on which the Pi SDK sidecar Node.js process listens. The sidecar bridges AI CLI tools (Claude, Gemini, Cursor) for features such as conventional title suggestions and cherry-pick conflict resolution. The sidecar is started automatically by `entrypoint.sh` if `sidecar-helper/dist/server.js` exists.

```bash
# Override default port
SIDECAR_PORT=9200 uv run entrypoint.sh

# Docker Compose
environment:
  - SIDECAR_PORT=9100
```

The container health check probes both the main server and the sidecar:

```yaml
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:5000/webhook_server/healthcheck && curl -f http://localhost:${SIDECAR_PORT:-9100}/health"]
```

> **Note:** If the sidecar binary is not present or fails to start within 15 seconds, the main server still starts, but AI features will not be available.

---

### `ACPX_AGENTS`

| Property | Value |
|----------|-------|
| **Type** | String |
| **Default** | Unset |
| **Required** | No |
| **Read by** | Pi SDK sidecar |

Enables model discovery for the specified AI agent. Set to `cursor` to enable Cursor model discovery for AI features.

```yaml
# Docker Compose
environment:
  - ACPX_AGENTS=cursor
```

---

### `VERTEX_CLAUDE_1M`

| Property | Value |
|----------|-------|
| **Type** | Boolean string (`true`) |
| **Default** | Unset |
| **Required** | No |
| **Read by** | Pi SDK sidecar |

Enables Claude 1M context window models via Google Vertex AI. Requires Google Cloud credentials to be mounted into the container.

```yaml
# Docker Compose
environment:
  - VERTEX_CLAUDE_1M=true
volumes:
  - $HOME/.config/gcloud:/home/podman/.config/gcloud:ro
```

---

## AI CLI API Keys

These environment variables provide authentication credentials for the AI CLI tools used by the sidecar. They are required only when the corresponding AI provider is configured in the `ai-features` section of `config.yaml`. See [Configuration Reference](configuration-reference.html) for `ai-features` settings.

| Variable | Provider | Description |
|----------|----------|-------------|
| `ANTHROPIC_API_KEY` | Claude Code | API key for Anthropic Claude CLI |
| `GEMINI_API_KEY` | Gemini CLI | API key for Google Gemini CLI |
| `CURSOR_API_KEY` | Cursor Agent | API key for Cursor Agent (API key method) |

```yaml
# Docker Compose — set only the key for your chosen provider
environment:
  - ANTHROPIC_API_KEY=sk-ant-xxx
  # OR
  - GEMINI_API_KEY=xxx
  # OR
  - CURSOR_API_KEY=xxx
```

> **Tip:** For Cursor interactive login (instead of API key), use: `docker exec -it github-webhook-server agent`

---

## Container Runtime

### `PUID`

| Property | Value |
|----------|-------|
| **Type** | Integer (Unix user ID) |
| **Default** | `1000` |
| **Required** | No |
| **Read by** | `scripts/podman-cleanup.sh` |

User ID used by the Podman runtime cleanup script to locate stale runtime directories at `/tmp/storage-run-{PUID}/`.

```yaml
environment:
  - PUID=1000
```

---

### `PGID`

| Property | Value |
|----------|-------|
| **Type** | Integer (Unix group ID) |
| **Default** | `1000` |
| **Required** | No |

Group ID for container process ownership. Standard Docker/Podman convention for controlling file permissions on mounted volumes.

```yaml
environment:
  - PGID=1000
```

---

### `TZ`

| Property | Value |
|----------|-------|
| **Type** | String (IANA timezone identifier) |
| **Default** | Container OS default (typically UTC) |
| **Required** | No |

Sets the container timezone. Affects log timestamps and any time-dependent operations.

```yaml
environment:
  - TZ=Asia/Jerusalem
```

---

### `FORCE_COLOR`

| Property | Value |
|----------|-------|
| **Type** | Boolean string |
| **Default** | Unset |
| **Required** | No |
| **Read by** | Uvicorn (via standard convention) |

Enables colored terminal output in Uvicorn HTTP request logs. Useful when viewing Docker container logs in a terminal that supports ANSI colors. Application-level logs use `simple-logger` with `console=True`, which provides colored output independently.

```yaml
environment:
  - FORCE_COLOR=1
```

---

## Docker Compose Configuration

The example `docker-compose.yaml` at `examples/docker-compose.yaml` includes several environment variables that map directly to keys in `config.yaml`. These are **not** read via `os.environ` by the Python application — they are passed as container environment variables and are available for shell-level substitution or container configuration.

> **Note:** Server settings such as bind address, port, webhook secret, and IP verification are configured in `config.yaml`, not via environment variables. See [Configuration Reference](configuration-reference.html) for all `config.yaml` options.

### Docker Compose Example

```yaml
services:
  github-webhook-server:
    container_name: github-webhook-server
    build: ghcr.io/myk-org/github-webhook-server:latest
    volumes:
      - "./webhook_server_data_dir:/home/podman/data:Z"
      - "/tmp/podman-storage-${USER:-1000}:/tmp/storage-run-1000"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Jerusalem
      - ENABLE_LOG_SERVER=true
      - ENABLE_MCP_SERVER=false
      # - SIDECAR_PORT=9100
      # - ACPX_AGENTS=cursor
      # - VERTEX_CLAUDE_1M=true
      # - ANTHROPIC_API_KEY=sk-ant-xxx
    ports:
      - "5000:5000"
    privileged: true
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:5000/webhook_server/healthcheck && curl -f http://localhost:${SIDECAR_PORT:-9100}/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    restart: unless-stopped
```

### Exposed Container Ports

| Port | Service | Description |
|------|---------|-------------|
| `5000` | Webhook server | Main FastAPI application (webhook endpoint, health check, log viewer, MCP) |
| `5001` | Tool server | Internal async tool server for AI custom tools (binds to `127.0.0.1` only) |
| `9100` | Pi SDK sidecar | AI feature sidecar (configurable via `SIDECAR_PORT`) |

---

## E2E Test Variables

These variables are used exclusively by the end-to-end test infrastructure. They are loaded from a `.dev/.env` file and are not relevant to production deployments.

| Variable | Type | Description |
|----------|------|-------------|
| `SERVER_PORT` | Integer | Local server port that webhooks are forwarded to |
| `SMEE_URL` | URL | Smee.io webhook proxy URL for forwarding GitHub webhooks to local dev |
| `TEST_REPO` | String | GitHub repository for E2E tests (`owner/repo-name` format) |
| `DOCKER_COMPOSE_FILE` | Path | Path to docker-compose.yaml for E2E test infrastructure |

```bash
# .dev/.env
SERVER_PORT=5000
SMEE_URL=https://smee.io/YOUR_UNIQUE_CHANNEL
TEST_REPO=owner/repo-name
DOCKER_COMPOSE_FILE=.dev/docker-compose.yaml
```

---

## Quick Reference

All environment variables in one table:

| Variable | Default | Category | Description |
|----------|---------|----------|-------------|
| `WEBHOOK_SERVER_DATA_DIR` | `/home/podman/data` | Server | Path to data directory containing `config.yaml` |
| `WEBHOOK_SERVER_DEV_MODE` | Disabled | Server | Enable Uvicorn auto-reload for development |
| `ENABLE_LOG_SERVER` | Disabled | Server | Enable `/logs` endpoints |
| `ENABLE_MCP_SERVER` | Disabled | Server | Enable `/mcp` endpoint for AI agents |
| `SIDECAR_PORT` | `9100` | AI Sidecar | Pi SDK sidecar listen port |
| `ACPX_AGENTS` | Unset | AI Sidecar | AI agent model discovery (e.g., `cursor`) |
| `VERTEX_CLAUDE_1M` | Unset | AI Sidecar | Enable Claude 1M models via Vertex AI |
| `ANTHROPIC_API_KEY` | Unset | AI Keys | Anthropic Claude API key |
| `GEMINI_API_KEY` | Unset | AI Keys | Google Gemini API key |
| `CURSOR_API_KEY` | Unset | AI Keys | Cursor Agent API key |
| `PUID` | `1000` | Container | Container user ID |
| `PGID` | `1000` | Container | Container group ID |
| `TZ` | UTC | Container | Container timezone |
| `FORCE_COLOR` | Unset | Container | Enable colored Uvicorn log output |

## Related Pages

- [Deploying with Docker](deploying-with-docker.html)
- [Configuration Reference](configuration-reference.html)
- [Enabling AI Features](enabling-ai-features.html)
- [Using the Log Viewer](using-the-log-viewer.html)
- [Getting Started](quickstart.html)

---

Source: log-viewer-api.md

Let me start by reading the knowledge graph and page manifest, then explore the relevant source files.

Now let me look at the `LogEntry` model and the `parse_datetime_string` utility:

Now let me check the `APP_URL_ROOT_PATH` and routing info:

Now let me also check the `get_workflow_steps_core` reference:

Now let me look at the actual response structures more carefully by checking tests:

Let me now look at the tests related to endpoints specifically to understand the actual response format:

Now let me check the PR flow response by looking at `_analyze_pr_flow` more closely and the actual JSON response structure:

Now I have a comprehensive understanding of the API. Let me write the reference page.

# Log Viewer API Reference

All Log Viewer endpoints are served by the webhook server at the same host and port. Endpoints require the `ENABLE_LOG_SERVER=true` environment variable to be set.

> **Warning:** Log Viewer endpoints are **unauthenticated**. Never expose them to the public internet. Deploy on trusted networks only. See [Environment Variables](environment-variables.html) for configuration details.

## Prerequisites

| Requirement | Details |
|---|---|
| Environment variable | `ENABLE_LOG_SERVER=true` |
| Disabled response (REST) | `404` — `"Log server is disabled. Set ENABLE_LOG_SERVER=true to enable."` |
| Disabled response (WebSocket) | Close code `1008` (Policy Violation) — `"Log server is disabled"` |

## Endpoints Overview

| Method | Path | Description |
|---|---|---|
| `GET` | `/logs` | Log viewer web UI |
| `GET` | `/logs/api/entries` | Query log entries with filters and pagination |
| `GET` | `/logs/api/export` | Export filtered logs as a JSON file download |
| `GET` | `/logs/api/pr-flow/{hook_id}` | PR workflow flow visualization data |
| `GET` | `/logs/api/workflow-steps/{hook_id}` | Detailed workflow step timeline |
| `GET` | `/logs/api/step-logs/{hook_id}/{step_name}` | Log entries within a specific step's time window |
| `WebSocket` | `/logs/ws` | Real-time log streaming |

---

## `GET /logs`

Serves the log viewer web UI as an HTML page.

**Response:** `200` — `text/html`

```
GET /logs
```

---

## `GET /logs/api/entries`

Retrieve historical log entries with filtering and pagination. Uses memory-efficient streaming internally.

### Query Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `hook_id` | `string` | `null` | GitHub webhook delivery ID (`X-GitHub-Delivery` header value) |
| `pr_number` | `integer` | `null` | Pull request number |
| `repository` | `string` | `null` | Repository in `owner/repo` format |
| `event_type` | `string` | `null` | GitHub event type (e.g., `pull_request`, `push`, `issue_comment`, `pull_request_review`) |
| `github_user` | `string` | `null` | GitHub username who triggered the event |
| `level` | `string` | `null` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `SUCCESS` |
| `start_time` | `string` | `null` | ISO 8601 datetime (e.g., `2024-01-15T10:00:00Z`) |
| `end_time` | `string` | `null` | ISO 8601 datetime (e.g., `2024-01-15T18:00:00Z`) |
| `search` | `string` | `null` | Case-insensitive full-text search across log messages |
| `limit` | `integer` | `100` | Maximum entries to return. Range: `1`–`10000` |
| `offset` | `integer` | `0` | Number of entries to skip for pagination. Must be ≥ `0` |

### Response Body

```json
{
  "entries": [
    {
      "timestamp": "2024-01-15T14:30:25.123456",
      "level": "INFO",
      "logger_name": "webhook_server.app",
      "message": "Processing webhook for repository: myakove/test-repo",
      "hook_id": "f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f",
      "event_type": "pull_request",
      "repository": "myakove/test-repo",
      "github_user": "contributor123",
      "pr_number": 42,
      "task_id": null,
      "task_type": null,
      "task_status": null,
      "token_spend": null
    }
  ],
  "entries_processed": 1542,
  "filtered_count_min": 100,
  "total_log_count_estimate": "12.5K",
  "limit": 100,
  "offset": 0,
  "is_partial_scan": false
}
```

### Response Fields

| Field | Type | Description |
|---|---|---|
| `entries` | `array` | Log entry objects matching all applied filters |
| `entries_processed` | `integer` or `string` | Number of log entries examined. A `"+"` suffix (e.g., `"50000+"`) means the streaming limit was reached and more entries exist |
| `filtered_count_min` | `integer` | Lower bound of total matching entries (`len(entries) + offset`) |
| `total_log_count_estimate` | `string` | Estimated total entries across all log files (e.g., `"12.5K"`, `"1.3M"`, `"0"`, `"Unknown"`) |
| `limit` | `integer` | Echo of the requested `limit` |
| `offset` | `integer` | Echo of the requested `offset` |
| `is_partial_scan` | `boolean` | `true` if the scan stopped before examining all log files |

### Log Entry Object

Each entry in the `entries` array has these fields:

| Field | Type | Description |
|---|---|---|
| `timestamp` | `string` | ISO 8601 timestamp |
| `level` | `string` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `SUCCESS`) |
| `logger_name` | `string` | Name of the Python logger that emitted the entry |
| `message` | `string` | Log message text |
| `hook_id` | `string` or `null` | Webhook delivery ID |
| `event_type` | `string` or `null` | GitHub event type |
| `repository` | `string` or `null` | Repository name (`owner/repo`) |
| `pr_number` | `integer` or `null` | Pull request number |
| `github_user` | `string` or `null` | GitHub username |
| `task_id` | `string` or `null` | Workflow task identifier |
| `task_type` | `string` or `null` | Workflow task type |
| `task_status` | `string` or `null` | Workflow task status |
| `token_spend` | `integer` or `null` | GitHub API token consumption count |

### Error Responses

| Status | Condition |
|---|---|
| `400` | Invalid `limit` (outside 1–10000), negative `offset`, or malformed datetime in `start_time`/`end_time` |
| `404` | Log server is disabled |
| `500` | Log file access errors or internal server errors |

### Examples

Fetch errors from the last 24 hours:

```
GET /logs/api/entries?level=ERROR&start_time=2024-01-14T00:00:00Z&limit=50
```

Fetch logs for a specific PR:

```
GET /logs/api/entries?repository=myakove/test-repo&pr_number=42
```

Paginated access:

```
GET /logs/api/entries?repository=myakove/test-repo&limit=50&offset=100
```

Search for rate limit issues:

```
GET /logs/api/entries?search=rate%20limit&level=WARNING
```

Debug a specific webhook delivery:

```
GET /logs/api/entries?hook_id=f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f
```

> **Note:** Infrastructure logger entries (MCP server, log viewer) without webhook context are automatically excluded from results to reduce noise.

---

## `GET /logs/api/export`

Export filtered logs as a downloadable JSON file. Supports the same filter parameters as `/logs/api/entries`.

### Query Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `format_type` | `string` | `"json"` | Export format. Only `"json"` is supported |
| `hook_id` | `string` | `null` | Filter by webhook delivery ID |
| `pr_number` | `integer` | `null` | Filter by PR number |
| `repository` | `string` | `null` | Filter by repository (`owner/repo`) |
| `event_type` | `string` | `null` | Filter by GitHub event type |
| `github_user` | `string` | `null` | Filter by GitHub username |
| `level` | `string` | `null` | Filter by log level |
| `start_time` | `string` | `null` | ISO 8601 start time |
| `end_time` | `string` | `null` | ISO 8601 end time |
| `search` | `string` | `null` | Full-text search in messages |
| `limit` | `integer` | `10000` | Maximum entries to export. Range: `1`–`100000`. Hard cap: `50000` entries in the export itself |

### Response

Returns a `StreamingResponse` with file download headers:

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `Content-Disposition` | `attachment; filename=webhook_logs_YYYYMMDD_HHMMSS.json` |

### Export File Format

```json
{
  "export_metadata": {
    "generated_at": "2024-01-15T14:30:25.123456+00:00",
    "filters_applied": {
      "repository": "myakove/test-repo",
      "level": "ERROR"
    },
    "total_entries": 156,
    "export_format": "json"
  },
  "log_entries": [
    {
      "timestamp": "2024-01-15T14:30:25.123456",
      "level": "ERROR",
      "logger_name": "webhook_server.app",
      "message": "Container build failed for PR #42",
      "hook_id": "delivery-id-123",
      "repository": "myakove/test-repo",
      "event_type": "pull_request",
      "github_user": "contributor",
      "pr_number": 42,
      "task_id": null,
      "task_type": null,
      "task_status": null,
      "token_spend": null
    }
  ]
}
```

### Error Responses

| Status | Condition |
|---|---|
| `400` | Invalid `format_type` (not `"json"`) or malformed datetime parameters |
| `404` | Log server is disabled |
| `413` | Export limit exceeds `50000` entries |
| `500` | File system or export generation errors |

### Examples

Export all errors for a repository:

```
GET /logs/api/export?format_type=json&repository=myakove/test-repo&level=ERROR
```

Export a month of logs:

```
GET /logs/api/export?format_type=json&start_time=2024-01-01T00:00:00Z&end_time=2024-01-31T23:59:59Z&limit=50000
```

---

## `GET /logs/api/pr-flow/{hook_id}`

Get PR workflow flow visualization data. Analyzes log entries to identify processing stages and timing.

### Path Parameters

| Parameter | Type | Description |
|---|---|---|
| `hook_id` | `string` | Identifier in one of these formats: raw hook ID, `hook-{id}` prefix, `pr-{number}` prefix, or a bare PR number |

### Hook ID Format Resolution

| Input | Interpretation |
|---|---|
| `hook-abc123` | Filters by hook ID `abc123` |
| `pr-42` | Filters by PR number `42` |
| `42` | Filters by PR number `42` |
| `abc123` | Filters by hook ID `abc123` |

### Response Body

```json
{
  "identifier": "hook-abc123",
  "stages": [
    {
      "name": "Webhook Received",
      "timestamp": "2024-01-15T14:30:25.000000",
      "duration_ms": null
    },
    {
      "name": "Validation Complete",
      "timestamp": "2024-01-15T14:30:25.050000",
      "duration_ms": 50
    },
    {
      "name": "Labels Applied",
      "timestamp": "2024-01-15T14:30:26.200000",
      "duration_ms": 1150,
      "error": "Label not found: size/XL"
    },
    {
      "name": "Processing Complete",
      "timestamp": "2024-01-15T14:30:28.000000",
      "duration_ms": 1800
    }
  ],
  "total_duration_ms": 3000,
  "success": true
}
```

### Response Fields

| Field | Type | Description |
|---|---|---|
| `identifier` | `string` | Echo of the `hook_id` path parameter |
| `stages` | `array` | Detected workflow stages in chronological order |
| `total_duration_ms` | `integer` | Total processing duration in milliseconds |
| `success` | `boolean` | `true` if no `ERROR`-level log entries were found |
| `error` | `string` | Present only when `success` is `false`; first error message |

### Workflow Stages

Stages are detected by matching log messages against these patterns:

| Stage Name | Matches Log Messages Containing |
|---|---|
| Webhook Received | `Processing webhook` |
| Validation Complete | `Signature verification successful` or `Processing webhook for` |
| Reviewers Assigned | `Added reviewer`, `OWNERS file`, or `reviewer assignment` |
| Labels Applied | `label` or `tag` |
| Checks Started | `check`, `test`, or `build` |
| Checks Complete | `check.*complete`, `test.*pass`, or `build.*success` |
| Processing Complete | `completed successfully` or `processing complete` |

> **Note:** Not all stages appear in every response. Only stages with matching log entries are included.

### Error Responses

| Status | Condition |
|---|---|
| `400` | Invalid hook ID format |
| `404` | No log data found for the given hook ID or PR number |
| `500` | Internal server error |

### Example

```
GET /logs/api/pr-flow/hook-f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f
```

```
GET /logs/api/pr-flow/pr-42
```

---

## `GET /logs/api/workflow-steps/{hook_id}`

Get a detailed timeline of individual workflow steps for a webhook processing event. Prioritizes structured JSON logs and falls back to text log parsing.

### Path Parameters

| Parameter | Type | Description |
|---|---|---|
| `hook_id` | `string` | GitHub webhook delivery ID (`X-GitHub-Delivery` header value) |

### Response Body

```json
{
  "hook_id": "test-hook-123",
  "start_time": "2025-01-05T10:00:00.000000Z",
  "total_duration_ms": 5000,
  "step_count": 3,
  "steps": [
    {
      "timestamp": "2025-01-05T10:00:01.000000Z",
      "step_name": "clone_repository",
      "message": "clone_repository: completed (1500ms)",
      "level": "INFO",
      "repository": "org/test-repo",
      "event_type": "pull_request",
      "pr_number": 456,
      "task_id": "clone_repository",
      "task_type": null,
      "task_status": "completed",
      "duration_ms": 1500,
      "error": null,
      "step_details": {
        "timestamp": "2025-01-05T10:00:01.000000Z",
        "status": "completed",
        "duration_ms": 1500
      },
      "relative_time_ms": 1000
    },
    {
      "timestamp": "2025-01-05T10:00:03.500000Z",
      "step_name": "apply_labels",
      "message": "apply_labels: failed - Label not found",
      "level": "ERROR",
      "repository": "org/test-repo",
      "event_type": "pull_request",
      "pr_number": 456,
      "task_id": "apply_labels",
      "task_type": null,
      "task_status": "failed",
      "duration_ms": 200,
      "error": {
        "type": "ValueError",
        "message": "Label not found"
      },
      "step_details": {
        "timestamp": "2025-01-05T10:00:03.500000Z",
        "status": "failed",
        "duration_ms": 200,
        "error": {
          "type": "ValueError",
          "message": "Label not found"
        }
      },
      "relative_time_ms": 3500
    }
  ],
  "token_spend": 35,
  "event_type": "pull_request",
  "action": "opened",
  "repository": "org/test-repo",
  "sender": "test-user",
  "pr": {
    "number": 456,
    "title": "Test PR",
    "url": "https://github.com/org/test-repo/pull/456"
  },
  "success": false,
  "error": null
}
```

### Response Fields

| Field | Type | Description |
|---|---|---|
| `hook_id` | `string` | Webhook delivery ID |
| `start_time` | `string` or `null` | ISO 8601 timestamp of processing start |
| `total_duration_ms` | `integer` | Total processing duration in milliseconds |
| `step_count` | `integer` | Number of workflow steps |
| `steps` | `array` | Ordered list of step objects (see below) |
| `token_spend` | `integer` or `null` | GitHub API token consumption count |
| `event_type` | `string` or `null` | GitHub event type (`pull_request`, `check_run`, etc.) |
| `action` | `string` or `null` | Event action (`opened`, `synchronize`, etc.) |
| `repository` | `string` or `null` | Repository name (`owner/repo`) |
| `sender` | `string` or `null` | GitHub username who triggered the event |
| `pr` | `object` or `null` | PR info with `number`, `title`, `url` |
| `success` | `boolean` or `null` | Whether webhook processing succeeded |
| `error` | `string` or `null` | Error message if processing failed |

### Step Object Fields

| Field | Type | Description |
|---|---|---|
| `timestamp` | `string` | ISO 8601 timestamp when step was recorded |
| `step_name` | `string` | Step identifier (e.g., `clone_repository`, `assign_reviewers`) |
| `message` | `string` | Human-readable step summary |
| `level` | `string` | Derived log level: `DEBUG` for `started`, `INFO` for `completed`, `ERROR` for `failed` |
| `repository` | `string` or `null` | Repository name |
| `event_type` | `string` or `null` | Event type |
| `pr_number` | `integer` or `null` | PR number |
| `task_id` | `string` | Same as `step_name` |
| `task_type` | `string` or `null` | Task type from the JSON log |
| `task_status` | `string` | Step status: `started`, `completed`, `failed`, or `unknown` |
| `duration_ms` | `integer` or `null` | Step execution duration in milliseconds |
| `error` | `object` or `null` | Error details with `type` and `message` fields |
| `step_details` | `object` | Raw step data from JSON log |
| `relative_time_ms` | `integer` | Milliseconds elapsed since `start_time` |

### Error Responses

| Status | Condition |
|---|---|
| `400` | Invalid hook ID |
| `404` | No workflow data or steps found for the hook ID |
| `500` | Malformed log entry or internal server error |

### Example

```
GET /logs/api/workflow-steps/f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f
```

---

## `GET /logs/api/step-logs/{hook_id}/{step_name}`

Retrieve log entries that occurred during a specific workflow step's execution time window.

> **Note:** This endpoint requires access from a trusted network (private IP ranges, loopback, or link-local addresses). Requests from public IPs receive a `403` response.

### Path Parameters

| Parameter | Type | Constraints | Description |
|---|---|---|---|
| `hook_id` | `string` | 1–100 characters | GitHub webhook delivery ID |
| `step_name` | `string` | 1–100 characters | Workflow step name (e.g., `clone_repository`, `webhook_routing`) |

### Response Body

```json
{
  "step": {
    "name": "clone_repository",
    "status": "completed",
    "timestamp": "2025-01-05T10:00:01.000000Z",
    "duration_ms": 1500,
    "error": null
  },
  "logs": [
    {
      "timestamp": "2025-01-05T10:00:01.100000",
      "level": "INFO",
      "logger_name": "webhook_server.app",
      "message": "Cloning repository org/test-repo",
      "hook_id": "test-hook-123",
      "event_type": "pull_request",
      "repository": "org/test-repo",
      "github_user": null,
      "pr_number": 456,
      "task_id": null,
      "task_type": null,
      "task_status": null,
      "token_spend": null
    }
  ],
  "log_count": 1
}
```

### Response Fields

| Field | Type | Description |
|---|---|---|
| `step` | `object` | Step metadata |
| `step.name` | `string` | Step name |
| `step.status` | `string` | Step status (`started`, `completed`, `failed`, `unknown`) |
| `step.timestamp` | `string` | ISO 8601 timestamp |
| `step.duration_ms` | `integer` or `null` | Step execution duration. When `null`, a 60-second default window is used |
| `step.error` | `object` or `null` | Error details if the step failed |
| `logs` | `array` | Log entries within the step's execution time window (max 500 entries) |
| `log_count` | `integer` | Number of log entries returned |

### Error Responses

| Status | Condition |
|---|---|
| `403` | Request from untrusted (public) IP address |
| `404` | Hook ID not found, or step name not found within the hook's workflow steps |
| `500` | Step has no timestamp, or invalid timestamp format |

### Example

```
GET /logs/api/step-logs/test-hook-123/clone_repository
```

---

## `WebSocket /logs/ws`

Real-time log streaming via WebSocket. Monitors log files for new entries and pushes them to connected clients. Supports server-side filtering.

### Connection URL

```
ws://<host>:<port>/logs/ws
```

### Query Parameters

All parameters are optional. When no filters are provided, all new log entries are streamed.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `hook_id` | `string` | `null` | Stream only entries for this webhook delivery ID |
| `pr_number` | `integer` | `null` | Stream only entries for this PR number |
| `repository` | `string` | `null` | Stream only entries for this repository |
| `event_type` | `string` | `null` | Stream only entries for this event type |
| `github_user` | `string` | `null` | Stream only entries for this GitHub user |
| `level` | `string` | `null` | Stream only entries at this log level |

### Connection Lifecycle

1. Client connects to `ws://<host>:<port>/logs/ws?<filters>`
2. Server accepts the connection
3. Server monitors the log directory for new entries
4. Matching entries are sent as JSON messages
5. Connection closes on client disconnect or server shutdown (close code `1001`)
6. On internal error, server closes with code `1011`

### Message Format

Each WebSocket message is a JSON object with the same structure as a [Log Entry Object](#log-entry-object):

```json
{
  "timestamp": "2024-01-15T14:30:25.123456",
  "level": "INFO",
  "logger_name": "webhook_server.app",
  "message": "Processing webhook for repository: myakove/test-repo",
  "hook_id": "f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f",
  "event_type": "pull_request",
  "repository": "myakove/test-repo",
  "github_user": "contributor123",
  "pr_number": 42,
  "task_id": null,
  "task_type": null,
  "task_status": null,
  "token_spend": null
}
```

### Error Messages

If the log directory is not found, the server sends an error object before the stream starts:

```json
{
  "error": "Log directory not found"
}
```

### WebSocket Close Codes

| Code | Meaning |
|---|---|
| `1001` | Server shutdown |
| `1008` | Log server is disabled (`ENABLE_LOG_SERVER` is not `true`) |
| `1011` | Internal server error during streaming |

### Examples

Connect with no filters (stream all entries):

```
ws://localhost:8080/logs/ws
```

Stream only errors for a specific repository:

```
ws://localhost:8080/logs/ws?repository=myakove/test-repo&level=ERROR
```

Monitor a specific PR:

```
ws://localhost:8080/logs/ws?pr_number=42
```

> **Tip:** The server tracks all active WebSocket connections and closes them gracefully during shutdown.

---

## Datetime Format

All datetime parameters and response fields use ISO 8601 format. The server accepts:

| Format | Example |
|---|---|
| UTC with `Z` suffix | `2024-01-15T10:00:00Z` |
| With timezone offset | `2024-01-15T10:00:00+00:00` |
| With microseconds | `2024-01-15T10:00:00.123456` |
| With microseconds and `Z` | `2024-01-15T10:00:00.123456Z` |

Internally, `Z` is converted to `+00:00` for parsing.

---

## Scanning Limits

The API uses memory-efficient streaming with processing caps to prevent resource exhaustion:

| Context | Max Files Scanned | Max Entries Processed |
|---|---|---|
| `/logs/api/entries` (no filters) | 25 | 20,000 |
| `/logs/api/entries` (with filters) | 25 | 50,000 |
| `/logs/api/export` (with filters) | 25 | up to 100,000 |
| `/logs/api/pr-flow/{hook_id}` | 15 | 10,000 |
| `/logs/api/workflow-steps/{hook_id}` | 25 | 50,000 |
| `/logs/api/step-logs/{hook_id}/{step_name}` | 25 | 50,000 |
| Step logs per step | — | 500 (hard cap) |

When the processing limit is reached, `is_partial_scan` is `true` in the entries response and `entries_processed` has a `"+"` suffix.

---

## Related Pages

- See [Using the Log Viewer](using-the-log-viewer.html) for a guide on browsing, searching, and filtering logs through the web UI.
- See [Environment Variables](environment-variables.html) for `ENABLE_LOG_SERVER` and `WEBHOOK_SERVER_DATA_DIR` configuration.
- See [Webhook Events and Handlers](webhook-events-reference.html) for the list of `event_type` values used in filters.
- See [Configuration Reference](configuration-reference.html) for the `logs-server-log-file` config key.

## Related Pages

- [Using the Log Viewer](using-the-log-viewer.html)
- [Environment Variables](environment-variables.html)
- [Webhook Events and Handlers](webhook-events-reference.html)
- [MCP Server for AI Agents](mcp-server-integration.html)
- [Configuration Reference](configuration-reference.html)

---

Source: publishing-to-pypi.md

Let me explore the repository to understand the PyPI publishing functionality.

Now let me check the events configuration to understand the push event requirement:

Now let me check the test for push handler to understand failure scenarios:

Now let me check how events are configured to make sure `push` is needed:

Now I have a comprehensive understanding. Let me also check how the `.github-webhook-server.yaml` is loaded for per-repo config:

Now I have all the information I need. Let me write the documentation page.

# Publishing Packages to PyPI

Automatically publish your Python package to PyPI every time you push a Git tag, with built-in failure alerts and optional Slack notifications.

## Prerequisites

- A [PyPI API token](https://pypi.org/help/#apitoken) scoped to your project
- Your repository configured in the webhook server (see [Configuring Repositories](configuring-repositories.html))
- The `push` event enabled in your repository's events list
- Your project must be buildable with `uv build --sdist`

## Quick Example

Add the `pypi` section to your repository configuration:

```yaml
# In config.yaml, under your repository
repositories:
  my-org/my-python-package:
    pypi:
      token: pypi-AgEIcH...your-token-here
    events:
      - push
      - pull_request
      - pull_request_review
      - issue_comment
      - check_run
      - status
```

Now push a tag:

```bash
git tag v1.2.0
git push origin v1.2.0
```

The webhook server builds your package and uploads it to PyPI automatically.

## Step-by-Step Setup

### 1. Generate a PyPI API Token

1. Log in to [pypi.org](https://pypi.org) and go to **Account Settings → API Tokens**.
2. Create a token scoped to your specific project (recommended) or account-wide.
3. Copy the token — it starts with `pypi-`.

### 2. Add the Token to Your Configuration

You can configure PyPI publishing in either location:

| Config file | Scope |
|---|---|
| `config.yaml` | Centralized — managed by the server admin |
| `.github-webhook-server.yaml` (in repo root) | Per-repository — managed by repo maintainers |

**Option A: Central config (`config.yaml`)**

```yaml
repositories:
  my-org/my-python-package:
    pypi:
      token: pypi-AgEIcH...your-token-here
```

**Option B: Per-repo config (`.github-webhook-server.yaml`)**

```yaml
pypi:
  token: pypi-AgEIcH...your-token-here
```

> **Tip:** Per-repo config in `.github-webhook-server.yaml` overrides the central `config.yaml` values, so teams can manage their own PyPI tokens independently.


> **Warning:** PyPI tokens are secrets. Use environment variable substitution or a secret manager to avoid committing tokens in plain text. The schema marks the token field with `format: password`, and the server redacts it from logs when `mask-sensitive-data` is enabled (the default).

### 3. Include the `push` Event

Make sure `push` is in your repository's event list. If you omit the `events` key entirely, all events are listened to by default. If you specify events explicitly, include `push`:

```yaml
events:
  - push
  - pull_request
  - pull_request_review
  - issue_comment
  - check_run
  - status
```

### 4. Push a Git Tag to Trigger Publishing

The webhook server only processes tag pushes — regular branch pushes are skipped. Any tag format works:

```bash
# Semantic version tags
git tag v1.0.0
git push origin v1.0.0

# Tags with slashes
git tag release/v2.0.0
git push origin release/v2.0.0
```

> **Note:** Branch/tag deletions are automatically ignored — deleting a tag does not trigger a publish.

## What Happens During Publishing

When a tag push is received, the server runs these steps in order:

1. **Checkout** — The tagged commit is checked out into a temporary worktree
2. **Build** — Runs `uv build --sdist` to create a source distribution
3. **Validate** — Runs `twine check` to verify the package metadata
4. **Upload** — Runs `twine upload` with the `--skip-existing` flag to publish to PyPI

If any step fails, the process stops immediately and a GitHub issue is created in the repository (see [Failure Handling](#failure-handling) below).

> **Note:** The `--skip-existing` flag means re-pushing an already-published tag version will not cause an error — the upload is silently skipped.

## Slack Notifications on Success

When a package is successfully published and a Slack webhook URL is configured for the repository, a notification is sent:

```
my-org/my-python-package Version v1.2.0 published to PYPI.
```

See [Setting Up Slack Notifications](setting-up-notifications.html) for how to configure `slack-webhook-url`.

## The `python-module-install` PR Check

When `pypi` is configured, the webhook server automatically adds a **`python-module-install`** check run to every pull request. This check:

- Runs `pip wheel --no-cache-dir` against the PR branch to verify the package builds correctly
- Reports success or failure as a GitHub check run on the PR
- Is automatically added to required status checks for branch protection

This catches packaging errors (missing files, broken `pyproject.toml`, import issues) **before** a release tag is pushed, so you won't discover build failures at publish time.

You can re-run this check on a PR with:

```
/retest python-module-install
```

See [Setting Up CI Checks](setting-up-ci-checks.html) for more details on CI check configuration.

## Failure Handling

If any step in the publish process fails, the webhook server automatically creates a **GitHub issue** in the repository with:

- **Title**: A sanitized summary of the error (truncated to 250 characters)
- **Body**: The full error message from the failing command

This ensures publish failures are visible to the team even if no one is watching the server logs.

Common failure scenarios that trigger issue creation:

| Failure | Cause |
|---|---|
| Checkout failure | Tag doesn't exist or repo can't be cloned |
| Build failure | `uv build --sdist` fails (e.g., missing `pyproject.toml`) |
| Twine check failure | Package metadata is invalid |
| Upload failure | Invalid token, network error, or PyPI API issue |

## Advanced Usage

### Combining PyPI Publishing with Container Builds

PyPI publishing and container builds can both trigger on the same tag push. If both `pypi` and `container` (with `release: true`) are configured, the server runs PyPI upload first, then builds and pushes the container image. A PyPI upload failure stops processing — the container build will not run.

```yaml
repositories:
  my-org/my-python-package:
    pypi:
      token: pypi-AgEIcH...your-token-here
    container:
      repository: quay.io/my-org/my-package
      username: my-user
      password: my-password
      release: true
```

### Token Security

The PyPI token is redacted from all command logs. The server passes it as a `--password` argument to `twine upload` and registers it as a secret to redact, so even if a command fails and the error output is logged, the token value is masked.

To verify sensitive data masking is active for your repository:

```yaml
# In config.yaml (global) or .github-webhook-server.yaml (per-repo)
mask-sensitive-data: true  # This is the default
```

> **Warning:** Setting `mask-sensitive-data: false` disables log redaction for that repository. Only use this temporarily for debugging, and never in production.

## Troubleshooting

**Package uploads but the wrong version is published**
The server checks out the exact commit pointed to by the tag. Make sure your `pyproject.toml` version matches the tag. Consider using a tool like `release-it` or `setuptools-scm` to keep versions in sync.

**Issue created with "twine upload failed" error**
Verify your PyPI token is valid and has upload permissions for the project. Account-scoped tokens work for any project; project-scoped tokens only work for the specified project.

**`python-module-install` check fails on PRs but local builds work**
The check runs `pip wheel` in an isolated environment from the PR branch merged with the base branch. Ensure all build dependencies are declared in `pyproject.toml` (not just installed locally).

**No publish happens when I push a tag**
- Confirm `push` is in your `events` list (or that `events` is not set, which subscribes to all events)
- Confirm `pypi.token` is set in your config
- Check the server logs for the tag push event

## Related Pages

- [Setting Up CI Checks](setting-up-ci-checks.html)
- [Configuring Repositories](configuring-repositories.html)
- [Setting Up Slack Notifications](setting-up-notifications.html)
- [Configuration Reference](configuration-reference.html)
- [Configuration Recipes](config-recipes.html)

---

Source: mcp-server-integration.md

Let me explore the codebase to understand the MCP server implementation.

Now let me check what API routes are exposed (and thus available as MCP tools), particularly the ones NOT tagged with `mcp_exclude`:

Now let me check the schema for the mcp-log-file config option:

Now let me check what examples config looks like for the mcp-log-file:

Now let me also check the `require_trusted_network` dependency to understand the security model for some endpoints:

Now let me understand the available MCP tools. The `FastApiMCP` creates tools from all API routes except those tagged `mcp_exclude`. Let me list all routes and their tags:

So the tools exposed via MCP are (excluding `mcp_exclude` tag and `include_in_schema=False`):
- `healthcheck` — GET /webhook_server/healthcheck
- `get_log_entries` — GET /logs/api/entries (if log server enabled)
- `export_logs` — GET /logs/api/export (if log server enabled)
- `get_pr_flow_data` — GET /logs/api/pr-flow/{hook_id} (if log server enabled)
- `get_workflow_steps` — GET /logs/api/workflow-steps/{hook_id} (if log server enabled)
- `get_step_logs` — GET /logs/api/step-logs/{hook_id}/{step_name} (if log server enabled)
- `get_log_viewer_page` — GET /logs (if log server enabled)

The `process_webhook` has `mcp_exclude` tag and the `/mcp` endpoint has `include_in_schema=False`.

Let me check if the log viewer page endpoint is also excluded or conditionally registered:

Now let me check how Claude Desktop, Cursor, etc. would connect:

Let me also check the pyproject.toml for the fastapi-mcp version:

Now let me verify the exclusions file one more time to be very sure what I should not mention:

So I should not link to quickstart.html or environment-variables.html. Let me now check what existing docs exist that I could link to:

Now I have a comprehensive understanding. Let me write the documentation page.

# MCP Server for AI Agents

Connect AI agents to your webhook server so they can query webhook logs, inspect PR workflows, and analyze processing steps programmatically using the Model Context Protocol (MCP).

## Prerequisites

- A running instance of the webhook server.
- The log server enabled (`ENABLE_LOG_SERVER=true`) — most MCP tools expose log viewer endpoints.
- An MCP-compatible AI client (Claude Desktop, Cursor, Windsurf, or any Streamable HTTP MCP client).

## Quick Example

Start the server with MCP enabled:

```bash
ENABLE_MCP_SERVER=true ENABLE_LOG_SERVER=true uv run entrypoint.py
```

Then point your AI client to the MCP endpoint:

```
http://localhost:5000/mcp
```

That's it — the AI agent can now call tools like `get_log_entries`, `get_pr_flow_data`, and `get_workflow_steps`.

## Enabling the MCP Server

### Local development

Set the environment variable before starting the server:

```bash
ENABLE_MCP_SERVER=true uv run entrypoint.py
```

### Docker Compose

Add the variable to your `environment` block:

```yaml
services:
  github-webhook-server:
    image: ghcr.io/myk-org/github-webhook-server:latest
    environment:
      - ENABLE_MCP_SERVER=true
      - ENABLE_LOG_SERVER=true
```

> **Note:** The MCP endpoint listens on the same port as the main webhook server. It uses the `/mcp` path exclusively and does not interfere with webhook processing.

### Verifying the endpoint

After starting the server, confirm MCP is active by checking the startup logs for:

```
MCP integration initialized successfully (no authentication configured)
```

## Connecting AI Clients

The MCP server uses Streamable HTTP transport in stateless mode. Any MCP client that supports HTTP transport can connect.

### Claude Desktop

Add this to your Claude Desktop MCP configuration file:

```json
{
  "mcpServers": {
    "webhook-server": {
      "url": "http://localhost:5000/mcp"
    }
  }
}
```

### Cursor

In Cursor's MCP settings, add a new server with:

- **Type:** Streamable HTTP
- **URL:** `http://localhost:5000/mcp`

### Other MCP clients

Any client that supports the MCP Streamable HTTP transport can connect to `http://<your-server-host>:<port>/mcp` using `GET`, `POST`, and `DELETE` HTTP methods.

## Available Tools

When connected, AI agents can invoke these tools:

| Tool | Description |
|------|-------------|
| `healthcheck` | Check if the webhook server is running |
| `get_log_entries` | Query webhook processing logs with filters (hook ID, PR number, repository, event type, user, level, time range, text search) and pagination |
| `export_logs` | Export filtered logs as downloadable JSON files for offline analysis |
| `get_pr_flow_data` | Get PR workflow visualization data for a specific webhook delivery ID — tracks the full lifecycle from receipt to completion |
| `get_workflow_steps` | Retrieve step-by-step timing, status, and diagnostic data for each operation in a webhook processing flow |
| `get_step_logs` | Get log entries that occurred during a specific workflow step's execution window |

> **Note:** The `get_log_entries`, `export_logs`, `get_pr_flow_data`, `get_workflow_steps`, and `get_step_logs` tools require `ENABLE_LOG_SERVER=true`. Without the log server, only `healthcheck` is available. See [Using the Log Viewer](using-the-log-viewer.html) for log server setup.

### Example agent queries

Once connected, you can ask your AI agent questions like:

- "Show me all ERROR-level logs from the last hour"
- "What happened during the processing of webhook delivery `f4b3c2d1-a9b8-4c5d-9e8f-1a2b3c4d5e6f`?"
- "Export all logs for PR #42 in `myorg/myrepo`"
- "Show me the workflow steps and timing for the last failed webhook"

The agent translates these into the appropriate tool calls automatically.

## Advanced Usage

### Customizing the MCP Log File

MCP traffic generates its own log output, kept separate from main webhook processing logs. By default, MCP logs write to `mcp_server.log` in your data directory. Override this in `config.yaml`:

```yaml
mcp-log-file: custom_mcp.log
```

> **Tip:** You must restart the server after changing `mcp-log-file` or `ENABLE_MCP_SERVER`. Logging bindings are established at startup.

See [Configuration Reference](configuration-reference.html) for all global config options.

### Stateless Session Mode

The MCP server runs in **stateless mode** — it does not track client sessions or store events between requests. Each tool invocation is independent. This means:

- No session cookies or IDs are required from clients.
- Multiple AI agents can connect simultaneously without interference.
- Server restarts do not break ongoing agent workflows (agents simply reconnect).

### Reverse Proxy Configuration

If you run the server behind a reverse proxy (Nginx, Traefik, Cloudflare Tunnel), ensure it forwards all three HTTP methods required by MCP:

- `GET`
- `POST`
- `DELETE`

If your proxy blocks or filters any of these, the MCP endpoint will not function.

### Webhook Processing Is Excluded

The webhook ingestion endpoint is intentionally excluded from MCP tools. AI agents can query and analyze webhook data but cannot trigger webhook processing through MCP.

## Security Considerations

> **Warning:** The `/mcp` endpoint has **no built-in authentication**. Never expose it to the public internet.

Secure the endpoint using one of these strategies:

- **Same-host access:** Run the AI agent on the same machine or container network as the webhook server.
- **Private network binding:** Bind the server to an internal network IP that is not publicly routable.
- **Authenticated reverse proxy:** Place an authenticating proxy (with TLS, client certificates, or token auth) in front of `/mcp`.

The same security considerations apply to the log viewer endpoints. See [Using the Log Viewer](using-the-log-viewer.html) for additional network isolation guidance.

## Troubleshooting

- **Endpoint returns 404:** Verify the environment contains exactly `ENABLE_MCP_SERVER=true` (lowercase `true`). The feature uses exact string matching.
- **Tools return errors about log server:** Ensure `ENABLE_LOG_SERVER=true` is also set. Most MCP tools depend on the log server being active.
- **Client times out or fails to connect:** Check that your reverse proxy permits `GET`, `POST`, and `DELETE` requests to `/mcp`.
- **Can't find MCP logs:** Look for `mcp_server.log` inside the `logs/` folder of your configured data directory. If the file doesn't exist, verify the directory is writable.
- **Noisy "ClosedResourceError" messages:** These are automatically suppressed. If you see them, the server is filtering client disconnect noise from the MCP transport — no action is needed.

## Related Pages

- [Using the Log Viewer](using-the-log-viewer.html)
- [Log Viewer API Reference](log-viewer-api.html)
- [Configuration Reference](configuration-reference.html)
- [Environment Variables](environment-variables.html)
- [Enabling AI Features](enabling-ai-features.html)

---
