Skip to content

Commit d781e90

Browse files
committed
GitHub Job Checker
1 parent 74a0df4 commit d781e90

File tree

9 files changed

+727
-0
lines changed

9 files changed

+727
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"time"
9+
)
10+
11+
// Checker holds configuration for checking GitHub actions.
12+
type Checker struct {
13+
Owner string
14+
Repo string
15+
Ref string
16+
CheckInterval time.Duration
17+
Timeout time.Duration
18+
Client GitHubClient
19+
}
20+
21+
// Run executes the check process.
22+
func (c *Checker) Run(ctx context.Context) error {
23+
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
24+
defer cancel()
25+
26+
ticker := time.NewTicker(c.CheckInterval)
27+
defer ticker.Stop()
28+
29+
for {
30+
select {
31+
case <-ctx.Done():
32+
return fmt.Errorf("timeout of %v reached. GitHub jobs did not finish in time", c.Timeout)
33+
case <-ticker.C:
34+
results, err := c.Client.FetchCheckRunsForRef(ctx, c.Owner, c.Repo, c.Ref)
35+
if err != nil {
36+
return err
37+
}
38+
39+
if results.GetTotal() == 0 {
40+
log.Print("No GitHub jobs configured for this commit.")
41+
return nil
42+
}
43+
44+
var anyFailure, anyPending int
45+
46+
for _, checkRun := range results.CheckRuns {
47+
status := checkRun.GetStatus()
48+
conclusion := checkRun.GetConclusion()
49+
50+
if status == "completed" && conclusion != "success" {
51+
anyFailure++
52+
}
53+
54+
if status == "in_progress" || status == "queued" {
55+
anyPending++
56+
}
57+
}
58+
59+
log.Printf("Number of failed check runs: %d", anyFailure)
60+
log.Printf("Number of pending check runs: %d", anyPending)
61+
62+
if anyFailure > 0 {
63+
return errors.New("one or more GitHub jobs failed")
64+
}
65+
66+
if anyPending == 0 {
67+
log.Print("All GitHub jobs succeeded.")
68+
return nil
69+
}
70+
71+
log.Printf("GitHub jobs are still running. Waiting for %v before rechecking.", c.CheckInterval)
72+
}
73+
}
74+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/go-github/v66/github"
7+
)
8+
9+
// GitHubClient defines the methods needed from the GitHub API.
10+
type GitHubClient interface {
11+
FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error)
12+
}
13+
14+
// GitHubAPIClient implements GitHubClient using the actual GitHub API.
15+
type GitHubAPIClient struct {
16+
client *github.Client
17+
}
18+
19+
func NewGitHubAPIClient(token string) *GitHubAPIClient {
20+
return &GitHubAPIClient{github.NewClient(nil).WithAuthToken(token)}
21+
}
22+
23+
// FetchCheckRunsForRef fetches check runs for a specific reference.
24+
func (c *GitHubAPIClient) FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
25+
results, _, err := c.client.Checks.ListCheckRunsForRef(ctx, owner, repo, ref, nil)
26+
return results, err
27+
}

tools/github-job-checker/cmd/main.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/input-output-hk/catalyst-forge/tools/github-job-checker/internal/config"
8+
)
9+
10+
// Run initializes the checker and starts the process.
11+
func Run() error {
12+
cfg, err := config.LoadConfig()
13+
if err != nil {
14+
return fmt.Errorf("failed to load configuration: %w", err)
15+
}
16+
17+
checker := &Checker{
18+
Owner: cfg.Owner,
19+
Repo: cfg.Repo,
20+
Ref: cfg.Ref,
21+
CheckInterval: cfg.CheckInterval,
22+
Timeout: cfg.Timeout,
23+
Client: NewGitHubAPIClient(cfg.Token),
24+
}
25+
26+
ctx := context.Background()
27+
return checker.Run(ctx)
28+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package cmd_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
"github.com/google/go-github/v66/github"
10+
"github.com/input-output-hk/catalyst-forge/tools/github-job-checker/cmd"
11+
)
12+
13+
// MockGitHubClient mocks the GitHubClient interface for testing.
14+
type MockGitHubClient struct {
15+
FetchFunc func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error)
16+
}
17+
18+
func (m *MockGitHubClient) FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
19+
if m.FetchFunc != nil {
20+
return m.FetchFunc(ctx, owner, repo, ref)
21+
}
22+
return nil, errors.New("FetchFunc not implemented")
23+
}
24+
25+
func TestChecker_Run_Success(t *testing.T) {
26+
client := &MockGitHubClient{
27+
FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
28+
return &github.ListCheckRunsResults{
29+
Total: github.Int(1),
30+
CheckRuns: []*github.CheckRun{
31+
{
32+
Status: github.String("completed"),
33+
Conclusion: github.String("success"),
34+
},
35+
},
36+
}, nil
37+
},
38+
}
39+
40+
checker := &cmd.Checker{
41+
Owner: "owner",
42+
Repo: "repo",
43+
Ref: "ref",
44+
CheckInterval: 1 * time.Second,
45+
Timeout: 5 * time.Second,
46+
Client: client,
47+
}
48+
49+
ctx := context.Background()
50+
err := checker.Run(ctx)
51+
if err != nil {
52+
t.Fatalf("expected no error, got %v", err)
53+
}
54+
}
55+
56+
func TestChecker_Run_Failure(t *testing.T) {
57+
client := &MockGitHubClient{
58+
FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
59+
return &github.ListCheckRunsResults{
60+
Total: github.Int(1),
61+
CheckRuns: []*github.CheckRun{
62+
{
63+
Status: github.String("completed"),
64+
Conclusion: github.String("failure"),
65+
},
66+
},
67+
}, nil
68+
},
69+
}
70+
71+
checker := &cmd.Checker{
72+
Owner: "owner",
73+
Repo: "repo",
74+
Ref: "ref",
75+
CheckInterval: 1 * time.Second,
76+
Timeout: 5 * time.Second,
77+
Client: client,
78+
}
79+
80+
ctx := context.Background()
81+
err := checker.Run(ctx)
82+
if err == nil || err.Error() != "one or more GitHub jobs failed" {
83+
t.Fatalf("expected failure error, got %v", err)
84+
}
85+
}
86+
87+
func TestChecker_Run_PendingToSuccess(t *testing.T) {
88+
callCount := 0
89+
client := &MockGitHubClient{
90+
FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
91+
defer func() { callCount++ }()
92+
if callCount == 0 {
93+
// First call: return pending
94+
return &github.ListCheckRunsResults{
95+
Total: github.Int(1),
96+
CheckRuns: []*github.CheckRun{
97+
{
98+
Status: github.String("queued"),
99+
Conclusion: nil,
100+
},
101+
},
102+
}, nil
103+
}
104+
// Subsequent calls: return success
105+
return &github.ListCheckRunsResults{
106+
Total: github.Int(1),
107+
CheckRuns: []*github.CheckRun{
108+
{
109+
Status: github.String("completed"),
110+
Conclusion: github.String("success"),
111+
},
112+
},
113+
}, nil
114+
},
115+
}
116+
117+
checker := &cmd.Checker{
118+
Owner: "owner",
119+
Repo: "repo",
120+
Ref: "ref",
121+
CheckInterval: 500 * time.Millisecond,
122+
Timeout: 2 * time.Second,
123+
Client: client,
124+
}
125+
126+
ctx := context.Background()
127+
err := checker.Run(ctx)
128+
if err != nil {
129+
t.Fatalf("expected no error, got %v", err)
130+
}
131+
}
132+
133+
func TestChecker_Run_Timeout(t *testing.T) {
134+
client := &MockGitHubClient{
135+
FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) {
136+
// Always return in-progress status
137+
return &github.ListCheckRunsResults{
138+
Total: github.Int(1),
139+
CheckRuns: []*github.CheckRun{
140+
{
141+
Status: github.String("in_progress"),
142+
Conclusion: nil,
143+
},
144+
},
145+
}, nil
146+
},
147+
}
148+
149+
checker := &cmd.Checker{
150+
Owner: "owner",
151+
Repo: "repo",
152+
Ref: "ref",
153+
CheckInterval: 500 * time.Millisecond,
154+
Timeout: 1 * time.Second,
155+
Client: client,
156+
}
157+
158+
ctx := context.Background()
159+
err := checker.Run(ctx)
160+
if err == nil || !errors.Is(err, context.DeadlineExceeded) && err.Error() != "timeout of 1s reached. GitHub jobs did not finish in time" {
161+
t.Fatalf("expected timeout error, got %v", err)
162+
}
163+
}

tools/github-job-checker/go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module github.com/input-output-hk/catalyst-forge/tools/github-job-checker
2+
3+
go 1.23.2
4+
5+
require (
6+
github.com/google/go-github/v66 v66.0.0
7+
github.com/mitchellh/mapstructure v1.5.0
8+
github.com/spf13/pflag v1.0.5
9+
github.com/spf13/viper v1.19.0
10+
)
11+
12+
require (
13+
github.com/fsnotify/fsnotify v1.7.0 // indirect
14+
github.com/google/go-querystring v1.1.0 // indirect
15+
github.com/hashicorp/hcl v1.0.0 // indirect
16+
github.com/magiconair/properties v1.8.7 // indirect
17+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
18+
github.com/sagikazarmark/locafero v0.4.0 // indirect
19+
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
20+
github.com/sourcegraph/conc v0.3.0 // indirect
21+
github.com/spf13/afero v1.11.0 // indirect
22+
github.com/spf13/cast v1.6.0 // indirect
23+
github.com/subosito/gotenv v1.6.0 // indirect
24+
go.uber.org/atomic v1.9.0 // indirect
25+
go.uber.org/multierr v1.9.0 // indirect
26+
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
27+
golang.org/x/sys v0.18.0 // indirect
28+
golang.org/x/text v0.14.0 // indirect
29+
gopkg.in/ini.v1 v1.67.0 // indirect
30+
gopkg.in/yaml.v3 v3.0.1 // indirect
31+
)

0 commit comments

Comments
 (0)