From d781e900704bb48a27c6e3b0313ba175ca610956 Mon Sep 17 00:00:00 2001 From: benbooth493 Date: Tue, 12 Nov 2024 14:08:30 +0000 Subject: [PATCH 1/3] GitHub Job Checker --- tools/github-job-checker/cmd/checker.go | 74 ++++++ tools/github-job-checker/cmd/github_client.go | 27 +++ tools/github-job-checker/cmd/main.go | 28 +++ .../cmd/tests/checker_test.go | 163 +++++++++++++ tools/github-job-checker/go.mod | 31 +++ tools/github-job-checker/go.sum | 77 ++++++ .../internal/config/config.go | 91 +++++++ .../internal/config/tests/config_test.go | 225 ++++++++++++++++++ tools/github-job-checker/main.go | 11 + 9 files changed, 727 insertions(+) create mode 100644 tools/github-job-checker/cmd/checker.go create mode 100644 tools/github-job-checker/cmd/github_client.go create mode 100644 tools/github-job-checker/cmd/main.go create mode 100644 tools/github-job-checker/cmd/tests/checker_test.go create mode 100644 tools/github-job-checker/go.mod create mode 100644 tools/github-job-checker/go.sum create mode 100644 tools/github-job-checker/internal/config/config.go create mode 100644 tools/github-job-checker/internal/config/tests/config_test.go create mode 100644 tools/github-job-checker/main.go diff --git a/tools/github-job-checker/cmd/checker.go b/tools/github-job-checker/cmd/checker.go new file mode 100644 index 0000000..182c4b4 --- /dev/null +++ b/tools/github-job-checker/cmd/checker.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "log" + "time" +) + +// Checker holds configuration for checking GitHub actions. +type Checker struct { + Owner string + Repo string + Ref string + CheckInterval time.Duration + Timeout time.Duration + Client GitHubClient +} + +// Run executes the check process. +func (c *Checker) Run(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, c.Timeout) + defer cancel() + + ticker := time.NewTicker(c.CheckInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout of %v reached. GitHub jobs did not finish in time", c.Timeout) + case <-ticker.C: + results, err := c.Client.FetchCheckRunsForRef(ctx, c.Owner, c.Repo, c.Ref) + if err != nil { + return err + } + + if results.GetTotal() == 0 { + log.Print("No GitHub jobs configured for this commit.") + return nil + } + + var anyFailure, anyPending int + + for _, checkRun := range results.CheckRuns { + status := checkRun.GetStatus() + conclusion := checkRun.GetConclusion() + + if status == "completed" && conclusion != "success" { + anyFailure++ + } + + if status == "in_progress" || status == "queued" { + anyPending++ + } + } + + log.Printf("Number of failed check runs: %d", anyFailure) + log.Printf("Number of pending check runs: %d", anyPending) + + if anyFailure > 0 { + return errors.New("one or more GitHub jobs failed") + } + + if anyPending == 0 { + log.Print("All GitHub jobs succeeded.") + return nil + } + + log.Printf("GitHub jobs are still running. Waiting for %v before rechecking.", c.CheckInterval) + } + } +} diff --git a/tools/github-job-checker/cmd/github_client.go b/tools/github-job-checker/cmd/github_client.go new file mode 100644 index 0000000..9e95b50 --- /dev/null +++ b/tools/github-job-checker/cmd/github_client.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "context" + + "github.com/google/go-github/v66/github" +) + +// GitHubClient defines the methods needed from the GitHub API. +type GitHubClient interface { + FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) +} + +// GitHubAPIClient implements GitHubClient using the actual GitHub API. +type GitHubAPIClient struct { + client *github.Client +} + +func NewGitHubAPIClient(token string) *GitHubAPIClient { + return &GitHubAPIClient{github.NewClient(nil).WithAuthToken(token)} +} + +// FetchCheckRunsForRef fetches check runs for a specific reference. +func (c *GitHubAPIClient) FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + results, _, err := c.client.Checks.ListCheckRunsForRef(ctx, owner, repo, ref, nil) + return results, err +} diff --git a/tools/github-job-checker/cmd/main.go b/tools/github-job-checker/cmd/main.go new file mode 100644 index 0000000..d9b9c06 --- /dev/null +++ b/tools/github-job-checker/cmd/main.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/input-output-hk/catalyst-forge/tools/github-job-checker/internal/config" +) + +// Run initializes the checker and starts the process. +func Run() error { + cfg, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + checker := &Checker{ + Owner: cfg.Owner, + Repo: cfg.Repo, + Ref: cfg.Ref, + CheckInterval: cfg.CheckInterval, + Timeout: cfg.Timeout, + Client: NewGitHubAPIClient(cfg.Token), + } + + ctx := context.Background() + return checker.Run(ctx) +} diff --git a/tools/github-job-checker/cmd/tests/checker_test.go b/tools/github-job-checker/cmd/tests/checker_test.go new file mode 100644 index 0000000..1f642f0 --- /dev/null +++ b/tools/github-job-checker/cmd/tests/checker_test.go @@ -0,0 +1,163 @@ +package cmd_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/google/go-github/v66/github" + "github.com/input-output-hk/catalyst-forge/tools/github-job-checker/cmd" +) + +// MockGitHubClient mocks the GitHubClient interface for testing. +type MockGitHubClient struct { + FetchFunc func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) +} + +func (m *MockGitHubClient) FetchCheckRunsForRef(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + if m.FetchFunc != nil { + return m.FetchFunc(ctx, owner, repo, ref) + } + return nil, errors.New("FetchFunc not implemented") +} + +func TestChecker_Run_Success(t *testing.T) { + client := &MockGitHubClient{ + FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + return &github.ListCheckRunsResults{ + Total: github.Int(1), + CheckRuns: []*github.CheckRun{ + { + Status: github.String("completed"), + Conclusion: github.String("success"), + }, + }, + }, nil + }, + } + + checker := &cmd.Checker{ + Owner: "owner", + Repo: "repo", + Ref: "ref", + CheckInterval: 1 * time.Second, + Timeout: 5 * time.Second, + Client: client, + } + + ctx := context.Background() + err := checker.Run(ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestChecker_Run_Failure(t *testing.T) { + client := &MockGitHubClient{ + FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + return &github.ListCheckRunsResults{ + Total: github.Int(1), + CheckRuns: []*github.CheckRun{ + { + Status: github.String("completed"), + Conclusion: github.String("failure"), + }, + }, + }, nil + }, + } + + checker := &cmd.Checker{ + Owner: "owner", + Repo: "repo", + Ref: "ref", + CheckInterval: 1 * time.Second, + Timeout: 5 * time.Second, + Client: client, + } + + ctx := context.Background() + err := checker.Run(ctx) + if err == nil || err.Error() != "one or more GitHub jobs failed" { + t.Fatalf("expected failure error, got %v", err) + } +} + +func TestChecker_Run_PendingToSuccess(t *testing.T) { + callCount := 0 + client := &MockGitHubClient{ + FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + defer func() { callCount++ }() + if callCount == 0 { + // First call: return pending + return &github.ListCheckRunsResults{ + Total: github.Int(1), + CheckRuns: []*github.CheckRun{ + { + Status: github.String("queued"), + Conclusion: nil, + }, + }, + }, nil + } + // Subsequent calls: return success + return &github.ListCheckRunsResults{ + Total: github.Int(1), + CheckRuns: []*github.CheckRun{ + { + Status: github.String("completed"), + Conclusion: github.String("success"), + }, + }, + }, nil + }, + } + + checker := &cmd.Checker{ + Owner: "owner", + Repo: "repo", + Ref: "ref", + CheckInterval: 500 * time.Millisecond, + Timeout: 2 * time.Second, + Client: client, + } + + ctx := context.Background() + err := checker.Run(ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestChecker_Run_Timeout(t *testing.T) { + client := &MockGitHubClient{ + FetchFunc: func(ctx context.Context, owner, repo, ref string) (*github.ListCheckRunsResults, error) { + // Always return in-progress status + return &github.ListCheckRunsResults{ + Total: github.Int(1), + CheckRuns: []*github.CheckRun{ + { + Status: github.String("in_progress"), + Conclusion: nil, + }, + }, + }, nil + }, + } + + checker := &cmd.Checker{ + Owner: "owner", + Repo: "repo", + Ref: "ref", + CheckInterval: 500 * time.Millisecond, + Timeout: 1 * time.Second, + Client: client, + } + + ctx := context.Background() + err := checker.Run(ctx) + if err == nil || !errors.Is(err, context.DeadlineExceeded) && err.Error() != "timeout of 1s reached. GitHub jobs did not finish in time" { + t.Fatalf("expected timeout error, got %v", err) + } +} diff --git a/tools/github-job-checker/go.mod b/tools/github-job-checker/go.mod new file mode 100644 index 0000000..7fd9995 --- /dev/null +++ b/tools/github-job-checker/go.mod @@ -0,0 +1,31 @@ +module github.com/input-output-hk/catalyst-forge/tools/github-job-checker + +go 1.23.2 + +require ( + github.com/google/go-github/v66 v66.0.0 + github.com/mitchellh/mapstructure v1.5.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.19.0 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tools/github-job-checker/go.sum b/tools/github-job-checker/go.sum new file mode 100644 index 0000000..73d5e5c --- /dev/null +++ b/tools/github-job-checker/go.sum @@ -0,0 +1,77 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/github-job-checker/internal/config/config.go b/tools/github-job-checker/internal/config/config.go new file mode 100644 index 0000000..66b1cf7 --- /dev/null +++ b/tools/github-job-checker/internal/config/config.go @@ -0,0 +1,91 @@ +package config + +import ( + "fmt" + "strings" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +// Config holds the configuration values for the application. +type Config struct { + Owner string `mapstructure:"owner"` + Repo string `mapstructure:"repo"` + Ref string `mapstructure:"ref"` + Token string `mapstructure:"token"` + CheckInterval time.Duration `mapstructure:"check_interval"` + Timeout time.Duration `mapstructure:"timeout"` +} + +// LoadConfig loads the configuration from flags, environment variables, or a config file. +func LoadConfig() (*Config, error) { + // Set up flag normalization to replace hyphens with underscores + pflag.CommandLine.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { + return pflag.NormalizedName(strings.ReplaceAll(name, "-", "_")) + }) + + // Define command-line flags. + pflag.String("owner", "", "Repository owner") + pflag.String("repo", "", "Repository name") + pflag.String("ref", "", "Commit hash or reference") + pflag.String("token", "", "GitHub token") + pflag.Duration("check-interval", 10*time.Second, "Interval between checks") + pflag.Duration("timeout", 300*time.Second, "Timeout for the operation") + + // Parse the command-line flags. + pflag.Parse() + + // Bind the command-line flags to Viper. + if err := viper.BindPFlags(pflag.CommandLine); err != nil { + return nil, fmt.Errorf("failed to bind flags: %w", err) + } + + // Set environment variable prefixes. + viper.SetEnvPrefix("GITHUB") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + + // // Check if a config file is already set. + if viper.ConfigFileUsed() == "" { + // Optionally read from a config file. + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + } + + if err := viper.ReadInConfig(); err != nil { + // It's okay if the config file doesn't exist. + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + } + + // Unmarshal the config into the Config struct. + var cfg Config + if err := viper.Unmarshal(&cfg, viper.DecodeHook( + mapstructure.StringToTimeDurationHookFunc(), + )); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + fmt.Printf("All Settings: %+v\n", viper.AllSettings()) + + // Validate required fields. + if cfg.Owner == "" { + return nil, fmt.Errorf("owner is required") + } + if cfg.Repo == "" { + return nil, fmt.Errorf("repo is required") + } + if cfg.Ref == "" { + return nil, fmt.Errorf("ref is required") + } + if cfg.Token == "" { + return nil, fmt.Errorf("token is required") + } + + return &cfg, nil +} diff --git a/tools/github-job-checker/internal/config/tests/config_test.go b/tools/github-job-checker/internal/config/tests/config_test.go new file mode 100644 index 0000000..4eacd1b --- /dev/null +++ b/tools/github-job-checker/internal/config/tests/config_test.go @@ -0,0 +1,225 @@ +package config_test + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/input-output-hk/catalyst-forge/tools/github-job-checker/internal/config" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func resetConfig() { + // Reset flags and viper before each test + pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) + viper.Reset() + os.Clearenv() + os.Args = os.Args[:1] // Keep only the program name +} + +func TestLoadConfig_FromFlags(t *testing.T) { + resetConfig() + + os.Args = []string{ + "cmd", + "--owner=test-owner", + "--repo=test-repo", + "--ref=test-ref", + "--token=test-token", + "--check-interval=15s", + "--timeout=600s", + } + + cfg, err := config.LoadConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if cfg.Owner != "test-owner" { + t.Errorf("expected owner 'test-owner', got '%s'", cfg.Owner) + } + if cfg.Repo != "test-repo" { + t.Errorf("expected repo 'test-repo', got '%s'", cfg.Repo) + } + if cfg.Ref != "test-ref" { + t.Errorf("expected ref 'test-ref', got '%s'", cfg.Ref) + } + if cfg.Token != "test-token" { + t.Errorf("expected token 'test-token', got '%s'", cfg.Token) + } + if cfg.CheckInterval != 15*time.Second { + t.Errorf("expected check-interval 15s, got %v", cfg.CheckInterval) + } + if cfg.Timeout != 600*time.Second { + t.Errorf("expected timeout 600s, got %v", cfg.Timeout) + } +} + +func TestLoadConfig_FromEnv(t *testing.T) { + resetConfig() + + os.Setenv("GITHUB_OWNER", "env-owner") + os.Setenv("GITHUB_REPO", "env-repo") + os.Setenv("GITHUB_REF", "env-ref") + os.Setenv("GITHUB_TOKEN", "env-token") + os.Setenv("GITHUB_CHECK_INTERVAL", "20s") + os.Setenv("GITHUB_TIMEOUT", "500s") + + cfg, err := config.LoadConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if cfg.Owner != "env-owner" { + t.Errorf("expected owner 'env-owner', got '%s'", cfg.Owner) + } + if cfg.Repo != "env-repo" { + t.Errorf("expected repo 'env-repo', got '%s'", cfg.Repo) + } + if cfg.Ref != "env-ref" { + t.Errorf("expected ref 'env-ref', got '%s'", cfg.Ref) + } + if cfg.Token != "env-token" { + t.Errorf("expected token 'env-token', got '%s'", cfg.Token) + } + if cfg.CheckInterval != 20*time.Second { + t.Errorf("expected check-interval 20s, got %v", cfg.CheckInterval) + } + if cfg.Timeout != 500*time.Second { + t.Errorf("expected timeout 500s, got %v", cfg.Timeout) + } +} + +func TestLoadConfig_FromFile(t *testing.T) { + resetConfig() + + // Create a temporary config file + configContent := `owner: file-owner +repo: file-repo +ref: file-ref +token: file-token +check_interval: 25s +timeout: 400s +` + tmpFile, err := os.CreateTemp("", "config_test_*.yaml") + if err != nil { + t.Fatalf("failed to create temp config file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte(configContent)); err != nil { + t.Fatalf("failed to write to temp config file: %v", err) + } + tmpFile.Close() + + viper.SetConfigFile(tmpFile.Name()) + + cfg, err := config.LoadConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if cfg.Owner != "file-owner" { + t.Errorf("expected owner 'file-owner', got '%s'", cfg.Owner) + } + if cfg.Repo != "file-repo" { + t.Errorf("expected repo 'file-repo', got '%s'", cfg.Repo) + } + if cfg.Ref != "file-ref" { + t.Errorf("expected ref 'file-ref', got '%s'", cfg.Ref) + } + if cfg.Token != "file-token" { + t.Errorf("expected token 'file-token', got '%s'", cfg.Token) + } + if cfg.CheckInterval != 25*time.Second { + t.Errorf("expected check-interval 25s, got %v", cfg.CheckInterval) + } + if cfg.Timeout != 400*time.Second { + t.Errorf("expected timeout 400s, got %v", cfg.Timeout) + } +} + +func TestLoadConfig_DefaultsAndRequiredFields(t *testing.T) { + resetConfig() + + // Set only the required fields + os.Setenv("GITHUB_OWNER", "default-owner") + os.Setenv("GITHUB_REPO", "default-repo") + os.Setenv("GITHUB_REF", "default-ref") + os.Setenv("GITHUB_TOKEN", "default-token") + + cfg, err := config.LoadConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if cfg.CheckInterval != 10*time.Second { + t.Errorf("expected default check-interval 10s, got %v", cfg.CheckInterval) + } + if cfg.Timeout != 300*time.Second { + t.Errorf("expected default timeout 300s, got %v", cfg.Timeout) + } +} + +func TestLoadConfig_MissingRequiredFields(t *testing.T) { + testCases := []struct { + name string + env map[string]string + expectedError string + }{ + { + name: "missing owner", + env: map[string]string{ + "GITHUB_REPO": "repo", + "GITHUB_REF": "ref", + "GITHUB_TOKEN": "token", + }, + expectedError: "owner is required", + }, + { + name: "missing repo", + env: map[string]string{ + "GITHUB_OWNER": "owner", + "GITHUB_REF": "ref", + "GITHUB_TOKEN": "token", + }, + expectedError: "repo is required", + }, + { + name: "missing ref", + env: map[string]string{ + "GITHUB_OWNER": "owner", + "GITHUB_REPO": "repo", + "GITHUB_TOKEN": "token", + }, + expectedError: "ref is required", + }, + { + name: "missing token", + env: map[string]string{ + "GITHUB_OWNER": "owner", + "GITHUB_REPO": "repo", + "GITHUB_REF": "ref", + }, + expectedError: "token is required", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resetConfig() + + // Set environment variables for the test case + for key, value := range tc.env { + os.Setenv(key, value) + } + + _, err := config.LoadConfig() + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Fatalf("expected error containing '%s', got '%v'", tc.expectedError, err) + } + }) + } +} diff --git a/tools/github-job-checker/main.go b/tools/github-job-checker/main.go new file mode 100644 index 0000000..0643be7 --- /dev/null +++ b/tools/github-job-checker/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "log" + + "github.com/input-output-hk/catalyst-forge/tools/github-job-checker/cmd" +) + +func main() { + log.Fatal(cmd.Run()) +} From 1c2fbc4dfeac85ed7ae3a59838fb7abc64de0f16 Mon Sep 17 00:00:00 2001 From: benbooth493 Date: Tue, 12 Nov 2024 14:24:06 +0000 Subject: [PATCH 2/3] Earthfile and blueprint.cue --- tools/github-job-checker/Earthfile | 69 ++++++++++++++++++++++++++ tools/github-job-checker/blueprint.cue | 31 ++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tools/github-job-checker/Earthfile create mode 100644 tools/github-job-checker/blueprint.cue diff --git a/tools/github-job-checker/Earthfile b/tools/github-job-checker/Earthfile new file mode 100644 index 0000000..9090948 --- /dev/null +++ b/tools/github-job-checker/Earthfile @@ -0,0 +1,69 @@ +VERSION 0.8 + +deps: + FROM golang:1.23.0-alpine3.19 + + WORKDIR /work + + RUN apk add git file + + RUN mkdir -p /go/cache && mkdir -p /go/modcache + ENV GOCACHE=/go/cache + ENV GOMODCACHE=/go/modcache + CACHE --persist --sharing shared /go + + COPY ../lib/project+src/src /lib/project + COPY ../lib/tools+src/src /lib/tools + + COPY go.mod go.sum . + RUN go mod download + +src: + FROM +deps + + CACHE --persist --sharing shared /go + + COPY . . + RUN go generate ./... + +check: + FROM +src + + RUN gofmt -l . | grep . && exit 1 || exit 0 + RUN go vet ./... + +build: + FROM +src + + ARG GOOS + ARG GOARCH + ARG version="0.0.0" + + ENV CGO_ENABLED=0 + RUN go build -ldflags="-extldflags=-static -X main.version=$version" -o bin/gh-job-checker main.go + RUN file bin/gh-job-checker + + SAVE ARTIFACT bin/gh-job-checker gh-job-checker + +test: + FROM +build + + RUN go test ./... + +github: + FROM scratch + + ARG version="dev" + + ARG TARGETOS + ARG TARGETARCH + ARG USERPLATFORM + + COPY \ + --platform=$USERPLATFORM \ + (+build/gh-job-checker \ + --GOOS=$TARGETOS \ + --GOARCH=$TARGETARCH \ + --version=$version) bin/gh-job-checker + + SAVE ARTIFACT bin/gh-job-checker gh-job-checker diff --git a/tools/github-job-checker/blueprint.cue b/tools/github-job-checker/blueprint.cue new file mode 100644 index 0000000..9cbcdbc --- /dev/null +++ b/tools/github-job-checker/blueprint.cue @@ -0,0 +1,31 @@ +version: "1.0" +project: { + name: "gh-job-checker" + ci: targets: { + github: { + args: { + version: string | *"dev" @forge(name="GIT_TAG") + } + platforms: [ + "linux/amd64", + "linux/arm64", + "darwin/amd64", + "darwin/arm64", + ] + } + test: retries: 3 + } + release: { + github: { + on: tag: {} + config: { + name: string | *"dev" @forge(name="GIT_TAG") + prefix: project.name + token: { + provider: "env" + path: "GITHUB_TOKEN" + } + } + } + } +} From 69709f20109606565ffd97e0b7f322f5c0ce02fa Mon Sep 17 00:00:00 2001 From: benbooth493 Date: Wed, 13 Nov 2024 08:19:12 +0000 Subject: [PATCH 3/3] A starting point at least --- tools/github-job-checker/Earthfile | 4 ++-- tools/github-job-checker/blueprint.cue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/github-job-checker/Earthfile b/tools/github-job-checker/Earthfile index 9090948..6a764c4 100644 --- a/tools/github-job-checker/Earthfile +++ b/tools/github-job-checker/Earthfile @@ -50,8 +50,8 @@ test: RUN go test ./... -github: - FROM scratch +docker: + FROM alpine:3 ARG version="dev" diff --git a/tools/github-job-checker/blueprint.cue b/tools/github-job-checker/blueprint.cue index 9cbcdbc..28c5378 100644 --- a/tools/github-job-checker/blueprint.cue +++ b/tools/github-job-checker/blueprint.cue @@ -2,7 +2,7 @@ version: "1.0" project: { name: "gh-job-checker" ci: targets: { - github: { + docker: { args: { version: string | *"dev" @forge(name="GIT_TAG") }