diff --git a/cli/pkg/integration/integration_test.go b/cli/pkg/integration/integration_test.go index 9cda6433c..a63449570 100644 --- a/cli/pkg/integration/integration_test.go +++ b/cli/pkg/integration/integration_test.go @@ -2,18 +2,19 @@ package integration import ( "context" - "github.com/diggerhq/digger/libs/ci/generic" - "github.com/diggerhq/digger/libs/execution" - "github.com/diggerhq/digger/libs/locking" - "github.com/diggerhq/digger/libs/locking/aws" - "github.com/diggerhq/digger/libs/storage" "log" "math/rand" "os" "testing" "time" - "github.com/diggerhq/digger/libs/comment_utils/summary" + "github.com/diggerhq/digger/libs/ci/generic" + "github.com/diggerhq/digger/libs/execution" + "github.com/diggerhq/digger/libs/locking" + "github.com/diggerhq/digger/libs/locking/aws" + "github.com/diggerhq/digger/libs/storage" + + comment_updater "github.com/diggerhq/digger/libs/comment_utils/summary" "github.com/diggerhq/digger/cli/pkg/digger" "github.com/diggerhq/digger/cli/pkg/github/models" @@ -238,9 +239,13 @@ var githubContextNewPullRequestMinJson = `{ "event": { "action": "opened", "number": 11, - "repository": { - "default_branch": "main" - }, + "repository": { + "default_branch": "main", + "full_name": "diggerhq/digger_demo" + }, + "sender": { + "login": "test-user" + }, "pull_request": { "active_lock_reason": null, "number": 11, diff --git a/libs/ci/github/github.go b/libs/ci/github/github.go index 26d8388ca..8fcc3b46f 100644 --- a/libs/ci/github/github.go +++ b/libs/ci/github/github.go @@ -334,23 +334,23 @@ func (svc GithubService) CreateCheckRun(name string, status string, conclusion s ctx := context.Background() checkRun, resp, err := client.Checks.CreateCheckRun(ctx, owner, repoName, opts) - + // Log rate limit information if resp != nil { limit := resp.Header.Get("X-RateLimit-Limit") remaining := resp.Header.Get("X-RateLimit-Remaining") reset := resp.Header.Get("X-RateLimit-Reset") - + if limit != "" && remaining != "" { limitInt, _ := strconv.Atoi(limit) remainingInt, _ := strconv.Atoi(remaining) - + // Calculate percentage remaining var percentRemaining float64 if limitInt > 0 { percentRemaining = (float64(remainingInt) / float64(limitInt)) * 100 } - + // Log based on severity if remainingInt == 0 { slog.Error("GitHub API rate limit EXHAUSTED", @@ -381,17 +381,17 @@ func (svc GithubService) CreateCheckRun(name string, status string, conclusion s } } } - + return checkRun, err } type GithubCheckRunUpdateOptions struct { - Status *string + Status *string Conclusion *string - Title *string - Summary *string - Text *string - Actions []*github.CheckRunAction + Title *string + Summary *string + Text *string + Actions []*github.CheckRunAction } func (svc GithubService) UpdateCheckRun(checkRunId string, options GithubCheckRunUpdateOptions) (*github.CheckRun, error) { @@ -477,8 +477,8 @@ func (svc GithubService) UpdateCheckRun(checkRunId string, options GithubCheckRu } opts := github.UpdateCheckRunOptions{ - Name: *existingCheckRun.Name, - Output: output, + Name: *existingCheckRun.Name, + Output: output, Actions: newActions, } @@ -490,7 +490,55 @@ func (svc GithubService) UpdateCheckRun(checkRunId string, options GithubCheckRu opts.Conclusion = github.String(*conclusion) } - checkRun, _, err := client.Checks.UpdateCheckRun(ctx, owner, repoName, checkRunIdInt64, opts) + checkRun, resp, err := client.Checks.UpdateCheckRun(ctx, owner, repoName, checkRunIdInt64, opts) + + // Log rate limit information + if resp != nil { + limit := resp.Header.Get("X-RateLimit-Limit") + remaining := resp.Header.Get("X-RateLimit-Remaining") + reset := resp.Header.Get("X-RateLimit-Reset") + + if limit != "" && remaining != "" { + limitInt, _ := strconv.Atoi(limit) + remainingInt, _ := strconv.Atoi(remaining) + + // Calculate percentage remaining + var percentRemaining float64 + if limitInt > 0 { + percentRemaining = (float64(remainingInt) / float64(limitInt)) * 100 + } + + // Log based on severity + if remainingInt == 0 { + slog.Error("GitHub API rate limit EXHAUSTED", + "operation", "UpdateCheckRun", + "checkRunId", checkRunId, + "limit", limit, + "remaining", remaining, + "reset", reset, + "owner", owner, + "repo", repoName) + } else if percentRemaining < 20 { + slog.Warn("GitHub API rate limit getting LOW", + "operation", "UpdateCheckRun", + "checkRunId", checkRunId, + "limit", limit, + "remaining", remaining, + "percentRemaining", fmt.Sprintf("%.1f%%", percentRemaining), + "reset", reset, + "owner", owner, + "repo", repoName) + } else { + slog.Debug("GitHub API rate limit status", + "operation", "UpdateCheckRun", + "checkRunId", checkRunId, + "limit", limit, + "remaining", remaining, + "percentRemaining", fmt.Sprintf("%.1f%%", percentRemaining)) + } + } + } + if err != nil { slog.Error("Failed to update check run", "inputCheckRunId", checkRunId, @@ -691,12 +739,53 @@ func (svc GithubService) CheckBranchExists(branchName string) (bool, error) { return true, nil } +// getStringValue safely dereferences a string pointer, returning empty string if nil +func getStringValue(s *string) string { + if s == nil { + return "" + } + return *s +} + +// getWorkflowCommands safely retrieves workflow commands, returning empty slice if configuration is nil +func getWorkflowCommands(config *digger_config.WorkflowConfiguration, commandType string) []string { + if config == nil { + return []string{} + } + + switch commandType { + case "OnCommitToDefault": + return config.OnCommitToDefault + case "OnPullRequestPushed": + return config.OnPullRequestPushed + case "OnPullRequestClosed": + return config.OnPullRequestClosed + case "OnPullRequestConvertedToDraft": + return config.OnPullRequestConvertedToDraft + default: + return []string{} + } +} + func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impactedProjects []digger_config.Project, requestedProject *digger_config.Project, config digger_config.DiggerConfig, performEnvVarInterpolation bool) ([]scheduler.Job, bool, error) { workflows := config.Workflows jobs := make([]scheduler.Job, 0) - defaultBranch := *payload.Repo.DefaultBranch - prBranch := payload.PullRequest.Head.GetRef() + if payload == nil || payload.Repo == nil || payload.PullRequest == nil { + return nil, false, fmt.Errorf("invalid payload: missing required fields") + } + + var defaultBranch string + if payload.Repo.DefaultBranch != nil { + defaultBranch = *payload.Repo.DefaultBranch + } else { + defaultBranch = "main" // fallback default + } + + var prBranch string + if payload.PullRequest.Head != nil { + prBranch = payload.PullRequest.Head.GetRef() + } coversAllImpactedProjects := true @@ -717,7 +806,13 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac runEnvVars := generic.GetRunEnvVars(defaultBranch, prBranch, project.Name, project.Dir) stateEnvVars, commandEnvVars := digger_config.CollectTerraformEnvConfig(workflow.EnvVars, performEnvVarInterpolation) - pullRequestNumber := payload.PullRequest.Number + var pullRequestNumber *int + if payload.PullRequest.Number != nil { + pullRequestNumber = payload.PullRequest.Number + } else { + defaultPRNumber := 0 + pullRequestNumber = &defaultPRNumber + } stateRole, cmdRole := "", "" if project.AwsRoleToAssume != nil { @@ -731,11 +826,23 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac } StateEnvProvider, CommandEnvProvider := scheduler.GetStateAndCommandProviders(project) - if *payload.Action == "closed" && *payload.PullRequest.Merged && *(payload.PullRequest.Base).Ref == *(payload.Repo).DefaultBranch { + action := getStringValue(payload.Action) + + var isMerged bool + if payload.PullRequest.Merged != nil { + isMerged = *payload.PullRequest.Merged + } + + var baseRef string + if payload.PullRequest.Base != nil && payload.PullRequest.Base.Ref != nil { + baseRef = *payload.PullRequest.Base.Ref + } + + if action == "closed" && isMerged && baseRef == defaultBranch { slog.Info("processing merged PR to default branch", "prNumber", *pullRequestNumber, "project", project.Name, - "action", *payload.Action) + "action", action) jobs = append(jobs, scheduler.Job{ ProjectName: project.Name, @@ -747,7 +854,7 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac Terragrunt: project.Terragrunt, OpenTofu: project.OpenTofu, Pulumi: project.Pulumi, - Commands: workflow.Configuration.OnCommitToDefault, + Commands: getWorkflowCommands(workflow.Configuration, "OnCommitToDefault"), ApplyStage: scheduler.ToConfigStage(workflow.Apply), PlanStage: scheduler.ToConfigStage(workflow.Plan), RunEnvVars: runEnvVars, @@ -755,8 +862,8 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac StateEnvVars: stateEnvVars, PullRequestNumber: pullRequestNumber, EventName: "pull_request", - Namespace: *payload.Repo.FullName, - RequestedBy: *payload.Sender.Login, + Namespace: getStringValue(payload.Repo.FullName), + RequestedBy: getStringValue(payload.Sender.Login), CommandEnvProvider: CommandEnvProvider, CommandRoleArn: cmdRole, StateRoleArn: stateRole, @@ -764,11 +871,11 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac CognitoOidcConfig: project.AwsCognitoOidcConfig, SkipMergeCheck: skipMerge, }) - } else if *payload.Action == "opened" || *payload.Action == "reopened" || *payload.Action == "synchronize" { + } else if action == "opened" || action == "reopened" || action == "synchronize" { slog.Info("processing PR update", "prNumber", *pullRequestNumber, "project", project.Name, - "action", *payload.Action) + "action", action) jobs = append(jobs, scheduler.Job{ ProjectName: project.Name, @@ -780,7 +887,7 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac Terragrunt: project.Terragrunt, OpenTofu: project.OpenTofu, Pulumi: project.Pulumi, - Commands: workflow.Configuration.OnPullRequestPushed, + Commands: getWorkflowCommands(workflow.Configuration, "OnPullRequestPushed"), ApplyStage: scheduler.ToConfigStage(workflow.Apply), PlanStage: scheduler.ToConfigStage(workflow.Plan), RunEnvVars: runEnvVars, @@ -788,8 +895,8 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac StateEnvVars: stateEnvVars, PullRequestNumber: pullRequestNumber, EventName: "pull_request", - Namespace: *payload.Repo.FullName, - RequestedBy: *payload.Sender.Login, + Namespace: getStringValue(payload.Repo.FullName), + RequestedBy: getStringValue(payload.Sender.Login), CommandEnvProvider: CommandEnvProvider, CommandRoleArn: cmdRole, StateRoleArn: stateRole, @@ -797,7 +904,7 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac CognitoOidcConfig: project.AwsCognitoOidcConfig, SkipMergeCheck: skipMerge, }) - } else if *payload.Action == "closed" { + } else if action == "closed" { slog.Info("processing PR closed", "prNumber", *pullRequestNumber, "project", project.Name) @@ -812,7 +919,7 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac Terragrunt: project.Terragrunt, OpenTofu: project.OpenTofu, Pulumi: project.Pulumi, - Commands: workflow.Configuration.OnPullRequestClosed, + Commands: getWorkflowCommands(workflow.Configuration, "OnPullRequestClosed"), ApplyStage: scheduler.ToConfigStage(workflow.Apply), PlanStage: scheduler.ToConfigStage(workflow.Plan), RunEnvVars: runEnvVars, @@ -820,8 +927,8 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac StateEnvVars: stateEnvVars, PullRequestNumber: pullRequestNumber, EventName: "pull_request", - Namespace: *payload.Repo.FullName, - RequestedBy: *payload.Sender.Login, + Namespace: getStringValue(payload.Repo.FullName), + RequestedBy: getStringValue(payload.Sender.Login), CommandEnvProvider: CommandEnvProvider, CommandRoleArn: cmdRole, StateRoleArn: stateRole, @@ -829,12 +936,12 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac CognitoOidcConfig: project.AwsCognitoOidcConfig, SkipMergeCheck: skipMerge, }) - } else if *payload.Action == "converted_to_draft" { + } else if action == "converted_to_draft" { var commands []string - if config.AllowDraftPRs == false && len(workflow.Configuration.OnPullRequestConvertedToDraft) == 0 { + if config.AllowDraftPRs == false && len(getWorkflowCommands(workflow.Configuration, "OnPullRequestConvertedToDraft")) == 0 { commands = []string{"digger unlock"} } else { - commands = workflow.Configuration.OnPullRequestConvertedToDraft + commands = getWorkflowCommands(workflow.Configuration, "OnPullRequestConvertedToDraft") } slog.Info("processing PR converted to draft", @@ -860,8 +967,8 @@ func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impac StateEnvVars: stateEnvVars, PullRequestNumber: pullRequestNumber, EventName: "pull_request_converted_to_draft", - Namespace: *payload.Repo.FullName, - RequestedBy: *payload.Sender.Login, + Namespace: getStringValue(payload.Repo.FullName), + RequestedBy: getStringValue(payload.Sender.Login), CommandEnvProvider: CommandEnvProvider, CommandRoleArn: cmdRole, StateRoleArn: stateRole, diff --git a/libs/digger_config/converters.go b/libs/digger_config/converters.go index 20a749190..387cfd28d 100644 --- a/libs/digger_config/converters.go +++ b/libs/digger_config/converters.go @@ -158,11 +158,13 @@ func copyStage(stage *StageYaml) *Stage { func copyWorkflowConfiguration(config *WorkflowConfigurationYaml) *WorkflowConfiguration { result := WorkflowConfiguration{} - result.OnPullRequestClosed = config.OnPullRequestClosed - result.OnPullRequestPushed = config.OnPullRequestPushed - result.OnCommitToDefault = config.OnCommitToDefault - result.OnPullRequestConvertedToDraft = config.OnPullRequestConvertedToDraft - result.SkipMergeCheck = config.SkipMergeCheck + if config != nil { + result.OnPullRequestClosed = config.OnPullRequestClosed + result.OnPullRequestPushed = config.OnPullRequestPushed + result.OnCommitToDefault = config.OnCommitToDefault + result.OnPullRequestConvertedToDraft = config.OnPullRequestConvertedToDraft + result.SkipMergeCheck = config.SkipMergeCheck + } return &result } @@ -193,13 +195,13 @@ func copyWorkflows(workflows map[string]*WorkflowYaml) map[string]Workflow { func copyReporterConfig(r *ReportingConfigYaml) ReporterConfig { if r == nil { return ReporterConfig{ - AiSummary: false, + AiSummary: false, CommentsEnabled: true, } } return ReporterConfig{ - AiSummary: r.AiSummary, + AiSummary: r.AiSummary, CommentsEnabled: r.CommentsEnabled, } diff --git a/libs/digger_config/digger_config.go b/libs/digger_config/digger_config.go index 5e2b8d19c..b13bb319d 100644 --- a/libs/digger_config/digger_config.go +++ b/libs/digger_config/digger_config.go @@ -543,8 +543,9 @@ func LoadDiggerConfigYaml(workingDir string, generateProjects bool, changedFiles } if fileName == "" { - slog.Error("digger config file not found", "workingDir", workingDir) - return nil, nil, fmt.Errorf("could not find digger.yml or digger.yaml in root of repository") + slog.Info("no digger config file found, using default empty configuration", "workingDir", workingDir) + // return empty configuration when no config file exists + configYaml = &DiggerConfigYaml{} } else { slog.Debug("reading digger config file", "fileName", fileName) data, err := os.ReadFile(fileName) diff --git a/libs/digger_config/digger_config_test.go b/libs/digger_config/digger_config_test.go index 5c26d0012..b63227aae 100644 --- a/libs/digger_config/digger_config_test.go +++ b/libs/digger_config/digger_config_test.go @@ -99,8 +99,27 @@ func TestNoDiggerYaml(t *testing.T) { defer deleteFile() os.Chdir(tempDir) - _, _, _, _, err := LoadDiggerConfig("./", true, nil, nil) - assert.Error(t, err, "expected error since digger.yml and digger.yaml is missing") + config, configYaml, _, _, err := LoadDiggerConfig("./", true, nil, nil) + assert.NoError(t, err, "should handle missing digger.yml gracefully") + assert.NotNil(t, config, "config should not be nil") + assert.NotNil(t, configYaml, "configYaml should not be nil") + assert.Equal(t, 0, len(config.Projects), "should have no projects when no config file exists") +} + +func TestNoDiggerYamlWithoutProjectGeneration(t *testing.T) { + tempDir, teardown := setUp() + defer teardown() + + terraformFile := "" + deleteFile := createFile(path.Join(tempDir, "main.tf"), terraformFile) + defer deleteFile() + + os.Chdir(tempDir) + config, configYaml, _, _, err := LoadDiggerConfig("./", false, nil, nil) + assert.NoError(t, err, "should handle missing digger.yml gracefully even without project generation") + assert.NotNil(t, config, "config should not be nil") + assert.NotNil(t, configYaml, "configYaml should not be nil") + assert.Equal(t, 0, len(config.Projects), "should have no projects when no config file exists") } func TestDefaultDiggerConfig(t *testing.T) {