├── .github ├── dependabot.yml ├── release-drafter.yml ├── template.tmpl └── workflows │ ├── go-pr-release.yml │ ├── release-drafter.yaml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── cli │ ├── cli.go │ └── cli_test.go ├── markdown │ ├── pr.go │ ├── pr_test.go │ └── testdata │ │ └── template.tmpl └── pkg │ ├── env │ ├── env.go │ └── env_test.go │ └── gh │ ├── gh.go │ ├── gh_test.go │ └── regexp.go └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "12:00" 8 | timezone: Asia/Tokyo 9 | open-pull-requests-limit: 10 10 | groups: 11 | dependencies: 12 | patterns: 13 | - "*" 14 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - title: '💪 Enhancement' 9 | labels: 10 | - 'enhancement' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | - title: '🧰 Maintenance' 17 | label: 'chore' 18 | - title: '🔧 Refactoring' 19 | label: 'refactor' 20 | - title: '📖 Documentation' 21 | label: 'documentation' 22 | - title: '⛓️ Dependency update' 23 | label: 'dependencies' 24 | 25 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 26 | 27 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 28 | 29 | version-resolver: 30 | major: 31 | labels: 32 | - 'major' 33 | minor: 34 | labels: 35 | - 'minor' 36 | patch: 37 | labels: 38 | - 'patch' 39 | default: minor 40 | 41 | template: | 42 | ## Changes 43 | $CHANGES 44 | autolabeler: 45 | - label: feature 46 | branch: 47 | - '/^feat(ure)?[/-].+/' 48 | - label: bug 49 | branch: 50 | - '/^fix[/-].+/' 51 | - '/^hotfix[/-].+/' 52 | - label: chore 53 | branch: 54 | - '/^chore[/-].+/' 55 | - label: refactor 56 | branch: 57 | - '/(refactor|refactoring)[/-].+/' 58 | - label: documentation 59 | branch: 60 | - '/doc(umentation)[/-].+/' 61 | files: 62 | - '*.md' 63 | - label: enhancement 64 | branch: 65 | - '/(enhancement|improve)[/-].+/' 66 | - label: github 67 | files: 68 | - '.github/**/*' 69 | - label: patch 70 | branch: 71 | - '/(enhancement|improve)[/-].+/' 72 | - '/doc(umentation)[/-].+/' 73 | -------------------------------------------------------------------------------- /.github/template.tmpl: -------------------------------------------------------------------------------- 1 | # Releases 2 | {{- range .PullRequests }} 3 | {{- if ne .User.LoginName "dependabot[bot]" }} 4 | {{ printf "- [ ] [%s] #%d @%s" (.MergedAt.Format "2006-01-02") .Number .User.LoginName}} 5 | {{- end }} 6 | {{- end }} 7 | 8 | # Dependabot 9 | {{- range .PullRequests }} 10 | {{- if eq .User.LoginName "dependabot[bot]" }} 11 | {{ printf "- [ ] #%d @%s" .Number .User.LoginName}} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /.github/workflows/go-pr-release.yml: -------------------------------------------------------------------------------- 1 | name: go-pr-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | test: 14 | name: go-pr-release 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 20 | 21 | - name: Install go-pr-release 22 | run: curl -s -L https://github.com/tomtwinkle/go-pr-release/releases/latest/download/go-pr-release_linux_x86_64.tar.gz | tar -xvz 23 | 24 | - name: debug 25 | run: ls -la 26 | 27 | - name: Run 28 | env: 29 | GO_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Required 30 | GO_PR_RELEASE_RELEASE: main # Required. Release Branch: Destination to be merged 31 | GO_PR_RELEASE_DEVELOP: develop # Required. Develop Branch: Merge source 32 | GO_PR_RELEASE_LABELS: release # Optional. PullRequest labels. Multiple labels can be specified, separated by `commas` 33 | GO_PR_RELEASE_TITLE: "" # Optional. specify the title of the pull request 34 | GO_PR_RELEASE_TEMPLATE: ".github/template.tmpl" # Optional. Specify a template file that can be described in `go template` 35 | GO_PR_RELEASE_REVIEWERS: tomtwinkle # Optional. PullRequest reviewers. Multiple reviewers can be specified, separated by `commas` 36 | GO_PR_RELEASE_DRY_RUN: false # Optional. if true, display only the results to be created without creating PullRequest 37 | run: ./go-pr-release 38 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | update_release_draft: 19 | permissions: 20 | contents: write 21 | pull-requests: write 22 | runs-on: ubuntu-latest 23 | steps: 24 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 25 | #- name: Set GHE_HOST 26 | # run: | 27 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 28 | 29 | # Drafts your next Release notes as Pull Requests are merged into "master" 30 | - uses: release-drafter/release-drafter@65c5fb495d1e69aa8c08a3317bc44ff8aabe9772 # v5.24.0 31 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 32 | # with: 33 | # config-name: my-config.yml 34 | # disable-autolabeler: true 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+\.[0-9]+\.[0-9]+' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 18 | with: 19 | fetch-depth: 0 20 | - run: git fetch --force --tags 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 24 | with: 25 | go-version: '^1.21.0' 26 | 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@3fa32b8bb5620a2c1afe798654bbad59f9da4906 # v4.4.0 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | test: 19 | name: Test 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 30 | with: 31 | go-version: '^1.21.0' 32 | 33 | - name: Get dependencies 34 | run: | 35 | go install gotest.tools/gotestsum@latest 36 | go get -v -t -d ./... 37 | 38 | - name: Test code 39 | run: gotestsum --junitfile unit-tests.xml -- -v -race ./... 40 | -------------------------------------------------------------------------------- /.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 | # Editor 18 | .idea 19 | .vscode 20 | 21 | # Build Binaries 22 | main 23 | 24 | # environment variables 25 | \.env 26 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | 9 | builds: 10 | - 11 | main: main.go 12 | 13 | binary: go-pr-release 14 | 15 | goos: 16 | - linux 17 | - windows 18 | - darwin 19 | 20 | goarch: 21 | - amd64 22 | - arm64 23 | 24 | ldflags: 25 | - -s -w 26 | - -X main.name={{.ProjectName}} 27 | - -X main.version={{.Version}} 28 | - -X main.commit={{.Commit}} 29 | - -X main.date={{.Date}} 30 | 31 | archives: 32 | - rlcp: true 33 | name_template: >- 34 | {{ .ProjectName }}_ 35 | {{- .Os }}_ 36 | {{- if eq .Arch "amd64" }}x86_64 37 | {{- else if eq .Arch "386" }}i386 38 | {{- else }}{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ end }} 39 | format_overrides: 40 | - goos: windows 41 | format: zip 42 | files: 43 | - README.md 44 | - LICENSE 45 | 46 | checksum: 47 | name_template: 'checksums.txt' 48 | 49 | release: 50 | draft: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 tom twinkle 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 | # go-pr-release 2 | CLI for creating PullRequest for release in Github Action. 3 | it was respected by [git-pr-release](https://github.com/x-motemen/git-pr-release) and rewritten in Golang. 4 | since it runs in one binary, it is expected to run quickly on CI. 5 | 6 | ![image](https://user-images.githubusercontent.com/47764757/179726677-2d5ee674-6f7a-4d3c-9c18-c7a979a8f25b.png) 7 | 8 | ## Configuration 9 | The git configuration is read from `.git/config`. 10 | It works directly under the directory from which you `git clone`. 11 | 12 | | Environment Variables | CLI Option | Description | 13 | |---|---|---| 14 | | GO_PR_RELEASE_TOKEN | --token | Required `secrets.GITHUB_TOKEN` or a personal token with repo privileges | 15 | | GO_PR_RELEASE_RELEASE | --release-branch, --to | Required. Release Branch: Destination to be merged | 16 | | GO_PR_RELEASE_DEVELOP | --develop-branch, --from | Required. Develop Branch: Merge source | 17 | | GO_PR_RELEASE_LABELS | --label, -l | Optional. PullRequest labels. Multiple labels can be specified, separated by `commas` | 18 | | GO_PR_RELEASE_REVIEWERS | --reviewer, -r | Optional. PullRequest reviewers. Multiple reviewers can be specified, separated by `commas` | 19 | | GO_PR_RELEASE_TITLE | --title | Optional. specify the title of the pull request | 20 | | GO_PR_RELEASE_TEMPLATE | --template, -t | Optional. Specify a template file that can be described in `go template` | 21 | | GO_PR_RELEASE_DRY_RUN | --dry-run, -n | Optional. if true, display only the results to be created without creating PullRequest | 22 | | | --verbose | Optional. Detailed logs will be output. Do not specify except for verification | 23 | | | --version, -v | Optional. Output CLI version information | 24 | 25 | ### Installation in Github Action 26 | 27 | ```yaml 28 | name: go-pr-release 29 | 30 | on: 31 | push: 32 | branches: 33 | - develop 34 | 35 | jobs: 36 | go-pr-release: 37 | name: go-pr-release 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Install go-pr-release 45 | run: curl -s -L https://github.com/tomtwinkle/go-pr-release/releases/latest/download/go-pr-release_linux_x86_64.tar.gz | tar -xvz 46 | 47 | - name: Run 48 | env: 49 | GO_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Required 50 | GO_PR_RELEASE_RELEASE: main # Required. Release Branch: Destination to be merged 51 | GO_PR_RELEASE_DEVELOP: develop # Required. Develop Branch: Merge source 52 | GO_PR_RELEASE_LABELS: release # Optional. PullRequest labels. Multiple labels can be specified, separated by `commas` 53 | GO_PR_RELEASE_TITLE: "" # Optional. specify the title of the pull request 54 | GO_PR_RELEASE_TEMPLATE: ".github/template.tmpl" # Optional. Specify a template file that can be described in `go template` 55 | GO_PR_RELEASE_REVIEWERS: tomtwinkle # Optional. PullRequest reviewers. Multiple reviewers can be specified, separated by `commas` 56 | GO_PR_RELEASE_DRY_RUN: false # Optional. if true, display only the results to be created without creating PullRequest 57 | run: ./go-pr-release 58 | ``` 59 | 60 | 61 | ### Template 62 | 63 | A `go template` file can be specified for template. 64 | Custom [sprig functions](https://github.com/Masterminds/sprig) can be used for `go template`. 65 | 66 | The following structs are available as go template parameters. 67 | 68 | ```go 69 | type TemplateParam struct { 70 | PullRequests []TemplateParamPullRequest 71 | } 72 | 73 | type TemplateParamPullRequest struct { 74 | Number int 75 | Title string 76 | MergedAt time.Time 77 | MergeCommitSHA string 78 | User TemplateParamUser 79 | URL string 80 | } 81 | 82 | type TemplateParamUser struct { 83 | LoginName string 84 | URL string 85 | Avatar string 86 | } 87 | ``` 88 | 89 | ### Example 90 | 91 | - [Example Github Action Workflow](https://github.com/tomtwinkle/go-pr-release-test/tree/develop/.github) 92 | - [Example PullRequest](https://github.com/tomtwinkle/go-pr-release-test/pull/16) 93 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tomtwinkle/go-pr-release 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/Masterminds/sprig/v3 v3.2.3 7 | github.com/bxcodec/faker/v3 v3.8.1 8 | github.com/go-git/go-git/v5 v5.8.1 9 | github.com/go-playground/locales v0.14.1 10 | github.com/go-playground/universal-translator v0.18.1 11 | github.com/go-playground/validator/v10 v10.15.0 12 | github.com/google/go-github/v45 v45.2.0 13 | github.com/joho/godotenv v1.5.1 14 | github.com/mkideal/cli v0.2.7 15 | github.com/stretchr/testify v1.8.4 16 | golang.org/x/oauth2 v0.11.0 17 | golang.org/x/sync v0.3.0 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.0 // indirect 22 | github.com/Masterminds/goutils v1.1.1 // indirect 23 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 24 | github.com/Microsoft/go-winio v0.6.1 // indirect 25 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect 26 | github.com/acomagu/bufpipe v1.0.4 // indirect 27 | github.com/cloudflare/circl v1.3.3 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/emirpasic/gods v1.18.1 // indirect 30 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 31 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 32 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 33 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 34 | github.com/golang/protobuf v1.5.3 // indirect 35 | github.com/google/go-querystring v1.1.0 // indirect 36 | github.com/google/uuid v1.3.0 // indirect 37 | github.com/huandu/xstrings v1.4.0 // indirect 38 | github.com/imdario/mergo v0.3.16 // indirect 39 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 40 | github.com/kevinburke/ssh_config v1.2.0 // indirect 41 | github.com/labstack/gommon v0.4.0 // indirect 42 | github.com/leodido/go-urn v1.2.4 // indirect 43 | github.com/mattn/go-colorable v0.1.13 // indirect 44 | github.com/mattn/go-isatty v0.0.19 // indirect 45 | github.com/mitchellh/copystructure v1.2.0 // indirect 46 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 47 | github.com/mkideal/expr v0.1.0 // indirect 48 | github.com/pjbgf/sha1cd v0.3.0 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/sergi/go-diff v1.3.1 // indirect 51 | github.com/shopspring/decimal v1.3.1 // indirect 52 | github.com/skeema/knownhosts v1.2.0 // indirect 53 | github.com/spf13/cast v1.5.1 // indirect 54 | github.com/xanzy/ssh-agent v0.3.3 // indirect 55 | golang.org/x/crypto v0.14.0 // indirect 56 | golang.org/x/mod v0.12.0 // indirect 57 | golang.org/x/net v0.17.0 // indirect 58 | golang.org/x/sys v0.13.0 // indirect 59 | golang.org/x/term v0.13.0 // indirect 60 | golang.org/x/text v0.13.0 // indirect 61 | golang.org/x/tools v0.12.0 // indirect 62 | google.golang.org/appengine v1.6.7 // indirect 63 | google.golang.org/protobuf v1.31.0 // indirect 64 | gopkg.in/warnings.v0 v0.1.2 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 4 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 5 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 6 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 7 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 8 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 9 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 10 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 11 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 12 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 13 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= 14 | github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 15 | github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= 16 | github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 17 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 18 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 19 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 20 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 21 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 22 | github.com/bxcodec/faker/v3 v3.8.1 h1:qO/Xq19V6uHt2xujwpaetgKhraGCapqY2CRWGD/SqcM= 23 | github.com/bxcodec/faker/v3 v3.8.1/go.mod h1:DdSDccxF5msjFo5aO4vrobRQ8nIApg8kq3QWPEQD6+o= 24 | github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= 25 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 26 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 27 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 29 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= 31 | github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 32 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 33 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 34 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 35 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 36 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 37 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 38 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= 39 | github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= 40 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 41 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 42 | github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= 43 | github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= 44 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= 45 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= 46 | github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= 47 | github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= 48 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 49 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 50 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 51 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 52 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 53 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 54 | github.com/go-playground/validator/v10 v10.15.0 h1:nDU5XeOKtB3GEa+uB7GNYwhVKsgjAR7VgKoNB6ryXfw= 55 | github.com/go-playground/validator/v10 v10.15.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 56 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 57 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 58 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 60 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 61 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 62 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 63 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 64 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 65 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 66 | github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= 67 | github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= 68 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 69 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 70 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 71 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 72 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 73 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 74 | github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= 75 | github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 76 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 77 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 78 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 79 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 80 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 81 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 82 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 83 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 84 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 85 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 86 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 87 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 88 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 89 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 90 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 91 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 92 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 93 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 94 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 95 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 96 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 97 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 98 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 99 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 100 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 101 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 102 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 103 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 104 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 105 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 106 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 107 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 108 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 109 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 110 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 111 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 112 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 113 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 114 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 115 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 116 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 117 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 118 | github.com/mkideal/cli v0.2.7 h1:mB/XrMzuddmTJ8f7KY1c+KzfYoM149tYGAnzmqRdvOU= 119 | github.com/mkideal/cli v0.2.7/go.mod h1:efaTeFI4jdPqzAe0bv3myLB2NW5yzMBLvWB70a6feco= 120 | github.com/mkideal/expr v0.1.0 h1:fzborV9TeSUmLm0aEQWTWcexDURFFo4v5gHSc818Kl8= 121 | github.com/mkideal/expr v0.1.0/go.mod h1:vL1DsSb87ZtU6IEjOtUfxw98z0FQbzS8xlGtnPkKdzg= 122 | github.com/mkideal/pkg v0.1.3/go.mod h1:u/enAxPeRcYSsxtu1NUifWSeOTU/31VsCaOPg54SMJ4= 123 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 124 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 125 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 126 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 127 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 128 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 129 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 130 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 131 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 132 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 133 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 134 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 135 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 136 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 137 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 138 | github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= 139 | github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= 140 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 141 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 142 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 143 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 144 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 145 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 146 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 147 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 148 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 149 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 150 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 151 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 152 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 153 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 154 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 155 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 156 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 157 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 158 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 159 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 160 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 161 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 162 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 163 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 164 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 165 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 166 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 167 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 168 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 169 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 170 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 171 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 172 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 173 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 174 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 175 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 176 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 177 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 178 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 179 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 180 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 181 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 182 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 183 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 184 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 185 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 186 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 187 | golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= 188 | golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= 189 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 190 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 193 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 194 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 195 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 196 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 197 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 206 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 208 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 209 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 210 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 211 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 212 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 213 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 214 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 215 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 216 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 218 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 219 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 220 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 221 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 222 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 223 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 224 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 225 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= 226 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 227 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 228 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 229 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 230 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 231 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 232 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 233 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 234 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 235 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 236 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 237 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 238 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 239 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 240 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 241 | golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= 242 | golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= 243 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 244 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 246 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 247 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 248 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 249 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 250 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 251 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 252 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 253 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 254 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 255 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 256 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 257 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 258 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 259 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 260 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 261 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 262 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 263 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 264 | -------------------------------------------------------------------------------- /internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/go-playground/locales/en" 13 | ut "github.com/go-playground/universal-translator" 14 | "github.com/go-playground/validator/v10" 15 | tren "github.com/go-playground/validator/v10/translations/en" 16 | "github.com/mkideal/cli" 17 | 18 | "github.com/tomtwinkle/go-pr-release/internal/markdown" 19 | "github.com/tomtwinkle/go-pr-release/internal/pkg/env" 20 | "github.com/tomtwinkle/go-pr-release/internal/pkg/gh" 21 | ) 22 | 23 | const ( 24 | gitDir = ".git" 25 | gitRemoteName = "origin" 26 | ) 27 | 28 | type Args struct { 29 | cli.Helper 30 | DryRun bool `cli:"n,dry-run" usage:"Do not create/update a PR. Just prints out" namev:"--dry-run or environment variable:GO_PR_RELEASE_DRY_RUN"` 31 | Token string `cli:"token" usage:"Token for GitHub API" validate:"required" namev:"--token or environment variable:GO_PR_RELEASE_TOKEN"` 32 | Title string `cli:"title" usage:"Title for GitHub PullRequest" namev:"--title"` 33 | ReleaseBranch string `cli:"to,release-branch" usage:"Branch to be released" validate:"required" namev:"--release-branch or environment variable:GO_PR_RELEASE_RELEASE"` 34 | DevelopBranch string `cli:"from,develop-branch" usage:"The Branch that will be merged into the release Branch" validate:"required" namev:"--develop-branch or environment variable:GO_PR_RELEASE_DEVELOP"` 35 | Template string `cli:"t,template" usage:"The template file path for pull requests created. This is an go template" namev:"--template or environment variable:GO_PR_RELEASE_TEMPLATE"` 36 | Labels []string `cli:"l,label" usage:"The labels list for adding to pull requests created. More than one can be specified" namev:"--label or environment variable:GO_PR_RELEASE_LABELS"` 37 | Reviewers []string `cli:"r,reviewer" usage:"Reviewers for pull requests. More than one can be specified" namev:"--reviewer or environment variable:GO_PR_RELEASE_REVIEWERS"` 38 | Verbose bool `cli:"verbose" usage:"Output detailed logs" namev:"--verbose"` 39 | Version bool `cli:"v,version" usage:"Version" namev:"--version"` 40 | } 41 | 42 | func Run(name, version, commit, date string) int { 43 | return cli.Run(new(Args), func(c *cli.Context) error { 44 | argv, ok := c.Argv().(*Args) 45 | if !ok { 46 | return fmt.Errorf("argument type mismatch [%T]", c.Argv()) 47 | } 48 | if argv.Version { 49 | c.String(c.Color().Green(fmt.Sprintf("%s %s %s [%s]", name, version, commit, date))) 50 | return nil 51 | } 52 | 53 | var arg *Args 54 | if v, err := LookupEnv(); err != nil { 55 | return err 56 | } else { 57 | var bindErr error 58 | if arg, bindErr = BindArgs(c, v); bindErr != nil { 59 | return fmt.Errorf("%s", c.Color().Red(bindErr.Error())) 60 | } 61 | } 62 | if arg.Verbose { 63 | c.JSONln(arg) 64 | } 65 | return MakePR(arg) 66 | }) 67 | } 68 | 69 | func LookupEnv() (*Args, error) { 70 | arg := new(Args) 71 | 72 | if dryRun, err := env.LookUpBool("GO_PR_RELEASE_DRY_RUN", false); err == nil { 73 | arg.DryRun = dryRun 74 | } else { 75 | return nil, err 76 | } 77 | if token, err := env.LookUpString("GO_PR_RELEASE_TOKEN", false); err == nil { 78 | arg.Token = token 79 | } else { 80 | return nil, err 81 | } 82 | if title, err := env.LookUpString("GO_PR_RELEASE_TITLE", false); err == nil { 83 | arg.Title = title 84 | } else { 85 | return nil, err 86 | } 87 | if releaseBranch, err := env.LookUpString("GO_PR_RELEASE_RELEASE", false); err == nil { 88 | arg.ReleaseBranch = releaseBranch 89 | } else { 90 | return nil, err 91 | } 92 | if developBranch, err := env.LookUpString("GO_PR_RELEASE_DEVELOP", false); err == nil { 93 | arg.DevelopBranch = developBranch 94 | } else { 95 | return nil, err 96 | } 97 | if template, err := env.LookUpString("GO_PR_RELEASE_TEMPLATE", false); err == nil { 98 | arg.Template = template 99 | } else { 100 | return nil, err 101 | } 102 | if labels, err := env.LookUpStringSlice("GO_PR_RELEASE_LABELS", false, ","); err == nil { 103 | arg.Labels = labels 104 | } else { 105 | return nil, err 106 | } 107 | if reviewers, err := env.LookUpStringSlice("GO_PR_RELEASE_REVIEWERS", false, ","); err == nil { 108 | arg.Reviewers = reviewers 109 | } else { 110 | return nil, err 111 | } 112 | 113 | return arg, nil 114 | } 115 | 116 | func BindArgs(ctx *cli.Context, arg *Args) (*Args, error) { 117 | argv, ok := ctx.Argv().(*Args) 118 | if !ok { 119 | return nil, fmt.Errorf("argument type mismatch [%T]", ctx.Argv()) 120 | } 121 | if argv.DryRun { 122 | arg.DryRun = argv.DryRun 123 | } 124 | if argv.Token != "" { 125 | arg.Token = argv.Token 126 | } 127 | if argv.DevelopBranch != "" { 128 | arg.DevelopBranch = argv.DevelopBranch 129 | } 130 | if argv.ReleaseBranch != "" { 131 | arg.ReleaseBranch = argv.ReleaseBranch 132 | } 133 | if argv.Template != "" { 134 | arg.Template = argv.Template 135 | } 136 | if len(argv.Labels) > 0 { 137 | arg.Labels = argv.Labels 138 | } 139 | if len(argv.Reviewers) > 0 { 140 | arg.Reviewers = argv.Reviewers 141 | } 142 | if argv.Verbose { 143 | arg.Verbose = argv.Verbose 144 | } 145 | 146 | if err := ValidateArgs(arg); err != nil { 147 | return nil, err 148 | } 149 | return arg, nil 150 | } 151 | 152 | func ValidateArgs(arg *Args) error { 153 | const structNameTag = "namev" 154 | translators := map[string]string{ 155 | "required": "{0} is required", 156 | } 157 | 158 | v := validator.New() 159 | uni := ut.New(en.New(), en.New()) 160 | trans, _ := uni.GetTranslator("en") 161 | if err := tren.RegisterDefaultTranslations(v, trans); err != nil { 162 | panic(err) 163 | } 164 | v.RegisterTagNameFunc(func(fld reflect.StructField) string { 165 | fieldName := fld.Tag.Get(structNameTag) 166 | if fieldName == "-" { 167 | return "" 168 | } 169 | return fieldName 170 | }) 171 | for tag, msgFormat := range translators { 172 | if err := v.RegisterTranslation(tag, trans, func(u ut.Translator) error { 173 | return u.Add(tag, msgFormat, true) 174 | }, transFunc); err != nil { 175 | panic(err) 176 | } 177 | } 178 | if err := v.Struct(arg); err != nil { 179 | errMessages := make([]string, 0) 180 | for _, m := range err.(validator.ValidationErrors).Translate(trans) { 181 | errMessages = append(errMessages, m) 182 | } 183 | return errors.New(strings.Join(errMessages, ",")) 184 | } 185 | return nil 186 | } 187 | 188 | func transFunc(ut ut.Translator, fe validator.FieldError) string { 189 | t, err := ut.T(fe.Tag(), fe.Field(), fe.Param()) 190 | if err != nil { 191 | return fe.Error() 192 | } 193 | return t 194 | } 195 | 196 | func MakePR(arg *Args) error { 197 | ctx := context.Background() 198 | 199 | logLevel := slog.LevelInfo 200 | if arg.Verbose { 201 | logLevel = slog.LevelDebug 202 | } 203 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 204 | Level: logLevel, 205 | })) 206 | g, err := gh.New(ctx, arg.Token, logger) 207 | if err != nil { 208 | return err 209 | } 210 | mergedPRs, err := g.GetMergedPRs(ctx, arg.DevelopBranch, arg.ReleaseBranch) 211 | if err != nil { 212 | return err 213 | } 214 | body, err := markdown.MakePRBody(mergedPRs, arg.Template) 215 | if err != nil { 216 | return err 217 | } 218 | if arg.DryRun { 219 | fmt.Println(body) 220 | return nil 221 | } 222 | var title string 223 | if arg.Title != "" { 224 | title = arg.Title 225 | } else { 226 | title = fmt.Sprintf("Merge to %s from %s", arg.ReleaseBranch, arg.DevelopBranch) 227 | } 228 | pr, err := g.CreateReleasePR(ctx, title, arg.DevelopBranch, arg.ReleaseBranch, body) 229 | if err != nil { 230 | return err 231 | } 232 | if len(arg.Reviewers) > 0 { 233 | if _, err := g.AssignReviews(ctx, pr.GetNumber(), arg.Reviewers...); err != nil { 234 | return err 235 | } 236 | } 237 | if len(arg.Labels) > 0 { 238 | if _, err := g.Labeling(ctx, pr.GetNumber(), arg.Labels...); err != nil { 239 | return err 240 | } 241 | } 242 | return nil 243 | } 244 | -------------------------------------------------------------------------------- /internal/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/bxcodec/faker/v3" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/tomtwinkle/go-pr-release/internal/cli" 11 | ) 12 | 13 | func TestLookupEnv(t *testing.T) { 14 | tests := map[string]struct { 15 | setEnv func(t *testing.T) 16 | wantArgs *cli.Args 17 | wantErr error 18 | }{ 19 | "All": { 20 | setEnv: func(t *testing.T) { 21 | t.Setenv("GO_PR_RELEASE_DRY_RUN", "true") 22 | t.Setenv("GO_PR_RELEASE_TOKEN", "dummy") 23 | t.Setenv("GO_PR_RELEASE_TITLE", "title") 24 | t.Setenv("GO_PR_RELEASE_RELEASE", "main") 25 | t.Setenv("GO_PR_RELEASE_DEVELOP", "develop") 26 | t.Setenv("GO_PR_RELEASE_TEMPLATE", "./template.tmpl") 27 | t.Setenv("GO_PR_RELEASE_LABELS", "label1,label2") 28 | t.Setenv("GO_PR_RELEASE_REVIEWERS", "reviewer1,reviewer2") 29 | }, 30 | wantArgs: &cli.Args{ 31 | DryRun: true, 32 | Token: "dummy", 33 | Title: "title", 34 | ReleaseBranch: "main", 35 | DevelopBranch: "develop", 36 | Template: "./template.tmpl", 37 | Labels: []string{"label1", "label2"}, 38 | Reviewers: []string{"reviewer1", "reviewer2"}, 39 | }, 40 | }, 41 | } 42 | 43 | for n, v := range tests { 44 | name := n 45 | tt := v 46 | t.Run(name, func(t *testing.T) { 47 | tt.setEnv(t) 48 | got, err := cli.LookupEnv() 49 | if tt.wantErr != nil { 50 | assert.Error(t, err) 51 | return 52 | } 53 | assert.NoError(t, err) 54 | assert.Equal(t, tt.wantArgs, got) 55 | }) 56 | } 57 | } 58 | 59 | func TestValidateArgs(t *testing.T) { 60 | tests := map[string]struct { 61 | args *cli.Args 62 | errors []string 63 | }{ 64 | "passed": { 65 | args: &cli.Args{ 66 | Token: faker.UUIDHyphenated(), 67 | ReleaseBranch: faker.UUIDDigit(), 68 | DevelopBranch: faker.UUIDDigit(), 69 | }, 70 | }, 71 | "required errors": { 72 | args: &cli.Args{}, 73 | errors: []string{ 74 | "--token or environment variable:GO_PR_RELEASE_TOKEN is required", 75 | "--release-branch or environment variable:GO_PR_RELEASE_RELEASE is required", 76 | "--develop-branch or environment variable:GO_PR_RELEASE_DEVELOP is required", 77 | }, 78 | }, 79 | } 80 | 81 | for n, v := range tests { 82 | name := n 83 | tt := v 84 | t.Run(name, func(t *testing.T) { 85 | err := cli.ValidateArgs(tt.args) 86 | if len(tt.errors) == 0 { 87 | assert.NoError(t, err) 88 | return 89 | } 90 | assert.Error(t, err) 91 | gotErrs := strings.Split(err.Error(), ",") 92 | if assert.Equal(t, len(tt.errors), len(gotErrs)) { 93 | for _, wantErr := range tt.errors { 94 | assert.Contains(t, err.Error(), wantErr) 95 | } 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/markdown/pr.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "os" 7 | "time" 8 | 9 | "github.com/Masterminds/sprig/v3" 10 | "github.com/google/go-github/v45/github" 11 | ) 12 | 13 | const defaultTmpl = `# Releases 14 | {{ range .PullRequests }} 15 | {{- printf "- [ ] #%d @%s" .Number .User.LoginName}} 16 | {{ end }} 17 | ` 18 | 19 | type TemplateParam struct { 20 | PullRequests []TemplateParamPullRequest 21 | } 22 | 23 | type TemplateParamPullRequest struct { 24 | Number int 25 | Title string 26 | MergedAt time.Time 27 | MergeCommitSHA string 28 | User TemplateParamUser 29 | URL string 30 | } 31 | 32 | type TemplateParamUser struct { 33 | LoginName string 34 | URL string 35 | Avatar string 36 | } 37 | 38 | func MakePRBody(prs []*github.PullRequest, templatePath string) (string, error) { 39 | tmpPrs := make([]TemplateParamPullRequest, len(prs)) 40 | for i, pr := range prs { 41 | tmpPrs[i] = TemplateParamPullRequest{ 42 | Number: pr.GetNumber(), 43 | Title: pr.GetTitle(), 44 | MergedAt: *pr.MergedAt, 45 | MergeCommitSHA: pr.GetMergeCommitSHA(), 46 | User: TemplateParamUser{ 47 | LoginName: pr.User.GetLogin(), 48 | URL: pr.User.GetHTMLURL(), 49 | Avatar: pr.User.GetAvatarURL(), 50 | }, 51 | URL: pr.GetURL(), 52 | } 53 | } 54 | 55 | var tpl *template.Template 56 | if templatePath != "" { 57 | b, err := os.ReadFile(templatePath) 58 | if err != nil { 59 | return "", err 60 | } 61 | tpl = template.Must(template.New("base").Funcs(sprig.FuncMap()).Parse(string(b))) 62 | } else { 63 | tpl = template.Must(template.New("base").Funcs(sprig.FuncMap()).Parse(defaultTmpl)) 64 | } 65 | m := TemplateParam{ 66 | PullRequests: tmpPrs, 67 | } 68 | var buf bytes.Buffer 69 | if err := tpl.Execute(&buf, m); err != nil { 70 | return "", err 71 | } 72 | return buf.String(), nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/markdown/pr_test.go: -------------------------------------------------------------------------------- 1 | package markdown_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/bxcodec/faker/v3" 8 | "github.com/google/go-github/v45/github" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/tomtwinkle/go-pr-release/internal/markdown" 12 | ) 13 | 14 | func TestMakePRBody(t *testing.T) { 15 | t.Run("make pr body default", func(t *testing.T) { 16 | now := time.Now() 17 | prs := []*github.PullRequest{ 18 | { 19 | Number: github.Int(1), 20 | Title: github.String(faker.Name()), 21 | CreatedAt: &now, 22 | UpdatedAt: &now, 23 | MergedAt: &now, 24 | Labels: nil, 25 | User: &github.User{ 26 | Login: github.String(faker.Name()), 27 | AvatarURL: github.String("https://example.com"), 28 | HTMLURL: github.String("https://example.com"), 29 | }, 30 | HTMLURL: github.String("https://example.com/1"), 31 | Assignees: nil, 32 | RequestedReviewers: nil, 33 | }, 34 | { 35 | Number: github.Int(2), 36 | Title: github.String(faker.Name()), 37 | CreatedAt: &now, 38 | UpdatedAt: &now, 39 | MergedAt: &now, 40 | Labels: nil, 41 | User: &github.User{ 42 | Login: github.String(faker.Name()), 43 | AvatarURL: github.String("https://example.com"), 44 | HTMLURL: github.String("https://example.com"), 45 | }, 46 | HTMLURL: github.String("https://example.com/2"), 47 | Assignees: nil, 48 | RequestedReviewers: nil, 49 | }, 50 | } 51 | got, err := markdown.MakePRBody(prs, "") 52 | assert.NoError(t, err) 53 | t.Log(got) 54 | }) 55 | 56 | t.Run("make pr body file", func(t *testing.T) { 57 | now := time.Now() 58 | prs := []*github.PullRequest{ 59 | { 60 | Number: github.Int(1), 61 | Title: github.String(faker.Name()), 62 | CreatedAt: &now, 63 | UpdatedAt: &now, 64 | MergedAt: &now, 65 | Labels: nil, 66 | User: &github.User{ 67 | Login: github.String(faker.Name()), 68 | AvatarURL: github.String("https://example.com"), 69 | HTMLURL: github.String("https://example.com"), 70 | }, 71 | HTMLURL: github.String("https://example.com/1"), 72 | Assignees: nil, 73 | RequestedReviewers: nil, 74 | }, 75 | { 76 | Number: github.Int(2), 77 | Title: github.String(faker.Name()), 78 | CreatedAt: &now, 79 | UpdatedAt: &now, 80 | MergedAt: &now, 81 | Labels: nil, 82 | User: &github.User{ 83 | Login: github.String(faker.Name()), 84 | AvatarURL: github.String("https://example.com"), 85 | HTMLURL: github.String("https://example.com"), 86 | }, 87 | HTMLURL: github.String("https://example.com/2"), 88 | Assignees: nil, 89 | RequestedReviewers: nil, 90 | }, 91 | { 92 | Number: github.Int(3), 93 | Title: github.String(faker.Name()), 94 | CreatedAt: &now, 95 | UpdatedAt: &now, 96 | MergedAt: &now, 97 | Labels: nil, 98 | User: &github.User{ 99 | Login: github.String("dependabot[bot]"), 100 | AvatarURL: github.String("https://example.com"), 101 | HTMLURL: github.String("https://example.com"), 102 | }, 103 | HTMLURL: github.String("https://example.com/3"), 104 | Assignees: nil, 105 | RequestedReviewers: nil, 106 | }, 107 | { 108 | Number: github.Int(4), 109 | Title: github.String(faker.Name()), 110 | CreatedAt: &now, 111 | UpdatedAt: &now, 112 | MergedAt: &now, 113 | Labels: nil, 114 | User: &github.User{ 115 | Login: github.String("dependabot[bot]"), 116 | AvatarURL: github.String("https://example.com"), 117 | HTMLURL: github.String("https://example.com"), 118 | }, 119 | HTMLURL: github.String("https://example.com/4"), 120 | Assignees: nil, 121 | RequestedReviewers: nil, 122 | }, 123 | { 124 | Number: github.Int(5), 125 | Title: github.String(faker.Name()), 126 | CreatedAt: &now, 127 | UpdatedAt: &now, 128 | MergedAt: &now, 129 | Labels: nil, 130 | User: &github.User{ 131 | Login: github.String(faker.Name()), 132 | AvatarURL: github.String("https://example.com"), 133 | HTMLURL: github.String("https://example.com"), 134 | }, 135 | HTMLURL: github.String("https://example.com/5"), 136 | Assignees: nil, 137 | RequestedReviewers: nil, 138 | }, 139 | } 140 | got, err := markdown.MakePRBody(prs, "testdata/template.tmpl") 141 | assert.NoError(t, err) 142 | t.Log(got) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /internal/markdown/testdata/template.tmpl: -------------------------------------------------------------------------------- 1 | # Releases 2 | {{- range .PullRequests }} 3 | {{- if ne .User.LoginName "dependabot[bot]" }} 4 | {{ printf "- [ ] [%s] #%d @%s" (.MergedAt.Format "2006-01-02") .Number .User.LoginName}} 5 | {{- end }} 6 | {{- end }} 7 | 8 | # Dependabot 9 | {{- range .PullRequests }} 10 | {{- if eq .User.LoginName "dependabot[bot]" }} 11 | {{ printf "- [ ] #%d @%s" .Number .User.LoginName}} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /internal/pkg/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func LookUpString(env string, required bool) (string, error) { 11 | if v, ok := os.LookupEnv(env); ok { 12 | return v, nil 13 | } 14 | if required { 15 | return "", fmt.Errorf("environment variable is not set to %s", env) 16 | } 17 | return "", nil 18 | } 19 | 20 | func LookUpStringSlice(env string, required bool, sep string) ([]string, error) { 21 | if v, err := LookUpString(env, required); err != nil { 22 | return nil, err 23 | } else if v == "" { 24 | return nil, nil 25 | } else { 26 | return strings.Split(v, sep), nil 27 | } 28 | } 29 | 30 | func LookUpInt(env string, required bool) (int, error) { 31 | if envValue, err := LookUpString(env, required); err != nil { 32 | return 0, err 33 | } else if envValue == "" { 34 | return 0, nil 35 | } else { 36 | if v, err := strconv.Atoi(envValue); err != nil { 37 | return 0, err 38 | } else { 39 | return v, nil 40 | } 41 | } 42 | } 43 | 44 | func LookUpBool(env string, required bool) (bool, error) { 45 | if envValue, err := LookUpString(env, required); err != nil { 46 | return false, err 47 | } else if envValue == "" { 48 | return false, nil 49 | } else { 50 | if v, err := strconv.ParseBool(envValue); err != nil { 51 | return false, err 52 | } else { 53 | return v, nil 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/pkg/env/env_test.go: -------------------------------------------------------------------------------- 1 | package env_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/bxcodec/faker/v3" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/tomtwinkle/go-pr-release/internal/pkg/env" 11 | ) 12 | 13 | func TestLookUpString(t *testing.T) { 14 | type Param struct { 15 | Key string 16 | Required bool 17 | } 18 | type Want struct { 19 | Val string 20 | Err bool 21 | } 22 | tests := map[string]struct { 23 | SetEnv func(t *testing.T) (Param, Want) 24 | }{ 25 | "required:key exists": { 26 | SetEnv: func(t *testing.T) (Param, Want) { 27 | key := faker.UUIDHyphenated() 28 | val := faker.Sentence() 29 | t.Setenv(key, val) 30 | return Param{ 31 | Key: key, 32 | Required: true, 33 | }, Want{ 34 | Val: val, 35 | Err: false, 36 | } 37 | }, 38 | }, 39 | "required:key not exists": { 40 | SetEnv: func(t *testing.T) (Param, Want) { 41 | key := faker.UUIDHyphenated() 42 | val := faker.Sentence() 43 | t.Setenv(key, val) 44 | return Param{ 45 | Key: "NOT_EXIST", 46 | Required: true, 47 | }, Want{ 48 | Err: true, 49 | } 50 | }, 51 | }, 52 | "not required:key exists": { 53 | SetEnv: func(t *testing.T) (Param, Want) { 54 | key := faker.UUIDHyphenated() 55 | val := faker.Sentence() 56 | t.Setenv(key, val) 57 | return Param{ 58 | Key: key, 59 | Required: false, 60 | }, Want{ 61 | Val: val, 62 | Err: false, 63 | } 64 | }, 65 | }, 66 | "not required:key not exists": { 67 | SetEnv: func(t *testing.T) (Param, Want) { 68 | key := faker.UUIDHyphenated() 69 | val := faker.Sentence() 70 | t.Setenv(key, val) 71 | return Param{ 72 | Key: "NOT_EXIST", 73 | Required: false, 74 | }, Want{ 75 | Val: "", 76 | Err: false, 77 | } 78 | }, 79 | }, 80 | } 81 | 82 | for n, v := range tests { 83 | name := n 84 | tt := v 85 | t.Run(name, func(t *testing.T) { 86 | p, want := tt.SetEnv(t) 87 | got, err := env.LookUpString(p.Key, p.Required) 88 | if want.Err { 89 | assert.Error(t, err) 90 | return 91 | } 92 | if assert.NoError(t, err) { 93 | assert.Equal(t, want.Val, got) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestLookUpStringSlice(t *testing.T) { 100 | type Param struct { 101 | Key string 102 | Required bool 103 | Sep string 104 | } 105 | type Want struct { 106 | Val []string 107 | Err bool 108 | } 109 | tests := map[string]struct { 110 | SetEnv func(t *testing.T) (Param, Want) 111 | }{ 112 | "required:key exists:sep=,": { 113 | SetEnv: func(t *testing.T) (Param, Want) { 114 | key := faker.UUIDHyphenated() 115 | val := []string{faker.Sentence(), faker.Sentence()} 116 | t.Setenv(key, strings.Join(val, ",")) 117 | return Param{ 118 | Key: key, 119 | Required: true, 120 | Sep: ",", 121 | }, Want{ 122 | Val: val, 123 | Err: false, 124 | } 125 | }, 126 | }, 127 | "required:key exists:sep=|": { 128 | SetEnv: func(t *testing.T) (Param, Want) { 129 | key := faker.UUIDHyphenated() 130 | val := []string{faker.Sentence(), faker.Sentence()} 131 | t.Setenv(key, strings.Join(val, "|")) 132 | return Param{ 133 | Key: key, 134 | Required: true, 135 | Sep: "|", 136 | }, Want{ 137 | Val: val, 138 | Err: false, 139 | } 140 | }, 141 | }, 142 | "required:key not exists": { 143 | SetEnv: func(t *testing.T) (Param, Want) { 144 | return Param{ 145 | Key: "NOT_EXIST", 146 | Required: true, 147 | Sep: ",", 148 | }, Want{ 149 | Err: true, 150 | } 151 | }, 152 | }, 153 | "not required:key exists": { 154 | SetEnv: func(t *testing.T) (Param, Want) { 155 | key := faker.UUIDHyphenated() 156 | val := []string{faker.Sentence(), faker.Sentence()} 157 | t.Setenv(key, strings.Join(val, ",")) 158 | return Param{ 159 | Key: key, 160 | Required: false, 161 | Sep: ",", 162 | }, Want{ 163 | Val: val, 164 | Err: false, 165 | } 166 | }, 167 | }, 168 | "not required:key not exists": { 169 | SetEnv: func(t *testing.T) (Param, Want) { 170 | return Param{ 171 | Key: "NOT_EXIST", 172 | Required: false, 173 | Sep: ",", 174 | }, Want{ 175 | Val: nil, 176 | Err: false, 177 | } 178 | }, 179 | }, 180 | } 181 | 182 | for n, v := range tests { 183 | name := n 184 | tt := v 185 | t.Run(name, func(t *testing.T) { 186 | p, want := tt.SetEnv(t) 187 | got, err := env.LookUpStringSlice(p.Key, p.Required, p.Sep) 188 | if want.Err { 189 | assert.Error(t, err) 190 | return 191 | } 192 | if assert.NoError(t, err) { 193 | assert.Equal(t, want.Val, got) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestLookUpInt(t *testing.T) { 200 | type Param struct { 201 | Key string 202 | Required bool 203 | } 204 | type Want struct { 205 | Val int 206 | Err bool 207 | } 208 | tests := map[string]struct { 209 | SetEnv func(t *testing.T) (Param, Want) 210 | }{ 211 | "required:key exists:int": { 212 | SetEnv: func(t *testing.T) (Param, Want) { 213 | key := faker.UUIDHyphenated() 214 | val := "1" 215 | t.Setenv(key, val) 216 | return Param{ 217 | Key: key, 218 | Required: true, 219 | }, Want{ 220 | Val: 1, 221 | Err: false, 222 | } 223 | }, 224 | }, 225 | "required:key exists:int = 0": { 226 | SetEnv: func(t *testing.T) (Param, Want) { 227 | key := faker.UUIDHyphenated() 228 | val := "0" 229 | t.Setenv(key, val) 230 | return Param{ 231 | Key: key, 232 | Required: true, 233 | }, Want{ 234 | Val: 0, 235 | Err: false, 236 | } 237 | }, 238 | }, 239 | "required:key exists:not int": { 240 | SetEnv: func(t *testing.T) (Param, Want) { 241 | key := faker.UUIDHyphenated() 242 | val := "hoge" 243 | t.Setenv(key, val) 244 | return Param{ 245 | Key: key, 246 | Required: true, 247 | }, Want{ 248 | Err: true, 249 | } 250 | }, 251 | }, 252 | "key exists:not int": { 253 | SetEnv: func(t *testing.T) (Param, Want) { 254 | key := faker.UUIDHyphenated() 255 | val := "hoge" 256 | t.Setenv(key, val) 257 | return Param{ 258 | Key: key, 259 | }, Want{ 260 | Err: true, 261 | } 262 | }, 263 | }, 264 | "required:key not exists": { 265 | SetEnv: func(t *testing.T) (Param, Want) { 266 | return Param{ 267 | Key: "NOT_EXIST", 268 | Required: true, 269 | }, Want{ 270 | Err: true, 271 | } 272 | }, 273 | }, 274 | "key not exists": { 275 | SetEnv: func(t *testing.T) (Param, Want) { 276 | return Param{ 277 | Key: "NOT_EXIST", 278 | }, Want{ 279 | Val: 0, 280 | Err: false, 281 | } 282 | }, 283 | }, 284 | } 285 | 286 | for n, v := range tests { 287 | name := n 288 | tt := v 289 | t.Run(name, func(t *testing.T) { 290 | p, want := tt.SetEnv(t) 291 | got, err := env.LookUpInt(p.Key, p.Required) 292 | if want.Err { 293 | assert.Error(t, err) 294 | return 295 | } 296 | if assert.NoError(t, err) { 297 | assert.Equal(t, want.Val, got) 298 | } 299 | }) 300 | } 301 | } 302 | 303 | func TestLookUpBool(t *testing.T) { 304 | type Param struct { 305 | Key string 306 | Required bool 307 | } 308 | type Want struct { 309 | Val bool 310 | Err bool 311 | } 312 | tests := map[string]struct { 313 | SetEnv func(t *testing.T) (Param, Want) 314 | }{ 315 | "required:key exists:true": { 316 | SetEnv: func(t *testing.T) (Param, Want) { 317 | key := faker.UUIDHyphenated() 318 | val := "true" 319 | t.Setenv(key, val) 320 | return Param{ 321 | Key: key, 322 | Required: true, 323 | }, Want{ 324 | Val: true, 325 | Err: false, 326 | } 327 | }, 328 | }, 329 | "required:key exists:false": { 330 | SetEnv: func(t *testing.T) (Param, Want) { 331 | key := faker.UUIDHyphenated() 332 | val := "false" 333 | t.Setenv(key, val) 334 | return Param{ 335 | Key: key, 336 | Required: true, 337 | }, Want{ 338 | Val: false, 339 | Err: false, 340 | } 341 | }, 342 | }, 343 | "required:key exists:1": { 344 | SetEnv: func(t *testing.T) (Param, Want) { 345 | key := faker.UUIDHyphenated() 346 | val := "1" 347 | t.Setenv(key, val) 348 | return Param{ 349 | Key: key, 350 | Required: true, 351 | }, Want{ 352 | Val: true, 353 | Err: false, 354 | } 355 | }, 356 | }, 357 | "required:key exists:0": { 358 | SetEnv: func(t *testing.T) (Param, Want) { 359 | key := faker.UUIDHyphenated() 360 | val := "0" 361 | t.Setenv(key, val) 362 | return Param{ 363 | Key: key, 364 | Required: true, 365 | }, Want{ 366 | Val: false, 367 | Err: false, 368 | } 369 | }, 370 | }, 371 | "required:key exists:not bool": { 372 | SetEnv: func(t *testing.T) (Param, Want) { 373 | key := faker.UUIDHyphenated() 374 | val := "hoge" 375 | t.Setenv(key, val) 376 | return Param{ 377 | Key: key, 378 | Required: true, 379 | }, Want{ 380 | Err: true, 381 | } 382 | }, 383 | }, 384 | "key exists:not bool": { 385 | SetEnv: func(t *testing.T) (Param, Want) { 386 | key := faker.UUIDHyphenated() 387 | val := "hoge" 388 | t.Setenv(key, val) 389 | return Param{ 390 | Key: key, 391 | }, Want{ 392 | Err: true, 393 | } 394 | }, 395 | }, 396 | "required:key not exists": { 397 | SetEnv: func(t *testing.T) (Param, Want) { 398 | return Param{ 399 | Key: "NOT_EXIST", 400 | Required: true, 401 | }, Want{ 402 | Err: true, 403 | } 404 | }, 405 | }, 406 | "key not exists": { 407 | SetEnv: func(t *testing.T) (Param, Want) { 408 | return Param{ 409 | Key: "NOT_EXIST", 410 | }, Want{ 411 | Val: false, 412 | Err: false, 413 | } 414 | }, 415 | }, 416 | } 417 | 418 | for n, v := range tests { 419 | name := n 420 | tt := v 421 | t.Run(name, func(t *testing.T) { 422 | p, want := tt.SetEnv(t) 423 | got, err := env.LookUpBool(p.Key, p.Required) 424 | if want.Err { 425 | assert.Error(t, err) 426 | return 427 | } 428 | if assert.NoError(t, err) { 429 | assert.Equal(t, want.Val, got) 430 | } 431 | }) 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /internal/pkg/gh/gh.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-git/go-git/v5" 14 | "github.com/go-git/go-git/v5/plumbing" 15 | "github.com/go-git/go-git/v5/plumbing/object" 16 | "github.com/go-git/go-git/v5/plumbing/storer" 17 | "github.com/go-git/go-git/v5/storage/memory" 18 | "github.com/google/go-github/v45/github" 19 | "golang.org/x/oauth2" 20 | "golang.org/x/sync/errgroup" 21 | ) 22 | 23 | const AsynchronousTimeout = 60 * time.Second 24 | 25 | const ( 26 | DefaultRemoteName = "origin" 27 | DefaultGitDir = ".git" 28 | ) 29 | 30 | var ( 31 | ErrBranchNotFound = errors.New("branch not found") 32 | ) 33 | 34 | type Github interface { 35 | CreateReleasePR(ctx context.Context, title, fromBranch, toBranch, body string) (*github.PullRequest, error) 36 | GetReleasePR(ctx context.Context, fromBranch, toBranch string) (*github.PullRequest, error) 37 | GetMergedPRs(ctx context.Context, fromBranch, toBranch string) (PullRequests, error) 38 | AssignReviews(ctx context.Context, prNumber int, reviewers ...string) (*github.PullRequest, error) 39 | Labeling(ctx context.Context, prNumber int, labels ...string) ([]*github.Label, error) 40 | } 41 | 42 | type gh struct { 43 | client *github.Client 44 | repository *git.Repository 45 | remote *git.Remote 46 | config *RemoteConfig 47 | 48 | logger *slog.Logger 49 | } 50 | 51 | type RemoteConfig struct { 52 | Owner string 53 | Repo string 54 | } 55 | 56 | type gitConfig struct { 57 | Owner string 58 | Repo string 59 | } 60 | 61 | func New(ctx context.Context, token string, logger *slog.Logger) (Github, error) { 62 | cnf, r, remote, err := gitRemoteConfig() 63 | if err != nil { 64 | return nil, err 65 | } 66 | return &gh{ 67 | client: newClient(ctx, token), 68 | repository: r, 69 | remote: remote, 70 | config: &RemoteConfig{ 71 | Owner: cnf.Owner, 72 | Repo: cnf.Repo, 73 | }, 74 | logger: logger, 75 | }, nil 76 | } 77 | 78 | func NewWithConfig(ctx context.Context, token string, logger *slog.Logger, remoteConfig RemoteConfig) (Github, error) { 79 | options, err := getOptions(token, remoteConfig) 80 | if err != nil { 81 | return nil, err 82 | } 83 | r, err := git.CloneContext(ctx, memory.NewStorage(), nil, options) 84 | if err != nil { 85 | return nil, err 86 | } 87 | remote, err := r.Remote(DefaultRemoteName) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if logger == nil { 92 | logger = slog.Default() 93 | } 94 | return &gh{ 95 | client: newClient(ctx, token), 96 | repository: r, 97 | remote: remote, 98 | config: &remoteConfig, 99 | logger: logger, 100 | }, nil 101 | } 102 | 103 | func getOptions(token string, remoteConfig RemoteConfig) (*git.CloneOptions, error) { 104 | options := &git.CloneOptions{ 105 | URL: fmt.Sprintf("https://%s@github.com/%s/%s", token, remoteConfig.Owner, remoteConfig.Repo), 106 | RemoteName: DefaultRemoteName, 107 | } 108 | if err := options.Validate(); err != nil { 109 | return nil, err 110 | } 111 | return options, nil 112 | } 113 | 114 | func gitRemoteConfig() (*gitConfig, *git.Repository, *git.Remote, error) { 115 | if f, err := os.Stat(DefaultGitDir); os.IsNotExist(err) || !f.IsDir() { 116 | return nil, nil, nil, fmt.Errorf("not found %s", DefaultGitDir) 117 | } 118 | r, err := git.PlainOpen(DefaultGitDir) 119 | if err != nil { 120 | return nil, nil, nil, err 121 | } 122 | remote, err := r.Remote(DefaultRemoteName) 123 | if err != nil { 124 | return nil, nil, nil, err 125 | } 126 | if len(remote.Config().URLs) == 0 { 127 | return nil, nil, nil, errors.New("no set origin git urls") 128 | } 129 | url := remote.Config().URLs[0] 130 | url = strings.TrimPrefix(url, "https://github.com/") 131 | url = strings.TrimPrefix(url, "git@github.com:") 132 | url = strings.TrimSuffix(url, ".git") 133 | ss := strings.Split(url, "/") 134 | owner := ss[0] 135 | repo := ss[1] 136 | return &gitConfig{Owner: owner, Repo: repo}, r, remote, nil 137 | } 138 | 139 | func newClient(ctx context.Context, token string) *github.Client { 140 | ts := oauth2.StaticTokenSource( 141 | &oauth2.Token{AccessToken: token}, 142 | ) 143 | tc := oauth2.NewClient(ctx, ts) 144 | 145 | return github.NewClient(tc) 146 | } 147 | 148 | type PullRequests []*github.PullRequest 149 | 150 | func (prs PullRequests) FindHash(sha string) (*github.PullRequest, bool) { 151 | for _, v := range prs { 152 | v.Head.GetSHA() 153 | if v.GetMergeCommitSHA() == sha { 154 | return v, true 155 | } 156 | } 157 | return nil, false 158 | } 159 | func (prs PullRequests) SHAs() []string { 160 | shas := make([]string, len(prs)) 161 | for i, v := range prs { 162 | shas[i] = v.GetMergeCommitSHA() 163 | } 164 | return shas 165 | } 166 | 167 | func (g *gh) GetMergedPRs(ctx context.Context, fromBranch, toBranch string) (PullRequests, error) { 168 | if err := g.remote.FetchContext(ctx, &git.FetchOptions{ 169 | RemoteName: DefaultRemoteName, 170 | }); err != nil { 171 | if !errors.Is(err, git.NoErrAlreadyUpToDate) { 172 | return nil, fmt.Errorf("git Remote Fetch error: %w", err) 173 | } 174 | } 175 | toHash, err := g.resolveBranch(ctx, toBranch) 176 | if err != nil { 177 | return nil, err 178 | } 179 | fromHash, err := g.resolveBranch(ctx, fromBranch) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | prNums, err := g.fetchMergedPRNumsFromGit(ctx, *fromHash, *toHash) 185 | if err != nil { 186 | return nil, err 187 | } 188 | prs, err := g.fetchPRsFromGithub(ctx, prNums) 189 | if err != nil { 190 | return nil, err 191 | } 192 | return prs, nil 193 | } 194 | 195 | func (g *gh) resolveBranch(ctx context.Context, remoteBranch string) (*plumbing.Hash, error) { 196 | revision := plumbing.Revision(fmt.Sprintf("refs/remotes/%s/%s", DefaultRemoteName, remoteBranch)) 197 | hash, err := g.repository.ResolveRevision(revision) 198 | if err != nil { 199 | return nil, fmt.Errorf("resolve error [%s]: %w", remoteBranch, err) 200 | } 201 | g.logger.DebugContext(ctx, fmt.Sprintf("resolve branch=%s, hash=%s", remoteBranch, hash.String())) 202 | return hash, nil 203 | } 204 | 205 | func (g *gh) fetchMergedPRNumsFromGit(ctx context.Context, fromHash, toHash plumbing.Hash) ([]int, error) { 206 | fromCommit, err := g.repository.CommitObject(fromHash) 207 | if err != nil { 208 | return nil, fmt.Errorf("github Git GetCommit fromHash error [%s]: %w", fromHash.String(), err) 209 | } 210 | toCommit, err := g.repository.CommitObject(toHash) 211 | if err != nil { 212 | return nil, fmt.Errorf("github Git GetCommit toHash error [%s]: %w", toHash.String(), err) 213 | } 214 | toCommitHashes := make(map[plumbing.Hash]struct{}, len(toCommit.ParentHashes)) 215 | for _, v := range toCommit.ParentHashes { 216 | toCommitHashes[v] = struct{}{} 217 | } 218 | 219 | itr, err := g.repository.Log(&git.LogOptions{ 220 | From: fromHash, 221 | Order: git.LogOrderCommitterTime, 222 | }) 223 | if err != nil { 224 | return nil, fmt.Errorf("git Repository Logs error [%s]: %w", fromHash, err) 225 | } 226 | 227 | var mergedFeatureHeadCommits = make(map[plumbing.Hash]*object.Commit) 228 | if err := itr.ForEach(func(c *object.Commit) error { 229 | if _, ok := toCommitHashes[c.Hash]; ok { 230 | return storer.ErrStop 231 | } 232 | mergeCommits, err := fromCommit.MergeBase(c) 233 | if err != nil { 234 | return fmt.Errorf("git MergeBase error: %w", err) 235 | } 236 | for _, mc := range mergeCommits { 237 | g.logger.DebugContext(ctx, fmt.Sprintf("git repo log hash=%s commit=%s", c.Hash.String(), c.Message)) 238 | mergedFeatureHeadCommits[mc.Hash] = mc 239 | } 240 | return nil 241 | }); err != nil { 242 | return nil, fmt.Errorf("git Repository Logs Iterate error: %w", err) 243 | } 244 | 245 | refs, err := g.remote.List(&git.ListOptions{ 246 | // Returns all references, including peeled references. 247 | PeelingOption: git.AppendPeeled, 248 | }) 249 | if err != nil { 250 | return nil, fmt.Errorf("git Remote refs List error: %w", err) 251 | } 252 | 253 | var prNums = make([]int, 0, len(refs)) 254 | for _, ref := range refs { 255 | if _, ok := mergedFeatureHeadCommits[ref.Hash()]; !ok { 256 | continue 257 | } 258 | refName := ref.Name().String() 259 | if RegRefPullRequest.MatchString(refName) { 260 | prNum, err := strconv.Atoi(RegRefPullRequest.ReplaceAllString(refName, "$1")) 261 | if err != nil { 262 | return nil, err 263 | } 264 | prNums = append(prNums, prNum) 265 | } 266 | } 267 | return prNums, nil 268 | } 269 | 270 | func (g *gh) fetchPRsFromGithub(ctx context.Context, prNums []int) (PullRequests, error) { 271 | var ( 272 | eg errgroup.Group 273 | fetchPrs = make(PullRequests, len(prNums)) 274 | ) 275 | for i, prNum := range prNums { 276 | i := i 277 | prNum := prNum 278 | eg.Go(func() error { 279 | pr, _, err := g.client.PullRequests.Get(ctx, g.config.Owner, g.config.Repo, prNum) 280 | if err != nil { 281 | return fmt.Errorf("github PullRequest Get error [%d]: %w", prNum, err) 282 | } 283 | fetchPrs[i] = pr 284 | return nil 285 | }) 286 | } 287 | if err := eg.Wait(); err != nil { 288 | return nil, err 289 | } 290 | 291 | prs := make(PullRequests, 0, len(prNums)) 292 | for _, pr := range fetchPrs { 293 | if pr == nil { 294 | continue 295 | } 296 | if pr.Merged != nil && *pr.Merged { 297 | prs = append(prs, pr) 298 | } 299 | } 300 | return prs, nil 301 | } 302 | 303 | func (g *gh) GetReleasePR(ctx context.Context, fromBranch, toBranch string) (*github.PullRequest, error) { 304 | var existsPR *github.PullRequest 305 | opt := &github.PullRequestListOptions{ 306 | State: "open", 307 | Head: fmt.Sprintf("%s/%s", DefaultRemoteName, fromBranch), 308 | Base: toBranch, 309 | Sort: "created", 310 | Direction: "desc", 311 | ListOptions: github.ListOptions{}, 312 | } 313 | prs, _, err := g.client.PullRequests.List(ctx, g.config.Owner, g.config.Repo, opt) 314 | if err != nil { 315 | return nil, fmt.Errorf("github PullRequest List error head=%s, base=%s: %w", opt.Head, toBranch, err) 316 | } 317 | for _, pr := range prs { 318 | if pr.GetBase().GetRef() == toBranch && pr.GetHead().GetRef() == fromBranch && pr.MergedAt == nil { 319 | existsPR = pr 320 | break 321 | } 322 | } 323 | if existsPR == nil { 324 | return nil, ErrBranchNotFound 325 | } 326 | return existsPR, nil 327 | } 328 | 329 | func (g *gh) CreateReleasePR(ctx context.Context, title, fromBranch, toBranch, body string) (*github.PullRequest, error) { 330 | var basePR *github.PullRequest 331 | if pr, err := g.GetReleasePR(ctx, fromBranch, toBranch); err != nil { 332 | if !errors.Is(ErrBranchNotFound, err) { 333 | return nil, err 334 | } 335 | } else { 336 | basePR = pr 337 | } 338 | 339 | if basePR != nil { 340 | basePR.Title = github.String(title) 341 | basePR.Body = github.String(body) 342 | pr, _, err := g.client.PullRequests.Edit(ctx, g.config.Owner, g.config.Repo, basePR.GetNumber(), basePR) 343 | if err != nil { 344 | return nil, err 345 | } 346 | return pr, nil 347 | } else { 348 | newPR := &github.NewPullRequest{ 349 | Title: github.String(title), 350 | Head: github.String(fmt.Sprintf("%s:%s", g.config.Owner, fromBranch)), 351 | Base: github.String(toBranch), 352 | Body: github.String(body), 353 | } 354 | pr, _, err := g.client.PullRequests.Create(ctx, g.config.Owner, g.config.Repo, newPR) 355 | if err != nil { 356 | return nil, err 357 | } 358 | return pr, nil 359 | } 360 | } 361 | 362 | func (g *gh) AssignReviews(ctx context.Context, prNumber int, reviewers ...string) (*github.PullRequest, error) { 363 | pr, _, err := g.client.PullRequests.RequestReviewers(ctx, g.config.Owner, g.config.Repo, prNumber, github.ReviewersRequest{ 364 | Reviewers: reviewers, 365 | }) 366 | if err != nil { 367 | return nil, err 368 | } 369 | return pr, nil 370 | } 371 | 372 | func (g *gh) Labeling(ctx context.Context, prNumber int, labels ...string) ([]*github.Label, error) { 373 | resLabels, _, err := g.client.Issues.AddLabelsToIssue(ctx, g.config.Owner, g.config.Repo, prNumber, labels) 374 | if err != nil { 375 | return nil, err 376 | } 377 | return resLabels, nil 378 | } 379 | -------------------------------------------------------------------------------- /internal/pkg/gh/gh_test.go: -------------------------------------------------------------------------------- 1 | package gh_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "testing" 8 | 9 | "github.com/joho/godotenv" 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/tomtwinkle/go-pr-release/internal/pkg/gh" 13 | ) 14 | 15 | func TestGh_GetMergedPRs(t *testing.T) { 16 | if _, ok := os.LookupEnv("CI"); ok { 17 | t.SkipNow() 18 | } 19 | assert.NoError(t, godotenv.Load("../../../.env")) 20 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 21 | Level: slog.LevelDebug, 22 | })) 23 | 24 | t.Run("GetMergedPRs", func(t *testing.T) { 25 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 26 | if !assert.True(t, ok) { 27 | return 28 | } 29 | owner := "tomtwinkle" 30 | repo := "go-pr-release-test" 31 | fromBranch := "develop" 32 | toBranch := "main" 33 | ctx := context.Background() 34 | 35 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 36 | assert.NoError(t, err) 37 | prs, err := g.GetMergedPRs(ctx, fromBranch, toBranch) 38 | assert.NoError(t, err) 39 | wantIDs := []int{9, 6} 40 | wantTitles := []string{"feat: pr8 can merge", "feat: pr5 can merge"} 41 | 42 | if assert.Equal(t, len(wantIDs), len(prs)) { 43 | for i, pr := range prs { 44 | assert.Equal(t, wantIDs[i], pr.GetNumber()) 45 | assert.Equal(t, wantTitles[i], pr.GetTitle()) 46 | t.Logf("%+v,%+v,%+v,%+v,%+v,%+v,%+v", pr.GetID(), pr.GetTitle(), pr.GetState(), pr.MergedAt, pr.GetMergeCommitSHA(), pr.GetUser().GetHTMLURL(), pr.GetURL()) 47 | } 48 | } 49 | }) 50 | } 51 | 52 | func TestGh_GetReleasePR(t *testing.T) { 53 | if _, ok := os.LookupEnv("CI"); ok { 54 | t.SkipNow() 55 | } 56 | assert.NoError(t, godotenv.Load("../../../.env")) 57 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 58 | Level: slog.LevelDebug, 59 | })) 60 | 61 | t.Run("Create PR from branch", func(t *testing.T) { 62 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 63 | if !assert.True(t, ok) { 64 | return 65 | } 66 | owner := "tomtwinkle" 67 | repo := "go-pr-release-test" 68 | ctx := context.Background() 69 | 70 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 71 | assert.NoError(t, err) 72 | pr, err := g.GetReleasePR(ctx, "develop", "main") 73 | assert.NoError(t, err) 74 | t.Logf("%+v", pr) 75 | }) 76 | } 77 | 78 | func TestGh_CreatePRFromBranch(t *testing.T) { 79 | if _, ok := os.LookupEnv("CI"); ok { 80 | t.SkipNow() 81 | } 82 | assert.NoError(t, godotenv.Load("../../../.env")) 83 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 84 | Level: slog.LevelDebug, 85 | })) 86 | 87 | t.Run("Create PR from branch", func(t *testing.T) { 88 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 89 | if !assert.True(t, ok) { 90 | return 91 | } 92 | owner := "tomtwinkle" 93 | repo := "go-pr-release-test" 94 | ctx := context.Background() 95 | 96 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 97 | assert.NoError(t, err) 98 | pr, err := g.CreateReleasePR(ctx, "Merge to main from develop", "develop", "main", "test") 99 | assert.NoError(t, err) 100 | t.Logf("%+v", pr) 101 | }) 102 | 103 | t.Run("Edit PR from branch", func(t *testing.T) { 104 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 105 | if !assert.True(t, ok) { 106 | return 107 | } 108 | owner := "tomtwinkle" 109 | repo := "go-pr-release-test" 110 | ctx := context.Background() 111 | 112 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 113 | assert.NoError(t, err) 114 | pr, err := g.CreateReleasePR(ctx, "Merge to main from develop", "develop", "main", "test") 115 | assert.NoError(t, err) 116 | t.Logf("%+v", pr) 117 | }) 118 | } 119 | 120 | func TestGh_AssignReviews(t *testing.T) { 121 | if _, ok := os.LookupEnv("CI"); ok { 122 | t.SkipNow() 123 | } 124 | assert.NoError(t, godotenv.Load("../../../.env")) 125 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 126 | Level: slog.LevelDebug, 127 | })) 128 | 129 | t.Run("AssignReviews", func(t *testing.T) { 130 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 131 | if !assert.True(t, ok) { 132 | return 133 | } 134 | owner := "tomtwinkle" 135 | repo := "go-pr-release-test" 136 | ctx := context.Background() 137 | 138 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 139 | assert.NoError(t, err) 140 | pr, err := g.CreateReleasePR(ctx, "Merge to main from develop", "develop", "main", "test") 141 | if !assert.NoError(t, err) { 142 | return 143 | } 144 | pr, err = g.AssignReviews(ctx, pr.GetNumber(), "soe-j") 145 | assert.NoError(t, err) 146 | t.Logf("%+v", pr.Assignees) 147 | }) 148 | } 149 | 150 | func TestGh_Labeling(t *testing.T) { 151 | if _, ok := os.LookupEnv("CI"); ok { 152 | t.SkipNow() 153 | } 154 | assert.NoError(t, godotenv.Load("../../../.env")) 155 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 156 | Level: slog.LevelDebug, 157 | })) 158 | 159 | t.Run("Labeling", func(t *testing.T) { 160 | token, ok := os.LookupEnv("GO_PR_RELEASE_TOKEN") 161 | if !assert.True(t, ok) { 162 | return 163 | } 164 | owner := "tomtwinkle" 165 | repo := "go-pr-release-test" 166 | ctx := context.Background() 167 | 168 | g, err := gh.NewWithConfig(ctx, token, logger, gh.RemoteConfig{Owner: owner, Repo: repo}) 169 | assert.NoError(t, err) 170 | pr, err := g.CreateReleasePR(ctx, "Merge to main from develop", "develop", "main", "test") 171 | if !assert.NoError(t, err) { 172 | return 173 | } 174 | labels, err := g.Labeling(ctx, pr.GetNumber(), "test", "release", "develop") 175 | assert.NoError(t, err) 176 | t.Logf("%+v", labels) 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /internal/pkg/gh/regexp.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import "regexp" 4 | 5 | var RegRefPullRequest = regexp.MustCompile(`^refs/pull/(\d+)/head$`) 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/tomtwinkle/go-pr-release/internal/cli" 7 | ) 8 | 9 | var ( 10 | name string 11 | version string 12 | commit string 13 | date string 14 | ) 15 | 16 | func main() { 17 | os.Exit(cli.Run(name, version, commit, date)) 18 | } 19 | --------------------------------------------------------------------------------