├── .cmdx.yaml ├── .devcontainer ├── devcontainer-lock.json └── devcontainer.json ├── .github ├── FUNDING.yml └── workflows │ ├── actionlint.yaml │ ├── autofix.yaml │ ├── check-commit-signing.yaml │ ├── release.yaml │ ├── test.yaml │ └── workflow_call_test.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── aqua-checksums.json ├── aqua.yaml ├── aqua ├── cosign.yaml ├── ghalint.yaml ├── ghcp.yaml ├── golangci-lint.yaml ├── goreleaser.yaml └── reviewdog.yaml ├── cmd └── ci-info │ └── main.go ├── ghalint.yaml ├── githooks └── pre-commit.sh ├── go.mod ├── go.sum ├── pkg ├── cli │ ├── run.go │ └── runner.go ├── controller │ ├── controller.go │ └── new.go ├── domain │ └── params.go ├── github │ ├── client.go │ └── get_pr.go ├── output │ ├── output.go │ └── output_internal_test.go └── write │ ├── mkdir.go │ ├── write.go │ └── write_internal_test.go ├── renovate.json5 └── scripts ├── coverage.sh ├── fmt.sh ├── githook.sh └── test-code-climate.sh /.cmdx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # the configuration file of cmdx - task runner 3 | # https://github.com/suzuki-shunsuke/cmdx 4 | tasks: 5 | - name: init 6 | short: i 7 | script: bash scripts/githook.sh 8 | description: setup git hooks 9 | usage: setup git hooks 10 | - name: coverage 11 | short: c 12 | description: test a package 13 | usage: test a package 14 | script: "bash scripts/coverage.sh {{.path}}" 15 | args: 16 | - name: path 17 | - name: test 18 | short: t 19 | description: test 20 | usage: test 21 | script: go test -v ./... -covermode=atomic 22 | - name: fmt 23 | description: format the go code 24 | usage: format the go code 25 | script: bash scripts/fmt.sh 26 | - name: vet 27 | short: v 28 | description: go vet 29 | usage: go vet 30 | script: go vet ./... 31 | - name: lint 32 | short: l 33 | description: lint the go code 34 | usage: lint the go code 35 | script: golangci-lint run 36 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "ghcr.io/aquaproj/devcontainer-features/aqua-installer:0.1.0": { 4 | "version": "0.1.0", 5 | "resolved": "ghcr.io/aquaproj/devcontainer-features/aqua-installer@sha256:fdd01ae397066786a20020ab67a38c75c6a9125075b392a5170e60c50f828e78", 6 | "integrity": "sha256:fdd01ae397066786a20020ab67a38c75c6a9125075b392a5170e60c50f828e78" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "image": "debian:bookworm-20240423@sha256:1aadfee8d292f64b045adb830f8a58bfacc15789ae5f489a0fedcd517a862cb9", 4 | "features": { 5 | "ghcr.io/aquaproj/devcontainer-features/aqua-installer:0.1.3": { 6 | "aqua_version": "v2.51.2" 7 | } 8 | }, 9 | "remoteEnv": { 10 | "PATH": "/root/.local/share/aquaproj-aqua/bin:${containerEnv:PATH}" 11 | }, 12 | "postStartCommand": "aqua i -l" 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository 2 | github: 3 | - suzuki-shunsuke 4 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: actionlint 3 | on: pull_request 4 | permissions: {} 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-24.04 8 | if: failure() 9 | timeout-minutes: 10 10 | permissions: {} 11 | needs: 12 | - main 13 | steps: 14 | - run: exit 1 15 | main: 16 | uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@dbe6151b36d408b24ca5c41a34291b2b6d1bff76 # v2.0.1 17 | permissions: 18 | pull-requests: write 19 | contents: read 20 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: autofix.ci 3 | on: pull_request 4 | permissions: {} 5 | jobs: 6 | autofix: 7 | runs-on: ubuntu-24.04 8 | permissions: {} 9 | timeout-minutes: 15 10 | steps: 11 | - uses: suzuki-shunsuke/go-autofix-action@0bb6ca06b2f0d2d23c200bbbaa650897824a6cb9 # v0.1.7 12 | with: 13 | aqua_version: v2.51.2 14 | -------------------------------------------------------------------------------- /.github/workflows/check-commit-signing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Check if all commits are signed 3 | on: 4 | pull_request_target: 5 | branches: [main] 6 | concurrency: 7 | group: ${{ github.workflow }}--${{ github.head_ref }} # github.ref is unavailable in case of pull_request_target 8 | cancel-in-progress: true 9 | jobs: 10 | check-commit-signing: 11 | uses: suzuki-shunsuke/check-commit-signing-workflow/.github/workflows/check.yaml@547eee345f56310a656f271ec5eaa900af46b0fb # v0.1.0 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | tags: [v*] 6 | permissions: {} 7 | jobs: 8 | release: 9 | uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@4602cd60ba10f19df17a074d76c518a9b8b979bb # v4.0.1 10 | with: 11 | go-version: 1.24.3 12 | aqua_version: v2.51.2 13 | permissions: 14 | contents: write 15 | id-token: write 16 | actions: read 17 | attestations: write 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | on: pull_request 4 | jobs: 5 | status-check: 6 | runs-on: ubuntu-24.04 7 | if: failure() 8 | timeout-minutes: 10 9 | permissions: {} 10 | needs: 11 | - test 12 | steps: 13 | - run: exit 1 14 | test: 15 | uses: ./.github/workflows/workflow_call_test.yaml 16 | permissions: 17 | pull-requests: write 18 | contents: read 19 | -------------------------------------------------------------------------------- /.github/workflows/workflow_call_test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test (workflow_call) 3 | on: workflow_call 4 | jobs: 5 | test: 6 | uses: suzuki-shunsuke/go-test-full-workflow/.github/workflows/test.yaml@05399afd417ae28382877ebe5bf7c9288b023df7 # v3.2.1 7 | with: 8 | aqua_version: v2.51.2 9 | permissions: 10 | pull-requests: write 11 | contents: read 12 | 13 | integration-test: 14 | runs-on: ubuntu-latest 15 | permissions: {} 16 | timeout-minutes: 20 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | persist-credentials: false 21 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 22 | with: 23 | go-version-file: go.mod 24 | - run: go run ./cmd/ci-info run 25 | env: 26 | GITHUB_TOKEN: ${{github.token}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .code-climate 3 | bin/* 4 | .git-rm-branch.yml 5 | .envrc 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - depguard 6 | - err113 7 | - exhaustruct 8 | - godot 9 | - godox 10 | - lll 11 | - mnd 12 | - musttag 13 | - nlreturn 14 | - varnamelen 15 | - wsl 16 | exclusions: 17 | generated: lax 18 | presets: 19 | - comments 20 | - common-false-positives 21 | - legacy 22 | - std-error-handling 23 | paths: 24 | - third_party$ 25 | - builtin$ 26 | - examples$ 27 | formatters: 28 | enable: 29 | - gci 30 | - gofmt 31 | - gofumpt 32 | - goimports 33 | exclusions: 34 | generated: lax 35 | paths: 36 | - third_party$ 37 | - builtin$ 38 | - examples$ 39 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | builds: 4 | - binary: ci-info 5 | main: cmd/ci-info/main.go 6 | env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - windows 10 | - darwin 11 | - linux 12 | goarch: 13 | - amd64 14 | - arm64 15 | release: 16 | # We update release page manually before releasing 17 | # So make prerelease true 18 | prerelease: true 19 | header: | 20 | [Pull Requests](https://github.com/suzuki-shunsuke/ci-info/pulls?q=is%3Apr+milestone%3A{{.Tag}}) | [Issues](https://github.com/suzuki-shunsuke/ci-info/issues?q=is%3Aissue+milestone%3A{{.Tag}}) | https://github.com/suzuki-shunsuke/ci-info/compare/{{.PreviousTag}}...{{.Tag}} 21 | brews: 22 | - 23 | # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the 24 | # same kind. We will probably unify this in the next major version like it is done with scoop. 25 | 26 | # GitHub/GitLab repository to push the formula to 27 | repository: 28 | owner: suzuki-shunsuke 29 | name: homebrew-ci-info 30 | # The project name and current git tag are used in the format string. 31 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 32 | # Your app's homepage. 33 | # Default is empty. 34 | homepage: https://github.com/suzuki-shunsuke/ci-info 35 | 36 | # Template of your app's description. 37 | # Default is empty. 38 | description: Get CI related information 39 | license: MIT 40 | 41 | # Setting this will prevent goreleaser to actually try to commit the updated 42 | # formula - instead, the formula file will be stored on the dist folder only, 43 | # leaving the responsibility of publishing it to the user. 44 | # If set to auto, the release will not be uploaded to the homebrew tap 45 | # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 46 | # Default is false. 47 | skip_upload: true 48 | 49 | # So you can `brew test` your formula. 50 | # Default is empty. 51 | test: | 52 | system "#{bin}/ci-info --version" 53 | 54 | signs: 55 | - cmd: cosign 56 | artifacts: checksum 57 | signature: ${artifact}.sig 58 | certificate: ${artifact}.pem 59 | output: true 60 | args: 61 | - sign-blob 62 | - "-y" 63 | - --output-signature 64 | - ${signature} 65 | - --output-certificate 66 | - ${certificate} 67 | - --oidc-provider 68 | - github 69 | - ${artifact} 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the following document. 4 | 5 | - https://github.com/suzuki-shunsuke/oss-contribution-guide 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shunsuke Suzuki 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ci-info 2 | 3 | [![Build Status](https://github.com/suzuki-shunsuke/ci-info/workflows/CI/badge.svg)](https://github.com/suzuki-shunsuke/ci-info/actions) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/ci-info)](https://goreportcard.com/report/github.com/suzuki-shunsuke/ci-info) 5 | [![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/ci-info.svg)](https://github.com/suzuki-shunsuke/ci-info) 6 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/ci-info/main/LICENSE) 7 | 8 | CLI tool to get CI related information. 9 | 10 | ## Motivation 11 | 12 | We develop this tool to get some information in CI. 13 | 14 | * Related Pull Request 15 | * PR Author 16 | * Pull Request Files 17 | * Labels 18 | * base and head branch 19 | * etc 20 | * etc 21 | 22 | ## Install 23 | 24 | * [Homebrew](#homebrew) 25 | * [aqua](#aqua) 26 | * [GitHub Releases](#github-releases) 27 | 28 | ### Homebrew 29 | 30 | You can install ci-info with [Homebrew](https://brew.sh/). 31 | 32 | ```console 33 | $ brew install suzuki-shunsuke/ci-info/ci-info 34 | ``` 35 | 36 | ## aqua 37 | 38 | You can install ci-info with [aqua](https://aquaproj.github.io/). 39 | 40 | ```console 41 | $ aqua g -i suzuki-shunsuke/ci-info 42 | ``` 43 | 44 | ## GitHub Releases 45 | 46 | Please download a binary from the [release page](https://github.com/suzuki-shunsuke/ci-info/releases). 47 | 48 |
49 | Verify downloaded binaries from GitHub Releases 50 | 51 | You can verify downloaded binaries using some tools. 52 | 53 | 1. [Cosign](https://github.com/sigstore/cosign) 54 | 1. [slsa-verifier](https://github.com/slsa-framework/slsa-verifier) 55 | 1. [GitHub CLI](https://cli.github.com/) 56 | 57 | #### 1. Cosign 58 | 59 | You can install Cosign by aqua. 60 | 61 | ```sh 62 | aqua g -i sigstore/cosign 63 | ``` 64 | 65 | ```sh 66 | gh release download -R suzuki-shunsuke/ci-info v2.3.1 67 | cosign verify-blob \ 68 | --signature ci-info_2.3.1_checksums.txt.sig \ 69 | --certificate ci-info_2.3.1_checksums.txt.pem \ 70 | --certificate-identity-regexp 'https://github\.com/suzuki-shunsuke/go-release-workflow/\.github/workflows/release\.yaml@.*' \ 71 | --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ 72 | ci-info_2.3.1_checksums.txt 73 | ``` 74 | 75 | Output: 76 | 77 | ``` 78 | Verified OK 79 | ``` 80 | 81 | After verifying the checksum, verify the artifact. 82 | 83 | ```sh 84 | cat ci-info_2.3.1_checksums.txt | sha256sum -c --ignore-missing 85 | ``` 86 | 87 | #### 2. slsa-verifier 88 | 89 | You can install slsa-verifier by aqua. 90 | 91 | ```sh 92 | aqua g -i slsa-framework/slsa-verifier 93 | ``` 94 | 95 | ```sh 96 | gh release download -R suzuki-shunsuke/ci-info v2.3.1 97 | slsa-verifier verify-artifact ci-info_2.3.1_darwin_arm64.tar.gz \ 98 | --provenance-path multiple.intoto.jsonl \ 99 | --source-uri github.com/suzuki-shunsuke/ci-info \ 100 | --source-tag v2.3.1 101 | ``` 102 | 103 | Output: 104 | 105 | ``` 106 | Verified signature against tlog entry index 136878875 at URL: https://rekor.sigstore.dev/api/v1/log/entries/108e9186e8c5677a7ac053c11af84554df024d7c465abc4ae459493bd09be4875df26f45c1ffda32 107 | Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0" at commit 69950dff0ec546640c90cbcaf23df344d0b612cd 108 | Verifying artifact ci-info_2.3.1_darwin_arm64.tar.gz: PASSED 109 | ``` 110 | 111 | #### 3. GitHub CLI 112 | 113 | ci-info >= v2.3.1 114 | 115 | You can install GitHub CLI by aqua. 116 | 117 | ```sh 118 | aqua g -i cli/cli 119 | ``` 120 | 121 | ```sh 122 | gh release download -R suzuki-shunsuke/ci-info v2.3.1 -p ci-info_2.3.1_darwin_arm64.tar.gz 123 | gh attestation verify ci-info_2.3.1_darwin_arm64.tar.gz \ 124 | -R suzuki-shunsuke/ci-info \ 125 | --signer-workflow suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml 126 | ``` 127 | 128 | Output: 129 | 130 | ``` 131 | Loaded digest sha256:7fec0b88d213986b16605dd8e64f6230e4b4fc605a0ce4c2fd9698fdc40d3e2d for file://ci-info_2.3.1_darwin_arm64.tar.gz 132 | Loaded 1 attestation from GitHub API 133 | ✓ Verification succeeded! 134 | 135 | sha256:7fec0b88d213986b16605dd8e64f6230e4b4fc605a0ce4c2fd9698fdc40d3e2d was attested by: 136 | REPO PREDICATE_TYPE WORKFLOW 137 | suzuki-shunsuke/go-release-workflow https://slsa.dev/provenance/v1 .github/workflows/release.yaml@7f97a226912ee2978126019b1e95311d7d15c97a 138 | ``` 139 | 140 |
141 | 142 | ## Requirements 143 | 144 | GitHub Access Token is required to get the information about the Pull Request. 145 | In the public repository, GitHub Access Token is optional. 146 | 147 | ## Getting Started 148 | 149 | Run the following command, which gets the information about https://github.com/suzuki-shunsuke/github-comment/pull/132 . 150 | 151 | ```console 152 | $ ci-info run --owner suzuki-shunsuke --repo github-comment --pr 132 153 | export CI_INFO_IS_PR=true 154 | export CI_INFO_HAS_ASSOCIATED_PR=true 155 | export CI_INFO_PR_NUMBER=132 156 | export CI_INFO_BASE_REF=master 157 | export CI_INFO_HEAD_REF=feat/add-silent-option 158 | export CI_INFO_PR_AUTHOR=suzuki-shunsuke 159 | export CI_INFO_PR_MERGED=true 160 | export CI_INFO_REPO_OWNER=suzuki-shunsuke 161 | export CI_INFO_REPO_NAME=github-comment 162 | export CI_INFO_TEMP_DIR=/var/folders/w0/kzjzgvd52wg5s4jy5h0lcyqh0000gn/T/ci-info497729786 163 | ``` 164 | 165 | Then the shell script to export the environment variables are outputted and some files are created. 166 | You can export them by `eval`. 167 | 168 | ``` 169 | $ eval "$(ci-info run --owner suzuki-shunsuke --repo github-comment --pr 132)" 170 | ``` 171 | 172 | Some files are created. 173 | 174 | ``` 175 | $ ls "$CI_INFO_TEMP_DIR" 176 | ``` 177 | 178 | * pr_files.txt: The list of pull request file paths which include a maximum of 3000 files 179 | * pr_all_filenames.txt: The list of pull request file paths which include a maximum of 3000 files. In addition to `pr_files.txt`, the list of renamed file's `previous_filename` is included too. 180 | * pr_files.json: [The response body of GitHub API List pull requests files](https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls#list-pull-requests-files) 181 | * pr.json: [The response body of GitHub API Get a pull request](https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls#get-a-pull-request) 182 | * labels.txt: The list of pull request label names 183 | 184 | Note that the created directory and files aren't removed automatically. 185 | 186 | ## Usage 187 | 188 | ```console 189 | $ ci-info help 190 | NAME: 191 | ci-info - get CI information. https://github.com/suzuki-shunsuke/ci-info 192 | 193 | USAGE: 194 | ci-info [global options] command [command options] [arguments...] 195 | 196 | VERSION: 197 | 0.1.0 198 | 199 | COMMANDS: 200 | run get CI information 201 | help, h Shows a list of commands or help for one command 202 | 203 | GLOBAL OPTIONS: 204 | --help, -h show help (default: false) 205 | --version, -v print the version (default: false) 206 | ``` 207 | 208 | ```console 209 | $ ci-info run --help 210 | NAME: 211 | ci-info run - get CI information 212 | 213 | USAGE: 214 | ci-info run [command options] [arguments...] 215 | 216 | OPTIONS: 217 | --owner value repository owner 218 | --repo value repository name 219 | --sha value commit SHA 220 | --dir value directory path where files are created. The directory is created by os.MkdirAll if it doesn't exist. By default the directory is created at Go's ioutil.TempDir 221 | --pr value pull request number (default: 0) 222 | --github-token value GitHub Access Token [$GITHUB_TOKEN, $GITHUB_ACCESS_TOKEN] 223 | --prefix value The prefix of environment variable name (default: "CI_INFO_") 224 | --log-level value log level 225 | --help, -h show help (default: false) 226 | ``` 227 | 228 | ## Complement options with Platform's built-in Environment variables 229 | 230 | With [suzuki-shunske/go-ci-env](https://github.com/suzuki-shunsuke/go-ci-env) some parameters like `owner` and `repo` are gotten from Platform's built-in Environment variables. 231 | 232 | ## LICENSE 233 | 234 | [MIT](LICENSE) 235 | -------------------------------------------------------------------------------- /aqua-checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "checksums": [ 3 | { 4 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-darwin-amd64.tar.gz", 5 | "checksum": "E091107C4CA7E283902343BA3A09D14FB56B86E071EFFD461CE9D67193EF580E", 6 | "algorithm": "sha256" 7 | }, 8 | { 9 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-darwin-arm64.tar.gz", 10 | "checksum": "90783FA092A0F64A4F7B7D419F3DA1F53207E300261773BABE962957240E9EA6", 11 | "algorithm": "sha256" 12 | }, 13 | { 14 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-linux-amd64.tar.gz", 15 | "checksum": "E55E0EB515936C0FBD178BCE504798A9BD2F0B191E5E357768B18FD5415EE541", 16 | "algorithm": "sha256" 17 | }, 18 | { 19 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-linux-arm64.tar.gz", 20 | "checksum": "582EB73880F4408D7FB89F12B502D577BD7B0B63D8C681DA92BB6B9D934D4363", 21 | "algorithm": "sha256" 22 | }, 23 | { 24 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-windows-amd64.zip", 25 | "checksum": "FD7298019C76CF414AB083491F87F6C0A3E537ED6D727D6FF9135E503D6F9C32", 26 | "algorithm": "sha256" 27 | }, 28 | { 29 | "id": "github_release/github.com/golangci/golangci-lint/v2.1.6/golangci-lint-2.1.6-windows-arm64.zip", 30 | "checksum": "0DC38C44D8270A0ED3267BCD3FBDCD8384761D04D0FD2D53B63FC502F0F39722", 31 | "algorithm": "sha256" 32 | }, 33 | { 34 | "id": "github_release/github.com/goreleaser/goreleaser/v2.9.0/goreleaser_Darwin_all.tar.gz", 35 | "checksum": "82953B65C4B64E73B1077827663D97BF8E32592B4FC2CDB55C738BD484260A47", 36 | "algorithm": "sha256" 37 | }, 38 | { 39 | "id": "github_release/github.com/goreleaser/goreleaser/v2.9.0/goreleaser_Linux_arm64.tar.gz", 40 | "checksum": "574E83F5F0FC97803FF734C9342F8FD446D77E5E7CCAC53DEBF09B4A8DBDED80", 41 | "algorithm": "sha256" 42 | }, 43 | { 44 | "id": "github_release/github.com/goreleaser/goreleaser/v2.9.0/goreleaser_Linux_x86_64.tar.gz", 45 | "checksum": "A066FCD713684ABED0D750D7559F1A5D794FA2FAA8E8F1AD2EECEC8C373668A7", 46 | "algorithm": "sha256" 47 | }, 48 | { 49 | "id": "github_release/github.com/goreleaser/goreleaser/v2.9.0/goreleaser_Windows_arm64.zip", 50 | "checksum": "EA19CAE5A322FEC6794252D3E9FE77D43201CC831D939964730E556BF3C1CC2C", 51 | "algorithm": "sha256" 52 | }, 53 | { 54 | "id": "github_release/github.com/goreleaser/goreleaser/v2.9.0/goreleaser_Windows_x86_64.zip", 55 | "checksum": "F56E85F8FD52875102DFC2B01DC07FC174486CAEBBAC7E3AA9F29B4F0057D495", 56 | "algorithm": "sha256" 57 | }, 58 | { 59 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_darwin_amd64.zip", 60 | "checksum": "6728BD668888A64C71BF01D9AFBA373F38D353B79D181B1401A4E5E4B329289D", 61 | "algorithm": "sha256" 62 | }, 63 | { 64 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_darwin_arm64.zip", 65 | "checksum": "6533FA0396CA6F4B9D3A74C2F0A5C4C89575A0D79F90684BAF6CD8B1511C95AC", 66 | "algorithm": "sha256" 67 | }, 68 | { 69 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_linux_amd64.zip", 70 | "checksum": "3C808E566F0B663182AD5B5DD6E6B05DC8346610EA5613EA8C22AB19F47A4493", 71 | "algorithm": "sha256" 72 | }, 73 | { 74 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_linux_arm64.zip", 75 | "checksum": "14068BF35B2ED187EE7D8DB854DF2E7913C2BBC981979D3F8AE533D058E35F16", 76 | "algorithm": "sha256" 77 | }, 78 | { 79 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_windows_amd64.zip", 80 | "checksum": "1A90AD7927F720EB8996AE143DF5D7AA2DF70689B7B5B9074D6FD32C244D688E", 81 | "algorithm": "sha256" 82 | }, 83 | { 84 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Darwin_arm64.tar.gz", 85 | "checksum": "A7FBF41913CE5B6F1872D10C136139B7A849190F4F1F0DC1ED4BF74C636F22A2", 86 | "algorithm": "sha256" 87 | }, 88 | { 89 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Darwin_x86_64.tar.gz", 90 | "checksum": "056DD0F43ECCB8651FB976B43AA91A1D34B2A0C3934F216997774A7CBC1F7EB1", 91 | "algorithm": "sha256" 92 | }, 93 | { 94 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Linux_arm64.tar.gz", 95 | "checksum": "BD0C4045B8F367F1CA6C0E7CFD80189CCD2A8CEAA22034ECBAD4AF0ACB3A3B82", 96 | "algorithm": "sha256" 97 | }, 98 | { 99 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Linux_x86_64.tar.gz", 100 | "checksum": "2C634DBC00BD4A86E4D4C47029D2AF9185FAB06643A9DF0AE10E7C4D644781B6", 101 | "algorithm": "sha256" 102 | }, 103 | { 104 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Windows_arm64.tar.gz", 105 | "checksum": "2DFD2C151AFF8B7D2DFDFC44FB47706667806AEA92F4F8238932BB89A0461D4A", 106 | "algorithm": "sha256" 107 | }, 108 | { 109 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.3/reviewdog_0.20.3_Windows_x86_64.tar.gz", 110 | "checksum": "068726CA98BBEB5E47378AB0B630133741E17BA1FEB5654A24EC5E604446EDEF", 111 | "algorithm": "sha256" 112 | }, 113 | { 114 | "id": "github_release/github.com/sigstore/cosign/v2.5.0/cosign-darwin-amd64", 115 | "checksum": "D61CC50F6F32C2B63CB81CD8A935E5DD1BE8520D639242031A1102092BEE44D4", 116 | "algorithm": "sha256" 117 | }, 118 | { 119 | "id": "github_release/github.com/sigstore/cosign/v2.5.0/cosign-darwin-arm64", 120 | "checksum": "780DA3654D9601367B0D54686AC65CB9716578610CABE292D725C7008DE4DB85", 121 | "algorithm": "sha256" 122 | }, 123 | { 124 | "id": "github_release/github.com/sigstore/cosign/v2.5.0/cosign-linux-amd64", 125 | "checksum": "1F6C194DD0891EB345B436BB71FF9F996768355F5E0CE02DDE88567029AC2188", 126 | "algorithm": "sha256" 127 | }, 128 | { 129 | "id": "github_release/github.com/sigstore/cosign/v2.5.0/cosign-linux-arm64", 130 | "checksum": "080A998F9878F22DAFDB9AD54D5B2E2B8E7A38C53527250F9D89A6763A28D545", 131 | "algorithm": "sha256" 132 | }, 133 | { 134 | "id": "github_release/github.com/sigstore/cosign/v2.5.0/cosign-windows-amd64.exe", 135 | "checksum": "2345667CBCF60767C1A6F678755CBB7465367761084E9D2CBB59AE0CC1A94437", 136 | "algorithm": "sha256" 137 | }, 138 | { 139 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_darwin_amd64.tar.gz", 140 | "checksum": "AD0D5893D9A4B38F6F8D35DC003A2BEEA63FA2EA48FF91DDD301773AB5711B21", 141 | "algorithm": "sha256" 142 | }, 143 | { 144 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_darwin_arm64.tar.gz", 145 | "checksum": "70DC52A85C207FCB40F1CDBA5F097CCEF7564C5D217E48C60541743CFC15239B", 146 | "algorithm": "sha256" 147 | }, 148 | { 149 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_linux_amd64.tar.gz", 150 | "checksum": "6A8EAA2568FA1FED64D63CCDD4538C3E329B873A7D78F49D207E9FA2FA6A65BB", 151 | "algorithm": "sha256" 152 | }, 153 | { 154 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_linux_arm64.tar.gz", 155 | "checksum": "1417F9B7CE201C69A959BD5E7DA56BFE4128D8C5333EEDB94038B731CA30A12C", 156 | "algorithm": "sha256" 157 | }, 158 | { 159 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_windows_amd64.zip", 160 | "checksum": "3C1EB280BDE931AD793A732B32C802D54C6DF418502A55393D9DC8573282259E", 161 | "algorithm": "sha256" 162 | }, 163 | { 164 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.4.1/ghalint_1.4.1_windows_arm64.zip", 165 | "checksum": "0802008325A617634398E0D73BB240F75551B4859769D045E6A91ECC9D85B1AE", 166 | "algorithm": "sha256" 167 | }, 168 | { 169 | "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.374.0/registry.yaml", 170 | "checksum": "F6EF8FAE28FDCD869BA071BB763686FA3B117E10F32A414B460924336F83B22B", 171 | "algorithm": "sha256" 172 | } 173 | ] 174 | } 175 | -------------------------------------------------------------------------------- /aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # aqua - Declarative CLI Version Manager 3 | # https://aquaproj.github.io/ 4 | registries: 5 | - type: standard 6 | ref: v4.374.0 # renovate: depName=aquaproj/aqua-registry 7 | packages: 8 | - import: aqua/*.yaml 9 | -------------------------------------------------------------------------------- /aqua/cosign.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: sigstore/cosign@v2.5.0 3 | -------------------------------------------------------------------------------- /aqua/ghalint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/ghalint@v1.4.1 3 | -------------------------------------------------------------------------------- /aqua/ghcp.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: int128/ghcp@v1.13.5 3 | -------------------------------------------------------------------------------- /aqua/golangci-lint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: golangci/golangci-lint@v2.1.6 3 | -------------------------------------------------------------------------------- /aqua/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: goreleaser/goreleaser@v2.9.0 3 | -------------------------------------------------------------------------------- /aqua/reviewdog.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: reviewdog/reviewdog@v0.20.3 3 | -------------------------------------------------------------------------------- /cmd/ci-info/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/suzuki-shunsuke/ci-info/pkg/cli" 10 | ) 11 | 12 | var ( 13 | version = "" 14 | commit = "" //nolint:gochecknoglobals 15 | date = "" //nolint:gochecknoglobals 16 | ) 17 | 18 | func main() { 19 | if err := core(); err != nil { 20 | logrus.Fatal(err) 21 | } 22 | } 23 | 24 | func core() error { 25 | runner := cli.Runner{ 26 | LDFlags: &cli.LDFlags{ 27 | Version: version, 28 | Commit: commit, 29 | Date: date, 30 | }, 31 | } 32 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 33 | defer stop() 34 | return runner.Run(ctx, os.Args...) //nolint:wrapcheck 35 | } 36 | -------------------------------------------------------------------------------- /ghalint.yaml: -------------------------------------------------------------------------------- 1 | excludes: 2 | - policy_name: deny_inherit_secrets 3 | workflow_file_path: .github/workflows/release.yaml 4 | job_name: release 5 | -------------------------------------------------------------------------------- /githooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | cmdx test || exit 1 2 | cmdx lint 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/suzuki-shunsuke/ci-info 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/google/go-github/v72 v72.0.0 7 | github.com/sirupsen/logrus v1.9.3 8 | github.com/spf13/afero v1.14.0 9 | github.com/suzuki-shunsuke/go-ci-env/v3 v3.1.0 10 | github.com/urfave/cli/v3 v3.3.3 11 | golang.org/x/oauth2 v0.30.0 12 | ) 13 | 14 | require ( 15 | github.com/google/go-querystring v1.1.0 // indirect 16 | golang.org/x/sys v0.31.0 // indirect 17 | golang.org/x/text v0.23.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 6 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 7 | github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= 8 | github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= 9 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 10 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 14 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 15 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 16 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/suzuki-shunsuke/go-ci-env/v3 v3.1.0 h1:WHVT7TZTVITrKiIvW7i5+4vr8ebxD6oXGRffixIXNJU= 22 | github.com/suzuki-shunsuke/go-ci-env/v3 v3.1.0/go.mod h1:qnHYP5fJLLNKqOwxCfix7XVzQGfbF72vbGgLhl8X2vA= 23 | github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 24 | github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 25 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 26 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 27 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 29 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 30 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 31 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /pkg/cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/afero" 10 | "github.com/suzuki-shunsuke/ci-info/pkg/controller" 11 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 12 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 13 | "github.com/suzuki-shunsuke/go-ci-env/v3/cienv" 14 | "github.com/urfave/cli/v3" 15 | ) 16 | 17 | func (r *Runner) runCommand() *cli.Command { 18 | return &cli.Command{ 19 | Name: "run", 20 | Usage: "get CI information", 21 | Action: r.action, 22 | Flags: []cli.Flag{ 23 | &cli.StringFlag{ 24 | Name: "owner", 25 | Usage: "repository owner", 26 | }, 27 | &cli.StringFlag{ 28 | Name: "repo", 29 | Usage: "repository name", 30 | }, 31 | &cli.StringFlag{ 32 | Name: "sha", 33 | Usage: "commit SHA", 34 | }, 35 | &cli.StringFlag{ 36 | Name: "dir", 37 | Usage: "directory path where files are created. The directory is created by os.MkdirAll if it doesn't exist. By default the directory is created at Go's ioutil.TempDir", 38 | }, 39 | &cli.IntFlag{ 40 | Name: "pr", 41 | Usage: "pull request number", 42 | }, 43 | &cli.StringFlag{ 44 | Name: "github-token", 45 | Usage: "GitHub Access Token [$GITHUB_TOKEN, $GITHUB_ACCESS_TOKEN]", 46 | }, 47 | &cli.StringFlag{ 48 | Name: "github-api-url", 49 | Usage: "GitHub API Base URL", 50 | Sources: cli.EnvVars("GITHUB_API_URL"), 51 | }, 52 | &cli.StringFlag{ 53 | Name: "github-graphql-url", 54 | Usage: "GitHub GraphQL API URL", 55 | Sources: cli.EnvVars("GITHUB_GRAPHQL_URL"), 56 | }, 57 | &cli.StringFlag{ 58 | Name: "prefix", 59 | Usage: "The prefix of environment variable name", 60 | Value: "CI_INFO_", 61 | }, 62 | &cli.StringFlag{ 63 | Name: "log-level", 64 | Usage: "log level", 65 | }, 66 | }, 67 | } 68 | } 69 | 70 | func (r *Runner) setCLIArg(cmd *cli.Command, params domain.Params) domain.Params { 71 | if owner := cmd.String("owner"); owner != "" { 72 | params.Owner = owner 73 | } 74 | if repo := cmd.String("repo"); repo != "" { 75 | params.Repo = repo 76 | } 77 | if token := cmd.String("github-token"); token != "" { 78 | params.GitHubToken = token 79 | } 80 | if logLevel := cmd.String("log-level"); logLevel != "" { 81 | params.LogLevel = logLevel 82 | } 83 | if prefix := cmd.String("prefix"); prefix != "" { 84 | params.Prefix = prefix 85 | } 86 | if sha := cmd.String("sha"); sha != "" { 87 | params.SHA = sha 88 | } 89 | if dir := cmd.String("dir"); dir != "" { 90 | params.Dir = dir 91 | } 92 | if prNum := cmd.Int("pr"); prNum > 0 { 93 | params.PRNum = prNum 94 | } 95 | params.GitHubAPIURL = cmd.String("github-api-url") 96 | params.GitHubGraphQLURL = cmd.String("github-graphql-url") 97 | return params 98 | } 99 | 100 | func (r *Runner) action(ctx context.Context, c *cli.Command) error { 101 | params := domain.Params{} 102 | params = r.setCLIArg(c, params) 103 | if err := setEnv(¶ms); err != nil { 104 | return err 105 | } 106 | setLogLevel(params.LogLevel) 107 | ghClient, err := github.New(ctx, github.ParamsNew{ 108 | Token: getGitHubToken(params.GitHubToken), 109 | BaseURL: params.GitHubAPIURL, 110 | GraphQLURL: params.GitHubGraphQLURL, 111 | }) 112 | if err != nil { 113 | return fmt.Errorf("create a GitHub client: %w", err) 114 | } 115 | 116 | fs := afero.NewOsFs() 117 | 118 | ctrl := controller.New(ghClient, fs) 119 | 120 | return ctrl.Run(ctx, params) //nolint:wrapcheck 121 | } 122 | 123 | func getGitHubToken(token string) string { 124 | if token != "" { 125 | return token 126 | } 127 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 128 | return token 129 | } 130 | return os.Getenv("GITHUB_ACCESS_TOKEN") 131 | } 132 | 133 | func setLogLevel(logLevel string) { 134 | if logLevel == "" { 135 | return 136 | } 137 | lvl, err := logrus.ParseLevel(logLevel) 138 | if err != nil { 139 | logrus.WithFields(logrus.Fields{ 140 | "log_level": logLevel, 141 | }).WithError(err).Error("the log level is invalid") 142 | } 143 | logrus.SetLevel(lvl) 144 | } 145 | 146 | func setEnv(params *domain.Params) error { 147 | platform := cienv.Get(nil) 148 | if platform == nil { 149 | return nil 150 | } 151 | if params.Owner == "" { 152 | params.Owner = platform.RepoOwner() 153 | } 154 | if params.Repo == "" { 155 | params.Repo = platform.RepoName() 156 | } 157 | if params.SHA == "" { 158 | params.SHA = platform.SHA() 159 | } 160 | if params.PRNum <= 0 { 161 | prNum, err := platform.PRNumber() 162 | if err != nil { 163 | return fmt.Errorf("get the pull request number: %w", err) 164 | } 165 | params.PRNum = prNum 166 | } 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /pkg/cli/runner.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/urfave/cli/v3" 8 | ) 9 | 10 | type LDFlags struct { 11 | Version string 12 | Commit string 13 | Date string 14 | } 15 | 16 | func (flags *LDFlags) AppVersion() string { 17 | return flags.Version + " (" + flags.Commit + ")" 18 | } 19 | 20 | type Runner struct { 21 | Stdin io.Reader 22 | Stdout io.Writer 23 | Stderr io.Writer 24 | LDFlags *LDFlags 25 | } 26 | 27 | func (r *Runner) Run(ctx context.Context, args ...string) error { 28 | cmd := cli.Command{ 29 | Name: "ci-info", 30 | Usage: "get CI information. https://github.com/suzuki-shunsuke/ci-info", 31 | Version: r.LDFlags.AppVersion(), 32 | Commands: []*cli.Command{ 33 | r.runCommand(), 34 | }, 35 | } 36 | 37 | return cmd.Run(ctx, args) //nolint:wrapcheck 38 | } 39 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 9 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 10 | "github.com/suzuki-shunsuke/ci-info/pkg/output" 11 | "github.com/suzuki-shunsuke/ci-info/pkg/write" 12 | ) 13 | 14 | func (c *Controller) Run(ctx context.Context, params domain.Params) error { 15 | if err := validateParams(params); err != nil { 16 | return fmt.Errorf("argument is invalid: %w", err) 17 | } 18 | 19 | isPR := params.PRNum > 0 20 | 21 | pr, err := c.gh.GetPR(ctx, params) 22 | if err != nil { 23 | return fmt.Errorf("get an associated pull request: %w", err) 24 | } 25 | 26 | if pr == nil { 27 | fmt.Fprintln(c.stdout, output.NonPREnv(params)) 28 | return nil 29 | } 30 | 31 | files, _, err := c.gh.GetPRFiles(ctx, github.ParamsGetPRFiles{ 32 | Owner: params.Owner, 33 | Repo: params.Repo, 34 | PRNum: pr.GetNumber(), 35 | FileSize: pr.GetChangedFiles(), 36 | }) 37 | if err != nil { 38 | return fmt.Errorf("get pull request files: %w", err) 39 | } 40 | 41 | dir, err := write.MkDir(c.fs, params.Dir) 42 | if err != nil { 43 | return fmt.Errorf("create a directory: %w", err) 44 | } 45 | 46 | fmt.Fprintln(c.stdout, output.PREnv(params.Prefix, dir, isPR, params.Owner, params.Repo, pr)) 47 | 48 | if err := write.Write(c.fs, dir, pr, files); err != nil { 49 | return fmt.Errorf("write files: %w", err) 50 | } 51 | return nil 52 | } 53 | 54 | var ( 55 | errOwnerRequired = errors.New("owner is required") 56 | errRepoRequired = errors.New("repo is required") 57 | errSHAOrPRNumRequired = errors.New("sha or pr number is required") 58 | ) 59 | 60 | func validateParams(params domain.Params) error { 61 | if params.Owner == "" { 62 | return errOwnerRequired 63 | } 64 | if params.Repo == "" { 65 | return errRepoRequired 66 | } 67 | if params.PRNum <= 0 && params.SHA == "" { 68 | return errSHAOrPRNumRequired 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/controller/new.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | 8 | "github.com/spf13/afero" 9 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 10 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 11 | ) 12 | 13 | type GitHub interface { 14 | GetPR(ctx context.Context, params domain.Params) (*github.PullRequest, error) 15 | GetPRFiles(ctx context.Context, params github.ParamsGetPRFiles) ([]*github.CommitFile, *github.Response, error) 16 | } 17 | 18 | type Controller struct { 19 | gh GitHub 20 | stdout io.Writer 21 | stderr io.Writer 22 | fs afero.Fs 23 | } 24 | 25 | func New(ghClient github.Client, fs afero.Fs) Controller { 26 | return Controller{ 27 | gh: &ghClient, 28 | fs: fs, 29 | stdout: os.Stdout, 30 | stderr: os.Stderr, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/domain/params.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Params struct { 4 | Owner string 5 | Repo string 6 | SHA string 7 | Dir string 8 | PRNum int 9 | GitHubToken string 10 | GitHubAPIURL string 11 | GitHubGraphQLURL string 12 | LogLevel string 13 | Prefix string 14 | } 15 | -------------------------------------------------------------------------------- /pkg/github/client.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/google/go-github/v72/github" 9 | "github.com/sirupsen/logrus" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | type ( 14 | PullRequest = github.PullRequest 15 | PullRequestBranch = github.PullRequestBranch 16 | CommitFile = github.CommitFile 17 | Response = github.Response 18 | Label = github.Label 19 | User = github.User 20 | ) 21 | 22 | type Client struct { 23 | Client *github.Client 24 | } 25 | 26 | type ParamsNew struct { 27 | Token string 28 | BaseURL string 29 | GraphQLURL string 30 | } 31 | 32 | func New(ctx context.Context, params ParamsNew) (Client, error) { 33 | gh := newGitHub(ctx, params.Token) 34 | if params.BaseURL != "" { 35 | gh, err := gh.WithEnterpriseURLs(params.BaseURL, params.BaseURL) 36 | if err != nil { 37 | return Client{}, fmt.Errorf("configure GitHub API Baase URL: %w", err) 38 | } 39 | return Client{ 40 | Client: gh, 41 | }, nil 42 | } 43 | return Client{ 44 | Client: gh, 45 | }, nil 46 | } 47 | 48 | func newGitHub(ctx context.Context, token string) *github.Client { 49 | return github.NewClient(newHTTP(ctx, token)) 50 | } 51 | 52 | func newHTTP(ctx context.Context, token string) *http.Client { 53 | if token == "" { 54 | return http.DefaultClient 55 | } 56 | return oauth2.NewClient(ctx, oauth2.StaticTokenSource( 57 | &oauth2.Token{AccessToken: token}, 58 | )) 59 | } 60 | 61 | type ParamsGetPR struct { 62 | Owner string 63 | Repo string 64 | PRNum int 65 | } 66 | 67 | type ParamsGetPRFiles struct { 68 | Owner string 69 | Repo string 70 | PRNum int 71 | FileSize int 72 | } 73 | 74 | type paramsListPRsWithCommit struct { 75 | Owner string 76 | Repo string 77 | SHA string 78 | } 79 | 80 | const maxPerPage = 100 81 | 82 | func (c *Client) GetPRFiles(ctx context.Context, params ParamsGetPRFiles) ([]*github.CommitFile, *github.Response, error) { 83 | ret := []*github.CommitFile{} 84 | if params.FileSize == 0 { 85 | logrus.Debug("file size is 0") 86 | return nil, nil, nil 87 | } 88 | n := (params.FileSize / maxPerPage) + 1 89 | var gResp *github.Response 90 | for i := 1; i <= n; i++ { 91 | opts := &github.ListOptions{ 92 | Page: i, 93 | PerPage: maxPerPage, 94 | } 95 | files, resp, err := c.Client.PullRequests.ListFiles(ctx, params.Owner, params.Repo, params.PRNum, opts) 96 | if err != nil { 97 | return files, resp, err //nolint:wrapcheck 98 | } 99 | gResp = resp 100 | ret = append(ret, files...) 101 | if len(files) != maxPerPage { 102 | return ret, gResp, nil 103 | } 104 | } 105 | 106 | return ret, gResp, nil 107 | } 108 | 109 | func (c *Client) ListPRsWithCommit(ctx context.Context, params paramsListPRsWithCommit) ([]*github.PullRequest, *github.Response, error) { 110 | return c.Client.PullRequests.ListPullRequestsWithCommit(ctx, params.Owner, params.Repo, params.SHA, nil) //nolint:wrapcheck 111 | } 112 | -------------------------------------------------------------------------------- /pkg/github/get_pr.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 9 | ) 10 | 11 | func (c *Client) getPRNum(ctx context.Context, params domain.Params) (int, error) { 12 | if params.PRNum > 0 { 13 | return params.PRNum, nil 14 | } 15 | logrus.WithFields(logrus.Fields{ 16 | "owner": params.Owner, 17 | "repo": params.Repo, 18 | "sha": params.SHA, 19 | }).Debug("get pull request from SHA") 20 | prs, _, err := c.ListPRsWithCommit(ctx, paramsListPRsWithCommit{ 21 | Owner: params.Owner, 22 | Repo: params.Repo, 23 | SHA: params.SHA, 24 | }) 25 | if err != nil { 26 | return 0, fmt.Errorf("list pull requests with a commit: %w", err) 27 | } 28 | logrus.WithFields(logrus.Fields{ 29 | "size": len(prs), 30 | }).Debug("the number of pull requests assosicated with the commit") 31 | if len(prs) == 0 { 32 | return 0, nil 33 | } 34 | return prs[0].GetNumber(), nil 35 | } 36 | 37 | func (c *Client) GetPR(ctx context.Context, params domain.Params) (*PullRequest, error) { 38 | prNum, err := c.getPRNum(ctx, params) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if prNum <= 0 { 43 | return nil, nil //nolint:nilnil 44 | } 45 | pr, _, err := c.Client.PullRequests.Get(ctx, params.Owner, params.Repo, prNum) 46 | if err != nil { 47 | return nil, fmt.Errorf("get a pull request: %w", err) 48 | } 49 | return pr, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 7 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 8 | ) 9 | 10 | func NonPREnv(params domain.Params) string { 11 | return fmt.Sprintf(`export %sHAS_ASSOCIATED_PR=false 12 | export %sIS_PR=false 13 | export %sREPO_OWNER=%s 14 | export %sREPO_NAME=%s`, 15 | params.Prefix, 16 | params.Prefix, 17 | params.Prefix, params.Owner, 18 | params.Prefix, params.Repo) 19 | } 20 | 21 | func PREnv(prefix, dir string, isPR bool, owner, repo string, pr *github.PullRequest) string { 22 | return fmt.Sprintf(`export %sIS_PR=%t 23 | export %sHAS_ASSOCIATED_PR=true 24 | export %sPR_NUMBER=%d 25 | export %sBASE_REF=%s 26 | export %sHEAD_REF=%s 27 | export %sPR_AUTHOR=%s 28 | export %sPR_MERGED=%t 29 | export %sTEMP_DIR=%s 30 | export %sREPO_OWNER=%s 31 | export %sREPO_NAME=%s`, 32 | prefix, isPR, 33 | prefix, 34 | prefix, pr.GetNumber(), 35 | prefix, pr.GetBase().GetRef(), 36 | prefix, pr.GetHead().GetRef(), 37 | prefix, pr.GetUser().GetLogin(), 38 | prefix, pr.GetMerged(), 39 | prefix, dir, 40 | prefix, owner, 41 | prefix, repo, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/output/output_internal_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/suzuki-shunsuke/ci-info/pkg/domain" 7 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 8 | ) 9 | 10 | func Test_nonPREnv(t *testing.T) { 11 | t.Parallel() 12 | params := domain.Params{ 13 | Prefix: "CI_INFO_", 14 | Owner: "suzuki-shunsuke", 15 | Repo: "foo", 16 | } 17 | s := NonPREnv(params) 18 | exp := `export CI_INFO_HAS_ASSOCIATED_PR=false 19 | export CI_INFO_IS_PR=false 20 | export CI_INFO_REPO_OWNER=suzuki-shunsuke 21 | export CI_INFO_REPO_NAME=foo` 22 | if s != exp { 23 | t.Fatalf("wanted %s, got %s", exp, s) 24 | } 25 | } 26 | 27 | func intP(i int) *int { 28 | return &i 29 | } 30 | 31 | func strP(i string) *string { 32 | return &i 33 | } 34 | 35 | func boolP(i bool) *bool { 36 | return &i 37 | } 38 | 39 | func Test_prEnv(t *testing.T) { 40 | t.Parallel() 41 | s := PREnv("CI_INFO_", "/tmp/ci-info_yoo", false, "suzuki-shunsuke", "foo", &github.PullRequest{ 42 | Number: intP(10), 43 | Merged: boolP(true), 44 | Base: &github.PullRequestBranch{ 45 | Ref: strP("dev"), 46 | }, 47 | Head: &github.PullRequestBranch{ 48 | Ref: strP("feature-branch"), 49 | }, 50 | User: &github.User{ 51 | Login: strP("octocat"), 52 | }, 53 | }) 54 | exp := `export CI_INFO_IS_PR=false 55 | export CI_INFO_HAS_ASSOCIATED_PR=true 56 | export CI_INFO_PR_NUMBER=10 57 | export CI_INFO_BASE_REF=dev 58 | export CI_INFO_HEAD_REF=feature-branch 59 | export CI_INFO_PR_AUTHOR=octocat 60 | export CI_INFO_PR_MERGED=true 61 | export CI_INFO_TEMP_DIR=/tmp/ci-info_yoo 62 | export CI_INFO_REPO_OWNER=suzuki-shunsuke 63 | export CI_INFO_REPO_NAME=foo` 64 | if s != exp { 65 | t.Fatalf("wanted %s, got %s", exp, s) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/write/mkdir.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | func MkDir(fs afero.Fs, dir string) (string, error) { 11 | if dir == "" { 12 | d, err := afero.TempDir(fs, "", "ci-info") 13 | if err != nil { 14 | return "", fmt.Errorf("create a temporal directory: %w", err) 15 | } 16 | return d, nil 17 | } 18 | if !filepath.IsAbs(dir) { 19 | d, err := filepath.Abs(dir) 20 | if err != nil { 21 | return "", fmt.Errorf("convert -dir %s to absolute path: %w", dir, err) 22 | } 23 | dir = d 24 | } 25 | if err := fs.MkdirAll(dir, 0o755); err != nil { //nolint:mnd 26 | return "", fmt.Errorf("create a directory %s: %w", dir, err) 27 | } 28 | return dir, nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/write/write.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/afero" 10 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 11 | ) 12 | 13 | func Write(fs afero.Fs, dir string, pr *github.PullRequest, files []*github.CommitFile) error { 14 | if err := writeJSON(fs, filepath.Join(dir, "pr_files.json"), files); err != nil { 15 | return err 16 | } 17 | 18 | if err := writeJSON(fs, filepath.Join(dir, "pr.json"), pr); err != nil { 19 | return err 20 | } 21 | 22 | if err := writeFile(fs, filepath.Join(dir, "pr_files.txt"), []byte(prFilesTxt(files)+"\n")); err != nil { 23 | return err 24 | } 25 | 26 | if err := writeFile(fs, filepath.Join(dir, "pr_all_filenames.txt"), []byte(prChangedFilesTxt(files)+"\n")); err != nil { 27 | return err 28 | } 29 | 30 | if err := writeFile(fs, filepath.Join(dir, "labels.txt"), []byte(labelsTxt(pr.Labels)+"\n")); err != nil { 31 | return fmt.Errorf("write labels.txt: %w", err) 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func writeFile(fs afero.Fs, p string, data []byte) error { 38 | return afero.WriteFile(fs, p, data, 0o644) //nolint:mnd,wrapcheck 39 | } 40 | 41 | func labelsTxt(labels []*github.Label) string { 42 | if len(labels) == 0 { 43 | return "" 44 | } 45 | labelNames := make([]string, len(labels)) 46 | for i, label := range labels { 47 | labelNames[i] = label.GetName() 48 | } 49 | return strings.Join(labelNames, "\n") 50 | } 51 | 52 | func prFilesTxt(files []*github.CommitFile) string { 53 | if len(files) == 0 { 54 | return "" 55 | } 56 | prFileNames := make([]string, len(files)) 57 | for i, file := range files { 58 | prFileNames[i] = file.GetFilename() 59 | } 60 | return strings.Join(prFileNames, "\n") 61 | } 62 | 63 | func prChangedFilesTxt(files []*github.CommitFile) string { 64 | prFileNames := make(map[string]struct{}, len(files)) 65 | for _, file := range files { 66 | prFileNames[file.GetFilename()] = struct{}{} 67 | prevFileName := file.GetPreviousFilename() 68 | if prevFileName != "" { 69 | prFileNames[prevFileName] = struct{}{} 70 | } 71 | } 72 | arr := make([]string, 0, len(prFileNames)) 73 | for k := range prFileNames { 74 | arr = append(arr, k) 75 | } 76 | return strings.Join(arr, "\n") 77 | } 78 | 79 | func writeJSON(fs afero.Fs, p string, data any) error { 80 | prJSON, err := fs.Create(p) 81 | if err != nil { 82 | return fmt.Errorf("create a file %s: %w", p, err) 83 | } 84 | defer prJSON.Close() 85 | if err := json.NewEncoder(prJSON).Encode(data); err != nil { 86 | return fmt.Errorf("encode data as JSON: %w", err) 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/write/write_internal_test.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/suzuki-shunsuke/ci-info/pkg/github" 7 | ) 8 | 9 | func strP(i string) *string { 10 | return &i 11 | } 12 | 13 | func Test_labelsTxt(t *testing.T) { 14 | t.Parallel() 15 | if labelsTxt(nil) != "" { 16 | t.Fatal("labelsTxt(nil) must be empty") 17 | } 18 | s := labelsTxt([]*github.Label{ 19 | { 20 | Name: strP("bug"), 21 | }, 22 | { 23 | Name: strP("foo"), 24 | }, 25 | }) 26 | exp := `bug 27 | foo` 28 | if s != exp { 29 | t.Fatalf("wanted %s, got %s", exp, s) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: [ 3 | "github>aquaproj/aqua-renovate-config#2.8.1", 4 | "github>aquaproj/aqua-renovate-config:file#2.8.1(aqua/.*\\.ya?ml)", 5 | "github>suzuki-shunsuke/renovate-config#3.2.1", 6 | "github>suzuki-shunsuke/renovate-config:nolimit#3.2.1", 7 | "github>suzuki-shunsuke/renovate-config:action-go-version#3.2.1", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ".coverage/$1" 2 | go test "./$1" -coverprofile=".coverage/$1/coverage.txt" -covermode=atomic 3 | go tool cover -html=".coverage/$1/coverage.txt" 4 | -------------------------------------------------------------------------------- /scripts/fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | cd "$(dirname "$0")/.." 7 | 8 | git ls-files | grep -E ".*\.go$" | xargs gofmt -l -s -w 9 | -------------------------------------------------------------------------------- /scripts/githook.sh: -------------------------------------------------------------------------------- 1 | echoEval() { 2 | echo "+ $@" 3 | eval "$@" 4 | } 5 | 6 | cd `dirname $0`/.. 7 | if [ ! -f .git/hooks/pre-commit ]; then 8 | echoEval ln -s ../../githooks/pre-commit.sh .git/hooks/pre-commit || exit 1 9 | fi 10 | echoEval chmod a+x githooks/* 11 | -------------------------------------------------------------------------------- /scripts/test-code-climate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | ee() { 7 | echo "+ $*" 8 | eval "$@" 9 | } 10 | 11 | cd "$(dirname "$0")/.." 12 | 13 | repo_name=${1:-} 14 | if [ -z "$repo_name" ]; then 15 | echo "the repository name is required" >&2 16 | exit 1 17 | fi 18 | 19 | ee cc-test-reporter before-build 20 | 21 | mkdir -p .code-climate 22 | 23 | for d in $(go list ./...); do 24 | echo "$d" 25 | profile=.code-climate/$d/profile.txt 26 | coverage=.code-climate/$d/coverage.json 27 | ee mkdir -p "$(dirname "$profile")" "$(dirname "$coverage")" 28 | ee go test -race -coverprofile="$profile" -covermode=atomic "$d" 29 | if [ "$(wc -l < "$profile")" -eq 1 ]; then 30 | continue 31 | fi 32 | ee cc-test-reporter format-coverage -t gocov -p "github.com/suzuki-shunsuke/${repo_name}" -o "$coverage" "$profile" 33 | done 34 | 35 | result=.code-climate/codeclimate.total.json 36 | # shellcheck disable=SC2046 37 | ee cc-test-reporter sum-coverage $(find .code-climate -name coverage.json) -o "$result" 38 | ee cc-test-reporter upload-coverage -i "$result" 39 | --------------------------------------------------------------------------------