├── .dockerignore
├── .github
└── workflows
│ ├── build-ci.yaml
│ ├── documentation-ci.yaml
│ └── merge-gatekeeper-latest.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── action.yml
├── assets
└── images
│ └── branch-protection-example.png
├── docs
├── action-usage.md
├── details.md
└── developer-guide.md
├── example
├── definitions.yaml
└── merge-gatekeeper.yml
├── go.mod
├── go.sum
├── internal
├── cli
│ ├── cli.go
│ ├── logger.go
│ ├── validate.go
│ └── validate_test.go
├── github
│ ├── github.go
│ └── mock
│ │ └── mock.go
├── multierror
│ └── multierror.go
├── ticker
│ ├── ticker.go
│ └── ticker_test.go
└── validators
│ ├── mock
│ └── mock.go
│ ├── status
│ ├── option.go
│ ├── status.go
│ ├── status_test.go
│ ├── validator.go
│ └── validator_test.go
│ └── validators.go
├── main.go
└── version.txt
/.dockerignore:
--------------------------------------------------------------------------------
1 | .github/
2 | docs/
3 | example/
4 | .gitignore
5 | action.yml
6 | LICENCE
7 | README.md
8 |
--------------------------------------------------------------------------------
/.github/workflows/build-ci.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - v*
9 | pull_request:
10 | branches:
11 | - main
12 | - release/v*
13 | paths:
14 | - main.go
15 | - version.txt
16 | - internal/**
17 | - go.mod
18 | - go.sum
19 |
20 | env:
21 | GO_VERSION: 1.16.7
22 |
23 | jobs:
24 | build:
25 | name: Build and Test
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Check out
29 | uses: actions/checkout@v2
30 | with:
31 | fetch-depth: 0
32 | - name: Set up Go
33 | uses: actions/setup-go@v1
34 | with:
35 | go-version: ${{ env.GO_VERSION }}
36 |
37 | - name: Run Go Build
38 | run: go build ./...
39 | - name: Run Go Test
40 | run: go test ./...
41 |
--------------------------------------------------------------------------------
/.github/workflows/documentation-ci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Run Importer for Markdowns
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 | paths:
9 | - "**.md"
10 |
11 | jobs:
12 | importer:
13 | name: Run Importer Generate for Markdowns
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Check out
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Setup Go
22 | uses: actions/setup-go@v3
23 | with:
24 | go-version: ">=1.18.0"
25 | check-latest: true
26 |
27 | - name: Install Importer
28 | run: go install github.com/upsidr/importer/cmd/importer@v0.1.4
29 |
30 | - name: Run Importer against all *.md files
31 | run: find . -name '*.md' -exec importer update {} \;
32 | - name: Check if any change compared to the branch HEAD
33 | run: |
34 | git status --short
35 | git diff-index --quiet HEAD
36 |
--------------------------------------------------------------------------------
/.github/workflows/merge-gatekeeper-latest.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Merge Gatekeeper
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | merge-gatekeeper:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | checks: read
14 | statuses: read
15 | steps:
16 | - name: Check out
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Run Merge Gatekeeper
22 | uses: ./
23 | with:
24 | token: ${{ secrets.GITHUB_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # Built binary
18 | merge-gatekeeper
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG GO_VERSION=1.16.7
2 |
3 | FROM golang:${GO_VERSION}-alpine
4 |
5 | ARG ORG=upsidr
6 | ARG REPO=merge-gatekeeper
7 |
8 | ENV GO111MODULE=on LANG=en_US.UTF-8
9 |
10 | RUN mkdir -p $GOPATH/src
11 |
12 | WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}
13 |
14 | COPY . .
15 |
16 | RUN CGO_ENABLED=0 go build . \
17 | && mv merge-gatekeeper /go/bin/
18 |
19 | ENTRYPOINT ["/go/bin/merge-gatekeeper"]
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Upsider
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TOKEN=${GITHUB_TOKEN}
2 | REF=main
3 | REPO=upsidr/merge-gatekeeper
4 | IGNORED=""
5 |
6 | go-build:
7 | GO111MODULE=on LANG=en_US.UTF-8 CGO_ENABLED=0 go build ./cmd/merge-gatekeeper
8 |
9 | go-run: go-build
10 | ./merge-gatekeeper validate --token=$(TOKEN) --ref $(REF) --repo $(REPO) --ignored "$(IGNORED)"
11 |
12 | docker-build:
13 | docker build -t merge-gatekeeper:latest .
14 |
15 | docker-run: docker-build
16 | docker run --rm -it --name merge-gatekeeper merge-gatekeeper:latest validate --token=$(TOKEN) --ref $(REF) --repo $(REPO) --ignored "$(IGNORED)"
17 |
18 | test:
19 | go test ./...
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Merge Gatekeeper
2 |
3 | Merge Gatekeeper provides extra control for Pull Request management.
4 |
5 | ## 🌄 What Does Merge Gatekeeper Provide, and Why?
6 |
7 |
8 |
9 | Pull Request plays a significant part in day-to-day development, and it is essential to ensure all merges are well controlled and managed to build robust system. GitHub provides controls over CI, reviews, etc., but there are some limitations around handling specific use cases. Merge Gatekeeper helps overcome those by adding extra controls, such as monorepo friendly branch protection.
10 |
11 | At UPSIDER, we have a few internal repositories set up with a monorepo structure, with many types of code in a single repository. This comes with its own pros and cons, but one difficulty is how we end up with various CI jobs, which only run for changes that touch relevant files. With GitHub's branch protection, there is no way to specify "Ensure Go build and test pass _if and only if_ Go code is updated", or "Ensure E2E tests are run and successful _if and only if_ frontend code is updated". This is due to the GitHub branch protection design to specify a list of jobs to pass, which is only driven by the target branch name, regardless of change details. Because of this limitation, we would either need to run all the CI jobs for any Pull Requests, or do not set any limitation based on the CI status. (\*1)
12 |
13 | **Merge Gatekeeper** was created to provide more control for merges. By placing Merge Gatekeeper to run for all PRs, it can check all other CI jobs that get kicked off, and ensure all the jobs are completed successfully. If there is any job that has failed, Merge Gatekeeper will fail as well. This allows merge protection based on Merge Gatekeeper, which can effectively ensure any CI failure will block merge. All you need is the Merge Gatekeeper as one of the PR based GitHub Action, and set the branch protection rule as shown below.
14 |
15 | 
16 |
17 | We are looking to add a few more features, such as extra signoff from non-coder, label based check, etc.
18 |
19 | NOTE:
20 | (\*1) There are some other hacks, such as using an empty job with the same name to override the status, but those solutions do not provide the flexible control we are after.
21 |
22 |
23 |
24 | You can find [more details here](/docs/details.md).
25 |
26 | ## 🚀 How Can I Use Merge Gatekeeper?
27 |
28 |
29 |
30 | The easiest approach is to copy the standard definition, and save it under `.github/workflows` directory. There is no further modification required unless you have some specific requirements.
31 |
32 | #### With `curl`
33 |
34 | ```bash
35 | curl -sSL https://raw.githubusercontent.com/upsidr/merge-gatekeeper/main/example/merge-gatekeeper.yml \
36 | > .github/workflows/merge-gatekeeper.yml
37 | ```
38 |
39 | #### Directly copy YAML
40 |
41 | The below is the copy of [`/example/merge-gatekeeper.yml`](/example/merge-gatekeeper.yml), with extra comments.
42 |
43 |
44 | ```yaml
45 | ---
46 | name: Merge Gatekeeper
47 |
48 | on:
49 | pull_request:
50 | branches:
51 | - main
52 | - master
53 |
54 | jobs:
55 | merge-gatekeeper:
56 | runs-on: ubuntu-latest
57 | # Restrict permissions of the GITHUB_TOKEN.
58 | # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
59 | permissions:
60 | checks: read
61 | statuses: read
62 | steps:
63 | - name: Run Merge Gatekeeper
64 | # NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
65 | # https://github.com/upsidr/merge-gatekeeper/tags
66 | # https://github.com/upsidr/merge-gatekeeper/branches
67 | uses: upsidr/merge-gatekeeper@v1
68 | with:
69 | token: ${{ secrets.GITHUB_TOKEN }}
70 | ```
71 |
72 |
73 |
74 |
75 | You can find [more details here](/docs/action-usage.md).
76 |
77 | ## 🧪 Action Inputs
78 |
79 | There are some customisation available for Merge Gatekeeper.
80 |
81 |
82 |
83 | | Name | Description | Required |
84 | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: |
85 | | `token` | `GITHUB_TOKEN` or Personal Access Token with `repo` scope | Yes |
86 | | `self` | The name of Merge Gatekeeper job, and defaults to `merge-gatekeeper`. This is used to check other job status, and do not check Merge Gatekeeper itself. If you updated the GitHub Action job name from `merge-gatekeeper` to something else, you would need to specify the new name with this value. | |
87 | | `interval` | Check interval to recheck the job status. Default is set to 5 (sec). | |
88 | | `timeout` | Timeout setup to give up further check. Default is set to 600 (sec). | |
89 | | `ignored` | Jobs to ignore regardless of their statuses. Defined as a comma-separated list. | |
90 | | `ref` | Git ref to check out. This falls back to the HEAD for given PR, but can be set to any ref. | |
91 |
92 |
93 |
94 | You can find [more details here](/docs/action-usage.md).
95 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: "Merge Gatekeeper"
2 | description: "Get better merge control"
3 | branding:
4 | icon: git-merge
5 | color: orange
6 | inputs:
7 | token:
8 | description: "set github token"
9 | required: true
10 | self:
11 | description: "set self job name"
12 | required: false
13 | default: "merge-gatekeeper"
14 | interval:
15 | description: "set validate interval second (default 5)"
16 | required: false
17 | default: "5"
18 | timeout:
19 | description: "set validate timeout second (default 600)"
20 | required: false
21 | default: "600"
22 | ignored:
23 | description: "set ignored jobs (comma-separated list)"
24 | required: false
25 | default: ""
26 | ref:
27 | description: "set ref of github repository. the ref can be a SHA, a branch name, or tag name"
28 | required: false
29 | default: ${{ github.event.pull_request.head.sha }}
30 | runs:
31 | using: "docker"
32 | image: "Dockerfile"
33 | args:
34 | - "validate"
35 | - "--token=${{ inputs.token }}"
36 | - "--self=${{ inputs.self }}"
37 | - "--interval=${{ inputs.interval }}"
38 | - "--ref=${{ inputs.ref }}"
39 | - "--timeout=${{ inputs.timeout }}"
40 | - "--ignored=${{ inputs.ignored }}"
41 |
--------------------------------------------------------------------------------
/assets/images/branch-protection-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/upsidr/merge-gatekeeper/12e1af4ecc92572c30bda0afff86b78f4d6baac5/assets/images/branch-protection-example.png
--------------------------------------------------------------------------------
/docs/action-usage.md:
--------------------------------------------------------------------------------
1 | # Action Details
2 |
3 | ## Action Inputs
4 |
5 |
6 |
7 | | Name | Description | Required |
8 | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: |
9 | | `token` | `GITHUB_TOKEN` or Personal Access Token with `repo` scope | Yes |
10 | | `self` | The name of Merge Gatekeeper job, and defaults to `merge-gatekeeper`. This is used to check other job status, and do not check Merge Gatekeeper itself. If you updated the GitHub Action job name from `merge-gatekeeper` to something else, you would need to specify the new name with this value. | |
11 | | `interval` | Check interval to recheck the job status. Default is set to 5 (sec). | |
12 | | `timeout` | Timeout setup to give up further check. Default is set to 600 (sec). | |
13 | | `ignored` | Jobs to ignore regardless of their statuses. Defined as a comma-separated list. | |
14 | | `ref` | Git ref to check out. This falls back to the HEAD for given PR, but can be set to any ref. | |
15 |
16 |
17 |
18 | ## Usage
19 |
20 | ### Copy Standard YAML
21 |
22 |
23 |
24 | The easiest approach is to copy the standard definition, and save it under `.github/workflows` directory. There is no further modification required unless you have some specific requirements.
25 |
26 | #### With `curl`
27 |
28 | ```bash
29 | curl -sSL https://raw.githubusercontent.com/upsidr/merge-gatekeeper/main/example/merge-gatekeeper.yml \
30 | > .github/workflows/merge-gatekeeper.yml
31 | ```
32 |
33 | #### Directly copy YAML
34 |
35 | The below is the copy of [`/example/merge-gatekeeper.yml`](/example/merge-gatekeeper.yml), with extra comments.
36 |
37 |
38 | ```yaml
39 | ---
40 | name: Merge Gatekeeper
41 |
42 | on:
43 | pull_request:
44 | branches:
45 | - main
46 | - master
47 |
48 | jobs:
49 | merge-gatekeeper:
50 | runs-on: ubuntu-latest
51 | # Restrict permissions of the GITHUB_TOKEN.
52 | # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
53 | permissions:
54 | checks: read
55 | statuses: read
56 | steps:
57 | - name: Run Merge Gatekeeper
58 | # NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
59 | # https://github.com/upsidr/merge-gatekeeper/tags
60 | # https://github.com/upsidr/merge-gatekeeper/branches
61 | uses: upsidr/merge-gatekeeper@v1
62 | with:
63 | token: ${{ secrets.GITHUB_TOKEN }}
64 | ```
65 |
66 |
67 |
68 |
69 | ### Using Importer
70 |
71 | You can also use the latest spec by using Importer to improt directly from the sample setup in this repository.
72 |
73 | Create a YAML file with just a single Importer Marker:
74 |
75 | ```yaml
76 | # == imptr: merge-gatekeeper / begin from: https://github.com/upsidr/merge-gatekeeper/blob/main/example/definitions.yaml#[standard-setup] ==
77 | # == imptr: merge-gatekeeper / end ==
78 | ```
79 |
80 | With that, you can simply run `importer update FILENAME` to get the latest spec. You can also update the file used to specific branch or version.
81 |
82 | ###
83 |
--------------------------------------------------------------------------------
/docs/details.md:
--------------------------------------------------------------------------------
1 | # Details of Merge Gatekeeper
2 |
3 | ## Background
4 |
5 |
6 |
7 | Pull Request plays a significant part in day-to-day development, and it is essential to ensure all merges are well controlled and managed to build robust system. GitHub provides controls over CI, reviews, etc., but there are some limitations around handling specific use cases. Merge Gatekeeper helps overcome those by adding extra controls, such as monorepo friendly branch protection.
8 |
9 | At UPSIDER, we have a few internal repositories set up with a monorepo structure, with many types of code in a single repository. This comes with its own pros and cons, but one difficulty is how we end up with various CI jobs, which only run for changes that touch relevant files. With GitHub's branch protection, there is no way to specify "Ensure Go build and test pass _if and only if_ Go code is updated", or "Ensure E2E tests are run and successful _if and only if_ frontend code is updated". This is due to the GitHub branch protection design to specify a list of jobs to pass, which is only driven by the target branch name, regardless of change details. Because of this limitation, we would either need to run all the CI jobs for any Pull Requests, or do not set any limitation based on the CI status. (\*1)
10 |
11 | **Merge Gatekeeper** was created to provide more control for merges. By placing Merge Gatekeeper to run for all PRs, it can check all other CI jobs that get kicked off, and ensure all the jobs are completed successfully. If there is any job that has failed, Merge Gatekeeper will fail as well. This allows merge protection based on Merge Gatekeeper, which can effectively ensure any CI failure will block merge. All you need is the Merge Gatekeeper as one of the PR based GitHub Action, and set the branch protection rule as shown below.
12 |
13 | 
14 |
15 | We are looking to add a few more features, such as extra signoff from non-coder, label based check, etc.
16 |
17 | NOTE:
18 | (\*1) There are some other hacks, such as using an empty job with the same name to override the status, but those solutions do not provide the flexible control we are after.
19 |
20 |
21 |
22 | ## Features
23 |
24 |
25 |
26 | Merge Gatekeeper provides additional control that may be useful for large and complex repositories.
27 |
28 | ### Ensure all CI jobs are successful
29 |
30 | By default, when Merge Gatekeeper is used for PR, it periodically checks the PR by checking all the other CI jobs. This means if you have complex CI scenarios where some CIs run only for specific changes, you can still ensure all the CI jobs have run successfully in order to merge the PR.
31 |
32 | ### Other validations
33 |
34 | We are currently considering additional validation controls such as:
35 |
36 | - extra approval by comment
37 | - label validation
38 |
39 |
40 |
41 | ## How does Merge Gatekeeper work?
42 |
43 |
44 |
45 | Merge Gatekeeper periodically validates the PR status by hitting GitHub API. The GitHub token is thus required for Merge Gatekeeper to operate, and it's often enough to have `${{ secrets.GITHUB_TOKEN }}` to be provided. The API call to list PR jobs will reveal how many jobs need to run for the given PR, check each job status, and finally return the validation status - success based on completing all the jobs, or timeout error. It is important for Merge Gatekeeper to know the Job name of itself, so that when API call returns Merge Gatekeeper as a part of the PR jobs, it would ignore its status (otherwise it will never succeed).
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs/developer-guide.md:
--------------------------------------------------------------------------------
1 | # Working with Merge Gatekeeper locally
2 |
3 | ## Requirements
4 |
5 | - [`go` >= 1.16.7](https://go.dev/doc/install)
6 | - [A valid github token with `repo` permissions](https://github.com/settings/tokens)
7 | - [make](https://en.wikipedia.org/wiki/Make_(software))
8 |
9 | ## Useful but not required
10 |
11 | - [`docker`](https://docs.docker.com/engine/install/)
12 | - required for building and running via docker
13 |
14 | ## Building Merge Gatekeeper
15 |
16 | Using the [`Makefile`](./../Makefile) run the following to build:
17 | ```bash
18 | # build go binary
19 | make go-build
20 |
21 | # build docker container
22 | make docker-build
23 | ```
24 |
25 | ## Running Merge Gatekeeper
26 |
27 | it is recommend to export you [github token with `repo` permissions](https://github.com/settings/tokens) to the environment using
28 | ```bash
29 | export GITHUB_TOKEN="your token"
30 | ```
31 | otherwise, you will need to pass your token in via
32 | ```bash
33 | GITHUB_TOKEN="your token" make go-run
34 | ```
35 |
36 | Using the [`Makefile`](./../Makefile) run the following to run:
37 | ```bash
38 | # build and run go binary
39 | make go-run
40 |
41 | # build and run docker container
42 | make docker-run
43 | ```
44 |
45 | ## Testing
46 | To test, use the makefile:
47 |
48 | ```bash
49 | make test
50 | ```
51 |
--------------------------------------------------------------------------------
/example/definitions.yaml:
--------------------------------------------------------------------------------
1 | # == export: standard-setup / begin ==
2 | ---
3 | name: Merge Gatekeeper
4 |
5 | on:
6 | pull_request:
7 | branches:
8 | - main
9 | - master
10 |
11 | jobs:
12 | merge-gatekeeper:
13 | runs-on: ubuntu-latest
14 | # Restrict permissions of the GITHUB_TOKEN.
15 | # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
16 | permissions:
17 | checks: read
18 | statuses: read
19 | steps:
20 | - name: Run Merge Gatekeeper
21 | # NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
22 | # https://github.com/upsidr/merge-gatekeeper/tags
23 | # https://github.com/upsidr/merge-gatekeeper/branches
24 | uses: upsidr/merge-gatekeeper@v1
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 | # == export: standard-setup / end ==
28 |
29 | # == export: custom-job-name / begin ==
30 | ---
31 | name: Merge Gatekeeper
32 |
33 | on:
34 | pull_request:
35 | branches:
36 | - main
37 | - master
38 |
39 | jobs:
40 | merge-gatekeeper-custom:
41 | runs-on: ubuntu-latest
42 | name: Custom Name for Merge Gatekeeper
43 | # Restrict permissions of the GITHUB_TOKEN.
44 | # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
45 | permissions:
46 | checks: read
47 | statuses: read
48 | steps:
49 | - name: Run Merge Gatekeeper
50 | uses: upsidr/merge-gatekeeper@main
51 | with:
52 | token: ${{ secrets.GITHUB_TOKEN }}
53 | self: Custom Name for Merge Gatekeeper # This must match with the Job name provided above.
54 | # == export: custom-job-name / end ==
55 |
--------------------------------------------------------------------------------
/example/merge-gatekeeper.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Merge Gatekeeper
3 |
4 | on:
5 | pull_request:
6 | branches:
7 | - main
8 | - master
9 |
10 | jobs:
11 | merge-gatekeeper:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | checks: read
15 | statuses: read
16 | steps:
17 | - name: Run Merge Gatekeeper
18 | uses: upsidr/merge-gatekeeper@v1
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/upsidr/merge-gatekeeper
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/google/go-github/v38 v38.1.0
7 | github.com/google/go-querystring v1.1.0 // indirect
8 | github.com/spf13/cobra v1.2.1
9 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
42 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
43 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
44 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
45 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
46 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
47 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
48 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
49 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
50 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
51 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
52 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
53 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
54 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
55 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
56 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
57 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
58 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
59 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
60 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
61 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
62 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
63 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
64 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
65 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
66 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
67 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
68 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
69 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
70 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
71 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
72 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
73 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
74 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
75 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
77 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
78 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
79 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
80 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
81 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
82 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
83 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
84 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
85 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
86 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
87 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
88 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
89 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
90 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
91 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
92 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
93 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
94 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
95 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
96 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
97 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
98 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
99 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
100 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
101 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
102 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
103 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
104 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
105 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
106 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
107 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
108 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
109 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
110 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
111 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
112 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
113 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
114 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
115 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
116 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
117 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
118 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
119 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
120 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
121 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
122 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
123 | github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3Uva+po=
124 | github.com/google/go-github/v38 v38.1.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4=
125 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
126 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
127 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
128 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
129 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
130 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
131 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
132 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
133 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
134 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
135 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
136 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
137 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
138 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
139 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
140 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
141 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
142 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
143 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
144 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
145 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
146 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
147 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
148 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
149 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
150 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
151 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
152 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
153 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
154 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
155 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
156 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
157 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
158 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
159 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
160 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
161 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
162 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
163 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
164 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
165 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
166 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
167 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
168 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
169 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
170 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
171 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
172 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
173 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
174 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
175 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
176 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
177 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
178 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
179 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
180 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
181 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
182 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
183 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
184 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
185 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
186 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
187 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
188 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
189 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
190 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
191 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
192 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
193 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
194 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
195 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
196 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
197 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
198 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
199 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
200 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
201 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
202 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
203 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
204 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
205 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
206 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
207 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
208 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
209 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
210 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
211 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
212 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
213 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
214 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
215 | github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
216 | github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
217 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
218 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
219 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
220 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
221 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
222 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
223 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
224 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
225 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
226 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
227 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
228 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
229 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
230 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
231 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
232 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
233 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
234 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
235 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
236 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
237 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
238 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
239 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
240 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
241 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
242 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
243 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
244 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
245 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
246 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
247 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
248 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
249 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
250 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
251 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
252 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
253 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
254 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
255 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
256 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
257 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
258 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
259 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
260 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
261 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
262 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
263 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
264 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
265 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
266 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
267 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
268 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
269 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
270 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
271 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
272 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
273 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
274 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
275 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
276 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
277 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
278 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
279 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
280 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
281 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
282 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
283 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
284 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
285 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
286 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
287 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
288 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
289 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
290 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
291 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
292 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
293 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
294 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
295 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
296 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
297 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
298 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
299 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
300 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
301 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
302 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
303 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
304 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
305 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
306 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
307 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
308 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
309 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
310 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
311 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
312 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
313 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
314 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
315 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
316 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
317 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
318 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
319 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
320 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
321 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
322 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
323 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
324 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
325 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
326 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
327 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
328 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
329 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
330 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
331 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
332 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
333 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
334 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
335 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
336 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
337 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
338 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
339 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw=
340 | golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
341 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
342 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
343 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
344 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
345 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
346 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
347 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
348 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
349 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
350 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
351 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
352 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
353 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
354 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
355 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
356 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
357 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
358 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
359 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
360 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
361 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
362 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
363 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
364 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
365 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
366 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
367 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
368 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
369 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
370 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
371 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
372 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
373 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
374 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
375 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
376 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
377 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
378 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
379 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
380 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
381 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
382 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
383 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
384 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
385 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
386 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
387 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
388 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
389 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
390 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
391 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
392 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
393 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
394 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
395 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
396 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
397 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
398 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
399 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
400 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
401 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
402 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
403 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
404 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
405 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
406 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
407 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
408 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
409 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
410 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
411 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
412 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
413 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
414 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
415 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
416 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
417 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
418 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
419 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
420 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
421 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
422 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
423 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
424 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
425 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
426 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
427 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
428 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
429 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
430 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
431 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
432 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
433 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
434 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
435 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
436 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
437 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
438 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
439 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
440 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
441 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
442 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
443 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
444 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
445 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
446 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
447 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
448 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
449 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
450 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
451 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
452 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
453 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
454 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
455 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
456 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
457 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
458 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
459 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
460 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
461 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
462 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
463 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
464 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
465 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
466 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
467 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
468 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
469 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
470 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
471 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
472 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
473 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
474 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
475 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
476 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
477 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
478 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
479 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
480 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
481 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
482 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
483 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
484 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
485 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
486 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
487 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
488 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
489 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
490 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
491 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
492 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
493 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
494 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
495 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
496 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
497 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
498 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
499 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
500 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
501 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
502 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
503 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
504 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
505 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
506 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
507 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
508 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
509 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
510 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
511 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
512 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
513 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
514 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
515 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
516 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
517 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
518 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
519 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
520 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
521 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
522 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
523 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
524 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
525 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
526 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
527 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
528 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
529 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
530 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
531 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
532 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
533 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
534 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
535 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
536 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
537 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
538 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
539 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
540 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
541 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
542 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
543 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
544 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
545 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
546 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
547 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
548 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
549 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
550 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
551 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
552 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
553 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
554 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
555 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
556 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
557 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
558 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
559 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
560 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
561 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
562 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
563 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
564 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
565 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
566 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
567 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
568 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
569 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
570 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
571 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
572 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
573 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
574 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
575 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
576 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
577 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
578 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
579 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
580 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
581 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
582 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
583 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
584 |
--------------------------------------------------------------------------------
/internal/cli/cli.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "os/signal"
6 | "syscall"
7 |
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | // These variables will be set by command line flags.
12 | var (
13 | ghToken string
14 | )
15 |
16 | func Run(version string, args ...string) error {
17 | cmd := &cobra.Command{
18 | Use: "merge-gatekeeper",
19 | Short: "Get more refined merge control",
20 | Version: version,
21 | }
22 | cmd.PersistentFlags().StringVarP(&ghToken, "token", "t", "", "set github token")
23 | cmd.MarkPersistentFlagRequired("token")
24 |
25 | cmd.AddCommand(validateCmd())
26 |
27 | ctx, cancel := signal.NotifyContext(context.Background(),
28 | syscall.SIGINT,
29 | syscall.SIGTERM,
30 | )
31 | defer cancel()
32 |
33 | if err := cmd.ExecuteContext(ctx); err != nil {
34 | return err
35 | }
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/internal/cli/logger.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | type logger interface {
4 | Print(i ...interface{})
5 | Println(i ...interface{})
6 | Printf(format string, i ...interface{})
7 | PrintErr(i ...interface{})
8 | PrintErrln(i ...interface{})
9 | PrintErrf(format string, i ...interface{})
10 | }
11 |
--------------------------------------------------------------------------------
/internal/cli/validate.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "strings"
8 | "time"
9 |
10 | "github.com/spf13/cobra"
11 |
12 | "github.com/upsidr/merge-gatekeeper/internal/github"
13 | "github.com/upsidr/merge-gatekeeper/internal/ticker"
14 | "github.com/upsidr/merge-gatekeeper/internal/validators"
15 | "github.com/upsidr/merge-gatekeeper/internal/validators/status"
16 | )
17 |
18 | const defaultSelfJobName = "merge-gatekeeper"
19 |
20 | // These variables will be set by command line flags.
21 | var (
22 | ghRepo string // e.g) upsidr/merge-gatekeeper
23 | ghRef string
24 | timeoutSecond uint
25 | validateInvalSecond uint
26 | selfJobName string
27 | ignoredJobs string
28 | )
29 |
30 | func validateCmd() *cobra.Command {
31 | cmd := &cobra.Command{
32 | Use: "validate",
33 | Short: "Validate other github actions job",
34 | PreRun: func(cmd *cobra.Command, args []string) {
35 | str := os.Getenv("GITHUB_REPOSITORY")
36 | if len(str) != 0 {
37 | ghRepo = str
38 | }
39 | },
40 | RunE: func(cmd *cobra.Command, args []string) error {
41 | ctx := cmd.Context()
42 |
43 | owner, repo := ownerAndRepository(ghRepo)
44 | if len(owner) == 0 || len(repo) == 0 {
45 | return fmt.Errorf("github owner or repository is empty. owner: %s, repository: %s", owner, repo)
46 | }
47 |
48 | statusValidator, err := status.CreateValidator(github.NewClient(ctx, ghToken),
49 | status.WithSelfJob(selfJobName),
50 | status.WithGitHubOwnerAndRepo(owner, repo),
51 | status.WithGitHubRef(ghRef),
52 | status.WithIgnoredJobs(ignoredJobs),
53 | )
54 | if err != nil {
55 | return fmt.Errorf("failed to create validator: %w", err)
56 | }
57 |
58 | cmd.SilenceUsage = true
59 | return doValidateCmd(ctx, cmd, statusValidator)
60 | },
61 | }
62 |
63 | cmd.PersistentFlags().StringVarP(&selfJobName, "self", "s", defaultSelfJobName, "set self job name")
64 |
65 | cmd.PersistentFlags().StringVarP(&ghRepo, "repo", "r", "", "set github repository")
66 |
67 | cmd.PersistentFlags().StringVar(&ghRef, "ref", "", "set ref of github repository. the ref can be a SHA, a branch name, or tag name")
68 | cmd.MarkPersistentFlagRequired("ref")
69 |
70 | cmd.PersistentFlags().UintVar(&timeoutSecond, "timeout", 600, "set validate timeout second")
71 | cmd.PersistentFlags().UintVar(&validateInvalSecond, "interval", 10, "set validate interval second")
72 |
73 | cmd.PersistentFlags().StringVarP(&ignoredJobs, "ignored", "i", "", "set ignored jobs (comma-separated list)")
74 |
75 | return cmd
76 | }
77 |
78 | func ownerAndRepository(str string) (owner string, repo string) {
79 | sp := strings.Split(str, "/")
80 | switch len(sp) {
81 | case 0:
82 | return "", ""
83 | case 1:
84 | return sp[0], ""
85 | case 2:
86 | return sp[0], sp[1]
87 | default:
88 | return sp[0], strings.Join(sp[1:], "/")
89 | }
90 | }
91 |
92 | func debug(logger logger, name string) func() {
93 | logger.Printf("Start processing %s....\n", name)
94 | return func() {
95 | logger.Printf("Finish %s processing.\n", name)
96 | }
97 | }
98 |
99 | func doValidateCmd(ctx context.Context, logger logger, vs ...validators.Validator) error {
100 | ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSecond)*time.Second)
101 | defer cancel()
102 |
103 | invalT := ticker.NewInstantTicker(time.Duration(validateInvalSecond) * time.Second)
104 | defer invalT.Stop()
105 |
106 | for {
107 | select {
108 | case <-ctx.Done():
109 | return ctx.Err()
110 | case <-invalT.C():
111 | var successCnt int
112 | for _, v := range vs {
113 | ok, err := validate(ctx, v, logger)
114 | if err != nil {
115 | return err
116 | }
117 | if ok {
118 | successCnt++
119 | }
120 | }
121 | if successCnt != len(vs) {
122 | logger.PrintErrln("")
123 | logger.PrintErrln(" WARNING: Validation is yet to be completed. This is most likely due to some other jobs still running.")
124 | logger.PrintErrf(" Waiting for %d seconds before retrying.\n\n", validateInvalSecond)
125 | break
126 | }
127 |
128 | logger.Println("All validations were successful!")
129 | return nil
130 | }
131 | }
132 | }
133 |
134 | func validate(ctx context.Context, v validators.Validator, logger logger) (bool, error) {
135 | defer debug(logger, "validator: "+v.Name())()
136 |
137 | st, err := v.Validate(ctx)
138 | if err != nil {
139 | return false, fmt.Errorf("validation failed, err: %v", err)
140 | }
141 |
142 | logger.Println(st.Detail())
143 |
144 | if !st.IsSuccess() {
145 | return false, nil
146 | }
147 | return true, nil
148 | }
149 |
--------------------------------------------------------------------------------
/internal/cli/validate_test.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "os"
7 | "testing"
8 |
9 | "github.com/spf13/cobra"
10 |
11 | "github.com/upsidr/merge-gatekeeper/internal/validators"
12 | "github.com/upsidr/merge-gatekeeper/internal/validators/mock"
13 | )
14 |
15 | func TestMain(m *testing.M) {
16 | validateInvalSecond = 1
17 | timeoutSecond = 2
18 | os.Exit(m.Run())
19 | }
20 |
21 | func Test_ownerAndRepository(t *testing.T) {
22 | tests := map[string]struct {
23 | str string
24 | wantOwner string
25 | wantRepo string
26 | }{
27 | "returns empty when str is empty": {
28 | str: "",
29 | wantOwner: "",
30 | wantRepo: "",
31 | },
32 | "returns (upsidr, repo) when str is upsidr/repo": {
33 | str: "upsidr/repo",
34 | wantOwner: "upsidr",
35 | wantRepo: "repo",
36 | },
37 | "returns (upsidr, '') when str is upsidr": {
38 | str: "upsidr",
39 | wantOwner: "upsidr",
40 | wantRepo: "",
41 | },
42 | "returns ('', repo) when str is /repo": {
43 | str: "/repo",
44 | wantOwner: "",
45 | wantRepo: "repo",
46 | },
47 | "returns (upsidr, repo/repo) when str is upsidr/repo/repo": {
48 | str: "upsidr/repo/repo",
49 | wantOwner: "upsidr",
50 | wantRepo: "repo/repo",
51 | },
52 | }
53 |
54 | for name, tt := range tests {
55 | t.Run(name, func(t *testing.T) {
56 | gotOwner, gotRepo := ownerAndRepository(tt.str)
57 | if gotOwner != tt.wantOwner {
58 | t.Errorf("ownerAndRepository() owner = %s, wantOwner: %s", gotOwner, tt.wantOwner)
59 | }
60 | if gotRepo != tt.wantRepo {
61 | t.Errorf("ownerAndRepository() repo = %s, wantOwner: %s", gotRepo, tt.wantRepo)
62 | }
63 | })
64 | }
65 | }
66 |
67 | func Test_doValidateCmd(t *testing.T) {
68 | tests := map[string]struct {
69 | ctx context.Context
70 | cmd *cobra.Command
71 | vs []validators.Validator
72 | wantErr bool
73 | }{
74 | "returns nil when the validation is success": {
75 | ctx: context.Background(),
76 | cmd: &cobra.Command{},
77 | vs: []validators.Validator{
78 | &mock.Validator{
79 | NameFunc: func() string { return "validator-1" },
80 | ValidateFunc: func(ctx context.Context) (validators.Status, error) {
81 | return &mock.Status{
82 | DetailFunc: func() string { return "success-1" },
83 | IsSuccessFunc: func() bool { return true },
84 | }, nil
85 | },
86 | },
87 | &mock.Validator{
88 | NameFunc: func() string { return "validator-2" },
89 | ValidateFunc: func(ctx context.Context) (validators.Status, error) {
90 | return &mock.Status{
91 | DetailFunc: func() string { return "success-2" },
92 | IsSuccessFunc: func() bool { return true },
93 | }, nil
94 | },
95 | },
96 | },
97 | wantErr: false,
98 | },
99 | "returns error when the validation timed out": {
100 | ctx: context.Background(),
101 | cmd: &cobra.Command{},
102 | vs: []validators.Validator{
103 | &mock.Validator{
104 | NameFunc: func() string { return "validator-1" },
105 | ValidateFunc: func(ctx context.Context) (validators.Status, error) {
106 | return &mock.Status{
107 | DetailFunc: func() string { return "fails-1" },
108 | IsSuccessFunc: func() bool { return false },
109 | }, nil
110 | },
111 | },
112 | &mock.Validator{
113 | NameFunc: func() string { return "validator-2" },
114 | ValidateFunc: func(ctx context.Context) (validators.Status, error) {
115 | return &mock.Status{
116 | DetailFunc: func() string { return "fails-2" },
117 | IsSuccessFunc: func() bool { return false },
118 | }, nil
119 | },
120 | },
121 | },
122 | wantErr: true,
123 | },
124 | "returns error when the validator return an error": {
125 | ctx: context.Background(),
126 | cmd: &cobra.Command{},
127 | vs: []validators.Validator{
128 | &mock.Validator{
129 | NameFunc: func() string { return "validator-1" },
130 | ValidateFunc: func(ctx context.Context) (validators.Status, error) {
131 | return nil, errors.New("err")
132 | },
133 | },
134 | },
135 | wantErr: true,
136 | },
137 | }
138 |
139 | for name, tt := range tests {
140 | t.Run(name, func(t *testing.T) {
141 | if err := doValidateCmd(tt.ctx, tt.cmd, tt.vs...); (err != nil) != tt.wantErr {
142 | t.Errorf("doValidateCmd() error = %v, wantErr %v", err, tt.wantErr)
143 | }
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/internal/github/github.go:
--------------------------------------------------------------------------------
1 | package github
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/google/go-github/v38/github"
7 | "golang.org/x/oauth2"
8 | )
9 |
10 | type (
11 | ListOptions = github.ListOptions
12 | CombinedStatus = github.CombinedStatus
13 | RepoStatus = github.RepoStatus
14 | Response = github.Response
15 | )
16 |
17 | type (
18 | CheckRun = github.CheckRun
19 | ListCheckRunsOptions = github.ListCheckRunsOptions
20 | ListCheckRunsResults = github.ListCheckRunsResults
21 | )
22 |
23 | type Client interface {
24 | GetCombinedStatus(ctx context.Context, owner, repo, ref string, opts *ListOptions) (*CombinedStatus, *Response, error)
25 | ListCheckRunsForRef(ctx context.Context, owner, repo, ref string, opts *ListCheckRunsOptions) (*ListCheckRunsResults, *Response, error)
26 | }
27 |
28 | type client struct {
29 | ghc *github.Client
30 | }
31 |
32 | func NewClient(ctx context.Context, token string) Client {
33 | return &client{
34 | ghc: github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(
35 | &oauth2.Token{
36 | AccessToken: token,
37 | },
38 | ))),
39 | }
40 | }
41 |
42 | func (c *client) GetCombinedStatus(ctx context.Context, owner, repo, ref string, opts *ListOptions) (*CombinedStatus, *Response, error) {
43 | return c.ghc.Repositories.GetCombinedStatus(ctx, owner, repo, ref, opts)
44 | }
45 |
46 | func (c *client) ListCheckRunsForRef(ctx context.Context, owner, repo, ref string, opts *ListCheckRunsOptions) (*ListCheckRunsResults, *Response, error) {
47 | return c.ghc.Checks.ListCheckRunsForRef(ctx, owner, repo, ref, opts)
48 | }
49 |
--------------------------------------------------------------------------------
/internal/github/mock/mock.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/upsidr/merge-gatekeeper/internal/github"
7 | )
8 |
9 | type Client struct {
10 | GetCombinedStatusFunc func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error)
11 | ListCheckRunsForRefFunc func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error)
12 | }
13 |
14 | func (c *Client) GetCombinedStatus(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
15 | return c.GetCombinedStatusFunc(ctx, owner, repo, ref, opts)
16 | }
17 |
18 | func (c *Client) ListCheckRunsForRef(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
19 | return c.ListCheckRunsForRefFunc(ctx, owner, repo, ref, opts)
20 | }
21 |
22 | var (
23 | _ github.Client = &Client{}
24 | )
25 |
--------------------------------------------------------------------------------
/internal/multierror/multierror.go:
--------------------------------------------------------------------------------
1 | package multierror
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | type Errors []error
9 |
10 | func (es Errors) Error() string {
11 | switch len(es) {
12 | case 0:
13 | return ""
14 | case 1:
15 | return fmt.Sprintf("%v", es[0])
16 | }
17 |
18 | rt := "composite error:"
19 | for _, e := range es {
20 | if e != nil {
21 | rt = fmt.Sprintf("%s\n\t%v", rt, e)
22 | }
23 | }
24 | return rt
25 | }
26 |
27 | func (es Errors) Is(target error) bool {
28 | if len(es) == 0 {
29 | return false
30 | }
31 |
32 | for _, e := range es {
33 | if errors.Is(e, target) {
34 | return true
35 | }
36 | }
37 | return false
38 | }
39 |
--------------------------------------------------------------------------------
/internal/ticker/ticker.go:
--------------------------------------------------------------------------------
1 | package ticker
2 |
3 | import (
4 | "sync/atomic"
5 | "time"
6 | )
7 |
8 | type InstantTicker interface {
9 | Stop()
10 | C() <-chan time.Time
11 | }
12 |
13 | type instantTicker struct {
14 | tic *time.Ticker
15 | tch chan time.Time
16 | instTicked int32
17 | stopped int32
18 | }
19 |
20 | func NewInstantTicker(d time.Duration) InstantTicker {
21 | return &instantTicker{
22 | tic: time.NewTicker(d),
23 | tch: make(chan time.Time, 1),
24 | }
25 | }
26 |
27 | func (it *instantTicker) Stop() {
28 | it.tic.Stop()
29 | if atomic.CompareAndSwapInt32(&it.stopped, 0, 1) {
30 | close(it.tch)
31 | }
32 | }
33 |
34 | func (it *instantTicker) C() <-chan time.Time {
35 | if atomic.CompareAndSwapInt32(&it.instTicked, 0, 1) {
36 | if atomic.LoadInt32(&it.stopped) != 1 {
37 | it.tch <- time.Now()
38 | return it.tch
39 | }
40 | }
41 | return it.tic.C
42 | }
43 |
--------------------------------------------------------------------------------
/internal/ticker/ticker_test.go:
--------------------------------------------------------------------------------
1 | package ticker
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestTicker(t *testing.T) {
9 | delta := 500 * time.Millisecond
10 |
11 | ticker := NewInstantTicker(delta)
12 | t1 := time.Now()
13 | t2 := <-ticker.C()
14 | if t1.Sub(t2) > delta {
15 | t.Errorf("not instant tic")
16 | }
17 | <-ticker.C()
18 | defer func() {
19 | if p := recover(); p != nil {
20 | t.Errorf("panic occurs: %v", p)
21 | }
22 | }()
23 | ticker.Stop()
24 | ticker.Stop()
25 | ticker.Stop()
26 |
27 | time.Sleep(2 * delta)
28 |
29 | select {
30 | case <-ticker.C():
31 | t.Error("ticker is not stopped")
32 | default:
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/internal/validators/mock/mock.go:
--------------------------------------------------------------------------------
1 | package mock
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/upsidr/merge-gatekeeper/internal/validators"
7 | )
8 |
9 | type Status struct {
10 | DetailFunc func() string
11 | IsSuccessFunc func() bool
12 | }
13 |
14 | func (s *Status) Detail() string {
15 | return s.DetailFunc()
16 | }
17 |
18 | func (s *Status) IsSuccess() bool {
19 | return s.IsSuccessFunc()
20 | }
21 |
22 | type Validator struct {
23 | NameFunc func() string
24 | ValidateFunc func(ctx context.Context) (validators.Status, error)
25 | }
26 |
27 | func (v *Validator) Name() string {
28 | return v.NameFunc()
29 | }
30 |
31 | func (v *Validator) Validate(ctx context.Context) (validators.Status, error) {
32 | return v.ValidateFunc(ctx)
33 | }
34 |
35 | var (
36 | _ validators.Validator = &Validator{}
37 | _ validators.Status = &Status{}
38 | )
39 |
--------------------------------------------------------------------------------
/internal/validators/status/option.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import "strings"
4 |
5 | type Option func(s *statusValidator)
6 |
7 | func WithSelfJob(name string) Option {
8 | return func(s *statusValidator) {
9 | if len(name) != 0 {
10 | s.selfJobName = name
11 | }
12 | }
13 | }
14 |
15 | func WithGitHubOwnerAndRepo(owner, repo string) Option {
16 | return func(s *statusValidator) {
17 | if len(owner) != 0 {
18 | s.owner = owner
19 | }
20 | if len(repo) != 0 {
21 | s.repo = repo
22 | }
23 | }
24 | }
25 |
26 | func WithGitHubRef(ref string) Option {
27 | return func(s *statusValidator) {
28 | if len(ref) != 0 {
29 | s.ref = ref
30 | }
31 | }
32 | }
33 |
34 | func WithIgnoredJobs(names string) Option {
35 | return func(s *statusValidator) {
36 | // TODO: Add more input validation, such as "," should not be a valid input.
37 | if len(names) == 0 {
38 | return // TODO: Return some clearer error
39 | }
40 |
41 | jobs := []string{}
42 | ss := strings.Split(names, ",")
43 | for _, s := range ss {
44 | jobName := strings.TrimSpace(s)
45 | if len(jobName) == 0 {
46 | continue // TODO: Provide more clue to users
47 | }
48 | jobs = append(jobs, jobName)
49 | }
50 | s.ignoredJobs = jobs
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/internal/validators/status/status.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import "fmt"
4 |
5 | type status struct {
6 | totalJobs []string
7 | completeJobs []string
8 | errJobs []string
9 | ignoredJobs []string
10 | succeeded bool
11 | }
12 |
13 | func prettyPrintJobList(jobs []string) string {
14 | result := ""
15 | if len(jobs) == 0 {
16 | result = "[]"
17 | }
18 | for i, job := range jobs {
19 | result += fmt.Sprintf("- %s", job)
20 | if i != len(jobs)-1 {
21 | result += "\n"
22 | }
23 | }
24 |
25 | return result
26 | }
27 |
28 | func (s *status) Detail() string {
29 | result := fmt.Sprintf(
30 | `%d out of %d
31 |
32 | Total job count: %d
33 | Completed job count: %d
34 | Incompleted job count: %d
35 | Failed job count: %d
36 | Ignored job count: %d
37 | `,
38 | len(s.completeJobs), len(s.totalJobs),
39 | len(s.totalJobs),
40 | len(s.completeJobs),
41 | len(s.getIncompleteJobs()),
42 | len(s.errJobs),
43 | len(s.ignoredJobs),
44 | )
45 |
46 | result = fmt.Sprintf(`%s
47 | ::group::Failed jobs
48 | %s
49 | ::endgroup::
50 |
51 | ::group::Completed jobs
52 | %s
53 | ::endgroup::
54 |
55 | ::group::Incomplete jobs
56 | %s
57 | ::endgroup::
58 |
59 | ::group::Ignored jobs
60 | %s
61 | ::endgroup::
62 |
63 | ::group::All jobs
64 | %s
65 | ::endgroup::
66 | `,
67 | result,
68 | prettyPrintJobList(s.errJobs),
69 | prettyPrintJobList(s.completeJobs),
70 | prettyPrintJobList(s.getIncompleteJobs()),
71 | prettyPrintJobList(s.ignoredJobs),
72 | prettyPrintJobList(s.totalJobs),
73 | )
74 |
75 | return result
76 | }
77 |
78 | func (s *status) IsSuccess() bool {
79 | // TDOO: Add test case
80 | return s.succeeded
81 | }
82 |
83 | func (s *status) getIncompleteJobs() []string {
84 | var incomplete []string
85 |
86 | for _, job := range s.totalJobs {
87 | found := false
88 | for _, complete := range s.completeJobs {
89 | if job == complete {
90 | found = true
91 | break
92 | }
93 | }
94 |
95 | for _, failed := range s.errJobs {
96 | if job == failed {
97 | found = true
98 | break
99 | }
100 | }
101 |
102 | for _, ignored := range s.ignoredJobs {
103 | if job == ignored {
104 | found = true
105 | break
106 | }
107 | }
108 | if !found {
109 | incomplete = append(incomplete, job)
110 | }
111 | }
112 | return incomplete
113 | }
114 |
--------------------------------------------------------------------------------
/internal/validators/status/status_test.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_status_Detail(t *testing.T) {
8 | tests := map[string]struct {
9 | s *status
10 | want string
11 | }{
12 | "return detail when totalJobs and completeJobs and errJobs is not empty": {
13 | s: &status{
14 | totalJobs: []string{
15 | "job-1",
16 | "job-2",
17 | "job-3",
18 | },
19 | completeJobs: []string{
20 | "job-2",
21 | },
22 | errJobs: []string{
23 | "job-3",
24 | },
25 | },
26 | want: `1 out of 3
27 |
28 | Total job count: 3
29 | Completed job count: 1
30 | Incompleted job count: 1
31 | Failed job count: 1
32 | Ignored job count: 0
33 |
34 | ::group::Failed jobs
35 | - job-3
36 | ::endgroup::
37 |
38 | ::group::Completed jobs
39 | - job-2
40 | ::endgroup::
41 |
42 | ::group::Incomplete jobs
43 | - job-1
44 | ::endgroup::
45 |
46 | ::group::Ignored jobs
47 | []
48 | ::endgroup::
49 |
50 | ::group::All jobs
51 | - job-1
52 | - job-2
53 | - job-3
54 | ::endgroup::
55 | `,
56 | },
57 | "return detail with ignored jobs input": {
58 | s: &status{
59 | totalJobs: []string{
60 | "job-1",
61 | "job-2",
62 | "job-3",
63 | "job-4",
64 | },
65 | completeJobs: []string{
66 | "job-2",
67 | "job-4",
68 | },
69 | errJobs: []string{
70 | "job-3",
71 | },
72 | ignoredJobs: []string{
73 | "job-4",
74 | },
75 | },
76 | want: `2 out of 4
77 |
78 | Total job count: 4
79 | Completed job count: 2
80 | Incompleted job count: 1
81 | Failed job count: 1
82 | Ignored job count: 1
83 |
84 | ::group::Failed jobs
85 | - job-3
86 | ::endgroup::
87 |
88 | ::group::Completed jobs
89 | - job-2
90 | - job-4
91 | ::endgroup::
92 |
93 | ::group::Incomplete jobs
94 | - job-1
95 | ::endgroup::
96 |
97 | ::group::Ignored jobs
98 | - job-4
99 | ::endgroup::
100 |
101 | ::group::All jobs
102 | - job-1
103 | - job-2
104 | - job-3
105 | - job-4
106 | ::endgroup::
107 | `,
108 | },
109 | "return detail when totalJobs and completeJobs is empty": {
110 | s: &status{
111 | totalJobs: []string{},
112 | completeJobs: []string{},
113 | },
114 | want: `0 out of 0
115 |
116 | Total job count: 0
117 | Completed job count: 0
118 | Incompleted job count: 0
119 | Failed job count: 0
120 | Ignored job count: 0
121 |
122 | ::group::Failed jobs
123 | []
124 | ::endgroup::
125 |
126 | ::group::Completed jobs
127 | []
128 | ::endgroup::
129 |
130 | ::group::Incomplete jobs
131 | []
132 | ::endgroup::
133 |
134 | ::group::Ignored jobs
135 | []
136 | ::endgroup::
137 |
138 | ::group::All jobs
139 | []
140 | ::endgroup::
141 | `,
142 | },
143 | }
144 |
145 | for name, tt := range tests {
146 | t.Run(name, func(t *testing.T) {
147 | got := tt.s.Detail()
148 | if got != tt.want {
149 | t.Errorf("status.Detail() didn't match\n got:\n%s\n\n want:\n%s", got, tt.want)
150 | }
151 | })
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/internal/validators/status/validator.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 |
8 | "github.com/upsidr/merge-gatekeeper/internal/github"
9 | "github.com/upsidr/merge-gatekeeper/internal/multierror"
10 | "github.com/upsidr/merge-gatekeeper/internal/validators"
11 | )
12 |
13 | const (
14 | successState = "success"
15 | errorState = "error"
16 | failureState = "failure"
17 | pendingState = "pending"
18 | )
19 |
20 | // NOTE: https://docs.github.com/en/rest/reference/checks
21 | const (
22 | checkRunCompletedStatus = "completed"
23 | )
24 | const (
25 | checkRunNeutralConclusion = "neutral"
26 | checkRunSuccessConclusion = "success"
27 | checkRunSkipConclusion = "skipped"
28 | )
29 |
30 | const (
31 | maxStatusesPerPage = 100
32 | maxCheckRunsPerPage = 100
33 | )
34 |
35 | var (
36 | ErrInvalidCombinedStatusResponse = errors.New("github combined status response is invalid")
37 | ErrInvalidCheckRunResponse = errors.New("github checkRun response is invalid")
38 | )
39 |
40 | type ghaStatus struct {
41 | Job string
42 | State string
43 | }
44 |
45 | type statusValidator struct {
46 | repo string
47 | owner string
48 | ref string
49 | selfJobName string
50 | ignoredJobs []string
51 | client github.Client
52 | }
53 |
54 | func CreateValidator(c github.Client, opts ...Option) (validators.Validator, error) {
55 | sv := &statusValidator{
56 | client: c,
57 | }
58 | for _, opt := range opts {
59 | opt(sv)
60 | }
61 | if err := sv.validateFields(); err != nil {
62 | return nil, err
63 | }
64 | return sv, nil
65 | }
66 |
67 | func (sv *statusValidator) Name() string {
68 | return sv.selfJobName
69 | }
70 |
71 | func (sv *statusValidator) validateFields() error {
72 | errs := make(multierror.Errors, 0, 6)
73 |
74 | if len(sv.repo) == 0 {
75 | errs = append(errs, errors.New("repository name is empty"))
76 | }
77 | if len(sv.owner) == 0 {
78 | errs = append(errs, errors.New("repository owner is empty"))
79 | }
80 | if len(sv.ref) == 0 {
81 | errs = append(errs, errors.New("reference of repository is empty"))
82 | }
83 | if len(sv.selfJobName) == 0 {
84 | errs = append(errs, errors.New("self job name is empty"))
85 | }
86 | if sv.client == nil {
87 | errs = append(errs, errors.New("github client is empty"))
88 | }
89 |
90 | if len(errs) != 0 {
91 | return errs
92 | }
93 |
94 | return nil
95 | }
96 |
97 | func (sv *statusValidator) Validate(ctx context.Context) (validators.Status, error) {
98 | ghaStatuses, err := sv.listGhaStatuses(ctx)
99 | if err != nil {
100 | return nil, err
101 | }
102 |
103 | st := &status{
104 | totalJobs: make([]string, 0, len(ghaStatuses)),
105 | completeJobs: make([]string, 0, len(ghaStatuses)),
106 | errJobs: make([]string, 0, len(ghaStatuses)/2),
107 | ignoredJobs: make([]string, 0, len(ghaStatuses)),
108 | succeeded: true,
109 | }
110 |
111 | st.ignoredJobs = append(st.ignoredJobs, sv.ignoredJobs...)
112 |
113 | var successCnt int
114 | for _, ghaStatus := range ghaStatuses {
115 | var toIgnore bool
116 | for _, ignored := range sv.ignoredJobs {
117 | if ghaStatus.Job == ignored {
118 | toIgnore = true
119 | break
120 | }
121 | }
122 |
123 | // Ignored jobs and this job itself should be considered as success regardless of their statuses.
124 | if toIgnore || ghaStatus.Job == sv.selfJobName {
125 | successCnt++
126 | continue
127 | }
128 |
129 | st.totalJobs = append(st.totalJobs, ghaStatus.Job)
130 |
131 | switch ghaStatus.State {
132 | case successState:
133 | st.completeJobs = append(st.completeJobs, ghaStatus.Job)
134 | successCnt++
135 | case errorState, failureState:
136 | st.errJobs = append(st.errJobs, ghaStatus.Job)
137 | }
138 | }
139 | if len(st.errJobs) != 0 {
140 | return nil, errors.New(st.Detail())
141 | }
142 |
143 | if len(ghaStatuses) != successCnt {
144 | st.succeeded = false
145 | return st, nil
146 | }
147 |
148 | return st, nil
149 | }
150 |
151 | func (sv *statusValidator) getCombinedStatus(ctx context.Context) ([]*github.RepoStatus, error) {
152 | var combined []*github.RepoStatus
153 | page := 1
154 | for {
155 | c, _, err := sv.client.GetCombinedStatus(ctx, sv.owner, sv.repo, sv.ref, &github.ListOptions{PerPage: maxStatusesPerPage, Page: page})
156 | if err != nil {
157 | return nil, err
158 | }
159 | combined = append(combined, c.Statuses...)
160 | if c.GetTotalCount() < maxStatusesPerPage {
161 | break
162 | }
163 | page++
164 | }
165 | return combined, nil
166 | }
167 |
168 | func (sv *statusValidator) listCheckRunsForRef(ctx context.Context) ([]*github.CheckRun, error) {
169 | var runResults []*github.CheckRun
170 | page := 1
171 | for {
172 | cr, _, err := sv.client.ListCheckRunsForRef(ctx, sv.owner, sv.repo, sv.ref, &github.ListCheckRunsOptions{ListOptions: github.ListOptions{
173 | Page: page,
174 | PerPage: maxCheckRunsPerPage,
175 | }})
176 | if err != nil {
177 | return nil, err
178 | }
179 | runResults = append(runResults, cr.CheckRuns...)
180 | if cr.GetTotal() <= len(runResults) {
181 | break
182 | }
183 | page++
184 | }
185 | return runResults, nil
186 | }
187 |
188 | func (sv *statusValidator) listGhaStatuses(ctx context.Context) ([]*ghaStatus, error) {
189 | combined, err := sv.getCombinedStatus(ctx)
190 | if err != nil {
191 | return nil, err
192 | }
193 |
194 | // Because multiple jobs with the same name may exist when jobs are created dynamically by third-party tools, etc.,
195 | // only the latest job should be managed.
196 | currentJobs := make(map[string]struct{})
197 |
198 | ghaStatuses := make([]*ghaStatus, 0, len(combined))
199 | for _, s := range combined {
200 | if s.Context == nil || s.State == nil {
201 | return nil, fmt.Errorf("%w context: %v, status: %v", ErrInvalidCombinedStatusResponse, s.Context, s.State)
202 | }
203 | if _, ok := currentJobs[*s.Context]; ok {
204 | continue
205 | }
206 | currentJobs[*s.Context] = struct{}{}
207 |
208 | ghaStatuses = append(ghaStatuses, &ghaStatus{
209 | Job: *s.Context,
210 | State: *s.State,
211 | })
212 | }
213 |
214 | runResults, err := sv.listCheckRunsForRef(ctx)
215 | if err != nil {
216 | return nil, err
217 | }
218 |
219 | for _, run := range runResults {
220 | if run.Name == nil || run.Status == nil {
221 | return nil, fmt.Errorf("%w name: %v, status: %v", ErrInvalidCheckRunResponse, run.Name, run.Status)
222 | }
223 | if _, ok := currentJobs[*run.Name]; ok {
224 | continue
225 | }
226 | currentJobs[*run.Name] = struct{}{}
227 |
228 | ghaStatus := &ghaStatus{
229 | Job: *run.Name,
230 | }
231 |
232 | if *run.Status != checkRunCompletedStatus {
233 | ghaStatus.State = pendingState
234 | ghaStatuses = append(ghaStatuses, ghaStatus)
235 | continue
236 | }
237 |
238 | switch *run.Conclusion {
239 | case checkRunNeutralConclusion, checkRunSuccessConclusion:
240 | ghaStatus.State = successState
241 | case checkRunSkipConclusion:
242 | continue
243 | default:
244 | ghaStatus.State = errorState
245 | }
246 | ghaStatuses = append(ghaStatuses, ghaStatus)
247 | }
248 |
249 | return ghaStatuses, nil
250 | }
251 |
--------------------------------------------------------------------------------
/internal/validators/status/validator_test.go:
--------------------------------------------------------------------------------
1 | package status
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "reflect"
8 | "testing"
9 |
10 | "github.com/upsidr/merge-gatekeeper/internal/github"
11 | "github.com/upsidr/merge-gatekeeper/internal/github/mock"
12 | "github.com/upsidr/merge-gatekeeper/internal/validators"
13 | )
14 |
15 | func stringPtr(str string) *string {
16 | return &str
17 | }
18 |
19 | func min(a, b int) int {
20 | if a < b {
21 | return a
22 | }
23 | return b
24 | }
25 |
26 | func TestCreateValidator(t *testing.T) {
27 | tests := map[string]struct {
28 | c github.Client
29 | opts []Option
30 | want validators.Validator
31 | wantErr bool
32 | }{
33 | "returns Validator when option is not empty": {
34 | c: &mock.Client{},
35 | opts: []Option{
36 | WithGitHubOwnerAndRepo("test-owner", "test-repo"),
37 | WithGitHubRef("sha"),
38 | WithSelfJob("job"),
39 | WithIgnoredJobs("job-01,job-02"),
40 | },
41 | want: &statusValidator{
42 | client: &mock.Client{},
43 | owner: "test-owner",
44 | repo: "test-repo",
45 | ref: "sha",
46 | selfJobName: "job",
47 | ignoredJobs: []string{"job-01", "job-02"},
48 | },
49 | wantErr: false,
50 | },
51 | "returns Validator when there are duplicate options": {
52 | c: &mock.Client{},
53 | opts: []Option{
54 | WithGitHubOwnerAndRepo("test", "test-repo"),
55 | WithGitHubRef("sha"),
56 | WithGitHubRef("sha-01"),
57 | WithSelfJob("job"),
58 | WithSelfJob("job-01"),
59 | },
60 | want: &statusValidator{
61 | client: &mock.Client{},
62 | owner: "test",
63 | repo: "test-repo",
64 | ref: "sha-01",
65 | selfJobName: "job-01",
66 | },
67 | wantErr: false,
68 | },
69 | "returns Validator when invalid string is provided for ignored jobs": {
70 | c: &mock.Client{},
71 | opts: []Option{
72 | WithGitHubOwnerAndRepo("test", "test-repo"),
73 | WithGitHubRef("sha"),
74 | WithGitHubRef("sha-01"),
75 | WithSelfJob("job"),
76 | WithSelfJob("job-01"),
77 | WithIgnoredJobs(","), // Malformed but handled
78 | },
79 | want: &statusValidator{
80 | client: &mock.Client{},
81 | owner: "test",
82 | repo: "test-repo",
83 | ref: "sha-01",
84 | selfJobName: "job-01",
85 | ignoredJobs: []string{}, // Not nil
86 | },
87 | wantErr: false,
88 | },
89 | "returns error when option is empty": {
90 | c: &mock.Client{},
91 | want: nil,
92 | wantErr: true,
93 | },
94 | "returns error when client is nil": {
95 | c: nil,
96 | opts: []Option{
97 | WithGitHubOwnerAndRepo("test", "test-repo"),
98 | WithGitHubRef("sha"),
99 | WithGitHubRef("sha-01"),
100 | WithSelfJob("job"),
101 | WithSelfJob("job-01"),
102 | },
103 | want: nil,
104 | wantErr: true,
105 | },
106 | }
107 | for name, tt := range tests {
108 | t.Run(name, func(t *testing.T) {
109 | got, err := CreateValidator(tt.c, tt.opts...)
110 | if (err != nil) != tt.wantErr {
111 | t.Errorf("CreateValidator error = %v, wantErr: %v", err, tt.wantErr)
112 | return
113 | }
114 | if !reflect.DeepEqual(got, tt.want) {
115 | t.Errorf("CreateValidator() = %v, want %v", got, tt.want)
116 | }
117 | })
118 | }
119 | }
120 |
121 | func TestName(t *testing.T) {
122 | tests := map[string]struct {
123 | c github.Client
124 | opts []Option
125 | want string
126 | }{
127 | "Name returns the correct job name which gets overridden": {
128 | c: &mock.Client{},
129 | opts: []Option{
130 | WithGitHubOwnerAndRepo("test-owner", "test-repo"),
131 | WithGitHubRef("sha"),
132 | WithSelfJob("job"),
133 | WithIgnoredJobs("job-01,job-02"),
134 | },
135 | want: "job",
136 | },
137 | }
138 | for name, tt := range tests {
139 | t.Run(name, func(t *testing.T) {
140 | got, err := CreateValidator(tt.c, tt.opts...)
141 | if err != nil {
142 | t.Errorf("Unexpected error with CreateValidator: %v", err)
143 | return
144 | }
145 | if tt.want != got.Name() {
146 | t.Errorf("Job name didn't match, want: %s, got: %v", tt.want, got.Name())
147 | }
148 | })
149 | }
150 | }
151 |
152 | func Test_statusValidator_Validate(t *testing.T) {
153 | type test struct {
154 | selfJobName string
155 | ignoredJobs []string
156 | client github.Client
157 | ctx context.Context
158 | wantErr bool
159 | wantErrStr string
160 | wantStatus validators.Status
161 | }
162 | tests := map[string]test{
163 | "returns error when listGhaStatuses return an error": {
164 | client: &mock.Client{
165 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
166 | return nil, nil, errors.New("err")
167 | },
168 | },
169 | wantErr: true,
170 | wantStatus: nil,
171 | wantErrStr: "err",
172 | },
173 | "returns succeeded status and nil when there is no job": {
174 | client: &mock.Client{
175 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
176 | return &github.CombinedStatus{}, nil, nil
177 | },
178 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
179 | return &github.ListCheckRunsResults{}, nil, nil
180 | },
181 | },
182 | wantErr: false,
183 | wantStatus: &status{
184 | succeeded: true,
185 | totalJobs: []string{},
186 | completeJobs: []string{},
187 | ignoredJobs: []string{},
188 | errJobs: []string{},
189 | },
190 | },
191 | "returns succeeded status and nil when there is one job, which is itself": {
192 | selfJobName: "self-job",
193 | client: &mock.Client{
194 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
195 | return &github.CombinedStatus{
196 | Statuses: []*github.RepoStatus{
197 | {
198 | Context: stringPtr("self-job"),
199 | State: stringPtr(pendingState), // should be irrelevant
200 | },
201 | },
202 | }, nil, nil
203 | },
204 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
205 | return &github.ListCheckRunsResults{}, nil, nil
206 | },
207 | },
208 | wantErr: false,
209 | wantStatus: &status{
210 | succeeded: true,
211 | totalJobs: []string{},
212 | completeJobs: []string{},
213 | ignoredJobs: []string{},
214 | errJobs: []string{},
215 | },
216 | },
217 | "returns failed status and nil when there is one job": {
218 | client: &mock.Client{
219 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
220 | return &github.CombinedStatus{
221 | Statuses: []*github.RepoStatus{
222 | {
223 | Context: stringPtr("job"),
224 | State: stringPtr(pendingState),
225 | },
226 | },
227 | }, nil, nil
228 | },
229 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
230 | return &github.ListCheckRunsResults{}, nil, nil
231 | },
232 | },
233 | wantErr: false,
234 | wantStatus: &status{
235 | succeeded: false,
236 | totalJobs: []string{"job"},
237 | completeJobs: []string{},
238 | ignoredJobs: []string{},
239 | errJobs: []string{},
240 | },
241 | },
242 | "returns error when there is a failed job": {
243 | selfJobName: "self-job",
244 | client: &mock.Client{
245 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
246 | return &github.CombinedStatus{
247 | Statuses: []*github.RepoStatus{
248 | {
249 | Context: stringPtr("job-01"),
250 | State: stringPtr(successState),
251 | },
252 | {
253 | Context: stringPtr("job-02"),
254 | State: stringPtr(errorState),
255 | },
256 | {
257 | Context: stringPtr("self-job"),
258 | State: stringPtr(pendingState),
259 | },
260 | },
261 | }, nil, nil
262 | },
263 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
264 | return &github.ListCheckRunsResults{}, nil, nil
265 | },
266 | },
267 | wantErr: true,
268 | wantErrStr: (&status{
269 | totalJobs: []string{
270 | "job-01", "job-02",
271 | },
272 | completeJobs: []string{
273 | "job-01",
274 | },
275 | errJobs: []string{
276 | "job-02",
277 | },
278 | ignoredJobs: []string{},
279 | }).Detail(),
280 | },
281 | "returns error when there is a failed job with failure state": {
282 | selfJobName: "self-job",
283 | client: &mock.Client{
284 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
285 | return &github.CombinedStatus{
286 | Statuses: []*github.RepoStatus{
287 | {
288 | Context: stringPtr("job-01"),
289 | State: stringPtr(successState),
290 | },
291 | {
292 | Context: stringPtr("job-02"),
293 | State: stringPtr(failureState),
294 | },
295 | {
296 | Context: stringPtr("self-job"),
297 | State: stringPtr(pendingState),
298 | },
299 | },
300 | }, nil, nil
301 | },
302 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
303 | return &github.ListCheckRunsResults{}, nil, nil
304 | },
305 | },
306 | wantErr: true,
307 | wantErrStr: (&status{
308 | totalJobs: []string{
309 | "job-01", "job-02",
310 | },
311 | completeJobs: []string{
312 | "job-01",
313 | },
314 | errJobs: []string{
315 | "job-02",
316 | },
317 | ignoredJobs: []string{},
318 | }).Detail(),
319 | },
320 | "returns failed status and nil when successful job count is less than total": {
321 | selfJobName: "self-job",
322 | client: &mock.Client{
323 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
324 | return &github.CombinedStatus{
325 | Statuses: []*github.RepoStatus{
326 | {
327 | Context: stringPtr("job-01"),
328 | State: stringPtr(successState),
329 | },
330 | {
331 | Context: stringPtr("job-02"),
332 | State: stringPtr(pendingState),
333 | },
334 | {
335 | Context: stringPtr("self-job"),
336 | State: stringPtr(pendingState),
337 | },
338 | },
339 | }, nil, nil
340 | },
341 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
342 | return &github.ListCheckRunsResults{}, nil, nil
343 | },
344 | },
345 | wantErr: false,
346 | wantStatus: &status{
347 | succeeded: false,
348 | totalJobs: []string{
349 | "job-01",
350 | "job-02",
351 | },
352 | completeJobs: []string{
353 | "job-01",
354 | },
355 | errJobs: []string{},
356 | ignoredJobs: []string{},
357 | },
358 | },
359 | "returns succeeded status and nil when validation is success": {
360 | selfJobName: "self-job",
361 | client: &mock.Client{
362 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
363 | return &github.CombinedStatus{
364 | Statuses: []*github.RepoStatus{
365 | {
366 | Context: stringPtr("job-01"),
367 | State: stringPtr(successState),
368 | },
369 | {
370 | Context: stringPtr("job-02"),
371 | State: stringPtr(successState),
372 | },
373 | {
374 | Context: stringPtr("self-job"),
375 | State: stringPtr(pendingState),
376 | },
377 | },
378 | }, nil, nil
379 | },
380 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
381 | return &github.ListCheckRunsResults{}, nil, nil
382 | },
383 | },
384 | wantErr: false,
385 | wantStatus: &status{
386 | succeeded: true,
387 | totalJobs: []string{
388 | "job-01",
389 | "job-02",
390 | },
391 | completeJobs: []string{
392 | "job-01",
393 | "job-02",
394 | },
395 | errJobs: []string{},
396 | ignoredJobs: []string{},
397 | },
398 | },
399 | "returns succeeded status and nil when only an ignored job is failing": {
400 | selfJobName: "self-job",
401 | ignoredJobs: []string{"job-02", "job-03"}, // String input here should be already TrimSpace'd
402 | client: &mock.Client{
403 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
404 | return &github.CombinedStatus{
405 | Statuses: []*github.RepoStatus{
406 | {
407 | Context: stringPtr("job-01"),
408 | State: stringPtr(successState),
409 | },
410 | {
411 | Context: stringPtr("job-02"),
412 | State: stringPtr(errorState),
413 | },
414 | {
415 | Context: stringPtr("self-job"),
416 | State: stringPtr(pendingState),
417 | },
418 | },
419 | }, nil, nil
420 | },
421 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
422 | return &github.ListCheckRunsResults{}, nil, nil
423 | },
424 | },
425 | wantErr: false,
426 | wantStatus: &status{
427 | succeeded: true,
428 | totalJobs: []string{"job-01"},
429 | completeJobs: []string{"job-01"},
430 | errJobs: []string{},
431 | ignoredJobs: []string{"job-02", "job-03"},
432 | },
433 | },
434 | "returns succeeded status and nil when only an ignored job is failing, with failure state": {
435 | selfJobName: "self-job",
436 | ignoredJobs: []string{"job-02", "job-03"},
437 | client: &mock.Client{
438 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
439 | return &github.CombinedStatus{
440 | Statuses: []*github.RepoStatus{
441 | {
442 | Context: stringPtr("job-01"),
443 | State: stringPtr(successState),
444 | },
445 | {
446 | Context: stringPtr("job-02"),
447 | State: stringPtr(failureState),
448 | },
449 | {
450 | Context: stringPtr("self-job"),
451 | State: stringPtr(pendingState),
452 | },
453 | },
454 | }, nil, nil
455 | },
456 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
457 | return &github.ListCheckRunsResults{}, nil, nil
458 | },
459 | },
460 | wantErr: false,
461 | wantStatus: &status{
462 | succeeded: true,
463 | totalJobs: []string{"job-01"},
464 | completeJobs: []string{"job-01"},
465 | errJobs: []string{},
466 | ignoredJobs: []string{"job-02", "job-03"},
467 | },
468 | },
469 | }
470 | for name, tt := range tests {
471 | t.Run(name, func(t *testing.T) {
472 | sv := &statusValidator{
473 | selfJobName: tt.selfJobName,
474 | ignoredJobs: tt.ignoredJobs,
475 | client: tt.client,
476 | }
477 | got, err := sv.Validate(tt.ctx)
478 | if (err != nil) != tt.wantErr {
479 | t.Errorf("statusValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
480 | return
481 | }
482 | if tt.wantErr {
483 | if err.Error() != tt.wantErrStr {
484 | t.Errorf("statusValidator.Validate() error.Error() = %s, wantErrStr %s", err.Error(), tt.wantErrStr)
485 | }
486 | }
487 | if !reflect.DeepEqual(got, tt.wantStatus) {
488 | t.Errorf("statusValidator.Validate() status = %v, want %v", got, tt.wantStatus)
489 | }
490 | })
491 | }
492 | }
493 |
494 | func Test_statusValidator_listStatuses(t *testing.T) {
495 | type fields struct {
496 | repo string
497 | owner string
498 | ref string
499 | selfJobName string
500 | client github.Client
501 | }
502 | type test struct {
503 | fields fields
504 | ctx context.Context
505 | wantErr bool
506 | want []*ghaStatus
507 | }
508 | tests := map[string]test{
509 | "succeeds to get job statuses even if the same job exists": func() test {
510 | c := &mock.Client{
511 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
512 | return &github.CombinedStatus{
513 | Statuses: []*github.RepoStatus{
514 | // The first element here is the latest state.
515 | {
516 | Context: stringPtr("job-01"),
517 | State: stringPtr(successState),
518 | },
519 | {
520 | Context: stringPtr("job-01"), // Same as above job name, and thus should be disregarded as old job status.
521 | State: stringPtr(errorState),
522 | },
523 | },
524 | }, nil, nil
525 | },
526 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
527 | return &github.ListCheckRunsResults{
528 | CheckRuns: []*github.CheckRun{
529 | // The first element here is the latest state.
530 | {
531 | Name: stringPtr("job-02"),
532 | Status: stringPtr("failure"),
533 | },
534 | {
535 | Name: stringPtr("job-02"), // Same as above job name, and thus should be disregarded as old job status.
536 | Status: stringPtr(checkRunCompletedStatus),
537 | Conclusion: stringPtr(checkRunNeutralConclusion),
538 | },
539 | {
540 | Name: stringPtr("job-03"),
541 | Status: stringPtr(checkRunCompletedStatus),
542 | Conclusion: stringPtr(checkRunNeutralConclusion),
543 | },
544 | {
545 | Name: stringPtr("job-04"),
546 | Status: stringPtr(checkRunCompletedStatus),
547 | Conclusion: stringPtr(checkRunSuccessConclusion),
548 | },
549 | {
550 | Name: stringPtr("job-05"),
551 | Status: stringPtr(checkRunCompletedStatus),
552 | Conclusion: stringPtr("failure"),
553 | },
554 | {
555 | Name: stringPtr("job-06"),
556 | Status: stringPtr(checkRunCompletedStatus),
557 | Conclusion: stringPtr(checkRunSkipConclusion),
558 | },
559 | },
560 | }, nil, nil
561 | },
562 | }
563 | return test{
564 | fields: fields{
565 | client: c,
566 | selfJobName: "self-job",
567 | owner: "test-owner",
568 | repo: "test-repo",
569 | ref: "main",
570 | },
571 | wantErr: false,
572 | want: []*ghaStatus{
573 | {
574 | Job: "job-01",
575 | State: successState,
576 | },
577 | {
578 | Job: "job-02",
579 | State: pendingState,
580 | },
581 | {
582 | Job: "job-03",
583 | State: successState,
584 | },
585 | {
586 | Job: "job-04",
587 | State: successState,
588 | },
589 | {
590 | Job: "job-05",
591 | State: errorState,
592 | },
593 | },
594 | }
595 | }(),
596 | "returns error when the GetCombinedStatus returns an error": func() test {
597 | c := &mock.Client{
598 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
599 | return nil, nil, errors.New("err")
600 | },
601 | }
602 | return test{
603 | fields: fields{
604 | client: c,
605 | selfJobName: "self-job",
606 | owner: "test-owner",
607 | repo: "test-repo",
608 | ref: "main",
609 | },
610 | wantErr: true,
611 | }
612 | }(),
613 | "returns error when the GetCombinedStatus response is invalid": func() test {
614 | c := &mock.Client{
615 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
616 | return &github.CombinedStatus{
617 | Statuses: []*github.RepoStatus{
618 | {},
619 | },
620 | }, nil, nil
621 | },
622 | }
623 | return test{
624 | fields: fields{
625 | client: c,
626 | selfJobName: "self-job",
627 | owner: "test-owner",
628 | repo: "test-repo",
629 | ref: "main",
630 | },
631 | wantErr: true,
632 | }
633 | }(),
634 | "returns error when the ListCheckRunsForRef returns an error": func() test {
635 | c := &mock.Client{
636 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
637 | return &github.CombinedStatus{}, nil, nil
638 | },
639 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
640 | return nil, nil, errors.New("error")
641 | },
642 | }
643 | return test{
644 | fields: fields{
645 | client: c,
646 | selfJobName: "self-job",
647 | owner: "test-owner",
648 | repo: "test-repo",
649 | ref: "main",
650 | },
651 | wantErr: true,
652 | }
653 | }(),
654 | "returns error when the ListCheckRunsForRef response is invalid": func() test {
655 | c := &mock.Client{
656 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
657 | return &github.CombinedStatus{}, nil, nil
658 | },
659 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
660 | return &github.ListCheckRunsResults{
661 | CheckRuns: []*github.CheckRun{
662 | {},
663 | },
664 | }, nil, nil
665 | },
666 | }
667 | return test{
668 | fields: fields{
669 | client: c,
670 | selfJobName: "self-job",
671 | owner: "test-owner",
672 | repo: "test-repo",
673 | ref: "main",
674 | },
675 | wantErr: true,
676 | }
677 | }(),
678 | "returns nil when no error occurs": func() test {
679 | c := &mock.Client{
680 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
681 | return &github.CombinedStatus{
682 | Statuses: []*github.RepoStatus{
683 | {
684 | Context: stringPtr("job-01"),
685 | State: stringPtr(successState),
686 | },
687 | },
688 | }, nil, nil
689 | },
690 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
691 | return &github.ListCheckRunsResults{
692 | CheckRuns: []*github.CheckRun{
693 | {
694 | Name: stringPtr("job-02"),
695 | Status: stringPtr("failure"),
696 | },
697 | {
698 | Name: stringPtr("job-03"),
699 | Status: stringPtr(checkRunCompletedStatus),
700 | Conclusion: stringPtr(checkRunNeutralConclusion),
701 | },
702 | {
703 | Name: stringPtr("job-04"),
704 | Status: stringPtr(checkRunCompletedStatus),
705 | Conclusion: stringPtr(checkRunSuccessConclusion),
706 | },
707 | {
708 | Name: stringPtr("job-05"),
709 | Status: stringPtr(checkRunCompletedStatus),
710 | Conclusion: stringPtr("failure"),
711 | },
712 | {
713 | Name: stringPtr("job-06"),
714 | Status: stringPtr(checkRunCompletedStatus),
715 | Conclusion: stringPtr(checkRunSkipConclusion),
716 | },
717 | },
718 | }, nil, nil
719 | },
720 | }
721 | return test{
722 | fields: fields{
723 | client: c,
724 | selfJobName: "self-job",
725 | owner: "test-owner",
726 | repo: "test-repo",
727 | ref: "main",
728 | },
729 | wantErr: false,
730 | want: []*ghaStatus{
731 | {
732 | Job: "job-01",
733 | State: successState,
734 | },
735 | {
736 | Job: "job-02",
737 | State: pendingState,
738 | },
739 | {
740 | Job: "job-03",
741 | State: successState,
742 | },
743 | {
744 | Job: "job-04",
745 | State: successState,
746 | },
747 | {
748 | Job: "job-05",
749 | State: errorState,
750 | },
751 | },
752 | }
753 | }(),
754 | "succeeds to retrieve 100 statuses": func() test {
755 | num_statuses := 100
756 | statuses := make([]*github.RepoStatus, num_statuses)
757 | checkRuns := make([]*github.CheckRun, num_statuses)
758 | expectedGhaStatuses := make([]*ghaStatus, num_statuses)
759 | for i := 0; i < num_statuses; i++ {
760 | statuses[i] = &github.RepoStatus{
761 | Context: stringPtr(fmt.Sprintf("job-%d", i)),
762 | State: stringPtr(successState),
763 | }
764 |
765 | checkRuns[i] = &github.CheckRun{
766 | Name: stringPtr(fmt.Sprintf("job-%d", i)),
767 | Status: stringPtr(checkRunCompletedStatus),
768 | Conclusion: stringPtr(checkRunNeutralConclusion),
769 | }
770 |
771 | expectedGhaStatuses[i] = &ghaStatus{
772 | Job: fmt.Sprintf("job-%d", i),
773 | State: successState,
774 | }
775 | }
776 |
777 | c := &mock.Client{
778 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
779 | max := min(opts.Page*opts.PerPage, len(statuses))
780 | sts := statuses[(opts.Page-1)*opts.PerPage : max]
781 | l := len(sts)
782 | return &github.CombinedStatus{
783 | Statuses: sts,
784 | TotalCount: &l,
785 | }, nil, nil
786 | },
787 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
788 | l := len(checkRuns)
789 | return &github.ListCheckRunsResults{
790 | CheckRuns: checkRuns,
791 | Total: &l,
792 | }, nil, nil
793 | },
794 | }
795 | return test{
796 | fields: fields{
797 | client: c,
798 | selfJobName: "self-job",
799 | owner: "test-owner",
800 | repo: "test-repo",
801 | ref: "main",
802 | },
803 | wantErr: false,
804 | want: expectedGhaStatuses,
805 | }
806 | }(),
807 | "succeeds to retrieve 162 statuses": func() test {
808 | num_statuses := 162
809 | statuses := make([]*github.RepoStatus, num_statuses)
810 | checkRuns := make([]*github.CheckRun, num_statuses)
811 | expectedGhaStatuses := make([]*ghaStatus, num_statuses)
812 | for i := 0; i < num_statuses; i++ {
813 | statuses[i] = &github.RepoStatus{
814 | Context: stringPtr(fmt.Sprintf("job-%d", i)),
815 | State: stringPtr(successState),
816 | }
817 |
818 | checkRuns[i] = &github.CheckRun{
819 | Name: stringPtr(fmt.Sprintf("job-%d", i)),
820 | Status: stringPtr(checkRunCompletedStatus),
821 | Conclusion: stringPtr(checkRunNeutralConclusion),
822 | }
823 |
824 | expectedGhaStatuses[i] = &ghaStatus{
825 | Job: fmt.Sprintf("job-%d", i),
826 | State: successState,
827 | }
828 | }
829 |
830 | c := &mock.Client{
831 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
832 | max := min(opts.Page*opts.PerPage, len(statuses))
833 | sts := statuses[(opts.Page-1)*opts.PerPage : max]
834 | l := len(sts)
835 | return &github.CombinedStatus{
836 | Statuses: sts,
837 | TotalCount: &l,
838 | }, nil, nil
839 | },
840 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
841 | l := len(checkRuns)
842 | return &github.ListCheckRunsResults{
843 | CheckRuns: checkRuns,
844 | Total: &l,
845 | }, nil, nil
846 | },
847 | }
848 | return test{
849 | fields: fields{
850 | client: c,
851 | selfJobName: "self-job",
852 | owner: "test-owner",
853 | repo: "test-repo",
854 | ref: "main",
855 | },
856 | wantErr: false,
857 | want: expectedGhaStatuses,
858 | }
859 | }(),
860 | "succeeds to retrieve 587 statuses": func() test {
861 | num_statuses := 587
862 | statuses := make([]*github.RepoStatus, num_statuses)
863 | checkRuns := make([]*github.CheckRun, num_statuses)
864 | expectedGhaStatuses := make([]*ghaStatus, num_statuses)
865 | for i := 0; i < num_statuses; i++ {
866 | statuses[i] = &github.RepoStatus{
867 | Context: stringPtr(fmt.Sprintf("job-%d", i)),
868 | State: stringPtr(successState),
869 | }
870 |
871 | checkRuns[i] = &github.CheckRun{
872 | Name: stringPtr(fmt.Sprintf("job-%d", i)),
873 | Status: stringPtr(checkRunCompletedStatus),
874 | Conclusion: stringPtr(checkRunNeutralConclusion),
875 | }
876 |
877 | expectedGhaStatuses[i] = &ghaStatus{
878 | Job: fmt.Sprintf("job-%d", i),
879 | State: successState,
880 | }
881 | }
882 |
883 | c := &mock.Client{
884 | GetCombinedStatusFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListOptions) (*github.CombinedStatus, *github.Response, error) {
885 | max := min(opts.Page*opts.PerPage, len(statuses))
886 | sts := statuses[(opts.Page-1)*opts.PerPage : max]
887 | l := len(sts)
888 | return &github.CombinedStatus{
889 | Statuses: sts,
890 | TotalCount: &l,
891 | }, nil, nil
892 | },
893 | ListCheckRunsForRefFunc: func(ctx context.Context, owner, repo, ref string, opts *github.ListCheckRunsOptions) (*github.ListCheckRunsResults, *github.Response, error) {
894 | l := len(checkRuns)
895 | return &github.ListCheckRunsResults{
896 | CheckRuns: checkRuns,
897 | Total: &l,
898 | }, nil, nil
899 | },
900 | }
901 | return test{
902 | fields: fields{
903 | client: c,
904 | selfJobName: "self-job",
905 | owner: "test-owner",
906 | repo: "test-repo",
907 | ref: "main",
908 | },
909 | wantErr: false,
910 | want: expectedGhaStatuses,
911 | }
912 | }(),
913 | }
914 | for name, tt := range tests {
915 | t.Run(name, func(t *testing.T) {
916 | sv := &statusValidator{
917 | repo: tt.fields.repo,
918 | owner: tt.fields.owner,
919 | ref: tt.fields.ref,
920 | selfJobName: tt.fields.selfJobName,
921 | client: tt.fields.client,
922 | }
923 | got, err := sv.listGhaStatuses(tt.ctx)
924 | if (err != nil) != tt.wantErr {
925 | t.Errorf("statusValidator.listStatuses() error = %v, wantErr %v", err, tt.wantErr)
926 | }
927 | if got, want := len(got), len(tt.want); got != want {
928 | t.Errorf("statusValidator.listStatuses() length = %v, want %v", got, want)
929 | }
930 | for i := range tt.want {
931 | if !reflect.DeepEqual(got[i], tt.want[i]) {
932 | t.Errorf("statusValidator.listStatuses() - %d = %v, want %v", i, got[i], tt.want[i])
933 | }
934 | }
935 | })
936 | }
937 | }
938 |
--------------------------------------------------------------------------------
/internal/validators/validators.go:
--------------------------------------------------------------------------------
1 | package validators
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type Status interface {
8 | Detail() string
9 | IsSuccess() bool
10 | }
11 |
12 | type Validator interface {
13 | Name() string
14 | Validate(ctx context.Context) (Status, error)
15 | }
16 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | _ "embed"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/upsidr/merge-gatekeeper/internal/cli"
10 | )
11 |
12 | var (
13 | //go:embed version.txt
14 | version string
15 | )
16 |
17 | func main() {
18 | if err := cli.Run(strings.TrimSuffix(version, "\n"), os.Args...); err != nil {
19 | fmt.Fprintf(os.Stderr, "failed to execute command: %v", err)
20 | os.Exit(1)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | v1.2.1
--------------------------------------------------------------------------------