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+ //
2327// Prerequisites:
2428// - Go must be installed (https://golang.org/doc/install).
2529// - A GitHub personal access token with repo scope must be set in the GITHUB_TOKEN environment variable.
2832//
2933// export GITHUB_TOKEN="<your GitHub token>"
3034// cd .github/scripts
31- // go run ./release_manager_merge_bot.go <PR URL>
35+ // go run ./release_manager_merge_bot.go -skip-kokoro -email="user@google.com" <PR URL>
3236
3337package main
3438
3539import (
3640 "context"
41+ "flag"
3742 "fmt"
3843 "log"
3944 "net/url"
4045 "os"
46+ "os/exec"
4147 "strconv"
4248 "strings"
4349 "time"
@@ -52,6 +58,11 @@ var labelsToAdd = []string{"kokoro:force-run", "kokoro:run"}
5258
5359// --- End of Configuration ---
5460
61+ var (
62+ skipKokoroOpt bool
63+ emailOpt string
64+ )
65+
5566// parseURL parses a GitHub pull request URL and returns the owner, repository, and PR number.
5667func parseURL (prURL string ) (string , string , int , error ) {
5768 parsedURL , err := url .Parse (prURL )
@@ -95,13 +106,43 @@ func getMissingLabels(ctx context.Context, client *github.Client, owner, repo st
95106 return missingLabels , nil
96107}
97108
109+ // sendEmail sends an email notification using the internal sendgmr tool.
110+ func sendEmail (to , subject , body string ) {
111+ if to == "" {
112+ return
113+ }
114+ sendgmrPath := "/google/bin/releases/gws-sre/files/sendgmr/sendgmr"
115+ cmd := exec .Command (sendgmrPath , "--to=" + to , "--subject=" + subject )
116+ cmd .Stdin = strings .NewReader (body )
117+ if err := cmd .Run (); err != nil {
118+ log .Printf ("Warning: Failed to send email: %v" , err )
119+ } else {
120+ log .Printf ("Email successfully sent to %s" , to )
121+ }
122+ }
123+
124+ // fatalError logs an error message, optionally sends an email, and exits.
125+ func fatalError (format string , v ... interface {}) {
126+ msg := fmt .Sprintf (format , v ... )
127+ log .Printf ("Error: %s" , msg )
128+ if emailOpt != "" {
129+ sendEmail (emailOpt , "❌ Release Manager Merge Bot Failed" , msg )
130+ }
131+ os .Exit (1 )
132+ }
133+
98134func main () {
99135 log .Println ("Starting the release manager merge bot." )
100136
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>" )
137+ flag .BoolVar (& skipKokoroOpt , "skip-kokoro" , false , "Skip applying kokoro rerunning labels on failure" )
138+ flag .StringVar (& emailOpt , "email" , "" , "Email address to send notifications to (requires LOAS/gcert)" )
139+ flag .Parse ()
140+
141+ args := flag .Args ()
142+ if len (args ) < 1 {
143+ log .Fatal ("Error: Pull request URL is required. Example: go run ./release_manager_merge_bot.go [flags] <PR URL>" )
103144 }
104- prURL := os . Args [ 1 ]
145+ prURL := args [ 0 ]
105146
106147 githubToken := os .Getenv ("GITHUB_TOKEN" )
107148 if githubToken == "" {
@@ -110,7 +151,11 @@ func main() {
110151
111152 owner , repo , prNumber , err := parseURL (prURL )
112153 if err != nil {
113- log .Fatalf ("Error parsing URL: %v" , err )
154+ fatalError ("Error parsing URL: %v" , err )
155+ }
156+
157+ if emailOpt != "" {
158+ log .Printf ("Notifications will be sent to: %s" , emailOpt )
114159 }
115160
116161 ctx := context .Background ()
@@ -120,21 +165,25 @@ func main() {
120165
121166 // --- Initial Label Check ---
122167 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 ++
168+ if ! skipKokoroOpt {
169+ log .Printf ("Performing initial label check for PR #%d..." , prNumber )
170+ missingLabels , err := getMissingLabels (ctx , client , owner , repo , prNumber )
171+ if err != nil {
172+ log .Printf ("Warning: could not perform initial label check: %v" , err )
135173 } else {
136- log .Println ("Required Kokoro labels are already present." )
174+ if len (missingLabels ) > 0 {
175+ log .Println ("Required Kokoro labels are missing. Adding them now..." )
176+ _ , _ , err := client .Issues .AddLabelsToIssue (ctx , owner , repo , prNumber , missingLabels )
177+ if err != nil {
178+ log .Printf ("Warning: failed to add labels: %v" , err )
179+ }
180+ retryCount ++
181+ } else {
182+ log .Println ("Required Kokoro labels are already present." )
183+ }
137184 }
185+ } else {
186+ log .Println ("Skipping initial Kokoro label check due to -skip-kokoro flag." )
138187 }
139188 // --- End of Initial Label Check ---
140189
@@ -166,8 +215,11 @@ func main() {
166215
167216 switch state {
168217 case "failure" :
218+ if skipKokoroOpt {
219+ fatalError ("PR #%d has failed checks and -skip-kokoro is enabled. Failing the script." , prNumber )
220+ }
169221 if retryCount >= 2 {
170- log . Fatal ("The PR has failed twice after applying the Kokoro labels. Failing the script." )
222+ fatalError ("The PR has failed twice after applying the Kokoro labels. Failing the script." )
171223 }
172224 log .Println ("Some checks have failed. Retrying the tests..." )
173225 _ , _ , err := client .Issues .AddLabelsToIssue (ctx , owner , repo , prNumber , labelsToAdd )
@@ -182,9 +234,13 @@ func main() {
182234 MergeMethod : "squash" ,
183235 })
184236 if err != nil {
185- log .Fatalf ("Failed to merge PR: %v" , err )
237+ fatalError ("Failed to merge PR: %v" , err )
238+ }
239+ successMsg := fmt .Sprintf ("Successfully squashed and merged PR #%d: %s" , prNumber , * mergeResult .Message )
240+ log .Println (successMsg )
241+ if emailOpt != "" {
242+ sendEmail (emailOpt , fmt .Sprintf ("✅ PR #%d Merged Successfully" , prNumber ), successMsg )
186243 }
187- log .Printf ("Successfully squashed and merged PR #%d: %s" , prNumber , * mergeResult .Message )
188244 return // Exit the program on success
189245 case "pending" :
190246 log .Println ("Some checks are still pending. Waiting for them to complete." )
0 commit comments