From 25a5c479cd2d1e072fecd23981db8dd4f7b54671 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Thu, 19 Dec 2024 12:08:52 -0500 Subject: [PATCH] internal/labels: generalize to multiple projects IssueCategory now supports multiple projects. Each project's categories are stored in a separate file. For golang/oscar#64. Change-Id: If941db708cc13ce229730d5f93a9d2548aab7ba7 Reviewed-on: https://go-review.googlesource.com/c/oscar/+/637856 Reviewed-by: Hyang-Ah Hana Kim LUCI-TryBot-Result: Go LUCI --- internal/devtools/cmd/labeleval/main.go | 1 + internal/gaby/labels.go | 2 +- internal/labels/labeler.go | 13 +++-- internal/labels/labels.go | 47 +++++++++++++++---- internal/labels/labels_test.go | 2 +- .../{categories.yaml => go-categories.yaml} | 1 + 6 files changed, 48 insertions(+), 18 deletions(-) rename internal/labels/static/{categories.yaml => go-categories.yaml} (99%) diff --git a/internal/devtools/cmd/labeleval/main.go b/internal/devtools/cmd/labeleval/main.go index 7599b55..51089ad 100644 --- a/internal/devtools/cmd/labeleval/main.go +++ b/internal/devtools/cmd/labeleval/main.go @@ -107,6 +107,7 @@ func run(ctx context.Context, categoryconfigFile, issueConfigFile string) error } var categoryConfig struct { + Project string Categories []labels.Category } diff --git a/internal/gaby/labels.go b/internal/gaby/labels.go index c5a4654..7c31362 100644 --- a/internal/gaby/labels.go +++ b/internal/gaby/labels.go @@ -100,7 +100,7 @@ func (g *Gaby) populateLabelsPage(r *http.Request) *labelsPage { } else if isBot(i.User.Login) { lr.Problem = "skipping: author is a bot" } else { - cat, exp, err := labels.IssueCategory(r.Context(), g.llm, i) + cat, exp, err := labels.IssueCategory(r.Context(), g.llm, project, i) if err != nil { p.Error = err return p diff --git a/internal/labels/labeler.go b/internal/labels/labeler.go index d401927..6e45e35 100644 --- a/internal/labels/labeler.go +++ b/internal/labels/labeler.go @@ -7,7 +7,6 @@ package labels import ( "context" "encoding/json" - "errors" "fmt" "log/slog" "maps" @@ -126,7 +125,11 @@ func (l *Labeler) Run(ctx context.Context) error { // Ensure that labels in GH match our config. for p := range l.projects { - if err := l.syncLabels(ctx, p, config.Categories); err != nil { + cats, ok := config.Categories[p] + if !ok { + return fmt.Errorf("Labeler.Run: unknown project %q", p) + } + if err := l.syncLabels(ctx, p, cats); err != nil { return err } } @@ -183,7 +186,7 @@ func (l *Labeler) logLabelIssue(ctx context.Context, e *github.Event) (advance b issue := e.Typed.(*github.Issue) l.slog.Debug("labels.Labeler consider", "url", issue.HTMLURL) - cat, explanation, err := IssueCategory(ctx, l.cgen, issue) + cat, explanation, err := IssueCategory(ctx, l.cgen, e.Project, issue) if err != nil { return false, fmt.Errorf("IssueCategory(%s): %w", issue.HTMLURL, err) } @@ -230,10 +233,6 @@ func (l *Labeler) skip(e *github.Event) (bool, string) { // Otherwise, if the descriptions don't agree, a warning is logged and nothing is done on the issue tracker. // This function makes no other changes. In particular, it never deletes labels. func (l *Labeler) syncLabels(ctx context.Context, project string, cats []Category) error { - // TODO(jba): generalize to other projects. - if project != "golang/go" { - return errors.New("labeling only supported for golang/go") - } l.slog.Info("syncing labels", "name", l.name, "project", project) tlabList, err := l.github.ListLabels(ctx, project) if err != nil { diff --git a/internal/labels/labels.go b/internal/labels/labels.go index cff626e..9491c06 100644 --- a/internal/labels/labels.go +++ b/internal/labels/labels.go @@ -3,6 +3,9 @@ // license that can be found in the LICENSE file. // Package labels classifies issues. +// +// The categories it uses are stored in static/*-categories.yaml +// files, one file per project. package labels import ( @@ -13,6 +16,7 @@ import ( "errors" "fmt" "html/template" + "io/fs" "iter" "log" "os" @@ -36,8 +40,12 @@ type Category struct { // IssueCategory returns the category chosen by the LLM for the issue, along with an explanation // of why it was chosen. It uses the built-in list of categories. -func IssueCategory(ctx context.Context, cgen llm.ContentGenerator, iss *github.Issue) (_ Category, explanation string, err error) { - return IssueCategoryFromList(ctx, cgen, iss, config.Categories) +func IssueCategory(ctx context.Context, cgen llm.ContentGenerator, project string, iss *github.Issue) (_ Category, explanation string, err error) { + cats, ok := config.Categories[project] + if !ok { + return Category{}, "", fmt.Errorf("IssueCategory: unknown project %q", project) + } + return IssueCategoryFromList(ctx, cgen, iss, cats) } // IssueCategoryFromList is like [IssueCategory], but uses the given list of Categories. @@ -226,22 +234,43 @@ type bodyArgs struct { } var config struct { - Categories []Category + // Key is project, e.g. "golang/go". + Categories map[string][]Category } //go:embed static/* var staticFS embed.FS +// Read all category files into config. func init() { - f, err := staticFS.Open("static/categories.yaml") + catFiles, err := fs.Glob(staticFS, "static/*-categories.yaml") if err != nil { log.Fatal(err) } - defer f.Close() + config.Categories = map[string][]Category{} + for _, file := range catFiles { + f, err := staticFS.Open(file) + if err != nil { + log.Fatalf("%s: %v", file, err) + } - dec := yaml.NewDecoder(f) - dec.KnownFields(true) - if err := dec.Decode(&config); err != nil { - log.Fatal(err) + var contents struct { + Project string + Categories []Category + } + + dec := yaml.NewDecoder(f) + dec.KnownFields(true) + if err := dec.Decode(&contents); err != nil { + log.Fatalf("%s: %v", file, err) + } + if contents.Project == "" { + log.Fatalf("%s: empty or missing project", file) + } + if _, ok := config.Categories[contents.Project]; ok { + log.Fatalf("%s: duplicate project %s", file, contents.Project) + } + config.Categories[contents.Project] = contents.Categories + f.Close() } } diff --git a/internal/labels/labels_test.go b/internal/labels/labels_test.go index ab57ebd..8b5ca86 100644 --- a/internal/labels/labels_test.go +++ b/internal/labels/labels_test.go @@ -21,7 +21,7 @@ func TestIssueLabels(t *testing.T) { Body: "body", } - cat, exp, err := IssueCategory(ctx, llm, iss) + cat, exp, err := IssueCategory(ctx, llm, "golang/go", iss) if err != nil { t.Fatal(err) } diff --git a/internal/labels/static/categories.yaml b/internal/labels/static/go-categories.yaml similarity index 99% rename from internal/labels/static/categories.yaml rename to internal/labels/static/go-categories.yaml index a2b3155..5a381dc 100644 --- a/internal/labels/static/categories.yaml +++ b/internal/labels/static/go-categories.yaml @@ -5,6 +5,7 @@ # description: the label description on the issue tracker # extra: additional information about the label, fed to the LLM along # with the description +project: golang/go categories: - name: bug label: Bug