Skip to content

Commit

Permalink
internal/labels: sync labels
Browse files Browse the repository at this point in the history
Add a method that will reconcile the issue tracker labels
with the configured labels.

For #64.

Change-Id: I51492fcfdd0ced31257b0355a8570489d3402499
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/637155
Reviewed-by: Tatiana Bradley <[email protected]>
Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
jba committed Dec 18, 2024
1 parent b7220bb commit d1bd35d
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 12 deletions.
96 changes: 96 additions & 0 deletions internal/labels/labeler.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,102 @@ func (l *Labeler) RequireApproval() {
l.requireApproval = true
}

// Run runs a single round of labeling to GitHub.
// It scans all open issues that have been created since the last call to [Labeler.Run]
// using a Labeler with the same name (see [New]).
// TODO(jba): more doc
func (l *Labeler) Run(ctx context.Context) error {
l.slog.Info("labels.Labeler start", "name", l.name, "label", l.label, "latest", l.watcher.Latest())
defer func() {
l.slog.Info("labels.Labeler end", "name", l.name, "latest", l.watcher.Latest())
}()

// Ensure that labels in GH match our config.
for p := range l.projects {
if err := l.syncLabels(ctx, p); err != nil {
return err
}
}
// TODO(jba): finish implementation.
return nil
}

func (l *Labeler) syncLabels(ctx context.Context, project string) 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)
return syncLabels(ctx, l.slog, config.Categories, ghLabels{l.github, project})
}

// trackerLabels manipulates the set of labels on an issue tracker.
// TODO: remove dependence on GitHub.
type trackerLabels interface {
CreateLabel(ctx context.Context, lab github.Label) error
EditLabel(ctx context.Context, name string, changes github.LabelChanges) error
ListLabels(ctx context.Context) ([]github.Label, error)
}

type ghLabels struct {
gh *github.Client
project string
}

func (g ghLabels) CreateLabel(ctx context.Context, lab github.Label) error {
return g.gh.CreateLabel(ctx, g.project, lab)
}

func (g ghLabels) EditLabel(ctx context.Context, name string, changes github.LabelChanges) error {
return g.gh.EditLabel(ctx, g.project, name, changes)
}

func (g ghLabels) ListLabels(ctx context.Context) ([]github.Label, error) {
return g.gh.ListLabels(ctx, g.project)
}

// labelColor is the color of labels created by syncLabels.
const labelColor = "4d0070"

// syncLabels attempts to reconcile the labels in cats with the labels on the issue tracker,
// modifying the issue tracker's labels to match.
// If a label in cats is not on the issue tracker, it is created.
// Otherwise, if the label description on the issue tracker is empty, it is set to the description in the Category.
// 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 syncLabels(ctx context.Context, lg *slog.Logger, cats []Category, tl trackerLabels) error {
tlabList, err := tl.ListLabels(ctx)
if err != nil {
return err
}
tlabs := map[string]github.Label{}
for _, lab := range tlabList {
tlabs[lab.Name] = lab
}

for _, cat := range cats {
lab, ok := tlabs[cat.Label]
if !ok {
lg.Info("creating label", "label", lab.Name)
if err := tl.CreateLabel(ctx, github.Label{
Name: cat.Label,
Description: cat.Description,
Color: labelColor,
}); err != nil {
return err
}
} else if lab.Description == "" {
lg.Info("setting empty label description", "label", lab.Name)
if err := tl.EditLabel(ctx, lab.Name, github.LabelChanges{Description: cat.Description}); err != nil {
return err
}
} else if lab.Description != cat.Description {
lg.Warn("descriptions disagree", "label", lab.Name)
}
}
return nil
}

type actioner struct {
l *Labeler
}
Expand Down
77 changes: 77 additions & 0 deletions internal/labels/labeler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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 labels

import (
"context"
"errors"
"maps"
"slices"
"testing"

"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/testutil"
)

func TestSyncLabels(t *testing.T) {
m := map[string]github.Label{
"A": {Name: "A", Description: "a", Color: "a"},
"B": {Name: "B", Description: "", Color: "b"},
"C": {Name: "C", Description: "c", Color: "c"},
"D": {Name: "D", Description: "d", Color: "d"},
}
cats := []Category{
{Label: "A", Description: "a"}, // same as tracker
{Label: "B", Description: "b"}, // set empty tracker description
{Label: "C", Description: "other"}, // different descriptions
// D in tracker but not in cats
{Label: "E", Description: "e"}, // create
}
tl := &testTrackerLabels{m}

if err := syncLabels(context.Background(), testutil.Slogger(t), cats, tl); err != nil {
t.Fatal(err)
}

want := map[string]github.Label{
"A": {Name: "A", Description: "a", Color: "a"},
"B": {Name: "B", Description: "b", Color: "b"}, // added B description
"C": {Name: "C", Description: "c", Color: "c"},
"D": {Name: "D", Description: "d", Color: "d"},
"E": {Name: "E", Description: "e", Color: labelColor}, // added E
}

if got := tl.m; !maps.Equal(got, want) {
t.Errorf("\ngot %v\nwant %v", got, want)
}
}

type testTrackerLabels struct {
m map[string]github.Label
}

func (t *testTrackerLabels) CreateLabel(ctx context.Context, lab github.Label) error {
if _, ok := t.m[lab.Name]; ok {
return errors.New("label exists")
}
t.m[lab.Name] = lab
return nil
}

func (t *testTrackerLabels) ListLabels(ctx context.Context) ([]github.Label, error) {
return slices.Collect(maps.Values(t.m)), nil
}

func (t *testTrackerLabels) EditLabel(ctx context.Context, name string, changes github.LabelChanges) error {
if changes.NewName != "" || changes.Color != "" {
return errors.New("unsupported edit")
}
if lab, ok := t.m[name]; ok {
lab.Description = changes.Description
t.m[name] = lab
return nil
}
return errors.New("not found")
}
24 changes: 12 additions & 12 deletions internal/labels/static/categories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
# with the description
categories:
- name: bug
label: BUGLABEL
label: Bug
description: "Issues describing a bug in the Go implementation."

- name: languageProposal
label: LANGPROPLABEL
label: LanguageProposal
description: Issues describing a requested change to the Go language specification.
extra: |
This should be used for any notable change or addition to the language.
Expand All @@ -20,7 +20,7 @@ categories:
changes in existing functionality.
- name: libraryProposal
label: LIBPROPLABEL
label: LibraryProposal
description: Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool
extra: |
This should be used for any notable change or addition to the libraries.
Expand All @@ -33,7 +33,7 @@ categories:
and probably a GODEBUG setting.
- name: toolProposal
label: TOOLPROPLABEL
label: ToolProposal
description: Issues describing a requested change to a Go tool or command-line program.
extra: |
This should be used for any notable change or addition to the tools.
Expand All @@ -47,11 +47,11 @@ categories:
This does NOT includ changes to tools in x repos, like gopls, or third-party tools.
- name: implementation
label: IMPLABEL
label: Implementation
description: Issues describing a semantics-preserving change to the Go implementation.

- name: accessRequest
label: accessRequestLABEL
label: AccessRequest
description: Issues requesting builder or gomote access.

- name: pkgsiteRemovalRequest
Expand All @@ -60,19 +60,19 @@ categories:
# We don't label issues posted by gopherbot, so this label is probably unnecessary.

- name: automation
label: automationLABEL
label: Automation
description: Issues created by gopherbot or watchflakes automation.

- name: backport
label: backportLABEL
label: Backport
description: Issues created for requesting a backport of a change to a previous Go version.

- name: builders
label: Builders
description: x/build issues (builders, bots, dashboards)

- name: question
label: questionLABEL
label: Question
description: Issues that are questions about using Go.
# It may be too challenging for the LLM to decide is something is WAI. Consider removing this.

Expand All @@ -81,7 +81,7 @@ categories:
description: Issues describing something that is working as it is supposed to.

- name: featureRequest
label: FeatureRequestLABEL
label: FeatureRequest
description: Issues asking for a new feature that does not need a proposal.

- name: documentation
Expand All @@ -90,10 +90,10 @@ categories:
# The LLM never seems to pick invalid.

- name: invalid
label: invalidLABEL
label: Invalid
description: Issues that are empty, incomplete, or spam.
# The LLM never seems to pick other.

- name: other
label: otherLABEL
label: Other
description: None of the above.

0 comments on commit d1bd35d

Please sign in to comment.