webhook

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package webhook handles GitHub webhook events for the Chetter service.

Package webhook handles GitHub webhook events for the Chetter service.

Package webhook handles GitHub webhook events for the Chetter service. It verifies webhook signatures, parses events, and submits review tasks to the chetter service.

Package webhook handles GitHub webhook events for the Chetter service.

Package webhook handles GitHub webhook events for the Chetter service.

Index

Constants

View Source
const (
	// Action values for the pull_request event.
	PullRequestActionOpened      = "opened"
	PullRequestActionSynchronize = "synchronize"
	PullRequestActionReopened    = "reopened"
	PullRequestActionLabeled     = "labeled"

	// EventType values for the X-GitHub-Event header.
	EventTypePullRequest              = "pull_request"
	EventTypeIssueComment             = "issue_comment"
	EventTypeIssues                   = "issues"
	EventTypePullRequestReview        = "pull_request_review"
	EventTypePullRequestReviewComment = "pull_request_review_comment"

	// ChetterReviewLabel is the label we add to PRs that should be reviewed.
	ChetterReviewLabel = "chetter-review"

	// ReviewTrigger comment that users post to request a review.
	ReviewTriggerCommand = "/chetter-review"

	// Trigger event values for trigger_config's "event" field.
	TriggerEventOpened      = "opened"
	TriggerEventLabeled     = "labeled"
	TriggerEventComment     = "comment"
	TriggerEventFork        = "fork"
	TriggerEventCreated     = "created" // issue created
	TriggerEventSynchronize = "synchronize"
)
View Source
const CommentReviewFailed = "🤖 Chetter review could not start. Please check the chetter service logs."

CommentReviewFailed is posted on a PR when Chetter fails to start a review.

Variables

This section is empty.

Functions

func TriggerMatchesLabels

func TriggerMatchesLabels(triggerLabels, issueLabels []string) bool

TriggerMatchesLabels checks if any of the issue's labels match the trigger's required labels. If the trigger has no match_labels, all issues match.

Types

type ArtifactRecorder

type ArtifactRecorder interface {
	RecordArtifact(ctx context.Context, params RecordArtifactParams) error
}

ArtifactRecorder is the interface for recording task artifacts discovered from webhook events (issues, PRs, comments with Chetter footer signatures).

type AuditEventParams

type AuditEventParams struct {
	EventType        string
	SourceType       string
	SourceID         string
	TargetType       string
	TargetID         string
	Repo             string
	GitHubEvent      string
	GitHubAction     string
	GitHubDeliveryID string
	ParentEventID    string
	Detail           string
	Payload          json.RawMessage
}

AuditEventParams holds the data for a single audit log entry.

type AuditLogger

type AuditLogger interface {
	LogAuditEvent(ctx context.Context, params AuditEventParams) error
}

AuditLogger is the interface for recording server-side audit events.

type CheckRunSummary

type CheckRunSummary struct {
	Total      int
	Completed  int
	Successful int
	Failed     int
	Pending    int
}

type Client

type Client struct {
	InstallationID int64
	// contains filtered or unexported fields
}

Client wraps the GitHub API for one immutable installation.

func NewClient

func NewClient(appID int64, installationID int64, privateKeyPEMBase64 string) (*Client, error)

NewClient is a compatibility wrapper for legacy single-installation callers.

func (*Client) AddIssueLabel

func (c *Client) AddIssueLabel(ctx context.Context, repo string, prNumber int, label string) error

AddIssueLabel adds a label to a PR (issues and PRs share the labels API).

func (*Client) CheckUserHasWriteAccess

func (c *Client) CheckUserHasWriteAccess(ctx context.Context, repo, username string) (bool, error)

CheckUserHasWriteAccess returns true if the given user has write or admin permission on the repo. Used to gate the /chetter-review comment trigger.

func (*Client) CreateBranch

func (c *Client) CreateBranch(ctx context.Context, repo, branch, sha string) error

func (*Client) CreateIssue

func (c *Client) CreateIssue(ctx context.Context, repo, title, body string, labels []string) (CreatedGitHubArtifact, error)

func (*Client) CreateIssueComment

func (c *Client) CreateIssueComment(ctx context.Context, repo string, prNumber int, body string) error

CreateIssueComment posts a comment on a PR.

func (*Client) CreateIssueCommentWithResponse

func (c *Client) CreateIssueCommentWithResponse(ctx context.Context, repo string, issueNumber int, body string) (CreatedGitHubArtifact, error)

func (*Client) CreatePullRequest

func (c *Client) CreatePullRequest(ctx context.Context, repo, title, body, head, base string, draft bool) (CreatedGitHubArtifact, error)

func (*Client) CreatePullRequestReview

func (c *Client) CreatePullRequestReview(ctx context.Context, repo string, prNumber int, event, body string) (CreatedGitHubArtifact, error)

func (*Client) CredentialForRepo

func (c *Client) CredentialForRepo(ctx context.Context, repo string, profile PermissionProfile) (Credential, error)

CredentialForRepo returns a repository-restricted credential from this client's immutable installation.

func (*Client) GetAppLogin

func (c *Client) GetAppLogin(ctx context.Context) (string, error)

GetAppLogin is retained for compatibility; App login is manager-scoped.

func (*Client) GetBranchSHA

func (c *Client) GetBranchSHA(ctx context.Context, repo, branch string) (string, error)

func (*Client) GetIssueDetails

func (c *Client) GetIssueDetails(ctx context.Context, repo string, issueNumber int) (IssueDetails, error)

GetIssueDetails fetches the authoritative metadata for a GitHub issue.

func (*Client) GetPullRequest

func (c *Client) GetPullRequest(ctx context.Context, repo string, prNumber int) (headRef, baseRef, cloneURL string, err error)

GetPullRequest fetches a pull request and returns the head ref, base ref, and clone URL of the head repository.

func (*Client) GetPullRequestDetails

func (c *Client) GetPullRequestDetails(ctx context.Context, repo string, prNumber int) (PullRequestDetails, error)

func (*Client) HasLabel

func (c *Client) HasLabel(ctx context.Context, repo string, prNumber int, label string) (bool, error)

HasLabel reports whether the label is already on the PR.

func (*Client) ListCheckRunsForRef

func (c *Client) ListCheckRunsForRef(ctx context.Context, repo, ref string) (CheckRunSummary, error)

func (*Client) ListPRFiles

func (c *Client) ListPRFiles(ctx context.Context, repo string, prNumber int) ([]string, error)

ListPRFiles returns the list of filenames changed in a pull request.

func (*Client) UpsertFile

func (c *Client) UpsertFile(ctx context.Context, repo, branch, path, content, message string) error

type Comment

type Comment struct {
	Body string `json:"body"`
	User struct {
		Login string `json:"login"`
	} `json:"user"`
}

Comment is the issue/PR comment object.

type CreatedGitHubArtifact

type CreatedGitHubArtifact struct {
	Number  int
	URL     string
	ID      int64
	HTMLURL string
}

type Credential

type Credential struct {
	Token     string
	ExpiresAt time.Time
}

Credential is a short-lived GitHub installation credential. Callers must not persist or log Token.

type DeliveryStore

type DeliveryStore interface {
	// RecordDelivery inserts a delivery row. Returns false (with nil error)
	// if the delivery_id was already recorded (idempotent dedup).
	RecordDelivery(ctx context.Context, params RecordDeliveryParams) (created bool, err error)
	// MarkDeliveryProcessing marks a delivery as currently being handled.
	MarkDeliveryProcessing(ctx context.Context, deliveryID string) error
	// MarkDeliveryCompleted marks a delivery as successfully processed.
	MarkDeliveryCompleted(ctx context.Context, deliveryID string) error
	// MarkDeliveryFailed marks a delivery as failed with the error and
	// schedules the next retry (or dead-letters if attempts exhausted).
	MarkDeliveryFailed(ctx context.Context, deliveryID string, errMsg string) error
}

DeliveryStore persists webhook deliveries for idempotency, retry, and delivery status tracking. When non-nil, the handler records every delivery before processing and updates its status on completion or failure. A background worker (in the service layer) retries failed deliveries with exponential backoff and dead-letters them after max_attempts. See issue #102.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler serves GitHub webhook events. Implements http.Handler.

func NewHandler

func NewHandler(cfg HandlerConfig, github *Manager, submitter TaskSubmitter, triggers TriggerResolver, audit AuditLogger, artifacts ArtifactRecorder, resumer SessionResumer, deliveryStore DeliveryStore) *Handler

NewHandler creates a webhook Handler. If the configuration is incomplete, the returned handler will accept requests but log "webhook disabled" for every event (kill switch behavior).

func (*Handler) ProcessDelivery

func (h *Handler) ProcessDelivery(event string, body []byte, deliveryID string) error

ProcessDelivery is the exported entry point for the retry worker to re-process a failed delivery. It runs the handler synchronously and updates the delivery store with the outcome. See issue #102.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles an incoming GitHub webhook request.

func (*Handler) Shutdown

func (h *Handler) Shutdown(ctx context.Context) error

Shutdown waits for in-flight webhook processing goroutines to finish, or until ctx expires. It must be called after the HTTP server has stopped accepting new requests but before the database is closed, so that in-flight events can complete their DB operations. If the deadline expires, remaining goroutines are abandoned (they continue running but the server proceeds with shutdown); the event is logged. See issue #57.

type HandlerConfig

type HandlerConfig struct {
	Disabled      bool
	WebhookSecret string
	MaxBodyBytes  int64
}

HandlerConfig is the configuration for the webhook handler.

type Installation

type Installation struct {
	ID int64 `json:"id"`
}

Installation identifies the GitHub App installation that signed event work must use for installation-authenticated API calls.

type Issue

type Issue struct {
	Number      int    `json:"number"`
	Title       string `json:"title"`
	Body        string `json:"body"`
	HTMLURL     string `json:"html_url"`
	PullRequest *struct {
		URL string `json:"url"`
	} `json:"pull_request,omitempty"`
	Labels []Label `json:"labels"`
}

Issue is the issue/PR object (PRs come through the issues API).

type IssueCommentEvent

type IssueCommentEvent struct {
	Action       string       `json:"action"`
	Comment      Comment      `json:"comment"`
	Issue        Issue        `json:"issue"`
	Repository   Repository   `json:"repository"`
	Installation Installation `json:"installation"`
}

IssueCommentEvent is the top-level payload for an issue_comment webhook event. For PR comments, the Issue object includes a `pull_request` field which we use to determine that this is a PR comment (vs an issue comment).

func (*IssueCommentEvent) IsPullRequest

func (e *IssueCommentEvent) IsPullRequest() bool

IsPullRequest returns true if the issue is actually a pull request.

type IssueData

type IssueData struct {
	Number  int    `json:"number"`
	Title   string `json:"title"`
	State   string `json:"state"`
	Body    string `json:"body"`
	HTMLURL string `json:"html_url"`
	User    struct {
		Login string `json:"login"`
	} `json:"user"`
	Labels []Label `json:"labels"`
}

IssueData is the relevant subset of the issue object.

type IssueDetails

type IssueDetails struct {
	Number  int
	State   string
	Title   string
	Body    string
	HTMLURL string
	Labels  []string
}

IssueDetails is the authoritative issue metadata used by the manual trigger test flow. It is fetched from GitHub so label matching and the default prompt never trust editable client-supplied fields.

type IssueEvent

type IssueEvent struct {
	Action       string       `json:"action"`
	Issue        IssueData    `json:"issue"`
	Label        *Label       `json:"label,omitempty"`
	Repository   Repository   `json:"repository"`
	Installation Installation `json:"installation"`
}

IssueEvent is the top-level payload for an issues webhook event.

type Label

type Label struct {
	Name string `json:"name"`
}

Label is a PR or issue label.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns GitHub App credentials and installation-specific caches.

func NewManager

func NewManager(appID int64, privateKeyPEMBase64 string, opts ...ManagerOption) (*Manager, error)

NewManager parses GitHub App credentials and creates a process-wide manager.

func (*Manager) AppLogin

func (m *Manager) AppLogin(ctx context.Context) (string, error)

AppLogin returns the App bot login (for example, "chetter[bot]").

func (*Manager) ClientForInstallation

func (m *Manager) ClientForInstallation(ctx context.Context, installationID int64) (*Client, error)

ClientForInstallation returns an immutable client whose token cache is isolated from every other installation.

func (*Manager) ClientForRepo

func (m *Manager) ClientForRepo(ctx context.Context, repo string) (*Client, error)

ClientForRepo discovers the App installation authorized for repo using an App JWT and caches the mapping for a bounded TTL.

func (*Manager) CredentialForRepo

func (m *Manager) CredentialForRepo(ctx context.Context, repo string, profile PermissionProfile) (Credential, error)

CredentialForRepo discovers the repository installation and returns a repository-restricted credential for profile.

func (*Manager) LegacyClient

func (m *Manager) LegacyClient() *Client

LegacyClient returns the optional fallback installation client.

type ManagerOption

type ManagerOption func(*Manager) error

ManagerOption customizes a GitHub App manager.

func WithAPIBaseURL

func WithAPIBaseURL(baseURL string) ManagerOption

WithAPIBaseURL overrides the GitHub API base URL. It is intended for tests and GitHub Enterprise-compatible API endpoints.

func WithHTTPClient

func WithHTTPClient(client *http.Client) ManagerOption

WithHTTPClient overrides the HTTP client used by the manager.

func WithLegacyInstallationID

func WithLegacyInstallationID(installationID int64) ManagerOption

WithLegacyInstallationID configures the optional installation used by repository-less legacy callers until they are migrated to ClientForRepo.

type PRBranch

type PRBranch struct {
	Ref  string `json:"ref"`
	SHA  string `json:"sha"`
	Repo struct {
		FullName string `json:"full_name"`
		CloneURL string `json:"clone_url"`
	} `json:"repo"`
}

PRBranch is the head or base ref of a pull request.

type PermissionProfile

type PermissionProfile string

PermissionProfile identifies the least-privilege permission set requested for a repository-restricted installation credential.

const (
	PermissionProfileTaskGit PermissionProfile = "task-git"
)

type PullRequest

type PullRequest struct {
	Number int    `json:"number"`
	State  string `json:"state"`
	Title  string `json:"title"`
	Body   string `json:"body"`

	Head PRBranch `json:"head"`
	Base PRBranch `json:"base"`

	User struct {
		Login string `json:"login"`
	} `json:"user"`

	Labels []Label `json:"labels"`
}

PullRequest is the relevant subset of the pull_request object.

type PullRequestDetails

type PullRequestDetails struct {
	Number  int
	State   string
	Merged  bool
	URL     string
	HeadRef string
	HeadSHA string
	BaseRef string
}

type PullRequestEvent

type PullRequestEvent struct {
	Action       string       `json:"action"`
	Number       int          `json:"number"`
	PullRequest  PullRequest  `json:"pull_request"`
	Label        *Label       `json:"label,omitempty"`
	Repository   Repository   `json:"repository"`
	Installation Installation `json:"installation"`
	Sender       struct {
		Login string `json:"login"`
	} `json:"sender"`
}

PullRequestEvent is the top-level payload for a pull_request webhook event.

type PullRequestReviewCommentEvent

type PullRequestReviewCommentEvent struct {
	Action       string       `json:"action"`
	PullRequest  PullRequest  `json:"pull_request"`
	Comment      Comment      `json:"comment"`
	Repository   Repository   `json:"repository"`
	Installation Installation `json:"installation"`
}

PullRequestReviewCommentEvent is the top-level payload for pull_request_review_comment.

type PullRequestReviewEvent

type PullRequestReviewEvent struct {
	Action       string       `json:"action"`
	PullRequest  PullRequest  `json:"pull_request"`
	Review       Review       `json:"review"`
	Repository   Repository   `json:"repository"`
	Installation Installation `json:"installation"`
}

PullRequestReviewEvent is the top-level payload for pull_request_review.

type RecentDeliveries

type RecentDeliveries struct {
	// contains filtered or unexported fields
}

RecentDeliveries tracks recent X-GitHub-Delivery IDs to prevent duplicate processing of the same webhook delivery. Not persisted — if the process restarts mid-review, the review is simply lost (acceptable; GitHub will not redeliver on 2xx).

func NewRecentDeliveries

func NewRecentDeliveries(ttl time.Duration, maxSize int) *RecentDeliveries

NewRecentDeliveries creates a tracker that holds IDs for ttl and caps the map at maxSize entries (evicting oldest by expiry).

func (*RecentDeliveries) Seen

func (r *RecentDeliveries) Seen(id string) bool

Seen returns true if the delivery ID was already recorded (and not expired). As a side effect, it records the ID with the current time.

func (*RecentDeliveries) Size

func (r *RecentDeliveries) Size() int

Size returns the current number of tracked IDs (for tests/debugging).

type RecordArtifactParams

type RecordArtifactParams struct {
	TaskID             string
	AgentSessionID     string
	UserPromptID       string
	ExecutionAttemptID string
	ArtifactType       string
	Repo               string
	Number             int
	URL                string
	Ref                string
	SHA                string
	DiscoverySource    string
}

RecordArtifactParams holds the data for a single task artifact entry.

type RecordDeliveryParams

type RecordDeliveryParams struct {
	DeliveryID string
	EventType  string
	Action     string
	Payload    []byte
}

RecordDeliveryParams holds the data for a single webhook delivery record.

type Repository

type Repository struct {
	FullName string `json:"full_name"`
	Name     string `json:"name"`
	Owner    struct {
		Login string `json:"login"`
	} `json:"owner"`
}

Repository is the relevant subset of the repository object.

type Review

type Review struct {
	User struct {
		Login string `json:"login"`
	} `json:"user"`
}

Review is the relevant subset of a pull request review object.

type ReviewContext

type ReviewContext struct {
	TeamID               string
	TriggerName          string
	TriggerType          string
	Trigger              string // "label", "fork", "file-pattern", "comment"
	Repo                 string // e.g., "chetter/chetter"
	PRNumber             int
	BaseRef              string
	HeadRef              string
	HeadCloneURL         string
	CommentAuthor        string // only set for comment triggers
	GitHubInstallationID int64
	Prompt               string // trigger-supplied prompt; empty falls back to the built-in template
	AgentImage           string // trigger-supplied agent image; empty falls back to the default
	Agent                string // reviewer agent name (from the trigger config)
	ProviderID           string // reviewer provider ID (from the trigger config)
	ModelID              string // reviewer model ID (from the trigger config)
	VariantID            string // reviewer variant ID (from the trigger config)
	Skills               []string
	TimeoutSec           int // reviewer task timeout (from the trigger config)
	SessionMode          string
	PauseReason          string
	TTLHours             int
	// Isolation marks review tasks as requiring enforced isolation (issue #291).
	Isolation string
}

ReviewContext is the data passed to TaskSubmitter for a single review.

type ReviewTrigger

type ReviewTrigger struct {
	TeamID      string
	Name        string
	TriggerType string
	Prompt      string
	AgentImage  string
	Agent       string
	ProviderID  string
	ModelID     string
	VariantID   string
	TimeoutSec  int
	GitURL      string
	GitRef      string
	Skills      []string
	Event       string   // which webhook action this trigger responds to (e.g. "opened", "labeled"), empty = all
	MatchLabels []string // required issue labels; empty = all labels match
	SessionMode string
	PauseReason string
	TTLHours    int
	// Isolation marks review tasks as requiring enforced isolation (issue #291).
	Isolation string
}

ReviewTrigger is the resolved data from a single trigger.

type SessionResumer

type SessionResumer interface {
	ResumeSessionForPR(ctx context.Context, repo string, prNumber int) error
}

SessionResumer is the interface for resuming paused or recoverable agent sessions.

type SubmitTaskRequest

type SubmitTaskRequest struct {
	TeamID               string
	Prompt               string
	GitURL               string
	GitRef               string
	GitHubRepo           string
	GitHubInstallationID int64
	AgentImage           string
	Agent                string
	ProviderID           string
	ModelID              string
	VariantID            string
	Skills               []string
	Env                  map[string]string
	TimeoutSec           int
	TriggerName          string
	TriggerType          string
	SessionMode          string
	PauseReason          string
	TTLHours             int
	// Isolation marks the task as requiring enforced isolation (issue #291).
	Isolation string
}

SubmitTaskRequest is a minimal copy of service.SubmitTaskRequest. We define it here to avoid importing the service package. The service adapter in main.go converts from service.SubmitTaskRequest to this type.

func BuildIssueTaskRequest

func BuildIssueTaskRequest(t ReviewTrigger, repo string, installationID int64, prompt string, env map[string]string) SubmitTaskRequest

BuildIssueTaskRequest converts a resolved issue trigger into a task submission request, mirroring the dispatch used by the issues and issue_comment webhook handlers.

func BuildReviewTaskRequest

func BuildReviewTaskRequest(review ReviewContext) SubmitTaskRequest

BuildReviewTaskRequest creates a SubmitTaskRequest for a PR review. It is the canonical conversion used by both the webhook dispatch path and the manual trigger test flow so the two never drift.

type TaskSubmitter

type TaskSubmitter interface {
	SubmitReviewTask(ctx context.Context, review ReviewContext) error
	SubmitTask(ctx context.Context, req SubmitTaskRequest) (any, error)
}

TaskSubmitter is the subset of service.Service that the webhook needs to submit tasks. Defined as an interface to allow mocking in tests.

func NewServiceSubmitter

func NewServiceSubmitter(svc TaskSubmitterService) TaskSubmitter

NewServiceSubmitter creates a TaskSubmitter that wraps the given service adapter. Use this in main.go to wire the webhook handler to the service.

type TaskSubmitterService

type TaskSubmitterService interface {
	// SubmitTask matches the signature of service.Service.SubmitTask. The
	// return value is ignored by the webhook caller.
	SubmitTask(ctx context.Context, req SubmitTaskRequest) (any, error)
}

TaskSubmitterService is the interface that the webhook package needs from the service package. It's defined in the webhook package so webhook doesn't need to import service. In main.go, an adapter is provided that satisfies this interface by calling the service's SubmitTask method.

type TriggerResolver

type TriggerResolver interface {
	ListEnabledPRReviewTriggersByRepo(ctx context.Context, repo string) ([]ReviewTrigger, error)
	ListEnabledIssueTriggersByRepo(ctx context.Context, repo string) ([]ReviewTrigger, error)
}

TriggerResolver is the subset of the service that the webhook needs to resolve triggers for a given repo. Defined as an interface to allow mocking in tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL