feat: add GitHub and GitLab webhook support #170

Merged
sneak merged 1 commits from feature/github-gitlab-webhook-support into main 2026-03-22 00:46:11 +01:00
Collaborator

Summary

Adds GitHub and GitLab push webhook support alongside the existing Gitea support.

closes #68

What Changed

Auto-detection of webhook source

The webhook handler now auto-detects which platform sent the webhook by examining HTTP headers:

  • Gitea: X-Gitea-Event
  • GitHub: X-GitHub-Event
  • GitLab: X-Gitlab-Event

Existing Gitea webhooks continue to work unchanged. Unknown sources fall back to Gitea format for backward compatibility.

Normalized push event

All three payload formats are parsed into a unified PushEvent struct containing:

  • Source platform, ref, branch, commit SHA
  • Repository name, clone URL, HTML URL
  • Commit URL (with per-platform fallback logic)
  • Pusher username/name

New files

  • internal/service/webhook/payloads.go: Source-specific payload structs (GiteaPushPayload, GitHubPushPayload, GitLabPushPayload), ParsePushPayload() dispatcher, per-platform parsers, branch extraction, and commit URL extraction functions.

Modified files

  • internal/service/webhook/types.go: Added Source type (gitea/github/gitlab/unknown), DetectWebhookSource(), DetectEventType(), and PushEvent normalized type. Moved GiteaPushPayload to payloads.go.
  • internal/service/webhook/webhook.go: HandleWebhook now accepts a Source parameter and uses ParsePushPayload() for unified parsing instead of directly unmarshaling Gitea payloads.
  • internal/handlers/webhook.go: Calls DetectWebhookSource() and DetectEventType() to auto-detect the platform before delegating to the webhook service.
  • internal/service/webhook/webhook_test.go: Comprehensive tests for source detection, event type extraction, payload parsing (all 3 platforms), commit URL fallback paths, and integration tests through HandleWebhook for GitHub and GitLab sources.
  • README.md: Updated description, features, non-goals, and architecture to reflect multi-platform webhook support.

Test coverage

Webhook package: 96.9% statement coverage. Tests cover:

  • DetectWebhookSource with all header combinations and precedence
  • DetectEventType for each platform
  • ParsePushPayload for Gitea, GitHub, GitLab, unknown source, invalid JSON, empty payloads
  • Commit URL extraction fallback paths for GitHub and GitLab
  • Direct struct deserialization for all three payload types
  • Full HandleWebhook integration tests with GitHub and GitLab sources
## Summary Adds GitHub and GitLab push webhook support alongside the existing Gitea support. closes #68 ## What Changed ### Auto-detection of webhook source The webhook handler now auto-detects which platform sent the webhook by examining HTTP headers: - **Gitea**: `X-Gitea-Event` - **GitHub**: `X-GitHub-Event` - **GitLab**: `X-Gitlab-Event` Existing Gitea webhooks continue to work unchanged. Unknown sources fall back to Gitea format for backward compatibility. ### Normalized push event All three payload formats are parsed into a unified `PushEvent` struct containing: - Source platform, ref, branch, commit SHA - Repository name, clone URL, HTML URL - Commit URL (with per-platform fallback logic) - Pusher username/name ### New files - **`internal/service/webhook/payloads.go`**: Source-specific payload structs (`GiteaPushPayload`, `GitHubPushPayload`, `GitLabPushPayload`), `ParsePushPayload()` dispatcher, per-platform parsers, branch extraction, and commit URL extraction functions. ### Modified files - **`internal/service/webhook/types.go`**: Added `Source` type (gitea/github/gitlab/unknown), `DetectWebhookSource()`, `DetectEventType()`, and `PushEvent` normalized type. Moved `GiteaPushPayload` to payloads.go. - **`internal/service/webhook/webhook.go`**: `HandleWebhook` now accepts a `Source` parameter and uses `ParsePushPayload()` for unified parsing instead of directly unmarshaling Gitea payloads. - **`internal/handlers/webhook.go`**: Calls `DetectWebhookSource()` and `DetectEventType()` to auto-detect the platform before delegating to the webhook service. - **`internal/service/webhook/webhook_test.go`**: Comprehensive tests for source detection, event type extraction, payload parsing (all 3 platforms), commit URL fallback paths, and integration tests through `HandleWebhook` for GitHub and GitLab sources. - **`README.md`**: Updated description, features, non-goals, and architecture to reflect multi-platform webhook support. ## Test coverage Webhook package: **96.9%** statement coverage. Tests cover: - `DetectWebhookSource` with all header combinations and precedence - `DetectEventType` for each platform - `ParsePushPayload` for Gitea, GitHub, GitLab, unknown source, invalid JSON, empty payloads - Commit URL extraction fallback paths for GitHub and GitLab - Direct struct deserialization for all three payload types - Full `HandleWebhook` integration tests with GitHub and GitLab sources
clawbot added 1 commit 2026-03-17 10:37:53 +01:00
feat: add GitHub and GitLab webhook support
All checks were successful
Check / check (pull_request) Successful in 1m50s
0c5c727e01
Add auto-detection of webhook source (Gitea, GitHub, GitLab) by
examining HTTP headers (X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event).

Parse push webhook payloads from all three platforms into a normalized
PushEvent type for unified processing. Each platform's payload format
is handled by dedicated parser functions with correct field mapping
and commit URL extraction.

The webhook handler now detects the source automatically — existing
Gitea webhooks continue to work unchanged, while GitHub and GitLab
webhooks are parsed with their respective payload formats.

Includes comprehensive tests for source detection, event type
extraction, payload parsing for all three platforms, commit URL
fallback logic, and integration tests via HandleWebhook.
Author
Collaborator

Code Review: PR #170 — GitHub and GitLab webhook support

Policy Compliance Check

Policy Status Evidence
External references pinned by hash PASS No new dependencies — go.mod/go.sum unchanged. All Docker base images in Dockerfile remain pinned by SHA256.
No linter/CI/test config modifications PASS .golangci.yml, Makefile, Dockerfile, .gitea/workflows/ — zero changes.
No new migration files (pre-1.0.0) PASS No migration changes.
nolint comments justified PASS //nolint:tagliatelle on payload structs (JSON field names match external API snake_case — standard justification, consistent with existing GiteaPushPayload pattern). //nolint:funlen on table-driven tests — consistent with pre-existing pattern.
README consistency PASS Description, features, non-goals, and architecture all updated for multi-platform support. Non-goals correctly changed from "Support for non-Gitea webhooks" to "Support for non-push webhook events".
No secrets/PII committed PASS Only test data with example.com domains.

Requirements Checklist (Issue #68)

Requirement Status Implementation
Parse GitHub push webhook payloads GitHubPushPayload struct + parseGitHubPush() in payloads.go
Parse GitLab push webhook payloads GitLabPushPayload struct + parseGitLabPush() in payloads.go
Auto-detect webhook source by headers DetectWebhookSource() in types.go — checks X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event with defined precedence
Branch extraction for each format Shared extractBranch() in payloads.go, applied uniformly across all three parsers

Test Coverage Check

All new exported types and functions have tests:

Export Test
Source type + constants TestWebhookSourceString
DetectWebhookSource() TestDetectWebhookSource (7 cases: each platform, unknown, empty, precedence)
DetectEventType() TestDetectEventType (5 cases: each platform, unknown, missing header)
ParsePushPayload() TestParsePushPayloadGitea, TestParsePushPayloadGitHub, TestParsePushPayloadGitLab, TestParsePushPayloadUnknownFallsBackToGitea, TestParsePushPayloadInvalidJSON, TestParsePushPayloadEmptyPayload
PushEvent struct TestPushEventConstruction
GiteaPushPayload TestGiteaPushPayloadParsing
GitHubPushPayload TestGitHubPushPayloadParsing
GitLabPushPayload TestGitLabPushPayloadParsing
Commit URL fallbacks TestGitHubCommitURLFallback (3 paths), TestGitLabCommitURLFallback (2 paths)
Integration (HandleWebhook) TestHandleWebhookGitHubSource, TestHandleWebhookGitLabSource — full end-to-end through service layer

Claimed 96.9% statement coverage for the webhook package.

Code Quality Assessment

  • Architecture: Clean separation — types in types.go, payload parsing in payloads.go, service logic in webhook.go. Handler in handlers/webhook.go does detection, delegates to service.
  • Error handling: Parse failures produce an empty PushEvent{Source: source}, allowing webhook events to still be recorded (graceful degradation, consistent with existing Gitea behavior).
  • Backward compatibility: SourceUnknown falls back to Gitea format — existing integrations sending webhooks without recognized headers continue working.
  • Edge cases covered: Empty payloads, invalid JSON, missing commits, missing head_commit, commit URL fallback chains.
  • Scope: Tight — only push webhook support as specified. No scope creep.

Build Result

$ docker build .
✅ make fmt-check — PASS
✅ make lint — PASS
✅ make test — PASS
✅ make build — PASS
✅ Image built successfully

Verdict: PASS

Clean, well-structured implementation that meets all requirements from issue #68, follows all repo policies, and has comprehensive test coverage. No policy violations found.

## Code Review: PR #170 — GitHub and GitLab webhook support ### Policy Compliance Check | Policy | Status | Evidence | |--------|--------|----------| | External references pinned by hash | ✅ PASS | No new dependencies — `go.mod`/`go.sum` unchanged. All Docker base images in Dockerfile remain pinned by SHA256. | | No linter/CI/test config modifications | ✅ PASS | `.golangci.yml`, `Makefile`, `Dockerfile`, `.gitea/workflows/` — zero changes. | | No new migration files (pre-1.0.0) | ✅ PASS | No migration changes. | | `nolint` comments justified | ✅ PASS | `//nolint:tagliatelle` on payload structs (JSON field names match external API snake_case — standard justification, consistent with existing `GiteaPushPayload` pattern). `//nolint:funlen` on table-driven tests — consistent with pre-existing pattern. | | README consistency | ✅ PASS | Description, features, non-goals, and architecture all updated for multi-platform support. Non-goals correctly changed from "Support for non-Gitea webhooks" to "Support for non-push webhook events". | | No secrets/PII committed | ✅ PASS | Only test data with `example.com` domains. | ### Requirements Checklist ([Issue #68](https://git.eeqj.de/sneak/upaas/issues/68)) | Requirement | Status | Implementation | |-------------|--------|----------------| | Parse GitHub push webhook payloads | ✅ | `GitHubPushPayload` struct + `parseGitHubPush()` in [`payloads.go`](https://git.eeqj.de/sneak/upaas/src/branch/feature/github-gitlab-webhook-support/internal/service/webhook/payloads.go) | | Parse GitLab push webhook payloads | ✅ | `GitLabPushPayload` struct + `parseGitLabPush()` in [`payloads.go`](https://git.eeqj.de/sneak/upaas/src/branch/feature/github-gitlab-webhook-support/internal/service/webhook/payloads.go) | | Auto-detect webhook source by headers | ✅ | `DetectWebhookSource()` in [`types.go`](https://git.eeqj.de/sneak/upaas/src/branch/feature/github-gitlab-webhook-support/internal/service/webhook/types.go) — checks `X-Gitea-Event`, `X-GitHub-Event`, `X-Gitlab-Event` with defined precedence | | Branch extraction for each format | ✅ | Shared `extractBranch()` in `payloads.go`, applied uniformly across all three parsers | ### Test Coverage Check All new exported types and functions have tests: | Export | Test | |--------|------| | `Source` type + constants | `TestWebhookSourceString` | | `DetectWebhookSource()` | `TestDetectWebhookSource` (7 cases: each platform, unknown, empty, precedence) | | `DetectEventType()` | `TestDetectEventType` (5 cases: each platform, unknown, missing header) | | `ParsePushPayload()` | `TestParsePushPayloadGitea`, `TestParsePushPayloadGitHub`, `TestParsePushPayloadGitLab`, `TestParsePushPayloadUnknownFallsBackToGitea`, `TestParsePushPayloadInvalidJSON`, `TestParsePushPayloadEmptyPayload` | | `PushEvent` struct | `TestPushEventConstruction` | | `GiteaPushPayload` | `TestGiteaPushPayloadParsing` | | `GitHubPushPayload` | `TestGitHubPushPayloadParsing` | | `GitLabPushPayload` | `TestGitLabPushPayloadParsing` | | Commit URL fallbacks | `TestGitHubCommitURLFallback` (3 paths), `TestGitLabCommitURLFallback` (2 paths) | | Integration (HandleWebhook) | `TestHandleWebhookGitHubSource`, `TestHandleWebhookGitLabSource` — full end-to-end through service layer | Claimed 96.9% statement coverage for the webhook package. ### Code Quality Assessment - **Architecture**: Clean separation — types in `types.go`, payload parsing in `payloads.go`, service logic in `webhook.go`. Handler in `handlers/webhook.go` does detection, delegates to service. - **Error handling**: Parse failures produce an empty `PushEvent{Source: source}`, allowing webhook events to still be recorded (graceful degradation, consistent with existing Gitea behavior). - **Backward compatibility**: `SourceUnknown` falls back to Gitea format — existing integrations sending webhooks without recognized headers continue working. - **Edge cases covered**: Empty payloads, invalid JSON, missing commits, missing head_commit, commit URL fallback chains. - **Scope**: Tight — only push webhook support as specified. No scope creep. ### Build Result ``` $ docker build . ✅ make fmt-check — PASS ✅ make lint — PASS ✅ make test — PASS ✅ make build — PASS ✅ Image built successfully ``` ### Verdict: ✅ PASS Clean, well-structured implementation that meets all requirements from [issue #68](https://git.eeqj.de/sneak/upaas/issues/68), follows all repo policies, and has comprehensive test coverage. No policy violations found.
clawbot added the merge-ready label 2026-03-17 10:43:23 +01:00
sneak was assigned by clawbot 2026-03-17 10:43:23 +01:00
sneak merged commit 57ec4331ef into main 2026-03-22 00:46:11 +01:00
sneak deleted branch feature/github-gitlab-webhook-support 2026-03-22 00:46:11 +01:00
Sign in to join this conversation.