feat(webhook): implement Forgejo webhook receiver #142

Merged
fuzzy merged 1 commit from feat/webhook-receiver into main 2026-08-03 05:40:37 +00:00
Owner

What

Implement the POST /webhook receiver that triggers site publication on Forgejo push events:

  • internal/webhook/webhook.goParse decodes the push payload (ref, repo name, after); VerifySignature checks the X-Gitea-Signature HMAC-SHA256 header against webhook.secret (empty secret skips verification).
  • internal/webhook/receiver.goReceiver.Handle looks up the site by repository name and delegates to SiteService.Publish.
  • SiteService.Publish (#72, #73) — lists the repository's tags (ForgejoClient.ListTags), selects the highest v* via pkg/version.Highest, updates the Kubernetes resources (KubernetesClient.PublishSite), and records an active Deployment in a new in-memory DeploymentRepository (internal/site/memory_deployments.go).
  • internal/httpapiPOST /webhook handler (#70, #74): 200 on success, 401 bad signature, 400 malformed payload, 404 unknown site, 500 processing failure. NewRouter now takes a Deps struct carrying the sites service and webhook receiver.
  • main.go wires the webhook receiver into the router.

Why

Phase 5 task #75 — the trigger for publishing a site's latest tagged version when its repository changes.

Testing

  • go build ./...
  • go vet ./...
  • go test -race ./... — webhook (7 tests), httpapi webhook handler (4), site Publish (3) + existing suite all pass
  • gofmt -l ./cmd ./internal ./pkg clean
  • golangci-lint run — 0 issues
  • pre-commit hooks all pass

Breaking Changes

None.

Notes

  • Content population deferred per the earlier storage decision: this task updates the Kubernetes resource definitions and records deployment state; writing the site's static files into the serving volume is a follow-on task. The Deployment keeps its current shape (SITE_VERSION hardcoded).
  • CommitSHA on the recorded Deployment is left empty — ListTags returns names only; a richer tag lookup can fill it later.
  • Issues #70–#75 were closed via the Forgejo API as part of this task per the workflow.

Closes #70
Closes #71
Closes #72
Closes #73
Closes #74
Closes #75

## What Implement the `POST /webhook` receiver that triggers site publication on Forgejo push events: - **`internal/webhook/webhook.go`** — `Parse` decodes the push payload (ref, repo name, after); `VerifySignature` checks the `X-Gitea-Signature` HMAC-SHA256 header against `webhook.secret` (empty secret skips verification). - **`internal/webhook/receiver.go`** — `Receiver.Handle` looks up the site by repository name and delegates to `SiteService.Publish`. - **`SiteService.Publish`** (#72, #73) — lists the repository's tags (`ForgejoClient.ListTags`), selects the highest `v*` via `pkg/version.Highest`, updates the Kubernetes resources (`KubernetesClient.PublishSite`), and records an active `Deployment` in a new in-memory `DeploymentRepository` (`internal/site/memory_deployments.go`). - **`internal/httpapi`** — `POST /webhook` handler (#70, #74): `200` on success, `401` bad signature, `400` malformed payload, `404` unknown site, `500` processing failure. `NewRouter` now takes a `Deps` struct carrying the sites service and webhook receiver. - **`main.go`** wires the webhook receiver into the router. ## Why Phase 5 task #75 — the trigger for publishing a site's latest tagged version when its repository changes. ## Testing - [x] `go build ./...` - [x] `go vet ./...` - [x] `go test -race ./...` — webhook (7 tests), httpapi webhook handler (4), site Publish (3) + existing suite all pass - [x] `gofmt -l ./cmd ./internal ./pkg` clean - [x] `golangci-lint run` — 0 issues - [x] pre-commit hooks all pass ## Breaking Changes None. ## Notes - **Content population deferred** per the earlier storage decision: this task updates the Kubernetes resource definitions and records deployment state; writing the site's static files into the serving volume is a follow-on task. The Deployment keeps its current shape (`SITE_VERSION` hardcoded). - `CommitSHA` on the recorded Deployment is left empty — `ListTags` returns names only; a richer tag lookup can fill it later. - Issues #70–#75 were closed via the Forgejo API as part of this task per the workflow. Closes #70 Closes #71 Closes #72 Closes #73 Closes #74 Closes #75
feat(webhook): implement Forgejo webhook receiver
All checks were successful
Test and Release / lint (pull_request) Successful in 7m23s
Test and Release / test (pull_request) Successful in 7m37s
01d3628e9d
Add the POST /webhook receiver that triggers site publication on
Forgejo push events:

- webhook.Parse decodes the push payload and VerifySignature checks the
  X-Gitea-Signature HMAC-SHA256 header against webhook.secret
- webhook.Receiver looks up the site by repository name and delegates
  to SiteService.Publish
- SiteService.Publish lists the repository tags, selects the highest
  v* version, updates the Kubernetes resources via PublishSite, and
  records an active Deployment in a new in-memory
  DeploymentRepository
- httpapi registers POST /webhook returning 200 on success, 401 on
  bad signature, 400 on malformed payload, 404 for unknown sites, and
  500 on processing failure
- httpapi.NewRouter now takes a Deps struct carrying the sites service
  and webhook receiver

Content population into the serving volume is deferred; this updates
the Kubernetes resource definitions and deployment state. Mark task
done in ROADMAP.md.

closes #70
closes #71
closes #72
closes #73
closes #74
closes #75
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-08-03 05:06:50 +00:00
the.auditor left a comment

Review Summary

Verified locally on the PR head (01d3628): go build, go vet, gofmt, golangci-lint run all clean; full suite passes with -race (webhook 7, httpapi webhook handler 4, site Publish 3 + existing).

The webhook receiver is well-built. VerifySignature uses constant-time hmac.Equal with correct hex decoding and clean skip-on-empty-secret semantics; the handler verifies the signature before parsing, so unauthenticated bodies are rejected before touching the decoder. Receiver.Handle correctly distinguishes unknown-site (404) from processing failures (500), and Publish composes the existing pieces cleanly: ListTagsversion.Highestk8s.PublishSite → deployment record. The Deps struct is a tidy evolution of the router wiring, and the in-memory DeploymentRepository has correct mutexed semantics.

No blocking issues.

Non-blocking observations:

  1. Receiver.Handle fires on any push event — branch pushes, tag deletions, etc. — re-selecting the highest v* tag and re-publishing even when nothing changed. The payload's Ref is already parsed; filtering on refs/tags/ (or refs/tags/ + deleted) would avoid redundant publishes. Worth doing before the endpoint sees real traffic.
  2. Empty webhook.secret (the config default) means the endpoint is effectively unauthenticated — anyone who can reach /webhook can trigger publishes. Impact is limited (publish only re-deploys the repo's own latest tag; no content injection), but worth documenting that a secret should be set in production.
  3. As noted in the PR, Publish records the deployment version but doesn't yet plumb it through to Kubernetes (SITE_VERSION remains hardcoded v0.0.0) or write content into the serving volume — acknowledged deferral, but the "active" record won't match what's actually served until that lands.
  4. MemoryDeploymentRepository.Create appends without dedupe — repeated publishes of the same tag produce duplicate history entries. Minor.

Approving.

## Review Summary Verified locally on the PR head (`01d3628`): `go build`, `go vet`, `gofmt`, `golangci-lint run` all clean; full suite passes with `-race` (webhook 7, httpapi webhook handler 4, site Publish 3 + existing). The webhook receiver is well-built. `VerifySignature` uses constant-time `hmac.Equal` with correct hex decoding and clean skip-on-empty-secret semantics; the handler verifies the signature *before* parsing, so unauthenticated bodies are rejected before touching the decoder. `Receiver.Handle` correctly distinguishes unknown-site (404) from processing failures (500), and `Publish` composes the existing pieces cleanly: `ListTags` → `version.Highest` → `k8s.PublishSite` → deployment record. The `Deps` struct is a tidy evolution of the router wiring, and the in-memory `DeploymentRepository` has correct mutexed semantics. **No blocking issues.** Non-blocking observations: 1. `Receiver.Handle` fires on *any* push event — branch pushes, tag deletions, etc. — re-selecting the highest `v*` tag and re-publishing even when nothing changed. The payload's `Ref` is already parsed; filtering on `refs/tags/` (or `refs/tags/` + `deleted`) would avoid redundant publishes. Worth doing before the endpoint sees real traffic. 2. Empty `webhook.secret` (the config default) means the endpoint is effectively unauthenticated — anyone who can reach `/webhook` can trigger publishes. Impact is limited (publish only re-deploys the repo's own latest tag; no content injection), but worth documenting that a secret should be set in production. 3. As noted in the PR, `Publish` records the deployment version but doesn't yet plumb it through to Kubernetes (`SITE_VERSION` remains hardcoded `v0.0.0`) or write content into the serving volume — acknowledged deferral, but the "active" record won't match what's actually served until that lands. 4. `MemoryDeploymentRepository.Create` appends without dedupe — repeated publishes of the same tag produce duplicate history entries. Minor. Approving.
fuzzy merged commit 01d3628e9d into main 2026-08-03 05:40:37 +00:00
fuzzy deleted branch feat/webhook-receiver 2026-08-03 05:40:37 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thwap/thwap-pagesd!142
No description provided.