Skip to content

refactor(api): extract internal/webhooks, and stop there - #255

Merged
houko merged 2 commits into
mainfrom
p3/webhooks
Sep 19, 2026
Merged

houko merged 2 commits into
mainfrom
p3/webhooks

Conversation

@houko

@houko houko commented Sep 18, 2026

Copy link
Copy Markdown
Member

Description

Moves the 7 webhook routes out of internal/project into internal/webhooks. A proof of concept that the seam is real, not the start of a general split.

internal/project is 172 files and ~37.7k lines with 672 methods on one *Handler serving 304 routes, and it is tempting to carve up. The analysis behind this PR says only two groups can move with zero leaked internals — webhooks (7 routes) and notifications (10) — and that every other candidate either drags 15-26 cross-boundary helpers, collides with #250, or contradicts a decision apps/api/README.md already records. This is the first of the two; the second waits for #250.

Zero leaked internals is achieved the way this repo already does it: copying the small helper kit rather than exporting it. That is the established pattern, not a shortcut — authenticated, internalError, newUUID and decodeJSON each exist five times across packages, liteUserJSON three times, and the file_assets GORM model seven. Verified by reading internal/user and internal/externalapi before copying.

Type of Change

  • Code refactoring

Screenshots and Media (if applicable)

N/A — backend only.

Test Scenarios

  • go build ./..., go vet ./..., gofmt -l . clean; go test -count=1 ./... no failures.
  • Route count: 304 before, 297 + 7 after. internal/project/handler_test.go's TestProjectRouteInventory asserts an exact bijection with router.Routes(), so a lost or renamed route fails the build. The new package gets its own equivalent inventory test.
  • The four moved tests need no database, so they run in the normal suite.
  • git diff -M --find-renames=90% records the moves as renames rather than delete-plus-add, which is what makes this diff readable as a move.

References

Corrections to the plan, verified rather than taken:

  • The plan said to copy roleGuest/roleMember/roleAdmin. Only roleAdmin is used — every webhook route is requireWorkspaceRole(c, user, roleAdmin) — so copying the other two would plant two dead constants in a new file, in a repo with an open PR removing dead code. One constant, with a comment saying why the others are absent.
  • The plan said to put the inventory test in webhook_handler_test.go. Doing that would drop that file below git's 90% rename-similarity threshold and destroy the rename proof the plan's own verification depends on. It lives in a new webhooks/handler_test.go, which also matches where workspace and project keep theirs.
  • 8 paths touched, not 6. And removing the three Webhook* keys from the projectapi.Settings literal changes which key is longest, so gofmt re-aligns the whole literal — a 25-line cosmetic diff a reviewer should expect.
  • The plan's correction of the older audit's route count is right: the expected map has exactly 304 entries, so the audit's 308 would have failed the package's own test.

Not done, deliberately: extracting internal/notifications must wait for #250, which rewrites project/notification_handler.go — moving the file first turns every one of its hunks into a manual resolution in a file that no longer exists on main. The README paragraph recording where the split stops belongs after that second move; the subsection added here explains only why internal/webhooks exists.

internal/project is 172 Go files and 37.7k lines because it accumulated everything the session API serves, not because those things belong together. The webhook routes are the one group that can leave without dragging anything behind: nothing outside webhook_handler.go referenced a single thing it declared, the webhooks and webhook_logs models are its own, its four tests are pure unit tests over webhookJSON and the url checks that build no router, and the three Settings fields it reads (WebhookAllowedIPs, WebhookAllowedHosts, WebhookDisallowedDomains) were read by no other file in the package. Moving it is a rename plus a package clause, which is what makes it worth doing first: it proves the seam is real on the cheapest possible subject rather than on a group that would need a query engine duplicated to follow it.

The small kit the handlers need — authenticated, requireWorkspaceRole and its role lookup, invalidDetail, internalError, isUniqueViolation, newUUID — is copied byte-for-byte into the new package rather than exported from internal/project. That is this repo's established pattern and not laziness: authenticated, internalError, newUUID and decodeJSON each already exist five times over across internal/space, internal/workspace, internal/externalapi, internal/project and internal/user, and the file_assets model seven times. Ninety duplicated lines buys a package that imports nothing from a 37k-line one; exporting instead would have coupled the two permanently for the same ninety lines. Only roleAdmin is copied of the three role constants, since every route here is admin only and the other two would be dead.

Route behaviour is unchanged and the counts say so: internal/project's inventory test goes from 304 registrations to 297 and the new TestWebhookRouteInventory asserts the missing 7, so the composed router still serves exactly what it did. TestEveryCutOverPathIsFullyServed in internal/server is the independent check — the community Caddyfile cuts over all of /api/*, so it requires every /api/ row of the 640-row Django fixture to be served, and a webhook path lost in the move would fail it by name.

apps/api/README.md records why the package exists, next to the module note it belongs to, so the next reader does not have to re-derive it from the file listing.
@houko
houko merged commit 56f5879 into main Sep 19, 2026
14 checks passed
@houko
houko deleted the p3/webhooks branch September 19, 2026 12:38
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