├── .github ├── CODEOWNERS └── workflows │ ├── ci.yaml │ ├── container.yaml │ └── github-actions-merger.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── go.mod ├── go.sum ├── main.go ├── main_test.go └── template └── commit.tmpl /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @na-ga @peaceiris @kanata2 @tetsuya28 @ucpr 2 | .* @na-ga @peaceiris @kanata2 @tetsuya28 @ucpr 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-24.04 12 | timeout-minutes: 5 13 | permissions: 14 | contents: read 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 18 | with: 19 | go-version-file: "go.mod" 20 | - run: go mod download 21 | - run: go build 22 | - run: go test ./... -race 23 | -------------------------------------------------------------------------------- /.github/workflows/container.yaml: -------------------------------------------------------------------------------- 1 | name: Container 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | 9 | jobs: 10 | build_and_push: 11 | runs-on: ubuntu-24.04 12 | timeout-minutes: 10 13 | permissions: 14 | contents: read 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: docker login 19 | run: echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin 20 | env: 21 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 22 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 23 | 24 | - run: make docker-build 25 | 26 | - run: make docker-push 27 | if: github.event_name != 'pull_request' 28 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-merger.yaml: -------------------------------------------------------------------------------- 1 | name: merge 2 | 3 | on: 4 | issue_comment: 5 | types: [created, edited] 6 | 7 | jobs: 8 | merge: 9 | if: ${{ (github.event.issue.pull_request) && (github.event.comment.body == '/merge') }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create merge commit with labels 13 | uses: abema/github-actions-merger@7a58d581874bf3a23720491331f4ab3d8072641b # ratchet:abema/github-actions-merger@main 14 | with: 15 | "github_token": ${{ secrets.GITHUB_TOKEN }} 16 | "owner": ${{ github.event.repository.owner.login }} 17 | "repo": ${{ github.event.repository.name }} 18 | "pr_number": ${{ github.event.issue.number }} 19 | "comment": ${{ github.event.comment.body }} 20 | -------------------------------------------------------------------------------- /.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 | # binary 18 | github-actions-merger 19 | 20 | # Goland 21 | .idea/ 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.4-alpine AS builder 2 | 3 | WORKDIR /app 4 | RUN apk add --no-cache curl 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | COPY *.go ./ 8 | COPY template ./template 9 | ENV GH_VERSION="2.64.0" 10 | RUN curl -s -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o "gh_${GH_VERSION}_linux_amd64.tar.gz" 11 | RUN tar -xvf "gh_${GH_VERSION}_linux_amd64.tar.gz" 12 | RUN cp "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/bin/gh 13 | RUN CGO_ENABLED=0 GOOS=linux go build -a -o /bin/app 14 | 15 | FROM alpine:3.21.0 16 | COPY --from=builder /bin/app /bin/app 17 | COPY --from=builder /usr/bin/gh /bin/gh 18 | ENTRYPOINT ["/bin/app"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AbemaTV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE = abema/github-actions-merger 2 | 3 | .PHONY: docker-build 4 | docker-build: TAG := latest 5 | docker-build: 6 | docker build -t ${DOCKER_IMAGE}:${TAG} . 7 | 8 | .PHONY: docker-push 9 | docker-push: TAG := latest 10 | docker-push: docker-build 11 | docker push ${DOCKER_IMAGE}:${TAG} 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-actions-merger 2 | 3 | github-actions-merger is a custom GitHub Action that merges pull request with metadata (commit message, pull request labels, release-note block, and git trailers). 4 | 5 | ## Usage 6 | 7 | Write your workflow file. 8 | 9 | ```yaml 10 | - name: merge 11 | uses: abema/github-actions-merger@main 12 | with: 13 | "github_token": ${{ secrets.GITHUB_TOKEN }} 14 | "owner": ${{ github.event.repository.owner.login }} 15 | "repo": ${{ github.event.repository.name }} 16 | "pr_number": ${{ github.event.issue.number }} 17 | "comment": ${{ github.event.comment.body }} 18 | "mergers": 'na-ga,0daryo' 19 | ``` 20 | 21 | https://github.com/abema/github-actions-merger/blob/main/.github/workflows/github-actions-merger.yaml 22 | 23 | Post a comment with ```/merge``` on a GitHub pull request. 24 | 25 | A pull-request body can include release-note block. 26 | 27 | e.g. 28 | 29 | ```release-note 30 | Breaking change! 31 | ``` 32 | 33 | The pull request will be merged, and commit message includes labels and release-note block as following. 34 | 35 | ~~~md 36 | fix: readme 37 | --- 38 | Labels: 39 | * documentation 40 | * enhancement 41 | ```release-note 42 | Breaking change! 43 | ``` 44 | ~~~ 45 | 46 | 47 | ## Parameters 48 | 49 | You need to set parameters in workflow. 50 | 51 | ```yaml 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | owner: ${{ github.event.repository.owner.login }} 54 | repo: ${{ github.event.repository.name }} 55 | pr_number: ${{ github.event.issue.number }} 56 | comment: ${{ github.event.comment.body }} 57 | merge_method: 'merge' 58 | mergers: 'comma separeted github usernames. every user is allowed if not specified' 59 | enable_auto_merge: true 60 | git_trailers: 'Co-authored-by=abema,Co-authored-by=actions' 61 | ``` 62 | 63 | 64 | ## Options 65 | 66 | ### Enable Auto Merge 67 | 68 | - [About auto merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request#about-auto-merge) 69 | - You can use the auto merge when `enable_auto_merge` is true. 70 | - Default is `false`. 71 | - For more information about enabling auto merge to see the Note: [Enabling auto-merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request#about-auto-merge). 72 | 73 | 74 | ## Note 75 | 76 | **Setting the branch protection rules is recommended to avoid unexpected merging of pull requests.** 77 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'github-actions-merger' 2 | description: 'Merge pull-requests with metadata' 3 | author: 'abema' 4 | branding: 5 | icon: 'git-merge' 6 | color: 'green' 7 | runs: 8 | using: 'docker' 9 | image: 'docker://abema/github-actions-merger@sha256:2fd78dac7f26b81a128e73107a6342356f27aca95477c6d1dba167483febd09b' 10 | inputs: 11 | merge_method: 12 | description: 'merge method' 13 | required: false 14 | default: 'merge' 15 | github_token: 16 | description: 'github token' 17 | required: true 18 | owner: 19 | description: 'owner' 20 | required: true 21 | repo: 22 | description: 'repository' 23 | required: true 24 | pr_number: 25 | description: 'pull request number' 26 | required: true 27 | comment: 28 | description: 'pull comment' 29 | required: true 30 | mergers: 31 | description: 'github username who can trigger merger. every user is allowed if not specified. format must be comma separated .e.g. na-ga,0daryo' 32 | required: false 33 | enable_auto_merge: 34 | description: 'enable auto merge' 35 | required: false 36 | git_trailers: 37 | description: 'git trailers' 38 | required: false 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abema/github-actions-merger 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/google/go-github v17.0.0+incompatible 9 | github.com/kelseyhightower/envconfig v1.4.0 10 | golang.org/x/oauth2 v0.24.0 11 | ) 12 | 13 | require github.com/google/go-querystring v1.1.0 // indirect 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 3 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 5 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 6 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 7 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 8 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 9 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 10 | golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= 11 | golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 12 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "text/template" 13 | "time" 14 | 15 | "github.com/google/go-github/github" 16 | "github.com/kelseyhightower/envconfig" 17 | "golang.org/x/oauth2" 18 | ) 19 | 20 | type env struct { 21 | GithubToken string `envconfig:"GITHUB_TOKEN"` 22 | Owner string `envconfig:"OWNER"` 23 | Repo string `envconfig:"REPO"` 24 | PRNumber int `envconfig:"PR_NUMBER"` 25 | Comment string `envconfig:"COMMENT"` 26 | MergeMethod string `envconfig:"MERGE_METHOD" default:"merge"` 27 | Mergers []string `envconfig:"MERGERS"` 28 | Actor string `envconfig:"GITHUB_ACTOR"` // github user who initiated the workflow. 29 | EnableAutoMerge bool `envconfig:"ENABLE_AUTO_MERGE" default:"false"` 30 | GitTrailers []string `envconfig:"GIT_TRAILERS"` 31 | } 32 | 33 | const ( 34 | mergeComment = "/merge" 35 | jobTimeout = 10 * 60 * time.Second 36 | ) 37 | 38 | func main() { 39 | var e env 40 | err := envconfig.Process("INPUT", &e) 41 | if err != nil { 42 | fmt.Printf("failed to load inputs: %s\n", err.Error()) 43 | panic(err.Error()) 44 | } 45 | err = os.Setenv("GITHUB_TOKEN", e.GithubToken) 46 | if err != nil { 47 | fmt.Printf("failed to set env: %s\n", err.Error()) 48 | panic(err.Error()) 49 | } 50 | 51 | ctx, f := context.WithTimeout(context.Background(), jobTimeout) 52 | defer f() 53 | client := newGHClient(e.GithubToken) 54 | if err := validateEnv(e); err != nil { 55 | if serr := client.sendMsg(ctx, e.Owner, e.Repo, e.PRNumber, errMsg(err)); serr != nil { 56 | fmt.Printf("failed to send message: %v original: %v", serr, err) 57 | panic(serr.Error()) 58 | } 59 | fmt.Printf("failed to validate env: %v", err) 60 | panic(err.Error()) 61 | } 62 | if err := client.merge(ctx, e.Owner, e.Repo, e.PRNumber, e.MergeMethod, e.EnableAutoMerge, e.GitTrailers); err != nil { 63 | if serr := client.sendMsg(ctx, e.Owner, e.Repo, e.PRNumber, errMsg(err)); serr != nil { 64 | fmt.Printf("failed to send message: %v original: %v", serr, err) 65 | panic(serr.Error()) 66 | } 67 | fmt.Printf("failed to merge: %v", err) 68 | panic(err.Error()) 69 | } 70 | var successMsg string 71 | if e.EnableAutoMerge { 72 | successMsg = "Enabled auto merge #" + fmt.Sprintf("%d", e.PRNumber) + " \nIf CI fails, fix problems and retry." 73 | } else { 74 | successMsg = "Merged PR #" + fmt.Sprintf("%d", e.PRNumber) + " successfully!" 75 | } 76 | if err := client.sendMsg(ctx, e.Owner, e.Repo, e.PRNumber, successMsg); err != nil { 77 | fmt.Printf("failed to send message: %v", err) 78 | panic(err.Error()) 79 | } 80 | fmt.Printf(successMsg) 81 | } 82 | 83 | func validateEnv(e env) error { 84 | if e.Comment != mergeComment { 85 | return fmt.Errorf("comment must be %s, got %s", mergeComment, e.Comment) 86 | } 87 | if len(e.Mergers) == 0 { 88 | return nil 89 | } 90 | for _, m := range e.Mergers { 91 | if e.Actor == m { 92 | // if actor matches specified mergers, then valid workflow run. 93 | return nil 94 | } 95 | } 96 | return fmt.Errorf("actor %s is not in mergers list", e.Actor) 97 | } 98 | 99 | type ghClient struct { 100 | client *github.Client 101 | } 102 | 103 | func newGHClient(token string) *ghClient { 104 | ctx := context.Background() 105 | ts := oauth2.StaticTokenSource( 106 | &oauth2.Token{AccessToken: token}, 107 | ) 108 | tc := oauth2.NewClient(ctx, ts) 109 | client := github.NewClient(tc) 110 | return &ghClient{ 111 | client: client, 112 | } 113 | } 114 | 115 | func (gh *ghClient) merge( 116 | ctx context.Context, owner, repo string, prNumber int, mergeMethod string, enableAutoMerge bool, gitTrailers []string, 117 | ) error { 118 | pr, _, err := gh.client.PullRequests.Get(ctx, owner, repo, prNumber) 119 | if err != nil { 120 | return fmt.Errorf("failed to get pull request: %w", err) 121 | } 122 | commitMsg, err := generateCommitBody(pr, gitTrailers) 123 | if err != nil { 124 | return fmt.Errorf("failed to generate template: %w", err) 125 | } 126 | 127 | if enableAutoMerge { 128 | // GitHub API docs: https://cli.github.com/manual/gh_pr_merge 129 | err = exec.Command("gh", "pr", "merge", strconv.Itoa(prNumber), fmt.Sprintf("--%s", mergeMethod), "--auto", "--subject", generateCommitSubject(pr), "--body", commitMsg, "--repo", fmt.Sprintf("%s/%s", owner, repo)).Run() 130 | } else { 131 | _, _, err = gh.client.PullRequests.Merge(ctx, owner, repo, prNumber, commitMsg, &github.PullRequestOptions{ 132 | CommitTitle: generateCommitSubject(pr), 133 | MergeMethod: mergeMethod, 134 | }) 135 | } 136 | if err != nil { 137 | return fmt.Errorf("failed to merge pull request: %w", err) 138 | } 139 | return nil 140 | } 141 | 142 | func generateCommitSubject(pr *github.PullRequest) string { 143 | return fmt.Sprintf("%s (#%d)", pr.GetTitle(), pr.GetNumber()) 144 | } 145 | 146 | func generateCommitBody(pr *github.PullRequest, gitTrailers []string) (string, error) { 147 | body := newCommitBody(pr, gitTrailers) 148 | t, err := getTemplate(body) 149 | if err != nil { 150 | return "", err 151 | } 152 | return t, nil 153 | } 154 | 155 | func (gh *ghClient) sendMsg(ctx context.Context, owner, repo string, prNumber int, msg string) error { 156 | _, _, err := gh.client.Issues.CreateComment(ctx, owner, repo, prNumber, &github.IssueComment{ 157 | Body: &msg, 158 | }) 159 | if err != nil { 160 | return fmt.Errorf("failed to send message: %w", err) 161 | } 162 | return nil 163 | } 164 | 165 | func newCommitBody(pr *github.PullRequest, gitTrailersInput []string) commitBody { 166 | labels := make([]string, 0, len(pr.Labels)) 167 | for _, l := range pr.Labels { 168 | labels = append(labels, l.GetName()) 169 | } 170 | 171 | description, releaseNote := splitReleaseNote(pr.GetBody()) 172 | 173 | gitTrailers := make([]gitTrailer, 0, len(gitTrailersInput)) 174 | for _, i := range gitTrailersInput { 175 | kv := strings.SplitN(i, "=", 2) 176 | if len(kv) != 2 { 177 | continue 178 | } 179 | gitTrailers = append(gitTrailers, gitTrailer{Key: kv[0], Value: kv[1]}) 180 | } 181 | 182 | return commitBody{ 183 | Message: description, 184 | Labels: labels, 185 | ReleaseNote: releaseNote, 186 | GitTrailers: gitTrailers, 187 | } 188 | } 189 | 190 | type commitBody struct { 191 | Labels []string 192 | Message string 193 | ReleaseNote string 194 | GitTrailers []gitTrailer 195 | } 196 | 197 | type gitTrailer struct { 198 | Key string 199 | Value string 200 | } 201 | 202 | //go:embed template/commit.tmpl 203 | var commitTemplateFile string 204 | 205 | func getTemplate(commitBody commitBody) (string, error) { 206 | tmpl, err := template.New("commit").Parse(commitTemplateFile) 207 | if err != nil { 208 | return "", err 209 | } 210 | 211 | data := struct { 212 | Labels []string 213 | Message string 214 | ReleaseNote string 215 | GitTrailers []gitTrailer 216 | }{ 217 | Labels: commitBody.Labels, 218 | Message: commitBody.Message, 219 | ReleaseNote: commitBody.ReleaseNote, 220 | GitTrailers: commitBody.GitTrailers, 221 | } 222 | 223 | var b strings.Builder 224 | if err := tmpl.Execute(&b, data); err != nil { 225 | return "", err 226 | } 227 | 228 | return b.String(), nil 229 | } 230 | 231 | var ( 232 | needApproveRegexp = regexp.MustCompile("At least ([0-9]+) approving review is required by reviewers with write access") 233 | releaseNoteRegexp = regexp.MustCompile("```release-note\n(.+?)\n```") 234 | ) 235 | 236 | // errMsg returns error message to post from error. 237 | // Especially handing error from github. go-github does not have error type for some cases. 238 | func errMsg(err error) string { 239 | if err == nil { 240 | return "Succeeded!" 241 | } 242 | ss := needApproveRegexp.FindStringSubmatch(err.Error()) 243 | if len(ss) == 2 { 244 | return fmt.Sprintf("Need %s approving review", ss[1]) 245 | } 246 | return err.Error() 247 | } 248 | 249 | // splitReleaseNote returns description and release note from commit body. 250 | // if release note is empty, return whole body and "NONE" 251 | func splitReleaseNote(body string) (description, releaseNote string) { 252 | ss := releaseNoteRegexp.FindStringSubmatch(body) 253 | if len(ss) != 2 { 254 | return body, "NONE" 255 | } 256 | if rn := strings.TrimSpace(ss[1]); rn != "" { 257 | return strings.ReplaceAll(body, ss[0], ""), rn 258 | } 259 | return body, "NONE" 260 | } 261 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/google/go-github/github" 8 | ) 9 | 10 | func Test_ghClient_generateCommitBody(t *testing.T) { 11 | type args struct { 12 | pr *github.PullRequest 13 | gitTrailers []string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want string 19 | wantErr bool 20 | }{ 21 | { 22 | name: "generate commit message with labels", 23 | args: args{ 24 | pr: &github.PullRequest{ 25 | Body: github.String("pull request body"), 26 | Labels: []*github.Label{ 27 | { 28 | Name: github.String("label1"), 29 | }, 30 | { 31 | Name: github.String("label2"), 32 | }, 33 | }, 34 | }, 35 | }, 36 | want: `--- 37 | Labels: 38 | * label1 39 | * label2 40 | --- 41 | pull request body 42 | --- 43 | ` + 44 | "```release-note\n" + 45 | "NONE\n" + 46 | "```", 47 | wantErr: false, 48 | }, 49 | { 50 | name: "generate commit message", 51 | args: args{ 52 | pr: &github.PullRequest{ 53 | Body: github.String("pull request body"), 54 | }, 55 | }, 56 | want: ` 57 | --- 58 | pull request body 59 | --- 60 | ` + 61 | "```release-note\n" + 62 | "NONE\n" + 63 | "```", 64 | wantErr: false, 65 | }, 66 | { 67 | name: "with release-note", 68 | args: args{ 69 | pr: &github.PullRequest{ 70 | Body: github.String("pull request body\n```release-note\nThis is greate a release!!!\n```"), 71 | }, 72 | }, 73 | want: ` 74 | --- 75 | pull request body 76 | 77 | --- 78 | ` + 79 | "```release-note\n" + 80 | "This is greate a release!!!\n" + 81 | "```", 82 | wantErr: false, 83 | }, 84 | { 85 | name: "with git trailers", 86 | args: args{ 87 | pr: &github.PullRequest{ 88 | Body: github.String("pull request body"), 89 | }, 90 | gitTrailers: []string{ 91 | "Co-authored-by=abema", 92 | "Co-authored-by=actions", 93 | }, 94 | }, 95 | want: `Co-authored-by: abema 96 | Co-authored-by: actions 97 | 98 | --- 99 | pull request body 100 | --- 101 | ` + 102 | "```release-note\n" + 103 | "NONE\n" + 104 | "```", 105 | wantErr: false, 106 | }, 107 | } 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | got, err := generateCommitBody(tt.args.pr, tt.args.gitTrailers) 111 | if (err != nil) != tt.wantErr { 112 | t.Errorf("err = %v, wantErr = %v", err, tt.wantErr) 113 | return 114 | } 115 | if got != tt.want { 116 | t.Errorf("got = %v, want = %v", got, tt.want) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func Test_generateCommitSubject(t *testing.T) { 123 | type args struct { 124 | pr *github.PullRequest 125 | } 126 | tests := []struct { 127 | name string 128 | args args 129 | want string 130 | }{ 131 | { 132 | name: "generate commit subject", 133 | args: args{ 134 | pr: &github.PullRequest{ 135 | Title: github.String("pull request title"), 136 | Number: github.Int(1), 137 | }, 138 | }, 139 | want: "pull request title (#1)", 140 | }, 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | if got := generateCommitSubject(tt.args.pr); got != tt.want { 145 | t.Errorf("generateCommitSubject() = %v, want %v", got, tt.want) 146 | } 147 | }) 148 | } 149 | } 150 | 151 | func Test_validateEnv(t *testing.T) { 152 | type args struct { 153 | e env 154 | } 155 | tests := []struct { 156 | name string 157 | args args 158 | wantErr bool 159 | }{ 160 | { 161 | name: "valid env", 162 | args: args{ 163 | e: env{ 164 | Comment: "/merge", 165 | Mergers: []string{"0daryo"}, 166 | Actor: "0daryo", 167 | }, 168 | }, 169 | }, 170 | { 171 | name: "invalid comment", 172 | args: args{ 173 | e: env{ 174 | Comment: "/approve", 175 | Mergers: []string{"0daryo"}, 176 | Actor: "0daryo", 177 | }, 178 | }, 179 | wantErr: true, 180 | }, 181 | { 182 | name: "actor is not merger", 183 | args: args{ 184 | e: env{ 185 | Comment: "/merge", 186 | Mergers: []string{"0daryo"}, 187 | Actor: "github", 188 | }, 189 | }, 190 | wantErr: true, 191 | }, 192 | } 193 | for _, tt := range tests { 194 | t.Run(tt.name, func(t *testing.T) { 195 | if err := validateEnv(tt.args.e); (err != nil) != tt.wantErr { 196 | t.Errorf("validateEnv() error = %v, wantErr %v", err, tt.wantErr) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func Test_errMsg(t *testing.T) { 203 | type args struct { 204 | err error 205 | } 206 | tests := []struct { 207 | name string 208 | args args 209 | want string 210 | }{ 211 | { 212 | name: "ok", 213 | args: args{}, 214 | want: "Succeeded!", 215 | }, 216 | { 217 | name: "need approval", 218 | args: args{ 219 | err: errors.New("failed to merge pull request: PUT https://api.github.com/repos/abema/github-actions-merger/pulls/1/merge: 405 At least 2 approving review is required by reviewers with write access. []"), 220 | }, 221 | want: "Need 2 approving review", 222 | }, 223 | { 224 | name: "internal server error", 225 | args: args{ 226 | err: errors.New("internal server error"), 227 | }, 228 | want: "internal server error", 229 | }, 230 | } 231 | for _, tt := range tests { 232 | t.Run(tt.name, func(t *testing.T) { 233 | if got := errMsg(tt.args.err); got != tt.want { 234 | t.Errorf("errMsg() = %v, want %v", got, tt.want) 235 | } 236 | }) 237 | } 238 | } 239 | 240 | func Test_splitReleaseNote(t *testing.T) { 241 | type args struct { 242 | body string 243 | } 244 | tests := []struct { 245 | name string 246 | args args 247 | wantDescription string 248 | wantReleaseNote string 249 | }{ 250 | { 251 | name: "release note description", 252 | args: args{ 253 | body: "release note description ```release-note\nThis is great release!!!\n```", 254 | }, 255 | wantDescription: "release note description ", 256 | wantReleaseNote: "This is great release!!!", 257 | }, 258 | { 259 | name: "no releaes note", 260 | args: args{ 261 | body: "release note description", 262 | }, 263 | wantDescription: "release note description", 264 | wantReleaseNote: "NONE", 265 | }, 266 | } 267 | for _, tt := range tests { 268 | t.Run(tt.name, func(t *testing.T) { 269 | gotDescription, gotReleaseNote := splitReleaseNote(tt.args.body) 270 | if gotDescription != tt.wantDescription { 271 | t.Errorf("splitReleaseNote() gotDescription = %v, want %v", gotDescription, tt.wantDescription) 272 | } 273 | if gotReleaseNote != tt.wantReleaseNote { 274 | t.Errorf("splitReleaseNote() gotReleaseNote = %v, want %v", gotReleaseNote, tt.wantReleaseNote) 275 | } 276 | }) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /template/commit.tmpl: -------------------------------------------------------------------------------- 1 | {{- if .GitTrailers -}} 2 | {{- range .GitTrailers -}} 3 | {{ .Key }}: {{ .Value }} 4 | {{ end -}} 5 | {{- end -}} 6 | {{ if .Labels -}} 7 | --- 8 | Labels: 9 | {{- range .Labels }} 10 | * {{ . }} 11 | {{- end -}} 12 | {{- end -}} 13 | {{ if .Message }} 14 | --- 15 | {{ .Message }} 16 | {{ end }} 17 | {{- if .ReleaseNote -}} 18 | --- 19 | ```release-note 20 | {{ .ReleaseNote }} 21 | ``` 22 | {{- end -}} 23 | --------------------------------------------------------------------------------