Testing and Maintenance
This project uses a layered verification model. Most day-to-day changes are checked with fast local tests under webhook_server/tests/, while full GitHub workflow validation lives in webhook_server/tests/e2e/. On top of that, the repository is maintained with checked-in automation for linting, releases, dependency updates, and PR or issue hygiene.
Note: Automation in this repository is driven mostly by checked-in tool and bot configuration. There are no
.github/workflowsfiles in the repo, so the practical "pipeline" lives inpytest,tox,pre-commit,release-it, Renovate, and repository bot settings.
Test strategy
Default local suite
The default pytest configuration is set up for normal contributor workflows: async tests work out of the box, coverage is always collected, logs are visible during the run, and E2E tests are skipped unless you opt in.
[pytest]
asyncio_mode = auto
addopts =
--pdbcls=IPython.terminal.debugger:TerminalPdb
--cov-config=pyproject.toml --cov-report=html --cov-report=term --cov=webhook_server
--log-cli-level=DEBUG
-m 'not e2e'
markers =
e2e: "End-to-end tests that require real GitHub interactions (deselect with '-m \"not e2e\"')"
The main tox test environment runs the default suite in parallel with pytest-xdist, using the command from tox.toml: uv run --extra tests pytest -n auto webhook_server/tests.
Tip: For everyday development, stay in the default non-E2E suite. It is faster, parallelized, and already includes coverage reporting.
Unit tests
The unit layer focuses on isolated behavior. Tests stub GitHub objects, patch network boundaries, and assert exact outcomes for helpers, handlers, and config logic. You can see this style clearly in webhook_server/tests/test_webhook.py, which verifies webhook creation without talking to GitHub:
@patch("webhook_server.utils.webhook.get_github_repo_api")
def test_process_github_webhook_success_no_existing_hooks(
self,
mock_get_repo_api: Mock,
sample_data: dict[str, Any],
apis_dict: dict[str, dict[str, Any]],
mock_repo: Mock,
) -> None:
mock_get_repo_api.return_value = mock_repo
success, message, _ = process_github_webhook(
repository_name="test-repo", data=sample_data, webhook_ip="http://example.com", apis_dict=apis_dict
)
assert success is True
assert "Create webhook is done" in message
mock_repo.create_hook.assert_called_once_with(
name="web",
config={"url": "http://example.com", "content_type": "json"},
events=["push", "pull_request"],
active=True,
)
In practice, this unit layer covers a lot of ground:
- webhook helper functions and GitHub API wrappers
- handler behavior such as
PullRequestHandler,CheckRunHandler,IssueCommentHandler, andPushHandler - configuration loading and schema validation
- log parsing, filtering, structured logging, and API-call accounting
- performance-sensitive internals such as log parsing and memory usage
Integration tests
The integration layer exercises larger slices of the application together. These tests usually run the real FastAPI app with TestClient, point it at checked-in test manifests, and stub only the true external boundary.
A representative example from webhook_server/tests/test_app.py posts a webhook into the app and verifies the real HTTP response:
@pytest.fixture
def client(self) -> TestClient:
return TestClient(FASTAPI_APP)
@patch.dict(os.environ, {"WEBHOOK_SERVER_DATA_DIR": "webhook_server/tests/manifests"})
@patch("webhook_server.app.GithubWebhook")
def test_process_webhook_success(
self, mock_github_webhook: Mock, client: TestClient, valid_webhook_payload: dict[str, Any], webhook_secret: str
) -> None:
payload_json = json.dumps(valid_webhook_payload)
signature = self.create_github_signature(payload_json, webhook_secret)
mock_webhook_instance = Mock()
mock_github_webhook.return_value = mock_webhook_instance
headers = {
"X-GitHub-Event": "pull_request",
"X-GitHub-Delivery": "test-delivery-123",
"x-hub-signature-256": signature,
"Content-Type": "application/json",
}
response = client.post("/webhook_server", content=payload_json, headers=headers)
assert response.status_code == 200
assert response.json()["message"] == "Webhook queued for processing"
This is where the project verifies behavior such as:
- request validation and signature handling
- health endpoints and background-task behavior
- event dispatch through
GithubWebhook.process() - log viewer API endpoints, exports, and WebSocket handling
- configuration loading from
webhook_server/tests/manifests
Only E2E tests have a dedicated pytest marker. Unit and integration tests live together in webhook_server/tests/ and are distinguished by what they exercise rather than by separate markers.
End-to-end tests
The E2E suite is intentionally closer to real life. It starts support infrastructure, uses a real GitHub repository, and verifies the observable side effects of webhook processing.
The core fixture in webhook_server/tests/e2e/conftest.py shows how that infrastructure is brought up:
@pytest.fixture(scope="session")
def e2e_server(server_envs: dict[str, str], github_webhook_cleanup: None) -> Generator[None]:
server_port = server_envs["server_port"]
smee_url = server_envs["smee_url"]
project_root = server_envs["project_root"]
docker_compose_file = server_envs["docker_compose_file"]
smee_process = start_smee_client(server_port=server_port, smee_url=smee_url)
start_docker_compose(docker_compose_file=docker_compose_file, project_root=project_root)
wait_for_container_health(
docker_compose_file=docker_compose_file,
project_root=project_root,
container_name="github-webhook-server-e2e",
timeout=60,
)
yield
The E2E tests themselves verify real GitHub outcomes. For example, webhook_server/tests/e2e/test_pull_request_flow.py waits for labels and check runs to appear on an actual PR instead of checking internal mocks.
The checked-in E2E guide runs the suite with uv run --group tests pytest webhook_server/tests/e2e/ -v -m e2e, and it expects a local .dev/.env file like this:
SERVER_PORT=5000
SMEE_URL=https://smee.io/YOUR_UNIQUE_CHANNEL
TEST_REPO=owner/repo-name
DOCKER_COMPOSE_FILE=.dev/docker-compose.yaml
TZ=America/New_York
A few practical details matter here:
- all GitHub operations in the E2E helpers use
ghCLI - the suite starts both a local Docker Compose stack and a
smeerelay client - test fixtures create and clean up real branches, PRs, and webhooks
.dev/is ignored by Git, so each developer keeps E2E configuration local
Warning: E2E tests use real GitHub credentials, a real test repository, and a public Smee relay. Use a disposable test repo, keep
.dev/local, and do not run the suite in parallel with-n auto.
Specialized tests
The suite also includes targeted operational checks outside the usual unit/integration/E2E split:
test_performance_benchmarks.pymeasures log parsing and filtering speed at larger scales.test_memory_optimization.pyandtest_frontend_performance.pyprotect log viewer scalability.test_api_call_counting.pyverifies GitHub API usage tracking.test_config_schema.pyandtest_schema_validator.pycover configuration correctness from both schema and runtime angles.
Coverage expectations
Coverage is enforced, not just reported. The threshold lives in pyproject.toml, and test files themselves are excluded from measurement so the percentage reflects application code.
[tool.coverage.run]
omit = ["webhook_server/tests/*"]
[tool.coverage.report]
fail_under = 90
skip_empty = true
[tool.coverage.html]
directory = ".tests_coverage"
That means:
- the test run fails if application coverage drops below 90%
- HTML coverage output is written to
.tests_coverage - the default pytest configuration already enables
--cov=webhook_server, so you do not need to remember extra coverage flags for normal runs
Pre-commit tooling and local maintenance
The repository uses pre-commit as the first quality gate before review. The configured hooks cover formatting, linting, types, JavaScript checks, and secret scanning.
A trimmed excerpt from .pre-commit-config.yaml shows the core stack:
ci:
autofix_prs: false
autoupdate_commit_msg: "ci: [pre-commit.ci] pre-commit autoupdate"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
- id: detect-private-key
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.6
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
exclude: (tests/)
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v10.0.3
hooks:
- id: eslint
files: \.js$
The full config also includes:
flake8with additional pluginsdetect-secretsgitleaks
Because autofix_prs is set to false, pre-commit.ci reports failures and opens hook update PRs, but it does not push automatic fixes back to contributor branches.
There is also a maintenance-focused tox setup in tox.toml with two environments: unittests for the main test suite and unused-code, which runs pyutils-unusedcode to catch dead code early.
One especially practical integration point is built into the server itself: when a managed repository contains .pre-commit-config.yaml, the project automatically adds the pre-commit.ci - pr status context to the default status checks. In other words, pre-commit.ci is treated as part of the review flow, not just as an optional extra.
Release automation
Releases are automated with release-it. The checked-in configuration handles version bumping, changelog generation, Git tagging and pushing, and GitHub Release creation.
{
"npm": {
"publish": false
},
"git": {
"commit": true,
"commitMessage": "Release ${version}",
"tag": true,
"tagAnnotation": "Release ${version}",
"push": true,
"pushArgs": ["--follow-tags"],
"changelog": "uv run scripts/generate_changelog.py ${from} ${to}"
},
"github": {
"release": true,
"releaseName": "Release ${version}",
"tokenRef": "GITHUB_TOKEN"
},
"plugins": {
"@release-it/bumper": {
"in": "pyproject.toml",
"out": { "file": "pyproject.toml", "path": "project.version" }
}
},
"hooks": {
"after:bump": "uv sync"
}
}
In practice, that automation does the following:
- updates
project.versioninpyproject.toml - regenerates dependencies after the bump with
uv sync - creates a release commit and annotated tag
- pushes commits and tags together
- creates a GitHub Release using
GITHUB_TOKEN
The release notes text comes from scripts/generate_changelog.py, which groups commits by conventional prefixes such as feat, fix, docs, test, and ci.
Tip: Conventional commit prefixes make release notes much easier to read in this project, because the changelog generator groups entries by prefix.
Dependency updates and repository bots
Dependency updates are handled by Renovate, and the repository has several other bots configured for review and hygiene.
The Renovate configuration is intentionally simple and low-noise:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
":dependencyDashboard",
":maintainLockFilesWeekly",
":prHourlyLimitNone",
":semanticCommitTypeAll(ci )"
],
"prConcurrentLimit": 0,
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchPackagePatterns": ["*"],
"groupName": "python-deps"
}
]
}
That setup means:
- Renovate keeps a dependency dashboard.
- Lock file maintenance is enabled weekly.
- Dependency PRs are not throttled by hourly or concurrent limits.
- Updates are grouped into a single
python-depsstream instead of a flood of unrelated PRs.
The rest of the repository bot setup looks like this:
pre-commit.ciis configured through.pre-commit-config.yamland is part of the expected status-check flow.- CodeRabbit is configured in
.coderabbit.yamlwith auto-review on non-draft PRs targetingmain,request_changes_workflow: true, and tool integrations including Ruff, Pylint, ESLint, ShellCheck, Yamllint, Gitleaks, Semgrep, Actionlint, and Hadolint. - The stale bot is configured in
.github/stale.ymlto mark inactive items stale after 60 days and close them 7 days later, while exemptingpinnedandsecurity. - The In Solidarity bot is configured in
.github/in-solidarity.ymlto enforce inclusive-language checks at failure level.
If you use github-webhook-server to manage your own repositories, the shipped example config also treats common automation accounts as trusted bots by listing renovate[bot] and pre-commit-ci[bot] under auto-verified-and-merged-users. That is a good starting point if you want dependency and hook-update PRs to fit cleanly into an automated review flow.