github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/scripts/ci/propagate-pr/main.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/environment"
    10  	"github.com/ActiveState/cli/internal/errs"
    11  	"github.com/ActiveState/cli/internal/osutils"
    12  	wc "github.com/ActiveState/cli/scripts/internal/workflow-controllers"
    13  	wh "github.com/ActiveState/cli/scripts/internal/workflow-helpers"
    14  	"github.com/andygrunwald/go-jira"
    15  	"github.com/blang/semver"
    16  	"github.com/google/go-github/v45/github"
    17  	"golang.org/x/net/context"
    18  )
    19  
    20  type Meta struct {
    21  	Repo          *github.Repository
    22  	ActivePR      *github.PullRequest
    23  	ActiveStory   *jira.Issue
    24  	ActiveVersion semver.Version
    25  }
    26  
    27  type MergeIntends []MergeIntend
    28  
    29  type MergeIntend struct {
    30  	SourceBranch string
    31  	TargetBranch string
    32  }
    33  
    34  func (m MergeIntend) String() string {
    35  	return fmt.Sprintf("Merge %s into %s", m.SourceBranch, m.TargetBranch)
    36  }
    37  
    38  func (m MergeIntends) String() string {
    39  	v := ""
    40  	for _, vv := range m {
    41  		v += fmt.Sprintf("\n%s", vv.String())
    42  	}
    43  	return v
    44  }
    45  
    46  func main() {
    47  	if err := run(); err != nil {
    48  		wc.Print("Error: %s\n", errs.JoinMessage(err))
    49  		os.Exit(1)
    50  	}
    51  }
    52  
    53  func run() error {
    54  	finish := wc.PrintStart("Initializing clients")
    55  	// Initialize Clients
    56  	ghClient := wh.InitGHClient()
    57  	jiraClient, err := wh.InitJiraClient()
    58  	if err != nil {
    59  		return errs.Wrap(err, "failed to initialize Jira client")
    60  	}
    61  	finish()
    62  
    63  	// Grab input
    64  	if len(os.Args) != 2 {
    65  		return errs.New("Usage: propagate-pr <pr-number>")
    66  	}
    67  	prNumber, err := strconv.Atoi(os.Args[1])
    68  	if err != nil {
    69  		return errs.Wrap(err, "pr number should be numeric")
    70  	}
    71  
    72  	finish = wc.PrintStart("Fetching meta for PR %d", prNumber)
    73  	// Collect meta information about the PR and all it's related resources
    74  	meta, err := fetchMeta(ghClient, jiraClient, prNumber)
    75  	if err != nil {
    76  		return errs.Wrap(err, "failed to fetch meta")
    77  	}
    78  	finish()
    79  
    80  	if meta.ActiveVersion.EQ(wh.VersionMaster) {
    81  		wc.Print("Target version is master, no propagation required")
    82  		return nil
    83  	}
    84  
    85  	// Find open version PRs
    86  	finish = wc.PrintStart("Finding open version PRs that need to adopt this PR")
    87  	versionPRs, err := wh.FetchVersionPRs(ghClient, wh.AssertGT, meta.ActiveVersion, -1)
    88  	if err != nil {
    89  		return errs.Wrap(err, "failed to fetch version PRs")
    90  	}
    91  	finish()
    92  
    93  	// Parse merge intends
    94  	intend := MergeIntends{}
    95  	currentBranch := meta.ActivePR.GetBase().GetRef()
    96  	targetBranches := []string{}
    97  	for _, pr := range versionPRs {
    98  		if pr.GetState() != "open" {
    99  			return errs.Wrap(err, "Version PR %d is not open, does the source PR have the right fixVersion associated?", pr.GetNumber())
   100  		}
   101  		intend = append(intend, MergeIntend{
   102  			SourceBranch: currentBranch,
   103  			TargetBranch: pr.GetHead().GetRef(),
   104  		})
   105  		targetBranches = append(targetBranches, pr.GetHead().GetRef())
   106  		currentBranch = pr.GetHead().GetRef()
   107  	}
   108  
   109  	// Always end with master
   110  	intend = append(intend, MergeIntend{
   111  		SourceBranch: currentBranch,
   112  		TargetBranch: wh.MasterBranch,
   113  	})
   114  	targetBranches = append(targetBranches, wh.MasterBranch)
   115  
   116  	wc.Print("Found %d branches that need to adopt this PR: %s\n", len(intend), strings.Join(targetBranches, ", "))
   117  
   118  	// Iterate over the merge intends and merge them
   119  	for i, v := range intend {
   120  		finish = wc.PrintStart("Merging %s into %s", v.SourceBranch, v.TargetBranch)
   121  
   122  		if os.Getenv("DRYRUN") == "true" {
   123  			wc.Print("DRY RUN: Skipping merge")
   124  			finish()
   125  			continue
   126  		}
   127  
   128  		root := environment.GetRootPathUnsafe()
   129  		stdout, stderr, err := osutils.ExecSimpleFromDir(root, "git", []string{"checkout", v.TargetBranch}, nil)
   130  		if err != nil {
   131  			return errs.Wrap(err, "failed to checkout %s, stdout:\n%s\nstderr:\n%s", v.TargetBranch, stdout, stderr)
   132  		}
   133  
   134  		commitMessage := fmt.Sprintf("Merge branch %s to adopt changes from PR #%d", v.SourceBranch, prNumber)
   135  		stdout, stderr, err = merge(root, v.SourceBranch, commitMessage)
   136  		if err != nil {
   137  			conflictOnlyInVersionTxt := strings.Count(stdout, "CONFLICT (content): Merge conflict in") == 1 &&
   138  				strings.Contains(stdout, "Merge conflict in version.txt")
   139  
   140  			if conflictOnlyInVersionTxt {
   141  				// Abort the conflicted merge and then redo it, preferring the target branch's version.txt.
   142  				stdout, stderr, err = osutils.ExecSimpleFromDir(root, "git", []string{"merge", "--abort"}, nil)
   143  				if err != nil {
   144  					return errs.Wrap(err, "failed to abort conflicted merge\nstdout: %s\nstderr: %s\n", stdout, stderr)
   145  				}
   146  				stdout, stderr, err = merge(root, v.SourceBranch, commitMessage, "-s", "ort", "-X", "ours")
   147  			}
   148  
   149  			if err != nil {
   150  				return errs.Wrap(err,
   151  					"failed to merge %s into %s. please manually merge the following branches: %s"+
   152  						"\nstdout:\n%s\nstderr:\n%s",
   153  					v.SourceBranch, v.TargetBranch, intend[i:].String(), stdout, stderr)
   154  			}
   155  		}
   156  
   157  		stdout, stderr, err = osutils.ExecSimpleFromDir(root, "git", []string{"push"}, nil)
   158  		if err != nil {
   159  			return errs.Wrap(err,
   160  				"failed to merge %s into %s. please manually merge the following branches: %s"+
   161  					"\nstdout:\n%s\nstderr:\n%s",
   162  				v.SourceBranch, v.TargetBranch, intend[i:].String(), stdout, stderr)
   163  		}
   164  
   165  		finish()
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func fetchMeta(ghClient *github.Client, jiraClient *jira.Client, prNumber int) (Meta, error) {
   172  	// Grab PR information about the PR that this automation is being ran on
   173  	finish := wc.PrintStart("Fetching Active PR %d", prNumber)
   174  	prBeingHandled, _, err := ghClient.PullRequests.Get(context.Background(), "ActiveState", "cli", prNumber)
   175  	if err != nil {
   176  		return Meta{}, errs.Wrap(err, "failed to get PR")
   177  	}
   178  	wc.Print("PR retrieved: %s", prBeingHandled.GetTitle())
   179  	finish()
   180  
   181  	if prBeingHandled.GetState() != "closed" && !prBeingHandled.GetMerged() {
   182  		if os.Getenv("DRYRUN") != "true" {
   183  			return Meta{}, errs.New("Active PR should be merged before it can be propagated.")
   184  		}
   185  	}
   186  
   187  	finish = wc.PrintStart("Extracting Jira Issue ID from Active PR: %s", prBeingHandled.GetTitle())
   188  	jiraIssueID, err := wh.ExtractJiraIssueID(prBeingHandled)
   189  	if err != nil {
   190  		return Meta{}, errs.Wrap(err, "PR does not have Jira issue ID associated with it: %s", prBeingHandled.Links.GetHTML().GetHRef())
   191  	}
   192  	wc.Print("Extracted Jira Issue ID: %s", jiraIssueID)
   193  	finish()
   194  
   195  	// Retrieve Relevant Jira Issue for PR being handled
   196  	finish = wc.PrintStart("Fetching Jira issue")
   197  	jiraIssue, err := wh.FetchJiraIssue(jiraClient, jiraIssueID)
   198  	if err != nil {
   199  		return Meta{}, errs.Wrap(err, "failed to get Jira issue")
   200  	}
   201  	finish()
   202  
   203  	finish = wc.PrintStart("Fetching Jira Versions")
   204  	availableVersions, err := wh.FetchAvailableVersions(jiraClient)
   205  	if err != nil {
   206  		return Meta{}, errs.Wrap(err, "Failed to fetch JIRA issue")
   207  	}
   208  	finish()
   209  
   210  	// Retrieve Relevant Fixversion
   211  	finish = wc.PrintStart("Extracting target fixVersion from Jira issue")
   212  	fixVersion, _, err := wh.ParseTargetFixVersion(jiraIssue, availableVersions)
   213  	if err != nil {
   214  		return Meta{}, errs.Wrap(err, "failed to get fixVersion")
   215  	}
   216  	wc.Print("Extracted fixVersion: %s", fixVersion)
   217  	finish()
   218  
   219  	if err := wh.ValidVersionBranch(prBeingHandled.GetBase().GetRef(), fixVersion.Version); err != nil {
   220  		return Meta{}, errs.Wrap(err, "Failed to validate that the target branch for the active PR is correct.")
   221  	}
   222  
   223  	result := Meta{
   224  		Repo:          &github.Repository{},
   225  		ActivePR:      prBeingHandled,
   226  		ActiveStory:   jiraIssue,
   227  		ActiveVersion: fixVersion.Version,
   228  	}
   229  
   230  	return result, nil
   231  }
   232  
   233  func merge(rootDir, sourceBranch, commitMessage string, extraGitArgs ...string) (string, string, error) {
   234  	args := append([]string{
   235  		"merge", sourceBranch,
   236  		"-m", commitMessage,
   237  		"--no-edit",
   238  	}, extraGitArgs...)
   239  	return osutils.ExecSimpleFromDir(rootDir, "git", args, nil)
   240  }