Skip to content

Commit a14608e

Browse files
authored
chore: Add script to automate PR merging (#11758)
This script automates the process of retrying and merging pull requests by checking the status of Kokoro tests. Prerequisites: - Go must be installed (https://golang.org/doc/install). - A GitHub personal access token with repo scope must be set in the GITHUB_TOKEN environment variable. Example Usage: ``` export GITHUB_TOKEN="<your GitHub token>" cd scripts go run ./release_manager_merge_bot.go <PR URL> ```
1 parent b129a37 commit a14608e

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

.github/scripts/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/googleapis/google-cloud-java/scripts
2+
3+
go 1.24.4
4+
5+
require (
6+
github.com/google/go-github/v62 v62.0.0
7+
golang.org/x/oauth2 v0.31.0
8+
)
9+
10+
require github.com/google/go-querystring v1.1.0 // indirect

.github/scripts/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
3+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4+
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
5+
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
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+
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
9+
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
10+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// release_manager_merge_bot is a script that automatically retries and merges a pull request.
16+
//
17+
// This script is designed to be run manually for a specific pull request. It will:
18+
// 1. Check the CI/CD status of the pull request.
19+
// 2. If the status is "failure", it will add Kokoro labels to retry the tests. It retries once.
20+
// 3. If the status is "success", it will squash and merge the pull request.
21+
// 4. If the status is "pending", it will wait and check again.
22+
//
23+
// Prerequisites:
24+
// - Go must be installed (https://golang.org/doc/install).
25+
// - A GitHub personal access token with repo scope must be set in the GITHUB_TOKEN environment variable.
26+
//
27+
// Example Usage:
28+
//
29+
// export GITHUB_TOKEN="<your GitHub token>"
30+
// cd .github/scripts
31+
// go run ./release_manager_merge_bot.go <PR URL>
32+
33+
package main
34+
35+
import (
36+
"context"
37+
"fmt"
38+
"log"
39+
"net/url"
40+
"os"
41+
"strconv"
42+
"strings"
43+
"time"
44+
45+
"github.com/google/go-github/v62/github"
46+
"golang.org/x/oauth2"
47+
)
48+
49+
// --- Configuration ---
50+
// The labels to add when a test fails.
51+
var labelsToAdd = []string{"kokoro:force-run", "kokoro:run"}
52+
53+
// --- End of Configuration ---
54+
55+
// parseURL parses a GitHub pull request URL and returns the owner, repository, and PR number.
56+
func parseURL(prURL string) (string, string, int, error) {
57+
parsedURL, err := url.Parse(prURL)
58+
if err != nil {
59+
return "", "", 0, fmt.Errorf("failed to parse URL: %w", err)
60+
}
61+
62+
pathParts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
63+
if len(pathParts) < 4 || pathParts[2] != "pull" {
64+
return "", "", 0, fmt.Errorf("invalid GitHub pull request URL format")
65+
}
66+
67+
owner := pathParts[0]
68+
repo := pathParts[1]
69+
prNumber, err := strconv.Atoi(pathParts[3])
70+
if err != nil {
71+
return "", "", 0, fmt.Errorf("failed to parse PR number: %w", err)
72+
}
73+
74+
return owner, repo, prNumber, nil
75+
}
76+
77+
// getMissingLabels checks for required labels on a PR and returns any that are missing.
78+
func getMissingLabels(ctx context.Context, client *github.Client, owner, repo string, prNumber int) ([]string, error) {
79+
currentLabels, _, err := client.Issues.ListLabelsByIssue(ctx, owner, repo, prNumber, nil)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to get PR labels: %w", err)
82+
}
83+
84+
labelSet := make(map[string]bool)
85+
for _, label := range currentLabels {
86+
labelSet[*label.Name] = true
87+
}
88+
89+
var missingLabels []string
90+
for _, requiredLabel := range labelsToAdd {
91+
if !labelSet[requiredLabel] {
92+
missingLabels = append(missingLabels, requiredLabel)
93+
}
94+
}
95+
return missingLabels, nil
96+
}
97+
98+
func main() {
99+
log.Println("Starting the release manager merge bot.")
100+
101+
if len(os.Args) < 2 {
102+
log.Fatal("Error: Pull request URL is required. Example: go run ./release_manager_merge_bot.go <PR URL>")
103+
}
104+
prURL := os.Args[1]
105+
106+
githubToken := os.Getenv("GITHUB_TOKEN")
107+
if githubToken == "" {
108+
log.Fatal("Error: GITHUB_TOKEN environment variable is not set.")
109+
}
110+
111+
owner, repo, prNumber, err := parseURL(prURL)
112+
if err != nil {
113+
log.Fatalf("Error parsing URL: %v", err)
114+
}
115+
116+
ctx := context.Background()
117+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: githubToken})
118+
tc := oauth2.NewClient(ctx, ts)
119+
client := github.NewClient(tc)
120+
121+
// --- Initial Label Check ---
122+
retryCount := 0
123+
log.Printf("Performing initial label check for PR #%d...", prNumber)
124+
missingLabels, err := getMissingLabels(ctx, client, owner, repo, prNumber)
125+
if err != nil {
126+
log.Printf("Warning: could not perform initial label check: %v", err)
127+
} else {
128+
if len(missingLabels) > 0 {
129+
log.Println("Required Kokoro labels are missing. Adding them now...")
130+
_, _, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, prNumber, missingLabels)
131+
if err != nil {
132+
log.Printf("Warning: failed to add labels: %v", err)
133+
}
134+
retryCount++
135+
} else {
136+
log.Println("Required Kokoro labels are already present.")
137+
}
138+
}
139+
// --- End of Initial Label Check ---
140+
141+
for {
142+
log.Printf("Checking status of PR #%d in %s/%s...", prNumber, owner, repo)
143+
144+
// Declare variables at the top of the loop to avoid 'goto jumps over declaration' errors.
145+
var (
146+
pr *github.PullRequest
147+
status *github.CombinedStatus
148+
state string
149+
err error
150+
)
151+
152+
pr, _, err = client.PullRequests.Get(ctx, owner, repo, prNumber)
153+
if err != nil {
154+
log.Printf("An error occurred while getting PR info: %v", err)
155+
goto wait
156+
}
157+
158+
status, _, err = client.Repositories.GetCombinedStatus(ctx, owner, repo, *pr.Head.SHA, nil)
159+
if err != nil {
160+
log.Printf("An error occurred while getting commit status: %v", err)
161+
goto wait
162+
}
163+
164+
state = *status.State
165+
log.Printf("Overall status: %s", state)
166+
167+
switch state {
168+
case "failure":
169+
if retryCount >= 2 {
170+
log.Fatal("The PR has failed twice after applying the Kokoro labels. Failing the script.")
171+
}
172+
log.Println("Some checks have failed. Retrying the tests...")
173+
_, _, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, prNumber, labelsToAdd)
174+
if err != nil {
175+
log.Printf("An error occurred while adding labels: %v", err)
176+
}
177+
retryCount++
178+
case "success":
179+
log.Println("All checks have passed. Merging the pull request...")
180+
commitMessage := fmt.Sprintf("Merge pull request #%d from %s/%s", prNumber, owner, repo)
181+
mergeResult, _, err := client.PullRequests.Merge(ctx, owner, repo, prNumber, commitMessage, &github.PullRequestOptions{
182+
MergeMethod: "squash",
183+
})
184+
if err != nil {
185+
log.Fatalf("Failed to merge PR: %v", err)
186+
}
187+
log.Printf("Successfully squashed and merged PR #%d: %s", prNumber, *mergeResult.Message)
188+
return // Exit the program on success
189+
case "pending":
190+
log.Println("Some checks are still pending. Waiting for them to complete.")
191+
default:
192+
log.Printf("Unknown state: %s. No action taken.", state)
193+
}
194+
195+
wait:
196+
log.Println("Waiting for 1 minute before retrying...")
197+
time.Sleep(60 * time.Second)
198+
}
199+
}

0 commit comments

Comments
 (0)