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
22 changes: 18 additions & 4 deletions pkg/workflow/maintenance_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func GenerateMaintenanceWorkflow(ctx context.Context, opts GenerateMaintenanceWo
runsOnValue := FormatRunsOn(configuredRunsOn, defaultRunsOn)

// Scan workflows for expires fields and track the minimum expires value
hasExpires, minExpires := scanWorkflowsForExpires(workflowDataList)
hasExpires, minExpires, triggerReason := scanWorkflowsForExpires(workflowDataList)

// Get the setup action reference (local or remote based on mode).
// Use the first available WorkflowData's ActionResolver to enable SHA pinning.
Expand Down Expand Up @@ -210,6 +210,7 @@ func GenerateMaintenanceWorkflow(ctx context.Context, opts GenerateMaintenanceWo
})
}

maintenanceLog.Printf("Maintenance workflow generation triggered: %s", triggerReason)
maintenanceLog.Printf("Generating maintenance workflow for expired discussions, issues, and pull requests (minimum expires: %d hours)", minExpires)

// Convert hours to days for cron schedule generation
Expand Down Expand Up @@ -338,10 +339,19 @@ func allCopilotWorkflowsUseOrgBilling(workflowDataList []*WorkflowData) bool {
}

// scanWorkflowsForExpires checks all workflow data for expires fields and returns
// whether any expires fields are set and the minimum expires value in hours.
func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
// whether any expires fields are set, the minimum expires value in hours, and the
// first reason that triggered maintenance workflow generation.
func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int, string) {
hasExpires := false
minExpires := 0 // Track minimum expires value in hours
triggerReason := ""

setTriggerReason := func(reason string) {
if triggerReason == "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decoupled invariant: setTriggerReason no longer owns hasExpires, creating an implicit call-site contract — any future check block that calls setTriggerReason without first setting hasExpires = true will silently return an inconsistent state (hasExpires=false with a non-empty triggerReason, or vice versa).

💡 Suggested fix: consolidate `hasExpires` back into the closure

The original closure atomically managed both variables. Keeping that invariant while still separating the two guards is straightforward:

setTriggerReason := func(reason string) {
    hasExpires = true
    if triggerReason == "" {
        triggerReason = reason
        maintenanceLog.Printf("Maintenance workflow became required: %s", reason)
    }
}

This restores a single source of truth for hasExpires and eliminates the 4 duplicated hasExpires = true assignments at the call sites. Every future check block automatically sets the flag by calling setTriggerReason, with no extra step to remember.

triggerReason = reason
maintenanceLog.Printf("Maintenance workflow became required: %s", reason)
}
Comment on lines +349 to +353
}

for _, workflowData := range workflowDataList {
if workflowData == nil || workflowData.SafeOutputs == nil {
Expand All @@ -352,6 +362,7 @@ func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
if workflowData.SafeOutputs.CreateDiscussions.Expires > 0 {
hasExpires = true
expires := workflowData.SafeOutputs.CreateDiscussions.Expires
setTriggerReason(fmt.Sprintf("workflow %q sets safe_outputs.create_discussions.expires=%dh", workflowData.Name, expires))
maintenanceLog.Printf("Workflow %s has expires field set to %d hours for discussions", workflowData.Name, expires)
if minExpires == 0 || expires < minExpires {
minExpires = expires
Expand All @@ -363,6 +374,7 @@ func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
if workflowData.SafeOutputs.CreateIssues.Expires > 0 {
hasExpires = true
expires := workflowData.SafeOutputs.CreateIssues.Expires
setTriggerReason(fmt.Sprintf("workflow %q sets safe_outputs.create_issues.expires=%dh", workflowData.Name, expires))
maintenanceLog.Printf("Workflow %s has expires field set to %d hours for issues", workflowData.Name, expires)
if minExpires == 0 || expires < minExpires {
minExpires = expires
Expand All @@ -374,6 +386,7 @@ func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
if workflowData.SafeOutputs.CreatePullRequests.Expires > 0 {
hasExpires = true
expires := workflowData.SafeOutputs.CreatePullRequests.Expires
setTriggerReason(fmt.Sprintf("workflow %q sets safe_outputs.create_pull_requests.expires=%dh", workflowData.Name, expires))
maintenanceLog.Printf("Workflow %s has expires field set to %d hours for pull requests", workflowData.Name, expires)
if minExpires == 0 || expires < minExpires {
minExpires = expires
Expand All @@ -385,6 +398,7 @@ func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
if isNoOpReportAsIssueEnabled(workflowData.SafeOutputs.NoOp.ReportAsIssue) {
hasExpires = true
expires := defaultNoOpIssueExpirationHours
setTriggerReason(fmt.Sprintf("workflow %q enables no-op issue reporting (default expiration %dh)", workflowData.Name, expires))
maintenanceLog.Printf("Workflow %s has no-op report-as-issue enabled, using %d-hour no-op issue expiration", workflowData.Name, expires)
if minExpires == 0 || expires < minExpires {
minExpires = expires
Expand All @@ -393,5 +407,5 @@ func scanWorkflowsForExpires(workflowDataList []*WorkflowData) (bool, int) {
}
}

return hasExpires, minExpires
return hasExpires, minExpires, triggerReason
}
40 changes: 40 additions & 0 deletions pkg/workflow/maintenance_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,46 @@ func TestGenerateMaintenanceWorkflow_WithExpires(t *testing.T) {
}
}

func TestScanWorkflowsForExpires_TriggerReason(t *testing.T) {
t.Run("no triggers", func(t *testing.T) {
hasExpires, minExpires, triggerReason := scanWorkflowsForExpires([]*WorkflowData{
{
Name: "no-safe-outputs",
SafeOutputs: nil,
},
})
require.False(t, hasExpires)
require.Equal(t, 0, minExpires)
require.Empty(t, triggerReason)
})

t.Run("captures first trigger reason", func(t *testing.T) {
hasExpires, minExpires, triggerReason := scanWorkflowsForExpires([]*WorkflowData{
{
Name: "first-trigger",
SafeOutputs: &SafeOutputsConfig{
CreateIssues: &CreateIssuesConfig{
Expires: 72,
},
},
},
{
Name: "second-trigger",
SafeOutputs: &SafeOutputsConfig{
CreateDiscussions: &CreateDiscussionsConfig{
Expires: 24,
},
},
},
})
require.True(t, hasExpires)
require.Equal(t, 24, minExpires)
require.Contains(t, triggerReason, "first-trigger")
require.Contains(t, triggerReason, "safe_outputs.create_issues.expires=72h")
require.NotContains(t, triggerReason, "second-trigger")
})
}

func TestGenerateMaintenanceWorkflow_CreatesWorkflowDirRecursively(t *testing.T) {
tmpDir := t.TempDir()
workflowDir := filepath.Join(tmpDir, "nested", "workflows")
Expand Down
Loading