mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Added support for automatic labeling of PRs.
This commit is contained in:
committed by
Russell Jones
parent
bcba332c4d
commit
a3d85ab5fe
@@ -44,4 +44,4 @@ jobs:
|
||||
uses: actions/setup-go@v2
|
||||
# Run "dismiss" subcommand on bot.
|
||||
- name: Dismiss
|
||||
run: cd .github/workflows/robot && go run main.go -workflow=dismiss -token="${{ secrets.GITHUB_TOKEN }}" -reviewers="${{ secrets.reviewers }}"
|
||||
run: cd .github/workflows/robot && go run main.go -workflow=dismiss -token="${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# This workflow is run whenever a pull request is opened, re-opened, or taken
|
||||
# out of draft (ready for review).
|
||||
#
|
||||
# NOTE: pull_request_target behaves the same as pull_request except it grants a
|
||||
# read/write token to workflows running on a pull request from a fork. While
|
||||
# this may seem unsafe, the permissions for the token are limited below and
|
||||
# the permissions can not be changed without merging to master which is
|
||||
# protected by CODEOWNERS.
|
||||
name: Label
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
|
||||
# Limit the permissions on the GitHub token for this workflow to the subset
|
||||
# that is required. In this case, the label workflow only needs to be able
|
||||
# to update labels on the PR, so it needs write access to "pull-requests",
|
||||
# nothing else.
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: none
|
||||
checks: none
|
||||
contents: none
|
||||
deployments: none
|
||||
issues: none
|
||||
packages: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
|
||||
jobs:
|
||||
auto-label-pr:
|
||||
name: Label Pull Request
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout master branch of Teleport repository. This is to prevent an
|
||||
# attacker from submitting their own bot logic.
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
- name: Installing the latest version of Go.
|
||||
uses: actions/setup-go@v2
|
||||
# Run "label" subcommand on bot.
|
||||
- name: Labeling PR
|
||||
run: cd .github/workflows/robot && go run main.go -workflow=label -token="${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -109,6 +109,10 @@ func (f *fakeGithub) ListFiles(ctx context.Context, organization string, reposit
|
||||
return f.files, nil
|
||||
}
|
||||
|
||||
func (f *fakeGithub) AddLabels(ctx context.Context, organization string, repository string, number int, labels []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGithub) ListWorkflows(ctx context.Context, organization string, repository string) ([]github.Workflow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2021 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
)
|
||||
|
||||
// Label parses the content of the PR (branch name, files, etc) and sets
|
||||
// appropriate labels.
|
||||
func (b *Bot) Label(ctx context.Context) error {
|
||||
labels, err := b.labels(ctx)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
err = b.c.GitHub.AddLabels(ctx,
|
||||
b.c.Environment.Organization,
|
||||
b.c.Environment.Repository,
|
||||
b.c.Environment.Number,
|
||||
labels)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) labels(ctx context.Context) ([]string, error) {
|
||||
var labels []string
|
||||
|
||||
// The branch name is unsafe, but here we are simply adding a label.
|
||||
if strings.HasPrefix(b.c.Environment.UnsafeBranch, "branch/") {
|
||||
log.Println("Label: Found backport branch.")
|
||||
labels = append(labels, "backport")
|
||||
}
|
||||
|
||||
files, err := b.c.GitHub.ListFiles(ctx,
|
||||
b.c.Environment.Organization,
|
||||
b.c.Environment.Repository,
|
||||
b.c.Environment.Number)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// Special case: skip vendor until it is removed in #9545.
|
||||
if strings.HasPrefix(file, "vendor/") {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range prefixes {
|
||||
if strings.HasPrefix(file, k) {
|
||||
log.Printf("Label: Found prefix %v, attaching labels: %v.", k, v)
|
||||
labels = append(labels, v...)
|
||||
}
|
||||
}
|
||||
for k, v := range suffixes {
|
||||
if strings.HasSuffix(file, k) {
|
||||
log.Printf("Label: Found suffix %v, attaching labels: %v.", k, v)
|
||||
labels = append(labels, v...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicate(labels), nil
|
||||
}
|
||||
|
||||
func deduplicate(s []string) []string {
|
||||
m := map[string]bool{}
|
||||
for _, v := range s {
|
||||
m[v] = true
|
||||
}
|
||||
|
||||
var out []string
|
||||
for k, _ := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
var prefixes map[string][]string = map[string][]string{
|
||||
"bpf/": []string{"bpf"},
|
||||
"docs/": []string{"documentation"},
|
||||
"rfd/": []string{"documentation", "rfd"},
|
||||
"examples/chart": []string{"helm"},
|
||||
"lib/bpf/": []string{"bpf"},
|
||||
"lib/kube": []string{"kubernetes"},
|
||||
"lib/srv/desktop": []string{"desktop-access"},
|
||||
"lib/srv/desktop/rdp": []string{"desktop-access", "rdp"},
|
||||
"lib/srv/app/": []string{"application-access"},
|
||||
"lib/srv/db": []string{"database-access"},
|
||||
"lib/web/desktop.go": []string{"desktop-access"},
|
||||
"tool/tctl/": []string{"tctl"},
|
||||
"tool/tsh/": []string{"tsh"},
|
||||
}
|
||||
|
||||
var suffixes map[string][]string = map[string][]string{
|
||||
".md": []string{"documentation"},
|
||||
".mdx": []string{"documentation"},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2021 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/teleport/.github/workflows/robot/internal/env"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestLabel checks that labels are correctly applied to a Pull Request.
|
||||
func TestLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
branch string
|
||||
files []string
|
||||
labels []string
|
||||
}{
|
||||
{
|
||||
desc: "code-only",
|
||||
branch: "foo",
|
||||
files: []string{
|
||||
"file.go",
|
||||
},
|
||||
labels: []string{},
|
||||
},
|
||||
{
|
||||
desc: "docs",
|
||||
branch: "foo",
|
||||
files: []string{
|
||||
"docs/docs.md",
|
||||
},
|
||||
labels: []string{"documentation"},
|
||||
},
|
||||
{
|
||||
desc: "helm",
|
||||
branch: "foo",
|
||||
files: []string{
|
||||
"examples/chart/index.html",
|
||||
},
|
||||
labels: []string{"helm"},
|
||||
},
|
||||
{
|
||||
desc: "docs-and-helm",
|
||||
branch: "foo",
|
||||
files: []string{
|
||||
"docs/docs.md",
|
||||
"examples/chart/index.html",
|
||||
},
|
||||
labels: []string{"documentation", "helm"},
|
||||
},
|
||||
{
|
||||
desc: "docs-and-backport",
|
||||
branch: "branch/foo",
|
||||
files: []string{
|
||||
"docs/docs.md",
|
||||
},
|
||||
labels: []string{"backport", "documentation"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
b := &Bot{
|
||||
c: &Config{
|
||||
Environment: &env.Environment{
|
||||
Organization: "foo",
|
||||
Repository: "bar",
|
||||
Number: 0,
|
||||
UnsafeBranch: test.branch,
|
||||
},
|
||||
GitHub: &fakeGithub{
|
||||
test.files,
|
||||
},
|
||||
},
|
||||
}
|
||||
labels, err := b.labels(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, labels, test.labels)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@ type Client interface {
|
||||
// ListFiles is used to list all the files within a PR.
|
||||
ListFiles(ctx context.Context, organization string, repository string, number int) ([]string, error)
|
||||
|
||||
// AddLabels will add labels to an Issue or Pull Request.
|
||||
AddLabels(ctx context.Context, organization string, repository string, number int, labels []string) error
|
||||
|
||||
// ListWorkflows lists all workflows within a repository.
|
||||
ListWorkflows(ctx context.Context, organization string, repository string) ([]Workflow, error)
|
||||
|
||||
@@ -215,6 +218,20 @@ func (c *client) ListFiles(ctx context.Context, organization string, repository
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// AddLabels will add labels to an Issue or Pull Request.
|
||||
func (c *client) AddLabels(ctx context.Context, organization string, repository string, number int, labels []string) error {
|
||||
_, _, err := c.client.Issues.AddLabelsToIssue(ctx,
|
||||
organization,
|
||||
repository,
|
||||
number,
|
||||
labels)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workflow contains information about a workflow.
|
||||
type Workflow struct {
|
||||
// ID of the workflow.
|
||||
|
||||
@@ -58,6 +58,8 @@ func main() {
|
||||
err = b.Check(ctx)
|
||||
case "dismiss":
|
||||
err = b.Dismiss(ctx)
|
||||
case "label":
|
||||
err = b.Label(ctx)
|
||||
default:
|
||||
err = trace.BadParameter("unknown workflow: %v", workflow)
|
||||
}
|
||||
@@ -82,8 +84,8 @@ func parseFlags() (string, string, string, error) {
|
||||
if *token == "" {
|
||||
return "", "", "", trace.BadParameter("token missing")
|
||||
}
|
||||
if *reviewers == "" {
|
||||
return "", "", "", trace.BadParameter("reviewers missing")
|
||||
if *reviewers == "" && (*workflow == "assign" || *workflow == "check") {
|
||||
return "", "", "", trace.BadParameter("reviewers required for assign and check")
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(*reviewers)
|
||||
|
||||
Reference in New Issue
Block a user