Merge pull request #22 from kununu/add-waitforchecks-config · kununu/go-github@cc24b43 · GitHub
Skip to content

Commit cc24b43

Browse files
authored
Merge pull request #22 from kununu/add-waitforchecks-config
Add WaitForChecks configuration
2 parents 2ec4f01 + eb7b1d8 commit cc24b43

8 files changed

Lines changed: 439 additions & 48 deletions

File tree

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion

‎apps.go‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ type GitHubApp struct {
2323

2424
// Configuration for the GitHub App interaction
2525
type GitHubAppConfig struct {
26-
repoName string
27-
RepoURL string
28-
ApplicationID int64
29-
InstallationID int64
30-
LocalPath string
31-
PrivateKey []byte
26+
repoName string
27+
RepoURL string
28+
ApplicationID int64
29+
InstallationID int64
30+
LocalPath string
31+
PrivateKey []byte
32+
WaitForChecksToPass bool
3233
}
3334

3435
var githubAPIURL string = "https://api.github.com"

‎git.go‎

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,54 +101,89 @@ func (ghApp *GitHubApp) NewBranch(name string, checkout bool) error {
101101

102102
}
103103

104+
// Polling configuration for waiting on a PR's CI checks. Declared as vars
105+
// (rather than consts) so tests can shrink them to keep polling loops fast.
106+
var (
107+
checksPollInterval = 10 * time.Second
108+
checksWaitTimeout = 10 * time.Minute
109+
)
110+
104111
// Create new pull request
105112
func (ghApp *GitHubApp) NewPullRequest(source, target, title, body string) error {
106113
ctx := context.Background()
107114

108115
// Create PR
109116
newPR := &github.NewPullRequest{
110-
Title: github.String(title),
111-
Head: github.String(source), // source branch
112-
Base: github.String(target), // target branch
113-
Body: github.String(body),
114-
MaintainerCanModify: github.Bool(true),
117+
Title: github.Ptr(title),
118+
Head: github.Ptr(source), // source branch
119+
Base: github.Ptr(target), // target branch
120+
Body: github.Ptr(body),
121+
MaintainerCanModify: github.Ptr(true),
115122
}
116123

117124
pr, _, err := ghApp.githubClient.PullRequests.Create(ctx, "kununu", ghApp.Config.repoName, newPR)
118125
if err != nil {
119126
return err
120127
}
121128

122-
// Wait for checks to pass
123-
err = waitForChecksToPass(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetNumber())
124-
if err != nil {
125-
return err
129+
// Optionally wait for checks to pass on the PR's head commit before merging.
130+
if ghApp.Config.WaitForChecksToPass {
131+
err = waitForChecksToPass(ctx, ghApp.githubClient, "kununu", ghApp.Config.repoName, pr.GetHead().GetSHA())
132+
if err != nil {
133+
return err
134+
}
126135
}
127136

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

132-
func waitForChecksToPass(ctx context.Context, client *github.Client, owner, repo string, number int) error {
133-
// Polling interval and timeout can be adjusted based on your requirements
134-
ticker := time.NewTicker(10 * time.Second)
141+
// waitForChecksToPass polls the GitHub Actions check runs for the given ref
142+
// until they all complete successfully, one of them fails, or the timeout is
143+
// reached. It relies on the Check Runs API (what Actions report to) rather than
144+
// the legacy commit-status API, which does not reflect Actions results.
145+
func waitForChecksToPass(ctx context.Context, client *github.Client, owner, repo, ref string) error {
146+
// Bound the whole wait — and every API call made within it — so a stalled
147+
// request can never hang indefinitely.
148+
ctx, cancel := context.WithTimeout(ctx, checksWaitTimeout)
149+
defer cancel()
150+
151+
ticker := time.NewTicker(checksPollInterval)
135152
defer ticker.Stop()
136153

137-
timeout := time.After(10 * time.Minute)
138-
139154
for {
155+
// Wait one interval before polling so GitHub has time to register the
156+
// check runs for a freshly created PR.
140157
select {
141-
case <-timeout:
142-
return fmt.Errorf("timeout while waiting for checks to pass")
158+
case <-ctx.Done():
159+
return fmt.Errorf("timeout while waiting for checks to pass: %w", ctx.Err())
143160
case <-ticker.C:
144-
combined, _, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, fmt.Sprintf("pull/%d/head", number), nil)
145-
if err != nil {
146-
return err
147-
}
161+
}
162+
163+
result, _, err := client.Checks.ListCheckRunsForRef(ctx, owner, repo, ref, nil)
164+
if err != nil {
165+
return fmt.Errorf("listing check runs for %s: %w", ref, err)
166+
}
148167

149-
if combined.GetState() == "success" {
150-
return nil
168+
pending := false
169+
for _, run := range result.CheckRuns {
170+
if run.GetStatus() != "completed" {
171+
pending = true
172+
continue
151173
}
174+
switch run.GetConclusion() {
175+
case "success", "skipped", "neutral":
176+
// Passed (or intentionally not blocking).
177+
default:
178+
// failure, cancelled, timed_out, action_required, stale...
179+
return fmt.Errorf("check %q did not pass: %s", run.GetName(), run.GetConclusion())
180+
}
181+
}
182+
183+
// Require at least one completed check so we never merge before CI has
184+
// started. Once every registered check has completed successfully, done.
185+
if !pending && result.GetTotal() > 0 {
186+
return nil
152187
}
153188
}
154189
}

‎git_test.go‎

Lines changed: 196 additions & 1 deletion

0 commit comments

Comments
 (0)