Development Workflow Automation (#8116)

This commit is contained in:
Jane Quintero 2021-10-25 14:29:38 -07:00 committed by GitHub
parent 17eb200b7a
commit 5a29168512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 3988 additions and 0 deletions

3
.github/CODEOWNERS vendored
View file

@ -7,3 +7,6 @@
# Frontend Engineering.
/lib/web/ @alex-kovoy @russjones @r0mant
/webassets/ @alex-kovoy @russjones @r0mant
# Repository Management
/.github/workflows/ @russjones @r0mant @zmb3

39
.github/workflows/assign.yaml vendored Normal file
View file

@ -0,0 +1,39 @@
# 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, we are limiting the permissions of the Github token below.
name: Assign
on:
pull_request_target:
types: [assigned, opened, reopened, ready_for_review]
# Limit the permissions on the GitHub token for this workflow to the subset
# that is required. In this case, the assign workflow only needs to be able
# to update the assigned reviewers, 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-request-review:
name: Auto Request Review
runs-on: ubuntu-latest
steps:
# Checkout master branch of Teleport repository. This is to prevent an
# attacker from submitting their own review assignment logic.
- name: Checkout master branch
uses: actions/checkout@master
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Run "assign-reviewers" subcommand on bot.
- name: Assigning reviewers
run: cd .github/workflows && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} --reviewers="${{ secrets.reviewers }}" assign-reviewers

42
.github/workflows/check.yaml vendored Normal file
View file

@ -0,0 +1,42 @@
# Workflow will trigger on all pull request (except draft), pull request review, and commit push to a
# pull request (synchronize) event types
#
# 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, we are limiting the permissions of the Github token below.
name: Check
on:
pull_request_review:
type: [submitted, edited, dismissed]
pull_request_target:
types: [assigned, opened, reopened, ready_for_review, synchronize]
# Limit the permissions on the GitHub token for this workflow to the subset
# that is required. In this case, the check workflow needs to invalidate
# reviews and delete workflow runs, so it needs write access to "actions" and
# "pull-requests", nothing else.
permissions:
actions: write
pull-requests: write
checks: none
contents: none
deployments: none
issues: none
packages: none
repository-projects: none
security-events: none
statuses: none
jobs:
check-reviews:
name: Checking reviewers
runs-on: ubuntu-latest
steps:
# Checkout master branch of Teleport repository. This is to prevent an
# attacker from submitting their own review assignment logic.
- name: Checkout master branch
uses: actions/checkout@master
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Run "check-reviewers" subcommand on bot.
- name: Checking reviewers
run: cd .github/workflows && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} --reviewers="${{ secrets.reviewers }}" check-reviewers

19
.github/workflows/ci/Makefile vendored Normal file
View file

@ -0,0 +1,19 @@
# Runs subcommand assign-reviewers
.PHONY: assign
assign:
go run cmd/bot.go assign-reviewers
.PHONY: check
# Runs subcommand check-reviewers
check:
go run cmd/bot.go check-reviewers
.PHONY: test
# Runs all Go tests and shows the coverage
test:
go test ./... -cover
.PHONY: lint
lint:
golangci-lint run

131
.github/workflows/ci/README.md vendored Normal file
View file

@ -0,0 +1,131 @@
# Development Workflow Automation Bot
## Purpose
This bot automates the workflow of the pull request process. It does this by automatically assigning reviewers to a pull request and checking pull request reviews for approvals.
## Prerequisites
### Set Up Secrets
[Documentation for setting up secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository)
#### Reviewers
Reviewers is a json object encoded as a string with authors mapped to their required reviewers. This map MUST contain a wildcard (*) that maps to default reviewers.
Example:
```json
{
"author1": ["reviewer0", "reviewer1"],
"author2": ["reviewer2", "reviewer3", "reviewer4"],
"*": ["defaultreviewer0", "defaultreviewer1"]
}
```
### Set up workflow configuration files
This bot supports the following events:
- Pull Request
- `assigned`
- `opened`
- `reopened`
- `ready_for_review`
- `synchronize`
- Pull Request Review
- `submitted`
- `edited`
- `dismissed`
The following subcommands are used in the workflow files:
| Subcommand | Description |
| ----------- | ----------- |
| `assign-reviewers` | Assigns reviewers to a pull request. |
| `check-reviewers` | Checks pull request for required reviewers. |
| `dismiss-runs` | Dismisses stale workflow runs on an interval configurable in the workflow configuration file.|
Create the following workflow files in the master branch:
_Assigning Reviewers_
```yaml
name: Assign
on:
pull_request_target:
types: [assigned, opened, reopened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-request-review:
name: Auto Request Review
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v2
with:
ref: <name of branch where bot code will persist>
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Running "assign-reviewers" subcommand on bot.
- name: Assigning reviewers
run: cd .github/workflows/ci && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} --reviewers=${{ secrets.reviewers }} assign-reviewers
```
_Checking reviews_
```yaml
name: Check
on:
pull_request_review:
type: [submitted, edited, dismissed]
pull_request_target:
types: [assigned, opened, reopened, ready_for_review, synchronize]
permissions: read-all
jobs:
check-reviews:
name: Checking reviewers
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v2
with:
ref: <name of branch where bot code will persist>
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Running "check-reviewers" subcommand on bot.
- name: Checking reviewers
run: cd .github/workflows/ci && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} --reviewers=${{ secrets.reviewers }} check-reviewers
```
_Dimiss stale workflow runs_
```yaml
# Workflow will run every 30 minutes and dismiss stale runs on all open pull requests.
name: Dismiss Stale Workflows Runs
on:
schedule:
# Runs every 30 minutes. You can configure this to any interval.
- cron: '0,30 * * * *'
jobs:
dismiss-stale-runs:
name: Dismiss Stale Workflow Runs
runs-on: ubuntu-latest
steps:
- name: Checkout master branch
uses: actions/checkout@master
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Run "dismiss-runs" subcommand on bot.
- name: Dismiss
run: cd .github/workflows/ci && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} dismiss-runs
```

127
.github/workflows/ci/cmd/main.go vendored Normal file
View file

@ -0,0 +1,127 @@
/*
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 main
import (
"context"
"flag"
"log"
"os"
"time"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/bot"
bots "github.com/gravitational/teleport/.github/workflows/ci/pkg/bot"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/environment"
"github.com/gravitational/trace"
"github.com/google/go-github/v37/github"
"golang.org/x/oauth2"
)
const (
usage = "The following subcommands are supported:\n" +
"\tassign-reviewers \n\t assigns reviewers to a pull request.\n" +
"\tcheck-reviewers \n\t checks pull request for required reviewers.\n" +
"\tdismiss-runs \n\t dismisses stale workflow runs for external contributors.\n"
reviewersHelp = `reviewers is a string representing a json object that maps authors to
required reviewers for that author. example: "{\"author1\": [\"reviewer0\", \"reviewer1\"], \"author2\":
[\"reviewer2\", \"reviewer3\"],\"*\": [\"default-reviewer0\", \"default-reviewer1\"]}"`
workflowRunTimeout = time.Minute
)
func main() {
var token = flag.String("token", "", "token is the Github authentication token.")
var reviewers = flag.String("reviewers", "", reviewersHelp)
flag.Parse()
if len(os.Args) < 2 {
log.Fatalf("Subcommand required. %s\n", usage)
}
subcommand := os.Args[len(os.Args)-1]
// Cancel run if it takes longer than `workflowRunTimeout`.
// Note: To re-run a job go to the Actions tab in the Github repo,
// go to the run that failed, and click the `Re-run all jobs` button
// in the top right corner.
ctx, cancel := context.WithTimeout(context.Background(), workflowRunTimeout)
defer cancel()
client := makeGithubClient(ctx, *token)
switch subcommand {
case ci.AssignSubcommand:
log.Println("Assigning reviewers.")
bot, err := constructBot(ctx, client, *reviewers)
if err != nil {
log.Fatal(err)
}
err = bot.Assign(ctx)
if err != nil {
log.Fatal(err)
}
log.Print("Assign completed.")
case ci.CheckSubcommand:
log.Println("Checking reviewers.")
bot, err := constructBot(ctx, client, *reviewers)
if err != nil {
log.Fatal(err)
}
err = bot.Check(ctx)
if err != nil {
log.Fatal(err)
}
log.Print("Check completed.")
case ci.Dismiss:
log.Println("Dismissing stale runs.")
// Constructing Bot without PullRequestEnvironment.
// Dismiss runs does not need PullRequestEnvironment because PullRequestEnvironment is only
// is used for pull request or PR adjacent (PR reviews, pushes to PRs, PR opening, reopening, etc.) events.
bot, err := bot.New(bots.Config{GithubClient: client})
if err != nil {
log.Fatal(err)
}
err = bot.DimissStaleWorkflowRuns(ctx)
if err != nil {
log.Fatal(err)
}
log.Println("Stale workflow run removal completed.")
default:
log.Fatalf("Unknown subcommand: %v.\n%s", subcommand, usage)
}
}
func constructBot(ctx context.Context, clt *github.Client, reviewers string) (*bots.Bot, error) {
env, err := environment.New(environment.Config{Client: clt,
Reviewers: reviewers,
Context: ctx,
})
if err != nil {
return nil, trace.Wrap(err)
}
bot, err := bots.New(bots.Config{Environment: env, GithubClient: clt})
if err != nil {
return nil, trace.Wrap(err)
}
return bot, nil
}
func makeGithubClient(ctx context.Context, token string) *github.Client {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(ctx, ts)
return github.NewClient(tc)
}

82
.github/workflows/ci/constants.go vendored Normal file
View file

@ -0,0 +1,82 @@
/*
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 ci
const (
// AssignSubcommand is the subcommand to assign reviewers
AssignSubcommand = "assign-reviewers"
// CheckSubcommand is the subcommand to check reviewers
CheckSubcommand = "check-reviewers"
// Dismiss is the subcommand to dismiss runs
Dismiss = "dismiss-runs"
// Open is a pull request state
Open = "open"
// GithubRepository is the environment variable
// that contains the repo owner and name
GithubRepository = "GITHUB_REPOSITORY"
// GithubEventPath is the env variable that
// contains the path to the event payload
GithubEventPath = "GITHUB_EVENT_PATH"
// GithubCommit is a string that is contained in the payload
// of a commit verified by GitHub.
// Used to verify commit was made by GH.
GithubCommit = "committer GitHub <noreply@github.com>"
// Approved is a pull request review status.
Approved = "APPROVED"
// Token is the env variable name that stores the Github authentication token
Token = "GITHUB_TOKEN"
// Completed is a workflow run status.
Completed = "completed"
// CheckWorkflow is the name of a workflow.
CheckWorkflow = "Check"
// Synchronize is an event type that is triggered when a commit is pushed to an
// open pull request.
Synchronize = "synchronize"
// Assigned is an event type that is triggered when a user is
// assigned to a pull request.
Assigned = "assigned"
// Opened is an event type that is triggered when a pull request is opened.
Opened = "opened"
// Reopened is an event type event that is triggered when a pull request
// is reopened.
Reopened = "reopened"
// Ready is an event type that is triggered when a pull request gets
// pulled out of a draft state.
Ready = "ready_for_review"
// Submitted is an event type that is triggered when a pull request review is submitted.
Submitted = "submitted"
// Created is an event type that is triggered when a pull request review is created.
Created = "created"
// AnyAuthor is the symbol used to get reviewers for external contributors/contrtibutors who
// do not have designated reviewers.
AnyAuthor = "*"
)

20
.github/workflows/ci/go.mod vendored Normal file
View file

@ -0,0 +1,20 @@
module github.com/gravitational/teleport/.github/workflows/ci
go 1.16
require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v37 v37.0.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/gravitational/trace v1.1.15
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20211013153659-ee2e9a082323 // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)

414
.github/workflows/ci/go.sum vendored Normal file
View file

@ -0,0 +1,414 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM=
github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gravitational/trace v1.1.15 h1:dfaFcARt110nCX6RSvrcRUbvRawEYAasXyCqnhXo0Xg=
github.com/gravitational/trace v1.1.15/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211013153659-ee2e9a082323 h1:kQRX9gVgKjPSdxDETrdc7WU9k8BxpyEYNmA+o1cxwt8=
golang.org/x/net v0.0.0-20211013153659-ee2e9a082323/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

40
.github/workflows/ci/pkg/bot/assign.go vendored Normal file
View file

@ -0,0 +1,40 @@
/*
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"
"github.com/gravitational/trace"
"github.com/google/go-github/v37/github"
)
// Assign assigns reviewers to the pull request in the
// current context.
func (a *Bot) Assign(ctx context.Context) error {
pullReq := a.Environment.Metadata
// Getting reviewers for author of pull request
r := a.Environment.GetReviewersForAuthor(pullReq.Author)
client := a.Environment.Client
// Assigning reviewers to pull request
_, _, err := client.PullRequests.RequestReviewers(ctx,
pullReq.RepoOwner,
pullReq.RepoName, pullReq.Number,
github.ReviewersRequest{Reviewers: r})
if err != nil {
return trace.Wrap(err)
}
return nil
}

124
.github/workflows/ci/pkg/bot/bot.go vendored Normal file
View file

@ -0,0 +1,124 @@
/*
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 (
"os"
"regexp"
"strings"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/environment"
"github.com/gravitational/trace"
"github.com/google/go-github/v37/github"
)
// Config is used to configure Bot
type Config struct {
Environment *environment.PullRequestEnvironment
GithubClient *github.Client
}
// Bot assigns reviewers and checks assigned reviewers for a pull request
type Bot struct {
Environment *environment.PullRequestEnvironment
GithubClient GithubClient
}
// GithubClient is a wrapper around the Github client
// to be used on methods that require the client, but don't
// don't need the full functionality of Bot with
// Environment.
type GithubClient struct {
Client *github.Client
}
// New returns a new instance of Bot
func New(c Config) (*Bot, error) {
err := c.CheckAndSetDefaults()
if err != nil {
return nil, trace.Wrap(err)
}
return &Bot{
Environment: c.Environment,
GithubClient: GithubClient{
Client: c.GithubClient,
},
}, nil
}
// CheckAndSetDefaults verifies configuration and sets defaults
func (c *Config) CheckAndSetDefaults() error {
if c.GithubClient == nil {
return trace.BadParameter("missing parameter GithubClient")
}
return nil
}
func getRepositoryMetadata() (repositoryOwner string, repositoryName string, err error) {
repository := os.Getenv(ci.GithubRepository)
if repository == "" {
return "", "", trace.BadParameter("environment variable GITHUB_REPOSITORY is not set")
}
metadata := strings.Split(repository, "/")
if len(metadata) != 2 {
return "", "", trace.BadParameter("environment variable GITHUB_REPOSITORY is not in the correct format,\n the valid format is '<repo owner>/<repo name>'")
}
return metadata[0], metadata[1], nil
}
// validatePullRequestFields checks that pull request fields needed for
// dismissing workflow runs are not nil.
func validatePullRequestFields(pr *github.PullRequest) error {
switch {
case pr.Base == nil:
return trace.BadParameter("missing base branch")
case pr.Base.User == nil:
return trace.BadParameter("missing base branch user")
case pr.Base.User.Login == nil:
return trace.BadParameter("missing repository owner")
case pr.Base.Repo == nil:
return trace.BadParameter("missing base repository")
case pr.Base.Repo.Name == nil:
return trace.BadParameter("missing repository name")
case pr.Head == nil:
return trace.BadParameter("missing head branch")
case pr.Head.Ref == nil:
return trace.BadParameter("missing branch name")
}
if err := validateField(*pr.Base.User.Login); err != nil {
return trace.Errorf("user login err: %v", err)
}
if err := validateField(*pr.Base.Repo.Name); err != nil {
return trace.Errorf("repository name err: %v", err)
}
if err := validateField(*pr.Head.Ref); err != nil {
return trace.Errorf("branch name err: %v", err)
}
return nil
}
// reg is used for validating various fields on Github types.
// Only allow strings that contain alphanumeric characters,
// underscores, and dashes for fields.
var reg = regexp.MustCompile(`^[\da-zA-Z-_/]+$`)
func validateField(field string) error {
found := reg.MatchString(field)
if !found {
return trace.BadParameter("invalid field, %s contains illegal characters or is empty", field)
}
return nil
}

133
.github/workflows/ci/pkg/bot/bot_test.go vendored Normal file
View file

@ -0,0 +1,133 @@
/*
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 (
"testing"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/environment"
"github.com/google/go-github/v37/github"
"github.com/stretchr/testify/require"
)
func TestNewBot(t *testing.T) {
clt := github.NewClient(nil)
tests := []struct {
cfg Config
checkErr require.ErrorAssertionFunc
expected *Bot
}{
{
cfg: Config{Environment: &environment.PullRequestEnvironment{}, GithubClient: clt},
checkErr: require.NoError,
},
{
cfg: Config{},
checkErr: require.Error,
},
}
for _, test := range tests {
_, err := New(test.cfg)
test.checkErr(t, err)
}
}
func TestValidatePullRequestFields(t *testing.T) {
testString := "testString"
invalidTestString := "&test"
tests := []struct {
pull *github.PullRequest
checkErr require.ErrorAssertionFunc
desc string
}{
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{Login: &testString}, Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{},
},
checkErr: require.Error,
desc: "missing Head.Ref",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{Login: &testString}, Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.NoError,
desc: "valid pull request",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{}, Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.Error,
desc: "missing Base.User.Login",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{}, Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.Error,
desc: "missing Base.Repo.Name",
},
{
pull: &github.PullRequest{
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.Error,
desc: "missing Base",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.Error,
desc: "missing Base.User",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{}},
Head: &github.PullRequestBranch{Ref: &testString},
},
checkErr: require.Error,
desc: "missing Base.Repo",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{}, Repo: &github.Repository{Name: &testString}},
},
checkErr: require.Error,
desc: "missing Head",
},
{
pull: &github.PullRequest{
Base: &github.PullRequestBranch{User: &github.User{Login: &testString}, Repo: &github.Repository{Name: &testString}},
Head: &github.PullRequestBranch{Ref: &invalidTestString},
},
checkErr: require.Error,
desc: "invalid pull request branch name, contains illegal character",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
err := validatePullRequestFields(test.pull)
test.checkErr(t, err)
})
}
}

353
.github/workflows/ci/pkg/bot/check.go vendored Normal file
View file

@ -0,0 +1,353 @@
/*
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"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/environment"
"github.com/google/go-github/v37/github"
"github.com/gravitational/trace"
)
// Check checks if all the reviewers have approved the pull request in the current context.
func (c *Bot) Check(ctx context.Context) error {
pr := c.Environment.Metadata
if c.Environment.IsInternal(pr.Author) {
return c.checkInternal(ctx)
}
return c.checkExternal(ctx)
}
// checkInternal is called to check if a PR reviewed and approved by the
// required reviewers for internal contributors. Unlike approvals for
// external contributors, approvals from internal team members will not be
// invalidated when new changes are pushed to the PR.
func (c *Bot) checkInternal(ctx context.Context) error {
pr := c.Environment.Metadata
// Remove any stale workflow runs. As only the current workflow run should
// be shown because it is the workflow that reflects the correct state of the pull request.
//
// Note: This is run for all workflow runs triggered by an event from an internal contributor,
// but has to be run again in cron workflow because workflows triggered by external contributors do not
// grant the Github actions token the correct permissions to dismiss workflow runs.
err := c.dismissStaleWorkflowRuns(ctx, pr.RepoOwner, pr.RepoName, pr.BranchName)
if err != nil {
return trace.Wrap(err)
}
mostRecentReviews, err := c.getMostRecentReviews(ctx)
if err != nil {
return trace.Wrap(err)
}
log.Printf("Checking if %v has approvals from the required reviewers %+v", pr.Author, c.Environment.GetReviewersForAuthor(pr.Author))
err = hasRequiredApprovals(mostRecentReviews, c.Environment.GetReviewersForAuthor(pr.Author))
if err != nil {
return trace.Wrap(err)
}
return nil
}
// checkExternal is called to check if a PR reviewed and approved by the
// required reviewers for external contributors. Approvals for external
// contributors are dismissed when new changes are pushed to the PR. The only
// case in which reviews are not dismissed is if they are from GitHub and
// only update the PR.
func (c *Bot) checkExternal(ctx context.Context) error {
var obsoleteReviews map[string]review
var validReviews map[string]review
pr := c.Environment.Metadata
mostRecentReviews, err := c.getMostRecentReviews(ctx)
if err != nil {
return trace.Wrap(err)
}
validReviews, obsoleteReviews = splitReviews(pr.HeadSHA, mostRecentReviews)
// External contributions require tighter scrutiny than team
// contributions. As such reviews from previous pushes must
// not carry over to when new changes are added. Github does
// not do this automatically, so we must dismiss the reviews
// manually if there is a file change.
if err = c.hasFileChangeFromLastApprovedReview(ctx); err != nil {
err = c.invalidateApprovals(ctx, obsoleteReviews)
if err != nil {
return trace.Wrap(err)
}
} else {
// If there are no file changes between current commit and commit where all
// reviewers have approved, then all most recent reviews are valid.
validReviews = mostRecentReviews
}
log.Printf("Checking if %v has approvals from the required reviewers %+v", pr.Author, c.Environment.GetReviewersForAuthor(pr.Author))
err = hasRequiredApprovals(validReviews, c.Environment.GetReviewersForAuthor(pr.Author))
if err != nil {
return trace.Wrap(err)
}
return nil
}
// splitReviews splits a list of reviews into two lists: `valid` (those reviews that refer to
// the current PR head revision) and `obsolete` (those that do not)
func splitReviews(headSHA string, reviews map[string]review) (valid, obsolete map[string]review) {
valid = make(map[string]review)
obsolete = make(map[string]review)
for _, r := range reviews {
if r.commitID == headSHA {
valid[r.name] = r
} else {
obsolete[r.name] = r
}
}
return valid, obsolete
}
// hasRequiredApprovals determines if all required reviewers have approved.
func hasRequiredApprovals(mostRecentReviews map[string]review, required []string) error {
if len(mostRecentReviews) == 0 {
return trace.BadParameter("pull request has no approvals")
}
var waitingOnApprovalsFrom []string
for _, requiredReviewer := range required {
ok := hasApproved(requiredReviewer, mostRecentReviews)
if !ok {
waitingOnApprovalsFrom = append(waitingOnApprovalsFrom, requiredReviewer)
}
}
if len(waitingOnApprovalsFrom) > 0 {
return trace.BadParameter("required reviewers have not yet approved, waiting on approval(s) from %v", waitingOnApprovalsFrom)
}
return nil
}
func (c *Bot) getMostRecentReviews(ctx context.Context) (map[string]review, error) {
env := c.Environment
pr := c.Environment.Metadata
reviews, _, err := env.Client.PullRequests.ListReviews(ctx, pr.RepoOwner,
pr.RepoName,
pr.Number,
&github.ListOptions{})
if err != nil {
return nil, trace.Wrap(err)
}
currentReviewsSlice := []review{}
for _, rev := range reviews {
// Because PRs can be submitted by anyone, input here is attacker controlled
// and do strict validation of input.
err := validateReviewFields(rev)
if err != nil {
return nil, trace.Wrap(err)
}
currReview := review{
name: *rev.User.Login,
status: *rev.State,
commitID: *rev.CommitID,
id: *rev.ID,
submittedAt: rev.SubmittedAt,
}
currentReviewsSlice = append(currentReviewsSlice, currReview)
}
return mostRecent(currentReviewsSlice), nil
}
// review is a pull request review
type review struct {
name string
status string
commitID string
id int64
submittedAt *time.Time
}
// validateReviewFields validates required fields exist and passes them
// through a restrictive allow list (alphanumerics only). This is done to
// mitigate impact of attacker controlled input (the PR).
func validateReviewFields(review *github.PullRequestReview) error {
switch {
case review.ID == nil:
return trace.Errorf("review ID is nil. review: %+v", review)
case review.State == nil:
return trace.Errorf("review State is nil. review: %+v", review)
case review.CommitID == nil:
return trace.Errorf("review CommitID is nil. review: %+v", review)
case review.SubmittedAt == nil:
return trace.Errorf("review SubmittedAt is nil. review: %+v", review)
case review.User.Login == nil:
return trace.Errorf("reviewer User.Login is nil. review: %+v", review)
}
if err := validateField(*review.State); err != nil {
return trace.Errorf("review ID err: %v", err)
}
if err := validateField(*review.CommitID); err != nil {
return trace.Errorf("commit ID err: %v", err)
}
if err := validateField(*review.User.Login); err != nil {
return trace.Errorf("user login err: %v", err)
}
return nil
}
// mostRecent returns a list of the most recent review from each required reviewer.
func mostRecent(currentReviews []review) map[string]review {
mostRecentReviews := make(map[string]review)
for _, rev := range currentReviews {
val, ok := mostRecentReviews[rev.name]
if !ok {
mostRecentReviews[rev.name] = rev
} else {
setTime := val.submittedAt
currTime := rev.submittedAt
if currTime.After(*setTime) {
mostRecentReviews[rev.name] = rev
}
}
}
return mostRecentReviews
}
// hasApproved determines if the reviewer has submitted an approval
// for the pull request.
func hasApproved(reviewer string, reviews map[string]review) bool {
for _, rev := range reviews {
if rev.name == reviewer && rev.status == ci.Approved {
return true
}
}
return false
}
// dimissMessage returns the dimiss message when a review is dismissed
func dismissMessage(pr *environment.Metadata, required []string) string {
var sb strings.Builder
sb.WriteString("new commit pushed, please re-review ")
for _, reviewer := range required {
sb.WriteString(fmt.Sprintf("@%s", reviewer))
}
return sb.String()
}
// hasFileChangeFromLastApproved checks if there is a file change from the last commit all
// reviewers approved (if all reviewers approved at a commit) to the current HEAD.
func (c *Bot) hasFileChangeFromLastApprovedReview(ctx context.Context) error {
pr := c.Environment.Metadata
lastReviewCommitID, err := c.getLastApprovedReviewCommitID(ctx)
if err != nil {
return trace.Wrap(err)
}
mostRecent, err := c.getMostRecentReviews(ctx)
if err != nil {
return trace.Wrap(err)
}
// Make sure all approvals are at the same commit.
err = hasAllRequiredApprovalsAtCommit(lastReviewCommitID, mostRecent, c.Environment.GetReviewersForAuthor(pr.Author))
if err != nil {
return trace.Wrap(err)
}
// Check for any differences
err = c.hasFileDiff(ctx, lastReviewCommitID, pr.HeadSHA)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// getLastApprovedReviewCommitID gets the last review's commit ID (last review where a commit was approved).
func (c *Bot) getLastApprovedReviewCommitID(ctx context.Context) (string, error) {
pr := c.Environment.Metadata
clt := c.Environment.Client
reviews, _, err := clt.PullRequests.ListReviews(ctx, pr.RepoOwner, pr.RepoName, pr.Number, &github.ListOptions{})
if err != nil {
return "", trace.Wrap(err)
}
if len(reviews) == 0 {
return "", trace.NotFound("pull request has no reviews")
}
// Sort reviews from newest to oldest.
sort.Slice(reviews, func(i, j int) bool {
time1, time2 := reviews[i].SubmittedAt, reviews[j].SubmittedAt
return time2.Before(*time1)
})
var lastApprovedReview *github.PullRequestReview
// Find last approved review.
for _, review := range reviews {
if review.State == nil {
continue
}
if *review.State == ci.Approved {
lastApprovedReview = review
break
}
}
if lastApprovedReview == nil {
return "", trace.NotFound("no approved reviews found")
}
if lastApprovedReview.CommitID == nil {
return "", trace.NotFound("commit ID not found")
}
return *lastApprovedReview.CommitID, nil
}
// hasFileDiff compares two commits and checks if there are changes.
func (c *Bot) hasFileDiff(ctx context.Context, base, head string) error {
pr := c.Environment.Metadata
clt := c.Environment.Client
comparison, _, err := clt.Repositories.CompareCommits(ctx, pr.RepoOwner, pr.RepoName, base, head)
if err != nil {
return trace.Wrap(err)
}
if len(comparison.Files) != 0 {
return trace.Errorf("detected file change")
}
return nil
}
func hasAllRequiredApprovalsAtCommit(commitSHA string, reviews map[string]review, required []string) error {
for _, requiredReviewer := range required {
review, ok := reviews[requiredReviewer]
if !ok {
return trace.BadParameter("all reviewers have not approved")
}
if review.commitID != commitSHA {
return trace.Errorf("all reviewers have not approved at %s", commitSHA)
}
}
return nil
}
// invalidateApprovals dismisses all approved reviews on a pull request.
func (c *Bot) invalidateApprovals(ctx context.Context, reviews map[string]review) error {
pr := c.Environment.Metadata
msg := dismissMessage(pr, c.Environment.GetReviewersForAuthor(pr.Author))
for _, v := range reviews {
if pr.HeadSHA != v.commitID {
_, _, err := c.Environment.Client.PullRequests.DismissReview(ctx,
pr.RepoOwner,
pr.RepoName,
pr.Number,
v.id,
&github.PullRequestReviewDismissalRequest{Message: &msg},
)
if err != nil {
return trace.Wrap(err)
}
}
}
return nil
}

View file

@ -0,0 +1,174 @@
/*
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 (
"testing"
"github.com/gravitational/teleport/.github/workflows/ci/pkg/environment"
"github.com/stretchr/testify/require"
)
func TestApproved(t *testing.T) {
bot := &Bot{Environment: &environment.PullRequestEnvironment{}}
pull := &environment.Metadata{Author: "test"}
tests := []struct {
botInstance *Bot
pr *environment.Metadata
required []string
currentReviews map[string]review
desc string
checkErr require.ErrorAssertionFunc
}{
{
botInstance: bot,
pr: pull,
required: []string{"foo", "bar", "baz"},
currentReviews: map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "ba0d35", id: 3},
},
desc: "PR does not have all required approvals",
checkErr: require.Error,
},
{
botInstance: bot,
pr: pull,
required: []string{"foo", "bar", "baz"},
currentReviews: map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"bar": {name: "bar", status: "APPROVED", commitID: "12ga34", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "12ga34", id: 3},
},
desc: "PR has required approvals, commit shas match",
checkErr: require.NoError,
},
{
botInstance: bot,
pr: pull,
required: []string{"foo", "bar"},
currentReviews: map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "fe324c", id: 1},
},
desc: "PR does not have all required approvals",
checkErr: require.Error,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
err := hasRequiredApprovals(test.currentReviews, test.required)
test.checkErr(t, err)
})
}
}
func TestContainsApprovalReview(t *testing.T) {
reviews := map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "ba0d35", id: 1},
}
// Has a review but no approval
ok := hasApproved("bar", reviews)
require.Equal(t, false, ok)
// Does not have revire from reviewer
ok = hasApproved("car", reviews)
require.Equal(t, false, ok)
// Has review and is approved
ok = hasApproved("foo", reviews)
require.Equal(t, true, ok)
}
func TestSplitReviews(t *testing.T) {
reviews := map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "ba0d35", id: 3},
}
valid, obs := splitReviews("fe324c", reviews)
expectedValid := map[string]review{
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
}
expectedObsolete := map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"baz": {name: "baz", status: "APPROVED", commitID: "ba0d35", id: 3},
}
require.Equal(t, expectedValid, valid)
require.Equal(t, expectedObsolete, obs)
}
func TestHasRequiredApprovals(t *testing.T) {
reviews := map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "12ga34", id: 1},
"bar": {name: "bar", status: "APPROVED", commitID: "ba0d35", id: 3},
}
required := []string{"foo", "bar"}
err := hasRequiredApprovals(reviews, required)
require.NoError(t, err)
reviews = map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "fe324c", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "fe324c", id: 3},
}
required = []string{"foo", "reviewer"}
err = hasRequiredApprovals(reviews, required)
require.Error(t, err)
}
func TestHasRequiredApprovalsFromLastCommit(t *testing.T) {
tests := []struct {
commitSHA string
reviews map[string]review
required []string
desc string
checkErr require.ErrorAssertionFunc
}{
{
reviews: map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "fe324c", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "fe324c", id: 3},
},
commitSHA: "fe324c",
required: []string{"foo", "baz"},
checkErr: require.NoError,
desc: "has required approvals at commit",
},
{
reviews: map[string]review{
"foo": {name: "foo", status: "APPROVED", commitID: "fe324c", id: 1},
"bar": {name: "bar", status: "Commented", commitID: "fe324c", id: 2},
"baz": {name: "baz", status: "APPROVED", commitID: "fe324c", id: 3},
},
commitSHA: "65fab3",
required: []string{"foo", "baz"},
checkErr: require.Error,
desc: "has no approvals at commit, all required approvers reviewed",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
err := hasAllRequiredApprovalsAtCommit(test.commitSHA, test.reviews, test.required)
test.checkErr(t, err)
})
}
}

159
.github/workflows/ci/pkg/bot/dismiss.go vendored Normal file
View file

@ -0,0 +1,159 @@
/*
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"
"fmt"
"net/http"
"net/url"
"path"
"sort"
"github.com/google/go-github/v37/github"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
)
// DimissStaleWorkflowRuns dismisses stale workflow runs for external contributors.
// Dismissing stale workflows for external contributors is done on a cron job and checks the whole repo for
// stale runs on PRs.
func (c *Bot) DimissStaleWorkflowRuns(ctx context.Context) error {
clt := c.GithubClient.Client
// Get the repository name and owner, on the Github Actions runner the
// GITHUB_REPOSITORY environment variable is in the format of
// repo-owner/repo-name.
repoOwner, repoName, err := getRepositoryMetadata()
if err != nil {
return trace.Wrap(err)
}
pullReqs, _, err := clt.PullRequests.List(ctx, repoOwner, repoName, &github.PullRequestListOptions{State: ci.Open})
if err != nil {
return trace.Wrap(err)
}
for _, pull := range pullReqs {
err := validatePullRequestFields(pull)
if err != nil {
// We do not want to stop dismissing stale workflow runs for the remaining PRs if there
// is a validation error, skip this iteration. Keep stale runs on PRs that have invalid fields in the event the
// invalid fields were malicious input.
log.Error(err)
continue
}
err = c.dismissStaleWorkflowRuns(ctx, *pull.Base.User.Login, *pull.Base.Repo.Name, *pull.Head.Ref)
if err != nil {
// Log the error, keep trying to dimiss remaining stale runs.
log.Error(err)
}
}
return nil
}
// dismissStaleWorkflowRuns dismisses stale Check workflow runs.
// Stale workflow runs are workflow runs that were previously ran and are no longer valid
// due to a new event triggering thus a change in state. The workflow running in the current context is the source of truth for
// the state of checks.
func (c *Bot) dismissStaleWorkflowRuns(ctx context.Context, owner, repoName, branch string) error {
// Get the target workflow to know get runs triggered by the `Check` workflow.
// The `Check` workflow is being targeted because it is the only workflow
// that runs multiple times per PR.
workflow, err := c.getCheckWorkflow(ctx, owner, repoName)
if err != nil {
return trace.Wrap(err)
}
runs, err := c.getWorkflowRuns(ctx, owner, repoName, branch, *workflow.ID)
if err != nil {
return trace.Wrap(err)
}
err = c.deleteRuns(ctx, owner, repoName, runs)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// deleteRuns deletes all workflow runs except the most recent one because that is
// the run in the current context.
func (c *Bot) deleteRuns(ctx context.Context, owner, repoName string, runs []*github.WorkflowRun) error {
// Sorting runs by time from oldest to newest.
sort.Slice(runs, func(i, j int) bool {
time1, time2 := runs[i].CreatedAt, runs[j].CreatedAt
return time1.Time.Before(time2.Time)
})
// Deleting all runs except the most recent one.
for i := 0; i < len(runs)-1; i++ {
run := runs[i]
err := c.deleteRun(ctx, owner, repoName, *run.ID)
if err != nil {
return trace.Wrap(err)
}
}
return nil
}
func (c *Bot) getWorkflowRuns(ctx context.Context, owner, repoName, branchName string, workflowID int64) ([]*github.WorkflowRun, error) {
clt := c.GithubClient.Client
list, _, err := clt.Actions.ListWorkflowRunsByID(ctx, owner, repoName, workflowID, &github.ListWorkflowRunsOptions{Branch: branchName})
if err != nil {
return nil, trace.Wrap(err)
}
return list.WorkflowRuns, nil
}
// getCheckWorkflow gets the workflow named 'Check'.
func (c *Bot) getCheckWorkflow(ctx context.Context, owner, repoName string) (*github.Workflow, error) {
clt := c.GithubClient.Client
workflows, _, err := clt.Actions.ListWorkflows(ctx, owner, repoName, &github.ListOptions{})
if err != nil {
return nil, trace.Wrap(err)
}
for _, w := range workflows.Workflows {
if *w.Name == ci.CheckWorkflow {
return w, nil
}
}
return nil, trace.NotFound("workflow %s not found", ci.CheckWorkflow)
}
// deleteRun deletes a workflow run.
// Note: the go-github client library does not support this endpoint.
func (c *Bot) deleteRun(ctx context.Context, owner, repo string, runID int64) error {
clt := c.GithubClient.Client
// Construct url
url := url.URL{
Scheme: scheme,
Host: githubAPIHostname,
Path: path.Join("repos", owner, repo, "actions", "runs", fmt.Sprint(runID)),
}
req, err := clt.NewRequest(http.MethodDelete, url.String(), nil)
if err != nil {
return trace.Wrap(err)
}
_, err = clt.Do(ctx, req, nil)
if err != nil {
return trace.Wrap(err)
}
return nil
}
const (
// githubAPIHostname is the Github API hostname.
githubAPIHostname = "api.github.com"
// scheme is the protocol scheme used when making
// a request to delete a workflow run to the Github API.
scheme = "https"
)

View file

@ -0,0 +1,321 @@
/*
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 environment
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/gravitational/trace"
"github.com/google/go-github/v37/github"
)
// Config is used to configure Environment
type Config struct {
// Context is the context for Environment
Context context.Context
// Client is the authenticated Github client.
Client *github.Client
// Reviewers is a json object encoded as a string with
// authors mapped to their respective required reviewers.
Reviewers string
// EventPath is the path of the file with the complete
// webhook event payload on the runner.
EventPath string
// users optional override to inject a user list for testing.
users githubUserGetter
}
// PullRequestEnvironment contains information about the environment
type PullRequestEnvironment struct {
// Client is the authenticated Github client
Client *github.Client
// Metadata is the pull request in the
// current context.
Metadata *Metadata
// reviewers is a map of reviewers where the key
// is the user name of the author and the value is a list
// of required reviewers.
reviewers map[string][]string
// defaultReviewers is a list of reviewers used for authors whose
// usernames are not a key in `reviewers`
defaultReviewers []string
// action is the action that triggered the workflow.
action string
}
// Metadata is the current pull request metadata
type Metadata struct {
// Author is the pull request author.
Author string
// RepoName is the repository name that the
// current pull request is trying to merge into.
RepoName string
// RepoOwner is the owner of the repository the
// author is trying to merge into.
RepoOwner string
// Number is the pull request number.
Number int
// HeadSHA is the commit sha of the author's branch.
HeadSHA string
// BaseSHA is the commit sha of the base branch.
BaseSHA string
// Reviewer is the reviewer's Github username.
// Only used for pull request review events.
Reviewer string
// BranchName is the name of the branch the author
// is trying to merge in.
BranchName string
}
// CheckAndSetDefaults verifies configuration and sets defaults.
func (c *Config) CheckAndSetDefaults() error {
if c.Context == nil {
c.Context = context.Background()
}
if c.Client == nil {
return trace.BadParameter("missing parameter Client")
}
if c.Reviewers == "" {
return trace.BadParameter("missing parameter Reviewers")
}
if c.EventPath == "" {
c.EventPath = os.Getenv(ci.GithubEventPath)
}
if c.users == nil {
c.users = c.Client.Users
}
return nil
}
// New creates a new instance of Environment.
func New(c Config) (*PullRequestEnvironment, error) {
err := c.CheckAndSetDefaults()
if err != nil {
return nil, trace.Wrap(err)
}
revs, err := unmarshalReviewers(c.Context, c.Reviewers, c.users)
if err != nil {
return nil, trace.Wrap(err)
}
pr, err := GetMetadata(c.EventPath)
if err != nil {
return nil, trace.Wrap(err)
}
return &PullRequestEnvironment{
Client: c.Client,
reviewers: revs,
defaultReviewers: revs[ci.AnyAuthor],
Metadata: pr,
}, nil
}
type githubUserGetter interface {
Get(context.Context, string) (*github.User, *github.Response, error)
}
// unmarshalReviewers converts the passed in string representing json object into a map.
func unmarshalReviewers(ctx context.Context, str string, users githubUserGetter) (map[string][]string, error) {
if str == "" {
return nil, trace.NotFound("reviewers not found")
}
m := make(map[string][]string)
err := json.Unmarshal([]byte(str), &m)
if err != nil {
return nil, trace.Wrap(err)
}
var hasDefaultReviewers bool
for author, requiredReviewers := range m {
for _, reviewer := range requiredReviewers {
_, err := userExists(ctx, reviewer, users)
if err != nil {
return nil, trace.Wrap(err)
}
}
if author == ci.AnyAuthor {
hasDefaultReviewers = true
continue
}
_, err := userExists(ctx, author, users)
if err != nil {
return nil, trace.Wrap(err)
}
}
if !hasDefaultReviewers {
return nil, trace.BadParameter("default reviewers are not set. set default reviewers with a wildcard (*) as a key")
}
return m, nil
}
// userExists checks if a user exists.
func userExists(ctx context.Context, userLogin string, users githubUserGetter) (*github.User, error) {
user, _, err := users.Get(ctx, userLogin)
if err != nil {
return nil, trace.Wrap(err)
}
return user, nil
}
// GetReviewersForAuthor gets the required reviewers for the current user.
func (e *PullRequestEnvironment) GetReviewersForAuthor(user string) []string {
value, ok := e.reviewers[user]
// Author is external or does not have set reviewers
if !ok {
return e.defaultReviewers
}
return value
}
// IsInternal determines if an author is an internal contributor.
func (e *PullRequestEnvironment) IsInternal(author string) bool {
_, ok := e.reviewers[author]
return ok
}
// GetMetadata gets the pull request metadata in the current context.
func GetMetadata(path string) (*Metadata, error) {
file, err := os.Open(path)
if err != nil {
return nil, trace.Wrap(err)
}
defer file.Close()
body, err := ioutil.ReadAll(file)
if err != nil {
return nil, trace.Wrap(err)
}
var actionType action
err = json.Unmarshal(body, &actionType)
if err != nil {
return nil, trace.Wrap(err)
}
return getMetadata(body, actionType.Action)
}
func getMetadata(body []byte, action string) (*Metadata, error) {
var pr *Metadata
switch action {
case ci.Synchronize:
var push PushEvent
err := json.Unmarshal(body, &push)
if err != nil {
return nil, trace.Wrap(err)
}
pr, err = push.toMetadata()
if err != nil {
return nil, trace.Wrap(err)
}
case ci.Assigned, ci.Opened, ci.Reopened, ci.Ready:
var pull PullRequestEvent
err := json.Unmarshal(body, &pull)
if err != nil {
return nil, trace.Wrap(err)
}
pr, err = pull.toMetadata()
if err != nil {
return nil, trace.Wrap(err)
}
case ci.Submitted, ci.Created:
var rev ReviewEvent
err := json.Unmarshal(body, &rev)
if err != nil {
return nil, trace.Wrap(err)
}
pr, err = rev.toMetadata()
if err != nil {
return nil, err
}
default:
return nil, trace.BadParameter("unknown action %s", action)
}
return pr, nil
}
func (r *ReviewEvent) toMetadata() (*Metadata, error) {
m := &Metadata{
Number: r.PullRequest.Number,
Author: r.PullRequest.Author.Login,
RepoOwner: r.Repository.Owner.Name,
RepoName: r.Repository.Name,
HeadSHA: r.PullRequest.Head.SHA,
BaseSHA: r.PullRequest.Base.SHA,
BranchName: r.PullRequest.Head.BranchName,
Reviewer: r.Review.User.Login,
}
if m.Reviewer == "" {
return nil, trace.BadParameter("missing reviewer username")
}
if err := m.validateFields(); err != nil {
return nil, err
}
return m, nil
}
func (p *PullRequestEvent) toMetadata() (*Metadata, error) {
m := &Metadata{
Number: p.Number,
Author: p.PullRequest.User.Login,
RepoOwner: p.Repository.Owner.Name,
RepoName: p.Repository.Name,
HeadSHA: p.PullRequest.Head.SHA,
BaseSHA: p.PullRequest.Base.SHA,
BranchName: p.PullRequest.Head.BranchName,
}
if err := m.validateFields(); err != nil {
return nil, err
}
return m, nil
}
func (s *PushEvent) toMetadata() (*Metadata, error) {
m := &Metadata{
Number: s.Number,
Author: s.PullRequest.User.Login,
RepoOwner: s.Repository.Owner.Name,
RepoName: s.Repository.Name,
HeadSHA: s.CommitSHA,
BaseSHA: s.BeforeSHA,
BranchName: s.PullRequest.Head.BranchName,
}
if err := m.validateFields(); err != nil {
return nil, err
}
return m, nil
}
func (m *Metadata) validateFields() error {
switch {
case m.Number == 0:
return trace.BadParameter("missing pull request number")
case m.Author == "":
return trace.BadParameter("missing user login")
case m.RepoOwner == "":
return trace.BadParameter("missing repository owner")
case m.RepoName == "":
return trace.BadParameter("missing repository name")
case m.HeadSHA == "":
return trace.BadParameter("missing head commit sha")
case m.BaseSHA == "":
return trace.BadParameter("missing base commit sha")
case m.BranchName == "":
return trace.BadParameter("missing branch name")
}
return nil
}

View file

@ -0,0 +1,245 @@
/*
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 environment
import (
"context"
"errors"
"io/ioutil"
"os"
"testing"
"github.com/gravitational/teleport/.github/workflows/ci"
"github.com/google/go-github/v37/github"
"github.com/stretchr/testify/require"
)
func TestNewPullRequestEnvironment(t *testing.T) {
pr := &Metadata{Author: "Codertocat",
RepoName: "Hello-World",
RepoOwner: "Codertocat",
Number: 2,
HeadSHA: "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
BaseSHA: "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
BranchName: "changes",
}
tests := []struct {
cfg Config
checkErr require.ErrorAssertionFunc
expected *PullRequestEnvironment
desc string
createFile bool
}{
{
cfg: Config{
Client: github.NewClient(nil),
EventPath: "",
users: &mockUserGetter{},
},
checkErr: require.Error,
desc: "invalid PullRequestEnvironment config without Reviewers parameter",
expected: nil,
createFile: true,
},
{
cfg: Config{
Client: github.NewClient(nil),
Reviewers: `{"foo": ["bar", "baz"], "*":["admin"]}`,
users: &mockUserGetter{},
},
checkErr: require.NoError,
desc: "valid PullRequestEnvironment config",
expected: &PullRequestEnvironment{
reviewers: map[string][]string{"foo": {"bar", "baz"}, "*": {"admin"}},
Client: github.NewClient(nil),
Metadata: pr,
defaultReviewers: []string{"admin"},
},
createFile: true,
},
{
cfg: Config{
Client: github.NewClient(nil),
Reviewers: `{"foo": ["bar", "baz"], "*":["admin"]}`,
users: &mockUserGetter{},
},
checkErr: require.NoError,
desc: "valid PullRequestEnvironment config",
expected: &PullRequestEnvironment{
reviewers: map[string][]string{"foo": {"bar", "baz"}, "*": {"admin"}},
Client: github.NewClient(nil),
Metadata: pr,
defaultReviewers: []string{"admin"},
},
createFile: true,
},
{
cfg: Config{
Client: github.NewClient(nil),
Reviewers: `{"foo": ["bar", "baz"]}`,
users: &mockUserGetter{},
},
checkErr: require.Error,
desc: "invalid PullRequestEnvironment config, has no default reviewers key",
expected: nil,
createFile: true,
},
{
cfg: Config{
Reviewers: `{"foo": "baz", "*":["admin"]}`,
Client: github.NewClient(nil),
},
checkErr: require.Error,
desc: "invalid reviewers object format",
expected: nil,
createFile: true,
},
{
cfg: Config{},
checkErr: require.Error,
desc: "invalid config with no client",
expected: nil,
createFile: true,
},
{
cfg: Config{
Client: github.NewClient(nil),
Reviewers: `{"invalidUser": ["bar", "baz"], "*":["admin"]}`,
users: &mockUserGetter{},
},
checkErr: require.Error,
desc: "invalid PullRequestEnvironment config, user does not exist",
expected: nil,
createFile: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if test.createFile {
f, err := ioutil.TempFile("", "payload")
require.NoError(t, err)
filePath := f.Name()
defer os.Remove(f.Name())
_, err = f.Write([]byte(pullRequest))
require.NoError(t, err)
test.cfg.EventPath = filePath
}
env, err := New(test.cfg)
test.checkErr(t, err)
require.Equal(t, test.expected, env)
})
}
}
func TestSetPullRequest(t *testing.T) {
tests := []struct {
checkErr require.ErrorAssertionFunc
env *PullRequestEnvironment
input []byte
desc string
value *Metadata
action string
}{
{
env: &PullRequestEnvironment{},
checkErr: require.NoError,
input: []byte(synchronize),
value: &Metadata{Author: "quinqu",
RepoName: "gh-actions-poc",
RepoOwner: "gravitational",
Number: 28,
HeadSHA: "ecabd9d97b218368ea47d17cd23815590b76e196",
BaseSHA: "cbb23161d4c33d70189430d07957d2d66d42fc30",
BranchName: "jane/ci",
},
desc: "sync, no error",
action: ci.Synchronize,
},
{
env: &PullRequestEnvironment{},
checkErr: require.NoError,
input: []byte(pullRequest),
value: &Metadata{Author: "Codertocat",
RepoName: "Hello-World",
RepoOwner: "Codertocat",
Number: 2,
HeadSHA: "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
BaseSHA: "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
BranchName: "changes",
},
desc: "pull request, no error",
action: ci.Opened,
},
{
env: &PullRequestEnvironment{action: "submitted"},
checkErr: require.NoError,
input: []byte(submitted),
value: &Metadata{Author: "Codertocat",
RepoName: "Hello-World",
RepoOwner: "Codertocat",
Number: 2,
HeadSHA: "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
BaseSHA: "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
BranchName: "changes",
Reviewer: "Codertocat",
},
desc: "review, no error",
action: ci.Submitted,
},
{
env: &PullRequestEnvironment{},
checkErr: require.Error,
input: []byte(submitted),
value: nil,
desc: "sync, error",
action: ci.Synchronize,
},
{
env: &PullRequestEnvironment{},
checkErr: require.Error,
input: []byte(submitted),
value: nil,
desc: "pull request, error",
action: ci.Opened,
},
{
env: &PullRequestEnvironment{},
checkErr: require.Error,
input: []byte(pullRequest),
value: nil,
desc: "review, error",
action: ci.Submitted,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
pr, err := getMetadata(test.input, test.action)
test.checkErr(t, err)
require.Equal(t, test.value, pr)
})
}
}
type mockUserGetter struct {
}
func (m *mockUserGetter) Get(ctx context.Context, userLogin string) (*github.User, *github.Response, error) {
if userLogin == "invalidUser" {
return nil, nil, errors.New("invalid user")
}
return nil, nil, nil
}

View file

@ -0,0 +1,97 @@
/*
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 environment
/*
Below are struct definitions used to transform pull request and review
events (represented as a json object) into Golang structs. The way these objects are
structured are different, therefore separate structs for each event are needed
to unmarshal appropiately.
*/
// PushEvent is used for unmarshalling push events
type PushEvent struct {
Number int `json:"number"`
PullRequest PR `json:"pull_request"`
Repository Repository `json:"repository"`
CommitSHA string `json:"after"`
BeforeSHA string `json:"before"`
}
// PullRequestEvent s used for unmarshalling pull request events
type PullRequestEvent struct {
Number int `json:"number"`
PullRequest PR `json:"pull_request"`
Repository Repository `json:"repository"`
}
// ReviewEvent contains metadata about the pull request
// review (used for the pull request review event)
type ReviewEvent struct {
Review Review `json:"review"`
Repository Repository `json:"repository"`
PullRequest PullRequest `json:"pull_request"`
}
// Head contains the commit sha at the head of the pull request
type Head struct {
SHA string `json:"sha"`
BranchName string `json:"ref"`
}
// Review contains information about the pull request review
type Review struct {
User User `json:"user"`
}
// User contains information about the user
type User struct {
Login string `json:"login"`
}
// PullRequest contains information about the pull request (used for pull request review event)
type PullRequest struct {
Author User `json:"user"`
Number int `json:"number"`
Head Head `json:"head"`
Base Base `json:"base"`
}
// Base contains the base branch commit SHA
type Base struct {
SHA string `json:"sha"`
}
// PR contains information about the pull request (used for the pull request event)
type PR struct {
User User
Head Head `json:"head"`
Base Base `json:"base"`
}
// Repository contains information about the repository
type Repository struct {
Name string `json:"name"`
Owner Owner `json:"owner"`
}
// Owner contains information about the repository owner
type Owner struct {
Name string `json:"login"`
}
// action represents the current action
type action struct {
Action string `json:"action"`
}

File diff suppressed because it is too large Load diff

41
.github/workflows/dismiss.yaml vendored Normal file
View file

@ -0,0 +1,41 @@
# This workflow will run every 30 minutes and dismiss stale workflow runs on open pull requests.
# Stale workflow runs on pull requests are runs that are no longer up-to-date due to a new
# pull_request_target or pull_request_review event occurring.
#
# This workflow is specifically in place to dismiss stale runs for external contributors because
# the `Check` workflow token does not have write access to actions when a pull_request_review event
# triggers it from a fork. Stale workflow runs need to be removed by this workflow otherwise they
# will persist on the pull request's requirements record and not reflect the
# the correct state of the checks.
name: Dismiss Stale Workflows Runs
on:
schedule:
# Runs every 30 minutes
- cron: '0,30 * * * *'
# Limit the permissions on the GitHub token for this workflow to the subset
# that is required. In this case, the dismiss workflow needs to read reviews and
# delete workflow runs so it needs write access to "actions" and read to
# "pull-requests", nothing else.
permissions:
actions: write
pull-requests: read
checks: none
contents: none
deployments: none
issues: none
packages: none
repository-projects: none
security-events: none
statuses: none
jobs:
dismiss-stale-runs:
name: Dismiss Stale Workflow Runs
runs-on: ubuntu-latest
steps:
- name: Checkout master branch
uses: actions/checkout@master
- name: Installing the latest version of Go.
uses: actions/setup-go@v2
# Run "dismiss-runs" subcommand on bot.
- name: Dismiss
run: cd .github/workflows && go run cmd/main.go --token=${{ secrets.GITHUB_TOKEN }} dismiss-runs