Skip to content

Commit

Permalink
internal/labels: labeler: Run
Browse files Browse the repository at this point in the history
Add Run method to Labeler.

Based heavily on related.Poster.Run.

For #64.

Change-Id: I4818214eb0f8bd00294cde47d41cad943b28d74e
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/637517
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
  • Loading branch information
jba committed Dec 18, 2024
1 parent 7ba6238 commit 83d6575
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 6 deletions.
48 changes: 43 additions & 5 deletions internal/labels/labeler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ import (

"golang.org/x/oscar/internal/actions"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/llm"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/storage/timed"
"rsc.io/ordered"
)

// A Labeler labels GitHub issues.
type Labeler struct {
slog *slog.Logger
db storage.DB
github *github.Client
cgen llm.ContentGenerator
projects map[string]bool
watcher *timed.Watcher[*github.Event]
name string
Expand All @@ -35,20 +38,20 @@ type Labeler struct {
}

// New creates and returns a new Labeler. It logs to lg, stores state in db,
//
// and watches for new GitHub issues using gh.
// manipulates GitHub issues using gh, and classifies issues using cgen.
//
// For the purposes of storing its own state, it uses the given name.
// Future calls to New with the same name will use the same state.
//
// Use the [Labeler] methods to configure the posting parameters
// (especially [Labeler.EnableProject] and [Labeler.EnableLabels])
// before calling [Labeler.Run].
func New(lg *slog.Logger, db storage.DB, gh *github.Client, name string) *Labeler {
func New(lg *slog.Logger, db storage.DB, gh *github.Client, cgen llm.ContentGenerator, name string) *Labeler {
l := &Labeler{
slog: lg,
db: db,
github: gh,
cgen: cgen,
projects: make(map[string]bool),
watcher: gh.EventWatcher("labels.Labeler:" + name),
name: name,
Expand Down Expand Up @@ -159,7 +162,36 @@ func (l *Labeler) logLabelIssue(ctx context.Context, e *github.Event) (advance b
e.Project, "issue", e.Issue, "reason", reason, "event", e)
return false, nil
}
return false, errors.New("unimplemented")
// If an action has already been logged for this event, do nothing.
// we don't need a lock. [actions.before] will lock to avoid multiple log entries.
if _, ok := actions.Get(l.db, l.actionKind, logKey(e)); ok {
l.slog.Info("labels.Labeler already logged", "name", l.name, "project", e.Project, "issue", e.Issue, "event", e)
// If labeling is enabled, we can advance the watcher because
// a comment has already been logged for this issue.
return l.label, nil
}
// If we didn't skip, it's definitely an issue.
issue := e.Typed.(*github.Issue)
l.slog.Debug("labels.Labeler consider", "url", issue.HTMLURL)

cat, explanation, err := IssueCategory(ctx, l.cgen, issue)
if err != nil {
return false, fmt.Errorf("IssueCategory(%s): %w", issue.HTMLURL, err)
}
l.slog.Info("labels.Labeler chose label", "name", l.name, "project", e.Project, "issue", e.Issue,
"label", cat.Label, "explanation", explanation)

if !l.label {
// Labeling is disabled so we did not handle this issue.
return false, nil
}

act := &action{
Issue: issue,
NewLabels: []string{cat.Label},
}
l.logAction(l.db, logKey(e), storage.JSON(act), l.requireApproval)
return true, nil
}

func (l *Labeler) skip(e *github.Event) (bool, string) {
Expand Down Expand Up @@ -206,7 +238,7 @@ func (l *Labeler) syncLabels(ctx context.Context, project string, cats []Categor
for _, cat := range cats {
lab, ok := tlabs[cat.Label]
if !ok {
l.slog.Info("creating label", "label", lab.Name)
l.slog.Info("creating label", "label", cat.Label)
if err := l.github.CreateLabel(ctx, project, github.Label{
Name: cat.Label,
Description: cat.Description,
Expand Down Expand Up @@ -242,3 +274,9 @@ func (ar *actioner) ForDisplay(data []byte) string {
// TODO: implement
return ""
}

// logKey returns the key for the event in the action log. This is only a portion
// of the database key; it is prefixed by the Labelers's action kind.
func logKey(e *github.Event) []byte {
return ordered.Encode(e.Project, e.Issue)
}
50 changes: 49 additions & 1 deletion internal/labels/labeler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ package labels

import (
"context"
"encoding/json"
"maps"
"slices"
"testing"

"github.com/google/go-cmp/cmp"
"golang.org/x/oscar/internal/actions"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/llm"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/testutil"
)
Expand All @@ -20,7 +24,7 @@ func TestSyncLabels(t *testing.T) {
lg := testutil.Slogger(t)
db := storage.MemDB()
gh := github.New(lg, db, nil, nil)
labeler := New(lg, nil, gh, "")
labeler := New(lg, nil, gh, nil, "")
m := map[string]github.Label{
"A": {Name: "A", Description: "a", Color: "a"},
"B": {Name: "B", Description: "", Color: "b"},
Expand Down Expand Up @@ -60,3 +64,47 @@ func TestSyncLabels(t *testing.T) {
t.Errorf("mismatch (-want, got):\n%s", diff)
}
}

func TestRun(t *testing.T) {
const project = "golang/go"
ctx := context.Background()
check := testutil.Checker(t)
lg := testutil.Slogger(t)
db := storage.MemDB()
gh := github.New(lg, db, nil, nil)
gh.Testing().AddLabel(project, github.Label{Name: "bug"})
gh.Testing().AddIssue(project, &github.Issue{
Number: 1,
Title: "title",
Body: "body",
})
gh.Testing().AddIssue("other/project", &github.Issue{
Number: 2,
Title: "title",
Body: "body",
})
cgen := llm.TestContentGenerator("test", func(context.Context, *llm.Schema, []llm.Part) (string, error) {
return `{"CategoryName": "bug", "Explanation": "exp"}`, nil
})
l := New(lg, db, gh, cgen, "test")
l.EnableProject(project)
l.EnableLabels()

check(l.Run(ctx))
entries := slices.Collect(actions.ScanAfterDBTime(lg, db, 0, nil))
if g := len(entries); g != 1 {
t.Fatalf("got %d actions, want 1", g)
}
var got action
check(json.Unmarshal(entries[0].Action, &got))
if got.Issue.Number != 1 || !slices.Equal(got.NewLabels, []string{"Bug"}) {
t.Errorf("got (%d, %v), want (1, [Bug])", got.Issue.Number, got.NewLabels)
}

// Second time, nothing should happen.
check(l.Run(ctx))
entries = slices.Collect(actions.ScanAfterDBTime(lg, db, entries[0].ModTime, nil))
if g := len(entries); g != 0 {
t.Fatalf("got %d actions, want 0", g)
}
}

0 comments on commit 83d6575

Please sign in to comment.