diff --git a/cmd/relayfile-cli/control_plane.go b/cmd/relayfile-cli/control_plane.go index 6d45f2a0..c4bdb9c8 100644 --- a/cmd/relayfile-cli/control_plane.go +++ b/cmd/relayfile-cli/control_plane.go @@ -154,10 +154,19 @@ type listWebhookSubscriptionsResponse struct { WorkspaceID string `json:"workspaceId,omitempty"` } +type webhookSubscriptionHealth struct { + LastDeliveryAt *string `json:"lastDeliveryAt"` + LastSuccessAt *string `json:"lastSuccessAt"` + LastError *string `json:"lastError"` + ConsecutiveFailures *int `json:"consecutiveFailures,omitempty"` +} + type webhookSubscriptionSummary struct { - SubscriptionID string `json:"subscriptionId"` - URL string `json:"url"` - PathGlobs []string `json:"pathGlobs"` + SubscriptionID string `json:"subscriptionId"` + URL string `json:"url"` + PathGlobs []string `json:"pathGlobs"` + GithubPrIdentityAuthorized *bool `json:"githubPrIdentityAuthorized,omitempty"` + Health *webhookSubscriptionHealth `json:"health,omitempty"` } func runControlPlane(args []string, stdout io.Writer) error { @@ -514,9 +523,11 @@ func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Reques return } var upstream []struct { - ID string `json:"id"` - URL string `json:"url"` - PathGlobs []string `json:"pathGlobs"` + ID string `json:"id"` + URL string `json:"url"` + PathGlobs []string `json:"pathGlobs"` + GithubPrIdentityAuthorized *bool `json:"githubPrIdentityAuthorized"` + Health *webhookSubscriptionHealth `json:"health"` } if err := commandClient.client.getJSON( r.Context(), @@ -529,9 +540,11 @@ func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Reques subscriptions := make([]webhookSubscriptionSummary, 0, len(upstream)) for _, item := range upstream { subscriptions = append(subscriptions, webhookSubscriptionSummary{ - SubscriptionID: item.ID, - URL: item.URL, - PathGlobs: item.PathGlobs, + SubscriptionID: item.ID, + URL: item.URL, + PathGlobs: item.PathGlobs, + GithubPrIdentityAuthorized: item.GithubPrIdentityAuthorized, + Health: item.Health, }) } writeControlPlaneJSON(w, http.StatusOK, listWebhookSubscriptionsResponse{ diff --git a/cmd/relayfile-cli/control_plane_test.go b/cmd/relayfile-cli/control_plane_test.go index 7a574018..d1de51cb 100644 --- a/cmd/relayfile-cli/control_plane_test.go +++ b/cmd/relayfile-cli/control_plane_test.go @@ -381,6 +381,11 @@ func TestControlPlaneCloudIntegrationConformance(t *testing.T) { listedSubscriptions.Subscriptions[0].PathGlobs[0] != "/github/repos/acme/widgets/issues/**" { t.Fatalf("unexpected webhook subscription list: %#v", listedSubscriptions) } + if listedSubscriptions.Subscriptions[0].Health == nil || + listedSubscriptions.Subscriptions[0].Health.ConsecutiveFailures == nil || + *listedSubscriptions.Subscriptions[0].Health.ConsecutiveFailures != 0 { + t.Fatalf("expected webhook subscription health to pass through: %#v", listedSubscriptions.Subscriptions[0].Health) + } var deleted map[string]bool status = controlPlaneJSON(t, client, http.MethodDelete, baseURL+"/v1/integrations/webhook-subscriptions", deleteWebhookSubscriptionRequest{ @@ -421,6 +426,38 @@ func TestControlPlaneCloudIntegrationConformance(t *testing.T) { } } +func TestWebhookSubscriptionHealthOmitsAbsentConsecutiveFailures(t *testing.T) { + var omitted webhookSubscriptionHealth + if err := json.Unmarshal([]byte(`{"lastDeliveryAt":null,"lastSuccessAt":null,"lastError":null}`), &omitted); err != nil { + t.Fatalf("unmarshal omitted consecutiveFailures: %v", err) + } + if omitted.ConsecutiveFailures != nil { + t.Fatalf("expected omitted consecutiveFailures to stay nil, got %#v", omitted.ConsecutiveFailures) + } + raw, err := json.Marshal(omitted) + if err != nil { + t.Fatalf("marshal omitted consecutiveFailures: %v", err) + } + if strings.Contains(string(raw), "consecutiveFailures") { + t.Fatalf("omitempty should drop absent consecutiveFailures: %s", raw) + } + + var present webhookSubscriptionHealth + if err := json.Unmarshal([]byte(`{"consecutiveFailures":0}`), &present); err != nil { + t.Fatalf("unmarshal zero consecutiveFailures: %v", err) + } + if present.ConsecutiveFailures == nil || *present.ConsecutiveFailures != 0 { + t.Fatalf("expected confirmed zero consecutiveFailures, got %#v", present.ConsecutiveFailures) + } + raw, err = json.Marshal(present) + if err != nil { + t.Fatalf("marshal zero consecutiveFailures: %v", err) + } + if !strings.Contains(string(raw), `"consecutiveFailures":0`) { + t.Fatalf("confirmed zero consecutiveFailures should serialize as 0: %s", raw) + } +} + func TestControlPlaneProviderStatusUsesCloudTokenFromEnvironment(t *testing.T) { t.Setenv("HOME", t.TempDir()) clearRelayfileEnv(t) diff --git a/openapi/relayfile-control-plane-v1.openapi.yaml b/openapi/relayfile-control-plane-v1.openapi.yaml index 28005d46..4110d4cd 100644 --- a/openapi/relayfile-control-plane-v1.openapi.yaml +++ b/openapi/relayfile-control-plane-v1.openapi.yaml @@ -617,6 +617,25 @@ components: type: array items: type: string + githubPrIdentityAuthorized: + type: boolean + description: Whether `/pulls//**` identity expansion (title slugs, reviews, comments, checks via resource_ref) is enabled for this subscription. + health: + type: object + properties: + lastDeliveryAt: + type: string + format: date-time + nullable: true + lastSuccessAt: + type: string + format: date-time + nullable: true + lastError: + type: string + nullable: true + consecutiveFailures: + type: integer ErrorEnvelope: type: object required: [error] diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index 54e2d971..82980149 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -611,6 +611,13 @@ describe('RelayfileControlPlaneClient integration webhook subscriptions', () => subscriptionId: 'whsub_123', url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', pathGlobs: ['/github/repos/acme/widgets/issues/**'], + githubPrIdentityAuthorized: true, + health: { + lastDeliveryAt: '2026-09-21T00:00:00.000Z', + lastSuccessAt: '2026-09-21T00:00:00.000Z', + lastError: null, + consecutiveFailures: 0, + }, }, ], }); @@ -622,6 +629,13 @@ describe('RelayfileControlPlaneClient integration webhook subscriptions', () => subscriptionId: 'whsub_123', url: 'https://cast.test/v1/integrations/relayfile/inbound/ws/ch', pathGlobs: ['/github/repos/acme/widgets/issues/**'], + githubPrIdentityAuthorized: true, + health: { + lastDeliveryAt: '2026-09-21T00:00:00.000Z', + lastSuccessAt: '2026-09-21T00:00:00.000Z', + lastError: null, + consecutiveFailures: 0, + }, }, ], }); diff --git a/packages/client/src/generated/control-plane.ts b/packages/client/src/generated/control-plane.ts index 83f92051..c30aa37a 100644 --- a/packages/client/src/generated/control-plane.ts +++ b/packages/client/src/generated/control-plane.ts @@ -334,6 +334,16 @@ export interface components { /** Format: uri */ url: string; pathGlobs: string[]; + /** @description Whether `/pulls//**` identity expansion (title slugs, reviews, comments, checks via resource_ref) is enabled for this subscription. */ + githubPrIdentityAuthorized?: boolean; + health?: { + /** Format: date-time */ + lastDeliveryAt?: string | null; + /** Format: date-time */ + lastSuccessAt?: string | null; + lastError?: string | null; + consecutiveFailures?: number; + }; }; ErrorEnvelope: { error: {