Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .github/scripts/release_manager_merge_bot
Binary file not shown.
100 changes: 21 additions & 79 deletions .github/scripts/release_manager_merge_bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@
// 3. If the status is "success", it will squash and merge the pull request.
// 4. If the status is "pending", it will wait and check again.
//
// Flags:
// -skip-kokoro (Optional) If set, skips applying Kokoro rerunning labels on failure.
// -email (Optional) Email address to send success/failure notifications to.
// Note: This relies on the internal sendgmr tool and is only
// supported on Cloudtop/gLinux with valid LOAS credentials.
//
// 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.
Expand All @@ -34,18 +28,16 @@
//
// export GITHUB_TOKEN="<your GitHub token>"
// cd .github/scripts
// go run ./release_manager_merge_bot.go -skip-kokoro -email="user@google.com" <PR URL>
// go run ./release_manager_merge_bot.go <PR URL>

package main

import (
"context"
"flag"
"fmt"
"log"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"time"
Expand All @@ -60,11 +52,6 @@ var labelsToAdd = []string{"kokoro:force-run", "kokoro:run"}

// --- End of Configuration ---

var (
skipKokoroOpt bool
emailOpt string
)

// parseURL parses a GitHub pull request URL and returns the owner, repository, and PR number.
func parseURL(prURL string) (string, string, int, error) {
parsedURL, err := url.Parse(prURL)
Expand Down Expand Up @@ -108,43 +95,13 @@ func getMissingLabels(ctx context.Context, client *github.Client, owner, repo st
return missingLabels, nil
}

// sendEmail sends an email notification using the internal sendgmr tool.
func sendEmail(to, subject, body string) {
if to == "" {
return
}
sendgmrPath := "/google/bin/releases/gws-sre/files/sendgmr/sendgmr"
cmd := exec.Command(sendgmrPath, "--to="+to, "--subject="+subject)
cmd.Stdin = strings.NewReader(body)
if err := cmd.Run(); err != nil {
log.Printf("Warning: Failed to send email: %v", err)
} else {
log.Printf("Email successfully sent to %s", to)
}
}

// fatalError logs an error message, optionally sends an email, and exits.
func fatalError(format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
log.Printf("Error: %s", msg)
if emailOpt != "" {
sendEmail(emailOpt, "❌ Release Manager Merge Bot Failed", msg)
}
os.Exit(1)
}

func main() {
log.Println("Starting the release manager merge bot.")

flag.BoolVar(&skipKokoroOpt, "skip-kokoro", false, "Skip applying kokoro rerunning labels on failure")
flag.StringVar(&emailOpt, "email", "", "Email address to send notifications to (requires Cloudtop/gLinux and LOAS/gcert)")
flag.Parse()

args := flag.Args()
if len(args) < 1 {
log.Fatal("Error: Pull request URL is required. Example: go run ./release_manager_merge_bot.go [flags] <PR URL>")
if len(os.Args) < 2 {
log.Fatal("Error: Pull request URL is required. Example: go run ./release_manager_merge_bot.go <PR URL>")
}
prURL := args[0]
prURL := os.Args[1]

githubToken := os.Getenv("GITHUB_TOKEN")
if githubToken == "" {
Expand All @@ -153,11 +110,7 @@ func main() {

owner, repo, prNumber, err := parseURL(prURL)
if err != nil {
fatalError("Error parsing URL: %v", err)
}

if emailOpt != "" {
log.Printf("Notifications will be sent to: %s", emailOpt)
log.Fatalf("Error parsing URL: %v", err)
}

ctx := context.Background()
Expand All @@ -167,25 +120,21 @@ func main() {

// --- Initial Label Check ---
retryCount := 0
if !skipKokoroOpt {
log.Printf("Performing initial label check for PR #%d...", prNumber)
missingLabels, err := getMissingLabels(ctx, client, owner, repo, prNumber)
if err != nil {
log.Printf("Warning: could not perform initial label check: %v", err)
} else {
if len(missingLabels) > 0 {
log.Println("Required Kokoro labels are missing. Adding them now...")
_, _, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, prNumber, missingLabels)
if err != nil {
log.Printf("Warning: failed to add labels: %v", err)
}
retryCount++
} else {
log.Println("Required Kokoro labels are already present.")
log.Printf("Performing initial label check for PR #%d...", prNumber)
missingLabels, err := getMissingLabels(ctx, client, owner, repo, prNumber)
if err != nil {
log.Printf("Warning: could not perform initial label check: %v", err)
} else {
if len(missingLabels) > 0 {
log.Println("Required Kokoro labels are missing. Adding them now...")
_, _, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, prNumber, missingLabels)
if err != nil {
log.Printf("Warning: failed to add labels: %v", err)
}
retryCount++
} else {
log.Println("Required Kokoro labels are already present.")
}
} else {
log.Println("Skipping initial Kokoro label check due to -skip-kokoro flag.")
}
// --- End of Initial Label Check ---

Expand Down Expand Up @@ -217,11 +166,8 @@ func main() {

switch state {
case "failure":
if skipKokoroOpt {
fatalError("PR #%d has failed checks and -skip-kokoro is enabled. Failing the script.", prNumber)
}
if retryCount >= 2 {
fatalError("The PR has failed twice after applying the Kokoro labels. Failing the script.")
log.Fatal("The PR has failed twice after applying the Kokoro labels. Failing the script.")
}
log.Println("Some checks have failed. Retrying the tests...")
_, _, err := client.Issues.AddLabelsToIssue(ctx, owner, repo, prNumber, labelsToAdd)
Expand All @@ -236,13 +182,9 @@ func main() {
MergeMethod: "squash",
})
if err != nil {
fatalError("Failed to merge PR: %v", err)
}
successMsg := fmt.Sprintf("Successfully squashed and merged PR #%d: %s", prNumber, *mergeResult.Message)
log.Println(successMsg)
if emailOpt != "" {
sendEmail(emailOpt, fmt.Sprintf("✅ PR #%d Merged Successfully", prNumber), successMsg)
log.Fatalf("Failed to merge PR: %v", err)
}
log.Printf("Successfully squashed and merged PR #%d: %s", prNumber, *mergeResult.Message)
return // Exit the program on success
case "pending":
log.Println("Some checks are still pending. Waiting for them to complete.")
Expand Down
5 changes: 5 additions & 0 deletions java-compute/google-cloud-compute/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@
<artifactId>google-cloud-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.api</groupId>
<artifactId>gax-grpc</artifactId>
<scope>test</scope>
</dependency>

<!-- Need testing utility classes for generated REST clients tests -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,33 @@ static DatastoreException throwInvalidRequest(String massage, Object... params)
static DatastoreException propagateUserException(Exception ex) {
throw new DatastoreException(BaseServiceException.UNKNOWN_CODE, ex.getMessage(), null, ex);
}

/**
* Extracts the status code name from the given throwable. Walks the exception cause chain looking
* for a {@link DatastoreException} that carries a reason string representing the status code
* (e.g. "ABORTED", "UNAVAILABLE"). The reason is set from {@link
* com.google.api.gax.rpc.StatusCode.Code} which is transport-neutral, supporting both gRPC and
* HttpJson. Falls back to "UNKNOWN" if the status cannot be determined.
*
* <p>Note: Some {@link DatastoreException} instances are constructed without a reason (e.g. via
* {@link DatastoreException#DatastoreException(int, String, Throwable)}). If all {@link
* DatastoreException} instances in the cause chain have a null or empty reason, this method
* returns "UNKNOWN" even if the underlying error carries a meaningful status.
*
* @param throwable the throwable to extract the status code from
* @return the status code name, or "UNKNOWN" if not determinable
*/
public static String extractStatusCode(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
if (current instanceof DatastoreException) {
String reason = ((DatastoreException) current).getReason();
if (!Strings.isNullOrEmpty(reason)) {
return reason;
}
}
current = current.getCause();
}
return StatusCode.Code.UNKNOWN.toString();
}
}
Loading
Loading