blob: 68a80ec5641d95e058d476251fc5a70dd3079eda [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"errors"
"fmt"
"net/http"
"slices"
"golang.org/x/oscar/internal/actions"
"golang.org/x/oscar/internal/docs"
"golang.org/x/oscar/internal/github"
)
// handleGitHubEvent handles incoming webhook requests from GitHub
// and reports whether the request was handled.
//
// If the incoming request was triggered by supported event, and sync
// is enabled, it syncs its relevant state. If changes are enabled,
// it takes relevant actions in response to the event.
//
// Otherwise, it logs the event and returns (false, nil).
//
// The supported events are:
// - new GitHub issue (see [Gaby.handleGitHubIssueEvent])
// - new GitHub issue comment (see [Gaby.handleGitHubIssueCommentEvent])
//
// handled is true if all appropriate syncs and actions were performed
// in response to the event, and false if the event was skipped or an
// error occurred. (handled is also false if either of [gabyFlags.enablesync]
// or [gabyFlags.enablechanges] is false.)
//
// handleGitHubEvent returns an error if any of the syncs or actions fails,
// or if the webhook request is invalid according to [github.ValidateWebhookRequest].
func (g *Gaby) handleGitHubEvent(r *http.Request, fl *gabyFlags) (handled bool, err error) {
event, err := github.ValidateWebhookRequest(r, g.secret)
if err != nil {
return false, fmt.Errorf("%w: %v", errInvalidWebhookRequest, err)
}
if !slices.Contains(g.githubProjects, event.Project()) {
g.slog.Warn("unexpected webhook request", "webhook_project", event.Project(), "gaby_project", g.githubProjects, "event", event)
return false, nil
}
switch p := event.Payload.(type) {
case *github.WebhookIssueEvent:
return g.handleGitHubIssueEvent(r.Context(), p, fl)
case *github.WebhookIssueCommentEvent:
return g.handleGitHubIssueCommentEvent(r.Context(), p, fl)
default:
g.slog.Info("ignoring GitHub event", "type", event.Type, "event", event)
}
return false, nil
}
var errInvalidWebhookRequest = errors.New("invalid webhook request")
// handleGitHubIssueEvent handles an incoming GitHub "issue" event and
// reports whether the event was handled.
//
// If the event is a new issue, and sync is enabled, the function
// syncs the corresponding GitHub project. If changes are also enabled,
// it posts related issues and fixes the body and comments of the issue.
//
// It returns an error immediately if any of the syncs or actions fails.
//
// Otherwise, it logs the event and returns (false, nil).
func (g *Gaby) handleGitHubIssueEvent(ctx context.Context, event *github.WebhookIssueEvent, fl *gabyFlags) (handled bool, _ error) {
if event.Action != github.WebhookIssueActionOpened {
g.slog.Info("ignoring GitHub issue event (action is not opened)", "event", event, "action", event.Action)
return false, nil
}
g.slog.Info("handling GitHub issue", "event", event)
project := event.Repository.Project
if fl.enablesync {
if err := g.syncGitHubProject(ctx, project); err != nil {
return false, err
}
if err := g.embedAll(ctx); err != nil {
return false, err
}
}
// Do not attempt changes unless sync is enabled and completely succeeded.
if fl.enablechanges && fl.enablesync {
// No need to lock; [related.Poster.Post] and [related.Poster.Run] can
// happen concurrently.
if err := g.relatedPoster.Post(ctx, project, event.Issue.Number); err != nil {
return false, err
}
if err := g.fixGitHubIssue(ctx, project, event.Issue.Number); err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// handleGitHubIssueCommentEvent handles an incoming GitHub "issue comment" event
// and reports whether the event was handled.
//
// If the event is a new issue comment, and sync is enabled, the function
// syncs the corresponding GitHub project. If changes are also enabled,
// it fixes the body and comments of the issue to which the comment
// was posted.
//
// It returns an error immediately if any of the syncs or actions fails.
//
// Otherwise, it logs the event and returns (false, nil).
func (g *Gaby) handleGitHubIssueCommentEvent(ctx context.Context, event *github.WebhookIssueCommentEvent, fl *gabyFlags) (handled bool, _ error) {
if event.Action != github.WebhookIssueCommentActionCreated {
g.slog.Info("ignoring GitHub issue comment event (action is not created)", "event", event, "action", event.Action)
return false, nil
}
g.slog.Info("handling GitHub issue comment", "event", event)
project := event.Repository.Project
if fl.enablesync {
if err := g.syncGitHubProject(ctx, project); err != nil {
return false, err
}
// Embeddings are not needed to apply fixes.
}
// Do not attempt changes unless sync is enabled and completely succeeded.
if fl.enablechanges && fl.enablesync {
if err := g.fixGitHubIssue(ctx, project, event.Issue.Number); err != nil {
return false, err
}
return true, nil
}
return false, nil
}
func (g *Gaby) fixGitHubIssue(ctx context.Context, project string, issue int64) error {
// No need to lock; [commentfix.Fixer.FixGitHubIssue] and
// [commentfix.Fixer.Run] can happen concurrently.
if err := g.commentFixer.LogFixGitHubIssue(ctx, project, issue); err != nil {
return err
}
if err := actions.Run(ctx, g.slog, g.db); err != nil {
return err
}
return nil
}
// syncGitHubProject syncs the document corpus with respect to a single
// GitHub project.
func (g *Gaby) syncGitHubProject(ctx context.Context, project string) error {
g.db.Lock(gabyGitHubSyncLock)
defer g.db.Unlock(gabyGitHubSyncLock)
if err := g.github.SyncProject(ctx, project); err != nil {
return err
}
docs.Sync(g.docs, g.github)
return nil
}