Skip to content

feat(webhook): GitLab + Gitea/Forgejo adapters#71

Open
russellromney wants to merge 3 commits into
mainfrom
feat/webhook-gitlab-gitea
Open

feat(webhook): GitLab + Gitea/Forgejo adapters#71
russellromney wants to merge 3 commits into
mainfrom
feat/webhook-gitlab-gitea

Conversation

@russellromney

Copy link
Copy Markdown
Owner

What

Adds the two remaining phase-1 webhook adapters behind the existing WebhookProvider trait — no engine or handler changes. /webhooks/{provider} now serves GitHub, GitLab, and Gitea/Forgejo.

Adding a provider is exactly what the design promised: implement one trait (verify + parse) and add a match arm. Everything downstream — CanonicalEvent, the allowlist, branch policy, delete cleanup, trigger_build coalescing — is already generic.

Adapters

  • GitLab (rust/src/webhook/gitlab.rs) — authenticates with the shared token echoed in X-Gitlab-Token (constant-time equality; GitLab uses no body HMAC, so the raw body is unused there). Acts on X-Gitlab-Event: Push Hook. Routes by project.path_with_namespace (subgroup-safe). Visibility from project.visibility_level (< 20 ⇒ non-public).
  • Gitea / Forgejo (rust/src/webhook/gitea.rs) — bare-hex HMAC-SHA256 in X-Gitea-Signature (no sha256= prefix). Handles push / delete / ping. Gitea's dedicated delete event carries a short branch name, which the adapter normalizes back to refs/heads/<branch> so the handler stays uniform. Covers Codeberg too.

Shared is_zero_sha lifted into webhook/mod.rs; provider_for returns adapters for GitHub/GitLab/Gitea — Bitbucket/Generic still None501.

Tests

Per-adapter unit tests (verify valid/wrong/missing/tampered; parse push/delete/ping/non-event, subgroup paths, visibility, short-ref normalization) plus server-level integration: gitlab push enqueues, gitlab bad token → 401, gitea push enqueues, gitea branch-delete cleans up the ref, provider-without-adapter → 501.

cargo fmt --check + cargo clippy --all-targets -- -D warnings + cargo test --release --all-targets --locked all green (lib 228 passed, 53 test binaries, 0 failures).

Follow-ups

A Bitbucket adapter (same pattern); repo-lifecycle events and tag/release pre-warm.

🤖 Generated with Claude Code

russellromney and others added 3 commits June 27, 2026 16:18
Adds the two remaining phase-1 webhook adapters behind the existing
`WebhookProvider` trait — no engine or handler changes. `/webhooks/{provider}`
now serves GitHub, GitLab, and Gitea/Forgejo.

- GitLab (`rust/src/webhook/gitlab.rs`): authenticates with the shared token in
  `X-Gitlab-Token` (constant-time equality, not a body HMAC); acts on
  `X-Gitlab-Event: Push Hook`; routes by `project.path_with_namespace`
  (subgroup-safe); visibility from `project.visibility_level` (<20 ⇒ private).
- Gitea/Forgejo (`rust/src/webhook/gitea.rs`): bare-hex HMAC-SHA256 in
  `X-Gitea-Signature` (no `sha256=` prefix); push / delete / ping. Its dedicated
  `delete` event carries a short branch name, normalized back to
  `refs/heads/<branch>` so the handler stays uniform.
- Shared `is_zero_sha` helper lifted into `webhook/mod.rs`; `provider_for` now
  returns adapters for GitHub/GitLab/Gitea (Bitbucket/Generic still None → 501).
- Tests: per-adapter verify (valid/wrong/missing/tampered) + parse
  (push/delete/ping/non-event); server-level gitlab push enqueues, gitlab bad
  token → 401, gitea push enqueues, gitea branch-delete cleans up the ref,
  provider-without-adapter → 501.
- Docs: README + WEBHOOKS.md updated (all three providers supported; adapter
  quirks documented).

fmt + clippy -D warnings + full release suite green (lib 228 passed, 53 test
binaries, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sts + docs)

From two independent adversarial reviews of the GitLab/Gitea adapters
(protocol-accuracy review confirmed zero format defects vs the real docs):

- Allowlist now matches a **natural key** (`RepoId::natural_key()`):
  `owner/repo` for GitHub, provider-prefixed unescaped for others
  (`gitlab/group/sub/proj`) — previously it matched the slash-*escaped* storage
  key, so the documented `owner/repo` form silently never matched GitLab/Gitea
  repos (medium footgun: operators would fall back to allow-all).
- Reject all-whitespace webhook secrets in `parse_secret` (a blank GitLab token
  is a guessable credential); secret kept verbatim otherwise.
- `validate_repo_path`: reject `..` path segments and ASCII control chars for
  non-github providers (defense in depth; escaping already blocked traversal).
- Clarify `CanonicalEvent.private` is informational (the warm path resolves
  credentials from the broker regardless of visibility).
- Tests: gitea bad-signature → 401, gitlab branch-delete-through-handler
  cleanup, gitlab allowlist via natural key, `natural_key()` unit test,
  `validate_repo_path` traversal/control rejection.
- Docs: allowlist natural-key format; per-provider setup notes (GitLab requires
  the secret-token scheme not signing-token; Gitea webhooks must enable the
  Delete event for branch-delete cleanup).

fmt + clippy -D warnings + full release suite green (lib 233 passed, 53 test
binaries, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e controls)

Third-round adversarial review (no regressions found) left three low-severity
debts; closing them so nothing's left half-done:

- Allowlist naming/docs: `WebhookConfig::allows` param renamed `storage_key` →
  `repo_key`; the `with_allowlist`/`allows`/`parse_allowlist` docs now say
  "natural key" (they receive the unescaped form, not the storage key).
- GitHub allowlist asymmetry: the github default now ALSO accepts the explicit
  `github/owner/repo` form (not just bare `owner/repo`), so an operator
  generalizing from the `gitlab/...` examples isn't silently bitten. New helper
  `webhook_repo_allowed` + test exercising both forms.
- `validate_repo_path`: reject ALL Unicode control chars (`char::is_control`),
  not just ASCII — catches the C1 range (e.g. U+0085 NEL).

fmt + clippy -D warnings + full release suite green (lib 234 passed, 53 test
binaries, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant