Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1f23d80
feat(labels): add configurable labels with enable/disable and custom …
myakove Jan 13, 2026
fbe147d
test(labels): add edge case tests for labels configuration
myakove Jan 13, 2026
0e02c5c
perf(clone): optimize git clone with shallow clone and targeted fetch
myakove Jan 13, 2026
a7583e4
fix(github-api): prevent AttributeError when log_prefix not yet assigned
myakove Jan 13, 2026
9bcab8d
fix(clone): fetch base branch before PR ref for non-default branch ta…
myakove Jan 13, 2026
4f613ba
feat(labels): add configurable labels with enable/disable and custom …
myakove Jan 13, 2026
c716fcb
refactor(labels): centralize label-category mapping and fix TimeoutWa…
myakove Jan 13, 2026
5511d03
refactor(labels): extract STATIC_PR_SIZE_THRESHOLDS constant
myakove Jan 13, 2026
ae34aad
fix(owners): handle no merge base error with fallback to two-dot diff
myakove Jan 13, 2026
f164ec6
fix(labels): handle misconfigured label_colors as list instead of dict
myakove Jan 13, 2026
8474f77
fix(handlers): improve type safety and code clarity
myakove Jan 13, 2026
295131a
fix: address coderabbit review comments for configurable labels
myakove Jan 13, 2026
cd18c05
fix: address coderabbit review comments for configurable labels
myakove Jan 13, 2026
eb4b63b
fix: address CodeRabbit review comments for configurable labels
myakove Jan 13, 2026
852bb0b
fix: address CodeRabbit review comments and add SafeRotatingFileHandler
myakove Jan 13, 2026
fb242c3
fix: address CodeRabbit review - cleanup unused fixtures and noqa dir…
myakove Jan 13, 2026
1f480b0
fix: address CodeRabbit review comments
myakove Jan 13, 2026
1baa0ab
fix: address CodeRabbit review comments
myakove Jan 13, 2026
546857c
fix: address CodeRabbit review and revert shallow clone
myakove Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ repos:
hooks:
- id: mypy
exclude: (tests/)
additional_dependencies: [types-requests, types-PyYAML, types-colorama, types-aiofiles]
additional_dependencies:
[types-requests, types-PyYAML, types-colorama, types-aiofiles]

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v10.0.0-rc.0
Expand Down
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,85 @@ Invalid color names automatically fall back to `lightgray`.
Configuration changes take effect immediately without server restart. The webhook
server re-reads configuration for each incoming webhook event.

### Configurable Labels

The webhook server supports enabling/disabling specific label categories and customizing label colors. This allows repository administrators to control which automation labels are applied to pull requests.

#### Configuration Options

```yaml
# Global configuration (applies to all repositories)
labels:
enabled-labels:
- verified
- hold
- wip
- needs-rebase
- has-conflicts
- can-be-merged
- size
- branch
- cherry-pick
- automerge
colors:
hold: red
verified: green
wip: orange

# Repository-specific configuration (overrides global)
repositories:
my-project:
name: my-org/my-project
labels:
enabled-labels:
- verified
- wip
- size
colors:
verified: lightgreen
```

#### Available Label Categories

| Category | Labels Applied | Description |
|----------|---------------|-------------|
| `verified` | `verified` | Manual verification status |
| `hold` | `hold` | Block PR merging |
| `wip` | `wip` | Work in progress status |
| `needs-rebase` | `needs-rebase` | PR needs rebasing |
| `has-conflicts` | `has-conflicts` | Merge conflicts detected |
| `can-be-merged` | `can-be-merged` | PR meets all merge requirements |
| `size` | `size/XS`, `size/S`, etc. | PR size labels |
| `branch` | `branch/<name>` | Target branch labels |
| `cherry-pick` | `cherry-pick/<branch>` | Cherry-pick tracking |
| `automerge` | `automerge` | Auto-merge enabled |

#### Configuration Rules

- **enabled-labels**: Optional array of label categories to enable
- If omitted, ALL label categories are enabled (default behavior)
- If empty array `[]`, all configurable labels are disabled
- **colors**: Optional object mapping label names to CSS3 color names
- Supports any valid CSS3 color name (e.g., `red`, `lightblue`, `darkgreen`)
- Invalid color names fall back to default colors
- **reviewed-by labels**: Always enabled (`approved-*`, `lgtm-*`, `changes-requested-*`, `commented-*`)
- These are the source of truth for the approval system and cannot be disabled
- **Hierarchy**: Repository-level configuration overrides global configuration
- **Real-time Updates**: Changes take effect immediately without server restart

#### Example: Minimal Labels Configuration

```yaml
# Only enable essential labels
labels:
enabled-labels:
- verified
- can-be-merged
- size
```

This configuration disables `hold`, `wip`, `needs-rebase`, `has-conflicts`, `branch`, `cherry-pick`, and `automerge` labels.

### Repository-Level Overrides

Create `.github-webhook-server.yaml` in your repository root to override or extend the global configuration for that specific repository. This file supports all repository-level configuration options.
Expand All @@ -428,6 +507,16 @@ set-auto-merge-prs:
pre-commit: true
conventional-title: "feat,fix,docs"

# Label configuration
labels:
enabled-labels:
- verified
- hold
- wip
colors:
hold: crimson
verified: limegreen

# Custom PR size labels for this repository
pr-size-thresholds:
Quick:
Expand Down
43 changes: 43 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ auto-verify-cherry-picked-prs: true # Default: true - automatically verify cher

create-issue-for-new-pr: true # Global default: create tracking issues for new PRs

# Labels configuration - control which labels are enabled and their colors
# If not set, all labels are enabled with default colors
labels:
# Optional: List of label categories to enable
# If not set, all labels are enabled. If set, only listed categories are enabled.
# Note: reviewed-by labels (approved-*, lgtm-*, etc.) are always enabled and cannot be disabled
enabled-labels:
- verified
- hold
- wip
- needs-rebase
- has-conflicts
- can-be-merged
- size
- branch
- cherry-pick
- automerge
# Optional: Custom colors for labels (CSS3 color names)
colors:
hold: red
verified: green
wip: orange
needs-rebase: darkred
has-conflicts: red
can-be-merged: limegreen
automerge: green
# Dynamic label prefixes
approved-: green
lgtm-: yellowgreen
changes-requested-: orange
commented-: gold
cherry-pick-: coral
branch-: royalblue

# Global PR size label configuration (optional)
# Define custom categories based on total lines changed (additions + deletions)
# threshold: positive integer or 'inf' for unbounded largest category
Expand Down Expand Up @@ -152,6 +186,15 @@ repositories:
minimum-lgtm: 0 # The minimum PR lgtm required before approve the PR
create-issue-for-new-pr: true # Override global setting: create tracking issues for new PRs (default: true)

# Repository-specific labels configuration (overrides global)
labels:
enabled-labels:
- verified
- hold
- size
colors:
hold: purple

# Repository-specific PR size labels (overrides global configuration)
pr-size-thresholds:
Express:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ exclude = [".git", ".venv", ".mypy_cache", ".tox", "__pycache__"]

[tool.mypy]
check_untyped_defs = true
disallow_any_generics = false
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_optional = true
Expand Down
20 changes: 10 additions & 10 deletions scripts/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import subprocess
import sys
from collections import OrderedDict
from typing import Any


def json_line(line: str) -> dict:
def json_line(line: str) -> dict[str, Any]:
"""
Format str line to str that can be parsed with json.

Expand Down Expand Up @@ -59,28 +60,27 @@ def execute_git_log(from_tag: str, to_tag: str) -> str:
sys.exit(1)


def parse_commit_line(line: str) -> dict:
def parse_commit_line(line: str) -> dict[str, Any]:
"""Parses a single JSON formatted git log line."""
try:
return json_line(line=line)
except json.decoder.JSONDecodeError as ex:
except json.JSONDecodeError as ex:
print(f"Error parsing JSON: {line} - {ex}")
return {}


def categorize_commit(commit: dict, title_to_type_map: dict, default_category: str = "Other Changes:") -> str:
def categorize_commit(
commit: dict[str, Any], title_to_type_map: dict[str, str], default_category: str = "Other Changes:"
) -> str:
"""Categorizes a commit based on its title prefix."""
if not commit or "title" not in commit:
return default_category
title = commit["title"]
try:
prefix = title.split(":", 1)[0].lower() # Extract the prefix before the first colon
return title_to_type_map.get(prefix, default_category)
except IndexError:
return default_category
prefix = title.split(":", 1)[0].lower() # Extract the prefix before the first colon
return title_to_type_map.get(prefix, default_category)


def format_changelog_entry(change: dict, section: str) -> str:
def format_changelog_entry(change: dict[str, Any], section: str) -> str:
"""Formats a single changelog entry."""
title = change["title"].split(":", 1)[1] if section != "Other Changes:" else change["title"]
return f"- {title} ({change['commit']}) by {change['author']} on {change['date']}\n"
Expand Down
7 changes: 4 additions & 3 deletions webhook_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from ipaddress import IPv4Network, IPv6Network
from typing import Any

import httpx
Expand Down Expand Up @@ -58,11 +59,11 @@
MCP_SERVER_ENABLED: bool = os.environ.get("ENABLE_MCP_SERVER") == "true"

# Global variables
ALLOWED_IPS: tuple[ipaddress._BaseNetwork, ...] = ()
ALLOWED_IPS: tuple[IPv4Network | IPv6Network, ...] = ()
LOGGER = get_logger_with_params()

_lifespan_http_client: httpx.AsyncClient | None = None
_background_tasks: set[asyncio.Task] = set()
_background_tasks: set[asyncio.Task[Any]] = set()

# MCP Globals
http_transport: Any | None = None
Expand Down Expand Up @@ -160,7 +161,7 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
LOGGER.debug(f"verify_github_ips: {verify_github_ips}, verify_cloudflare_ips: {verify_cloudflare_ips}")

global ALLOWED_IPS
networks: set[ipaddress._BaseNetwork] = set()
networks: set[IPv4Network | IPv6Network] = set()

if verify_cloudflare_ips:
try:
Expand Down
66 changes: 66 additions & 0 deletions webhook_server/config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,39 @@ properties:
type: boolean
description: Create a tracking issue for new pull requests (global default)
default: true
labels:
type: object
description: Configure which labels are enabled and their colors
properties:
enabled-labels:
type: array
description: |
List of label categories to enable. If not set, all labels are enabled.
Categories: verified, hold, wip, needs-rebase, has-conflicts, can-be-merged,
size, branch, cherry-pick, automerge.
Note: reviewed-by labels (approved-*, lgtm-*, changes-requested-*, commented-*)
are always enabled and cannot be disabled.
items:
type: string
enum:
- verified
- hold
- wip
- needs-rebase
- has-conflicts
- can-be-merged
- size
- branch
- cherry-pick
- automerge
colors:
type: object
description: |
Custom colors for labels (CSS3 color names like 'red', 'green', 'blue').
Use label name as key, color as value. For dynamic labels use prefix (e.g., 'approved-', 'branch-').
additionalProperties:
type: string
additionalProperties: false

pr-size-thresholds:
type: object
Expand Down Expand Up @@ -299,6 +332,39 @@ properties:
type: boolean
description: Create a tracking issue for new pull requests
default: true
labels:
type: object
description: Configure which labels are enabled and their colors
properties:
enabled-labels:
type: array
description: |
List of label categories to enable. If not set, all labels are enabled.
Categories: verified, hold, wip, needs-rebase, has-conflicts, can-be-merged,
size, branch, cherry-pick, automerge.
Note: reviewed-by labels (approved-*, lgtm-*, changes-requested-*, commented-*)
are always enabled and cannot be disabled.
items:
type: string
enum:
- verified
- hold
- wip
- needs-rebase
- has-conflicts
- can-be-merged
- size
- branch
- cherry-pick
- automerge
colors:
type: object
description: |
Custom colors for labels (CSS3 color names like 'red', 'green', 'blue').
Use label name as key, color as value. For dynamic labels use prefix (e.g., 'approved-', 'branch-').
additionalProperties:
type: string
additionalProperties: false
pr-size-thresholds:
type: object
description: Custom PR size thresholds with label names and colors (repository-specific override)
Expand Down
33 changes: 33 additions & 0 deletions webhook_server/libs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from github.GithubException import UnknownObjectException
from simple_logger.logger import get_logger

from webhook_server.utils.constants import CONFIGURABLE_LABEL_CATEGORIES


class Config:
def __init__(
Expand All @@ -20,6 +22,7 @@ def __init__(
self.repository = repository
self.exists()
self.repositories_exists()
self.validate_labels_config()

def exists(self) -> None:
if not os.path.isfile(self.config_path):
Expand All @@ -29,6 +32,36 @@ def repositories_exists(self) -> None:
if not self.root_data.get("repositories"):
raise ValueError(f"Config {self.config_path} does not have `repositories`")

def validate_labels_config(self) -> None:
"""Validate enabled-labels configuration against allowed categories.

Raises:
ValueError: If any label category in enabled-labels is not valid.
"""
data = self.root_data

# Check global labels config
if "labels" in data and "enabled-labels" in data["labels"]:
enabled_labels = set(data["labels"]["enabled-labels"])
invalid = enabled_labels - CONFIGURABLE_LABEL_CATEGORIES
if invalid:
raise ValueError(
f"Invalid label categories in enabled-labels: {sorted(invalid)}. "
f"Valid categories: {sorted(CONFIGURABLE_LABEL_CATEGORIES)}"
)

# Check repository-level labels config
for repo_name, repo_config in data.get("repositories", {}).items():
if isinstance(repo_config, dict) and "labels" in repo_config:
if "enabled-labels" in repo_config["labels"]:
enabled_labels = set(repo_config["labels"]["enabled-labels"])
invalid = enabled_labels - CONFIGURABLE_LABEL_CATEGORIES
if invalid:
raise ValueError(
f"Invalid label categories in enabled-labels for repository '{repo_name}': "
f"{sorted(invalid)}. Valid categories: {sorted(CONFIGURABLE_LABEL_CATEGORIES)}"
)

@property
def root_data(self) -> dict[str, Any]:
try:
Expand Down
Loading