├── .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 | ![Branch protection example](/assets/images/branch-protection-example.png) 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 | ![Branch protection example](/assets/images/branch-protection-example.png) 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 --------------------------------------------------------------------------------