Websocket transport#7
Merged
Merged
Conversation
Adds a second webhook ingest path for deployments without a public HTTP
endpoint (laptops, NAT'd hosts, demos). Behind WEBHOOK_TRANSPORT=websocket,
the app dials GitHub's relay (the same one used by 'gh webhook'), creates
a temporary repo/org hook, and feeds deliveries into the existing
ordering/persistence pipeline.
Refactor:
- handlers/webhook_handler.go: extracts a transport-agnostic Ingest(event,
delivery, body) IngestResult seam. The Gin Handle() is now a thin
adapter that preserves the original JSON response shape.
- internal/config/config.go: adds WEBHOOK_TRANSPORT, GITHUB_TOKEN,
GITHUB_HOST, GITHUB_REPO, GITHUB_ORG, GITHUB_EVENTS with validation.
New package internal/services/ghws:
- Subscriber.Run(ctx) handles create-hook -> wss dial -> activate -> read
frames -> dispatch -> ack loop with exponential backoff (1s..60s).
- Best-effort hook cleanup on shutdown.
- Emits WebhookEventsTotal{outcome=ws_accepted|ws_queue_full|ws_rejected}.
- Unit test exercises the full happy path against an httptest server.
cmd/server wires the subscriber when WEBHOOK_TRANSPORT=websocket and
joins it to graceful shutdown. README gets a new section documenting
the transport, env vars, token scopes, and caveats.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds GITHUB_ENTERPRISE alongside GITHUB_REPO and GITHUB_ORG. When set,
the subscriber creates the hook under POST /enterprises/{slug}/hooks
and otherwise follows the same protocol. Config validation now requires
exactly one of the three scopes when WEBHOOK_TRANSPORT=websocket.
The README documents the new option together with a caveat: upstream
gh-webhook only targets repo and org hooks, so the relay's enterprise
behaviour is best-effort and depends on the account having the feature
enabled at that scope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ency GitHub Enterprise Cloud with data residency exposes its API at https://api.<customer>.ghe.com, which is neither the public-GitHub shape (https://api.github.com) nor the GHES shape (https://<host>/api/v3). Detect any *.ghe.com host and pick the api.<host> form so customers on GHE.com can use WEBHOOK_TRANSPORT=websocket without further work. Adds a table-driven test covering all three deployment shapes and clarifies the GITHUB_HOST description in the README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
gh-webhook only uses the bare-token Authorization header on the WebSocket dial; its REST calls go through go-gh's api.NewClient, which sends the standard "token <T>" / "Bearer <T>" form. Sending a bare user-to-server OAuth token (the kind gh auth token returns) to the REST API gets a 401 "Requires authentication". Switch applyAuth to "Bearer <token>" for REST; the WebSocket dial still uses the bare form because the relay expects it that way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds an optional WebSocket-based webhook transport so the server can ingest GitHub events without requiring a publicly reachable POST /webhook endpoint. It introduces a new ghws subscriber that creates an (initially inactive) webhook via the GitHub REST API, dials a relay WebSocket, activates the hook, and forwards frames into the existing ingest/order pipeline via a transport-agnostic WebhookHandler.Ingest.
Changes:
- Added
internal/services/ghwsWebSocket subscriber implementation + tests. - Refactored webhook ingestion into a transport-agnostic
WebhookHandler.Ingestand wired an adapter incmd/server. - Added config/env validation for
WEBHOOK_TRANSPORTand updated README with WebSocket setup/caveats.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new WebSocket transport configuration, quick-starts, and caveats. |
| internal/services/ghws/subscriber.go | Implements the GitHub relay WebSocket subscription lifecycle and frame ACK protocol. |
| internal/services/ghws/subscriber_test.go | Adds integration-style tests using an httptest server + WebSocket upgrader to validate the protocol flow. |
| internal/config/config.go | Adds env vars for selecting transport and validates required GitHub subscription settings in WebSocket mode. |
| handlers/webhook_handler.go | Extracts transport-agnostic ingest logic into Ingest and simplifies HTTP handler to map results to responses. |
| cmd/server/server.go | Starts the optional WebSocket subscriber when enabled and adds parsing for GITHUB_EVENTS. |
| go.mod | Adds github.com/gorilla/websocket dependency. |
| go.sum | Records checksums for the added WebSocket dependency. |
Copilot's findings
- Files reviewed: 9/14 changed files
- Comments generated: 5
Comment on lines
+237
to
+249
| // splitEvents parses a comma-separated GITHUB_EVENTS value into a slice | ||
| // suitable for ghws.Config. Whitespace is trimmed and empty entries are | ||
| // dropped so trailing/duplicate commas are forgiving. | ||
| func splitEvents(s string) []string { | ||
| parts := strings.Split(s, ",") | ||
| out := make([]string, 0, len(parts)) | ||
| for _, p := range parts { | ||
| if t := strings.TrimSpace(p); t != "" { | ||
| out = append(out, t) | ||
| } | ||
| } | ||
| return out | ||
| } |
Comment on lines
+209
to
+226
| func (s *Subscriber) runOnce(ctx context.Context) error { | ||
| hook, err := s.createHook(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("create hook: %w", err) | ||
| } | ||
| defer s.deleteHook(context.Background(), hook) | ||
|
|
||
| conn, _, err := s.dialer.DialContext(ctx, hook.WsURL, http.Header{ | ||
| "Authorization": []string{s.cfg.Token}, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("dial relay: %w", err) | ||
| } | ||
| defer conn.Close() | ||
|
|
||
| if err := s.activateHook(ctx, hook); err != nil { | ||
| return fmt.Errorf("activate hook: %w", err) | ||
| } |
Comment on lines
+333
to
+350
| func (s *Subscriber) createHook(ctx context.Context) (*hookResponse, error) { | ||
| body, _ := json.Marshal(createHookRequest{ | ||
| Name: "cli", | ||
| Events: s.cfg.Events, | ||
| Active: false, | ||
| Config: hookConfig{ | ||
| ContentType: "json", | ||
| InsecureSSL: "0", | ||
| Secret: s.cfg.Secret, | ||
| }, | ||
| }) | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodPost, | ||
| s.apiBase()+s.hookPath(), bytes.NewReader(body)) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| s.applyAuth(req) | ||
| resp, err := s.httpClient.Do(req) |
Comment on lines
+354
to
+358
| defer resp.Body.Close() | ||
| if resp.StatusCode/100 != 2 { | ||
| b, _ := io.ReadAll(resp.Body) | ||
| return nil, fmt.Errorf("create hook: %s: %s", resp.Status, strings.TrimSpace(string(b))) | ||
| } |
Comment on lines
25
to
30
| github.com/go-playground/validator/v10 v10.14.0 // indirect | ||
| github.com/goccy/go-json v0.10.2 // indirect | ||
| github.com/google/uuid v1.6.0 // indirect | ||
| github.com/gorilla/websocket v1.5.3 // indirect | ||
| github.com/json-iterator/go v1.1.12 // indirect | ||
| github.com/klauspost/cpuid/v2 v2.2.4 // indirect |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request adds support for receiving GitHub webhook events via a WebSocket relay, allowing the app to function without a public HTTP endpoint. It introduces new configuration options for selecting the transport method and subscribing to specific repositories, organizations, or enterprises. The webhook ingestion logic is refactored to be transport-agnostic, and the documentation is updated to describe the new setup and caveats.