Skip to content
Merged
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
33 changes: 13 additions & 20 deletions git.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ var (
)

// Create new pull request
func (ghApp *GitHubApp) NewPullRequest(source, target, title, body string) error {
func (ghApp *GitHubApp) NewPullRequest(source, target, title, body string, merge bool) error {
ctx := context.Background()

// Create PR
Expand All @@ -126,34 +126,31 @@ func (ghApp *GitHubApp) NewPullRequest(source, target, title, body string) error
return err
}

// Optionally wait for checks to pass on the PR's head commit before merging.
if ghApp.Config.WaitForChecksToPass {
err = waitForChecksToPass(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetHead().GetSHA())
if err != nil {
return err
if merge {

if ghApp.Config.WaitForChecksToPass {
err = waitForChecksToPass(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetHead().GetSHA())
if err != nil {
return err
}
}

// Merge PR
return mergePullRequest(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetNumber())
}

// Merge PR
return mergePullRequest(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetNumber())
return nil

}

// waitForChecksToPass polls the GitHub Actions check runs for the given ref
// until they all complete successfully, one of them fails, or the timeout is
// reached. It relies on the Check Runs API (what Actions report to) rather than
// the legacy commit-status API, which does not reflect Actions results.
func waitForChecksToPass(ctx context.Context, client *github.Client, owner, repo, ref string) error {
// Bound the whole wait — and every API call made within it — so a stalled
// request can never hang indefinitely.
ctx, cancel := context.WithTimeout(ctx, checksWaitTimeout)
defer cancel()

ticker := time.NewTicker(checksPollInterval)
defer ticker.Stop()

for {
// Wait one interval before polling so GitHub has time to register the
// check runs for a freshly created PR.
select {
case <-ctx.Done():
return fmt.Errorf("timeout while waiting for checks to pass: %w", ctx.Err())
Expand All @@ -173,15 +170,11 @@ func waitForChecksToPass(ctx context.Context, client *github.Client, owner, repo
}
switch run.GetConclusion() {
case "success", "skipped", "neutral":
// Passed (or intentionally not blocking).
default:
// failure, cancelled, timed_out, action_required, stale...
return fmt.Errorf("check %q did not pass: %s", run.GetName(), run.GetConclusion())
}
}

// Require at least one completed check so we never merge before CI has
// started. Once every registered check has completed successfully, done.
if !pending && result.GetTotal() > 0 {
return nil
}
Expand Down
36 changes: 34 additions & 2 deletions git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestNewPullRequestSkipsChecksWhenDisabled(t *testing.T) {
githubClient: newTestClient(t, handler),
}

if err := ghApp.NewPullRequest("feature", "main", "title", "body"); err != nil {
if err := ghApp.NewPullRequest("feature", "main", "title", "body", true); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if checksCalled {
Expand Down Expand Up @@ -201,7 +201,7 @@ func TestNewPullRequestWaitsForChecksWhenEnabled(t *testing.T) {
githubClient: newTestClient(t, handler),
}

if err := ghApp.NewPullRequest("feature", "main", "title", "body"); err != nil {
if err := ghApp.NewPullRequest("feature", "main", "title", "body", true); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !checksCalled {
Expand All @@ -211,3 +211,35 @@ func TestNewPullRequestWaitsForChecksWhenEnabled(t *testing.T) {
t.Errorf("PR was not merged")
}
}

func TestNewPullRequestSkipsMergeWhenNotRequested(t *testing.T) {
var checksCalled, mergeCalled bool

handler := http.NewServeMux()
handler.HandleFunc("POST /repos/kununu/test-repo/pulls", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"number":7,"head":{"sha":"abc123"}}`))
})
handler.HandleFunc("GET /repos/kununu/test-repo/commits/{ref}/check-runs", func(w http.ResponseWriter, r *http.Request) {
checksCalled = true
w.Write([]byte(`{"total_count":1,"check_runs":[{"name":"build","status":"completed","conclusion":"success"}]}`))
})
handler.HandleFunc("PUT /repos/kununu/test-repo/pulls/7/merge", func(w http.ResponseWriter, r *http.Request) {
mergeCalled = true
w.Write([]byte(`{"merged":true}`))
})

ghApp := &GitHubApp{
Config: &GitHubAppConfig{repoName: "test-repo", WaitForChecksToPass: true},
githubClient: newTestClient(t, handler),
}

if err := ghApp.NewPullRequest("feature", "main", "title", "body", false); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if checksCalled {
t.Errorf("check runs were polled even though merge was not requested")
}
if mergeCalled {
t.Errorf("PR was merged even though merge was not requested")
}
}
Loading