2020// 3. If the status is "success", it will squash and merge the pull request.
2121// 4. If the status is "pending", it will wait and check again.
2222//
23+ // Flags:
24+ // -skip-kokoro (Optional) If set, skips applying Kokoro rerunning labels on failure.
25+ // -email (Optional) Email address to send success/failure notifications to.
26+ // Note: This relies on the internal sendgmr tool and is only
27+ // supported on Cloudtop/gLinux with valid LOAS credentials.
28+ //
2329// Prerequisites:
2430// - Go must be installed (https://golang.org/doc/install).
2531// - A GitHub personal access token with repo scope must be set in the GITHUB_TOKEN environment variable.
2834//
2935// export GITHUB_TOKEN="<your GitHub token>"
3036// cd .github/scripts
31- // go run ./release_manager_merge_bot.go <PR URL>
37+ // go run ./release_manager_merge_bot.go -skip-kokoro -email="user@google.com" <PR URL>
3238
3339package main
3440
3541import (
3642 "context"
43+ "flag"
3744 "fmt"
3845 "log"
3946 "net/url"
4047 "os"
48+ "os/exec"
4149 "strconv"
4250 "strings"
4351 "time"
@@ -52,6 +60,11 @@ var labelsToAdd = []string{"kokoro:force-run", "kokoro:run"}
5260
5361// --- End of Configuration ---
5462
63+ var (
64+ skipKokoroOpt bool
65+ emailOpt string
66+ )
67+
5568// parseURL parses a GitHub pull request URL and returns the owner, repository, and PR number.
5669func parseURL (prURL string ) (string , string , int , error ) {
5770 parsedURL , err := url .Parse (prURL )
@@ -95,13 +108,43 @@ func getMissingLabels(ctx context.Context, client *github.Client, owner, repo st
95108 return missingLabels , nil
96109}
97110
111+ // sendEmail sends an email notification using the internal sendgmr tool.
112+ func sendEmail (to , subject , body string ) {
113+ if to == "" {
114+ return
115+ }
116+ sendgmrPath := "/google/bin/releases/gws-sre/files/sendgmr/sendgmr"
117+ cmd := exec .Command (sendgmrPath , "--to=" + to , "--subject=" + subject )
118+ cmd .Stdin = strings .NewReader (body )
119+ if err := cmd .Run (); err != nil {
120+ log .Printf ("Warning: Failed to send email: %v" , err )
121+ } else {
122+ log .Printf ("Email successfully sent to %s" , to )
123+ }
124+ }
125+
126+ // fatalError logs an error message, optionally sends an email, and exits.
127+ func fatalError (format string , v ... interface {}) {
128+ msg := fmt .Sprintf (format , v ... )
129+ log .Printf ("Error: %s" , msg )
130+ if emailOpt != "" {
131+ sendEmail (emailOpt , "❌ Release Manager Merge Bot Failed" , msg )
132+ }
133+ os .Exit (1 )
134+ }
135+
98136func main () {
99137 log .Println ("Starting the release manager merge bot." )
100138
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>" )
139+ flag .BoolVar (& skipKokoroOpt , "skip-kokoro" , false , "Skip applying kokoro rerunning labels on failure" )
140+ flag .StringVar (& emailOpt , "email" , "" , "Email address to send notifications to (requires Cloudtop/gLinux and LOAS/gcert)" )
141+ flag .Parse ()
142+
143+ args := flag .Args ()
144+ if len (args ) < 1 {
145+ log .Fatal ("Error: Pull request URL is required. Example: go run ./release_manager_merge_bot.go [flags] <PR URL>" )
103146 }
104- prURL := os . Args [ 1 ]
147+ prURL := args [ 0 ]
105148
106149 githubToken := os .Getenv ("GITHUB_TOKEN" )
107150 if githubToken == "" {
@@ -110,7 +153,11 @@ func main() {
110153
111154 owner , repo , prNumber , err := parseURL (prURL )
112155 if err != nil {
113- log .Fatalf ("Error parsing URL: %v" , err )
156+ fatalError ("Error parsing URL: %v" , err )
157+ }
158+
159+ if emailOpt != "" {
160+ log .Printf ("Notifications will be sent to: %s" , emailOpt )
114161 }
115162
116163 ctx := context .Background ()
@@ -120,21 +167,25 @@ func main() {
120167
121168 // --- Initial Label Check ---
122169 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 ++
170+ if ! skipKokoroOpt {
171+ log .Printf ("Performing initial label check for PR #%d..." , prNumber )
172+ missingLabels , err := getMissingLabels (ctx , client , owner , repo , prNumber )
173+ if err != nil {
174+ log .Printf ("Warning: could not perform initial label check: %v" , err )
135175 } else {
136- log .Println ("Required Kokoro labels are already present." )
176+ if len (missingLabels ) > 0 {
177+ log .Println ("Required Kokoro labels are missing. Adding them now..." )
178+ _ , _ , err := client .Issues .AddLabelsToIssue (ctx , owner , repo , prNumber , missingLabels )
179+ if err != nil {
180+ log .Printf ("Warning: failed to add labels: %v" , err )
181+ }
182+ retryCount ++
183+ } else {
184+ log .Println ("Required Kokoro labels are already present." )
185+ }
137186 }
187+ } else {
188+ log .Println ("Skipping initial Kokoro label check due to -skip-kokoro flag." )
138189 }
139190 // --- End of Initial Label Check ---
140191
@@ -166,8 +217,11 @@ func main() {
166217
167218 switch state {
168219 case "failure" :
220+ if skipKokoroOpt {
221+ fatalError ("PR #%d has failed checks and -skip-kokoro is enabled. Failing the script." , prNumber )
222+ }
169223 if retryCount >= 2 {
170- log . Fatal ("The PR has failed twice after applying the Kokoro labels. Failing the script." )
224+ fatalError ("The PR has failed twice after applying the Kokoro labels. Failing the script." )
171225 }
172226 log .Println ("Some checks have failed. Retrying the tests..." )
173227 _ , _ , err := client .Issues .AddLabelsToIssue (ctx , owner , repo , prNumber , labelsToAdd )
@@ -182,9 +236,13 @@ func main() {
182236 MergeMethod : "squash" ,
183237 })
184238 if err != nil {
185- log .Fatalf ("Failed to merge PR: %v" , err )
239+ fatalError ("Failed to merge PR: %v" , err )
240+ }
241+ successMsg := fmt .Sprintf ("Successfully squashed and merged PR #%d: %s" , prNumber , * mergeResult .Message )
242+ log .Println (successMsg )
243+ if emailOpt != "" {
244+ sendEmail (emailOpt , fmt .Sprintf ("✅ PR #%d Merged Successfully" , prNumber ), successMsg )
186245 }
187- log .Printf ("Successfully squashed and merged PR #%d: %s" , prNumber , * mergeResult .Message )
188246 return // Exit the program on success
189247 case "pending" :
190248 log .Println ("Some checks are still pending. Waiting for them to complete." )
0 commit comments