From e57ed59fdfd60acc2fb1b2f99f228dbb541ea9dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:06:53 +0000 Subject: [PATCH 1/4] Add push trigger on main for .github/workflows/*.md to compile-workflows job in maintenance workflow generator Agent-Logs-Url: https://github.com/github/gh-aw/sessions/520bb413-72d8-43b4-8d6f-9bcc967cedf2 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 11 ++- pkg/workflow/maintenance_conditions.go | 53 +++++++---- pkg/workflow/maintenance_workflow_test.go | 104 +++++++++++++++++++++ pkg/workflow/maintenance_workflow_yaml.go | 20 +++- 4 files changed, 165 insertions(+), 23 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index b3238e040fc..c7df6e632cc 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -35,6 +35,11 @@ name: Agentic Maintenance on: schedule: - cron: "37 */2 * * *" # Every 2 hours (based on minimum expires: 1 days) + push: + branches: + - main + paths: + - '.github/workflows/*.md' workflow_dispatch: inputs: operation: @@ -84,7 +89,7 @@ permissions: {} jobs: close-expired-entities: - if: ${{ (!(github.event.repository.fork)) && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} + if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} runs-on: ubuntu-slim permissions: discussions: write @@ -131,7 +136,7 @@ jobs: await main(); cleanup-cache-memory: - if: ${{ (!(github.event.repository.fork)) && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }} + if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }} runs-on: ubuntu-slim permissions: actions: write @@ -590,7 +595,7 @@ jobs: await main(); secret-validation: - if: ${{ (!(github.event.repository.fork)) && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} + if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} runs-on: ubuntu-slim permissions: contents: read diff --git a/pkg/workflow/maintenance_conditions.go b/pkg/workflow/maintenance_conditions.go index 70f01544113..7c703ac1b90 100644 --- a/pkg/workflow/maintenance_conditions.go +++ b/pkg/workflow/maintenance_conditions.go @@ -33,31 +33,52 @@ func buildNotDispatchOrCallOrEmptyOperation() ConditionNode { ) } -// buildNotForkAndScheduledOrOperation creates a condition for jobs that run on -// schedule (or empty operation) AND when a specific operation is selected. -// Condition: !fork && (not_dispatch_or_call || operation == ” || operation == op) -func buildNotForkAndScheduledOrOperation(operation string) ConditionNode { - maintenanceConditionsLog.Printf("Building not-fork-and-scheduled-or-operation condition: %s", operation) +// buildNotForkAndScheduled creates a condition for jobs that should run on any +// non-dispatch/call event (e.g. schedule, push) or on workflow_dispatch/workflow_call +// with an empty operation, and never on forks. +// Condition: !fork && ((event_name != 'workflow_dispatch' && event_name != 'workflow_call') || operation == ”) +func buildNotForkAndScheduled() ConditionNode { return BuildAnd( buildNotForkCondition(), - BuildOr( - buildNotDispatchOrCallOrEmptyOperation(), - BuildEquals( - BuildPropertyAccess("inputs.operation"), - BuildStringLiteral(operation), + buildNotDispatchOrCallOrEmptyOperation(), + ) +} + +// buildNotForkAndScheduleOnly creates a condition for jobs that should run on schedule +// (or empty dispatch/call) but NOT on push events, and never on forks. +func buildNotForkAndScheduleOnly() ConditionNode { + return BuildAnd( + buildNotForkCondition(), + BuildAnd( + BuildNotEquals( + BuildPropertyAccess("github.event_name"), + BuildStringLiteral("push"), ), + buildNotDispatchOrCallOrEmptyOperation(), ), ) } -// buildNotForkAndScheduled creates a condition for jobs that should run on any -// non-dispatch/call event (e.g. schedule, push) or on workflow_dispatch/workflow_call -// with an empty operation, and never on forks. -// Condition: !fork && ((event_name != 'workflow_dispatch' && event_name != 'workflow_call') || operation == ”) -func buildNotForkAndScheduled() ConditionNode { +// buildNotForkAndScheduleOnlyOrOperation creates a condition for jobs that run on +// schedule (or empty dispatch/call) or when a specific operation is selected, +// but NOT on push events, and never on forks. +func buildNotForkAndScheduleOnlyOrOperation(operation string) ConditionNode { + maintenanceConditionsLog.Printf("Building not-fork-and-schedule-only-or-operation condition: %s", operation) return BuildAnd( buildNotForkCondition(), - buildNotDispatchOrCallOrEmptyOperation(), + BuildAnd( + BuildNotEquals( + BuildPropertyAccess("github.event_name"), + BuildStringLiteral("push"), + ), + BuildOr( + buildNotDispatchOrCallOrEmptyOperation(), + BuildEquals( + BuildPropertyAccess("inputs.operation"), + BuildStringLiteral(operation), + ), + ), + ), ) } diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 51a3991a18e..6b826dc0b47 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -536,6 +536,110 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } } +func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { + workflowDataList := []*WorkflowData{ + { + Name: "test-workflow", + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{ + Expires: 48, + }, + }, + }, + } + + t.Run("dev mode includes push trigger on main for workflow md files", func(t *testing.T) { + tmpDir := t.TempDir() + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + yaml := string(content) + + if !strings.Contains(yaml, " push:") { + t.Error("Dev mode workflow should include push trigger") + } + if !strings.Contains(yaml, " - main") { + t.Error("Dev mode push trigger should target main branch") + } + if !strings.Contains(yaml, " - '.github/workflows/*.md'") { + t.Error("Dev mode push trigger should target .github/workflows/*.md paths") + } + }) + + t.Run("release mode does not include push trigger", func(t *testing.T) { + tmpDir := t.TempDir() + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "", false, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + yaml := string(content) + + if strings.Contains(yaml, " push:") { + t.Error("Release mode workflow should NOT include push trigger") + } + }) + + t.Run("close-expired-entities and secret-validation exclude push events", func(t *testing.T) { + tmpDir := t.TempDir() + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + yaml := string(content) + pushExclusionCondition := "github.event_name != 'push'" + + const jobSectionSearchRange = 300 + scheduleOnlyJobs := []string{"close-expired-entities:", "secret-validation:"} + for _, job := range scheduleOnlyJobs { + jobIdx := strings.Index(yaml, "\n "+job) + if jobIdx == -1 { + t.Errorf("Job %q not found in generated workflow", job) + continue + } + jobSection := yaml[jobIdx : jobIdx+jobSectionSearchRange] + if !strings.Contains(jobSection, pushExclusionCondition) { + t.Errorf("Job %q should exclude push events (%q) but condition is:\n%s", job, pushExclusionCondition, jobSection) + } + } + }) + + t.Run("compile-workflows runs on push events (no push exclusion)", func(t *testing.T) { + tmpDir := t.TempDir() + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + content, err := os.ReadFile(filepath.Join(tmpDir, "agentics-maintenance.yml")) + if err != nil { + t.Fatalf("Expected maintenance workflow to be generated: %v", err) + } + yaml := string(content) + + const jobSectionSearchRange = 300 + compileIdx := strings.Index(yaml, "\n compile-workflows:") + if compileIdx == -1 { + t.Fatal("Job compile-workflows not found in generated workflow") + } + jobSection := yaml[compileIdx : compileIdx+jobSectionSearchRange] + if strings.Contains(jobSection, "github.event_name != 'push'") { + t.Errorf("Job compile-workflows should NOT exclude push events, but condition is:\n%s", jobSection) + } + }) +} + func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { workflowDataList := []*WorkflowData{ { diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index bda1644251e..d6fd11a8e43 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -44,7 +44,19 @@ Schedule frequency is automatically determined by the shortest expiration time.` on: schedule: - cron: "` + cronSchedule + `" # ` + scheduleDesc + ` (based on minimum expires: ` + strconv.Itoa(minExpiresDays) + ` days) - workflow_dispatch: +`) + + // Add push trigger in dev mode so compile-workflows runs when workflow files change + if actionMode == ActionModeDev { + yaml.WriteString(` push: + branches: + - main + paths: + - '.github/workflows/*.md' +`) + } + + yaml.WriteString(` workflow_dispatch: inputs: operation: description: 'Optional maintenance operation to run' @@ -93,7 +105,7 @@ permissions: {} jobs: close-expired-entities: - if: ${{ ` + RenderCondition(buildNotForkAndScheduled()) + ` }} + if: ${{ ` + RenderCondition(buildNotForkAndScheduleOnly()) + ` }} runs-on: ` + runsOnValue + ` permissions: discussions: write @@ -161,7 +173,7 @@ jobs: // Add cleanup-cache-memory job for scheduled runs and clean_cache_memories operation // This job lists all caches starting with "memory-", groups them by key prefix, // keeps the latest run ID per group, and deletes the rest. - cleanupCacheCondition := buildNotForkAndScheduledOrOperation("clean_cache_memories") + cleanupCacheCondition := buildNotForkAndScheduleOnlyOrOperation("clean_cache_memories") yaml.WriteString(` cleanup-cache-memory: if: ${{ ` + RenderCondition(cleanupCacheCondition) + ` }} @@ -646,7 +658,7 @@ jobs: await main(); secret-validation: - if: ${{ ` + RenderCondition(buildNotForkAndScheduled()) + ` }} + if: ${{ ` + RenderCondition(buildNotForkAndScheduleOnly()) + ` }} runs-on: ` + runsOnValue + ` permissions: contents: read From 251618b7e19b9f6b573e26e13a2cb3966b971d88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:13:02 +0000 Subject: [PATCH 2/4] Address code review: clarify comment on buildNotForkAndScheduled, consolidate test constant Agent-Logs-Url: https://github.com/github/gh-aw/sessions/520bb413-72d8-43b4-8d6f-9bcc967cedf2 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/maintenance_conditions.go | 5 +++-- pkg/workflow/maintenance_workflow_test.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/maintenance_conditions.go b/pkg/workflow/maintenance_conditions.go index 7c703ac1b90..176a8a80902 100644 --- a/pkg/workflow/maintenance_conditions.go +++ b/pkg/workflow/maintenance_conditions.go @@ -34,8 +34,9 @@ func buildNotDispatchOrCallOrEmptyOperation() ConditionNode { } // buildNotForkAndScheduled creates a condition for jobs that should run on any -// non-dispatch/call event (e.g. schedule, push) or on workflow_dispatch/workflow_call -// with an empty operation, and never on forks. +// non-dispatch/call event including push, or on workflow_dispatch/workflow_call +// with an empty operation, and never on forks. Unlike buildNotForkAndScheduleOnly, +// this function does NOT exclude push events. // Condition: !fork && ((event_name != 'workflow_dispatch' && event_name != 'workflow_call') || operation == ”) func buildNotForkAndScheduled() ConditionNode { return BuildAnd( diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 6b826dc0b47..be8afa2e7bf 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -537,6 +537,8 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { + const jobSectionSearchRange = 300 + workflowDataList := []*WorkflowData{ { Name: "test-workflow", @@ -601,7 +603,6 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { yaml := string(content) pushExclusionCondition := "github.event_name != 'push'" - const jobSectionSearchRange = 300 scheduleOnlyJobs := []string{"close-expired-entities:", "secret-validation:"} for _, job := range scheduleOnlyJobs { jobIdx := strings.Index(yaml, "\n "+job) @@ -628,7 +629,6 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { } yaml := string(content) - const jobSectionSearchRange = 300 compileIdx := strings.Index(yaml, "\n compile-workflows:") if compileIdx == -1 { t.Fatal("Job compile-workflows not found in generated workflow") From 40b5c2b315d77b24bb32567c150194c6e84cc946 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:30:25 +0000 Subject: [PATCH 3/4] Use GitHub API to determine default branch at compile time for push trigger Agent-Logs-Url: https://github.com/github/gh-aw/sessions/5320a8c7-8f36-45b4-a81f-431583ec8fe6 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_post_processing.go | 2 +- pkg/cli/init.go | 2 +- pkg/workflow/compiler_types.go | 5 ++ pkg/workflow/maintenance_workflow.go | 35 +++++++++- pkg/workflow/maintenance_workflow_test.go | 64 +++++++++++-------- pkg/workflow/maintenance_workflow_yaml.go | 5 +- .../side_repo_maintenance_integration_test.go | 10 +-- 7 files changed, 86 insertions(+), 37 deletions(-) diff --git a/pkg/cli/compile_post_processing.go b/pkg/cli/compile_post_processing.go index 20e7473e0ba..db0f2a477c6 100644 --- a/pkg/cli/compile_post_processing.go +++ b/pkg/cli/compile_post_processing.go @@ -75,7 +75,7 @@ func generateMaintenanceWorkflowWrapper( repoConfig = nil } - if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, compiler.GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig); err != nil { + if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, compiler.GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { if strict { return fmt.Errorf("failed to generate maintenance workflow: %w", err) } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 897a304ef26..1b6b23f9989 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -238,7 +238,7 @@ func ensureMaintenanceWorkflow(verbose bool) error { repoConfig = nil } - if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig); err != nil { + if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { return fmt.Errorf("failed to generate maintenance workflow: %w", err) } diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index f550cfe590a..8e77ecf56cc 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -278,6 +278,11 @@ func (c *Compiler) SetRepositorySlug(slug string) { c.repositorySlug = slug } +// GetRepositorySlug returns the repository slug (owner/repo) set on this compiler instance. +func (c *Compiler) GetRepositorySlug() string { + return c.repositorySlug +} + // GetScheduleWarnings returns all accumulated schedule warnings for this compiler instance func (c *Compiler) GetScheduleWarnings() []string { return c.scheduleWarnings diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index ed521b07873..7e64b7d9074 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" @@ -83,11 +84,37 @@ func getCLICmdPrefix(actionMode ActionMode) string { return "gh aw" } +// FetchDefaultBranch queries the GitHub API to determine the default branch of the +// given repository slug (owner/repo). Returns "main" as a fallback when the slug is +// empty, not in owner/repo format, or when the API call fails. +func FetchDefaultBranch(slug string) string { + const fallback = "main" + if slug == "" || strings.Count(slug, "/") != 1 { + maintenanceLog.Printf("No valid repository slug, using default branch fallback: %s", fallback) + return fallback + } + maintenanceLog.Printf("Fetching default branch for repository: %s", slug) + output, err := RunGH("Fetching default branch...", "api", "/repos/"+slug, "--jq", ".default_branch") + if err != nil { + maintenanceLog.Printf("Failed to fetch default branch for %s: %v, falling back to %s", slug, err, fallback) + return fallback + } + branch := strings.TrimSpace(string(output)) + if branch == "" { + maintenanceLog.Printf("Empty default branch response for %s, falling back to %s", slug, fallback) + return fallback + } + maintenanceLog.Printf("Default branch for %s: %s", slug, branch) + return branch +} + // GenerateMaintenanceWorkflow generates the agentics-maintenance.yml workflow // if any workflows use the expires field for discussions or issues. // When repoConfig is non-nil and repoConfig.MaintenanceDisabled is true the // maintenance workflow is deleted and the function returns immediately. -func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir string, version string, actionMode ActionMode, actionTag string, verbose bool, repoConfig *RepoConfig) error { +// repoSlug is the owner/repo slug used to determine the default branch for the push +// trigger; pass an empty string to fall back to "main". +func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir string, version string, actionMode ActionMode, actionTag string, verbose bool, repoConfig *RepoConfig, repoSlug string) error { maintenanceLog.Print("Checking if maintenance workflow is needed") // Respect explicit opt-out from aw.json: maintenance: false @@ -144,8 +171,12 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s cronSchedule, scheduleDesc := generateMaintenanceCron(minExpiresDays) maintenanceLog.Printf("Maintenance schedule: %s (%s)", cronSchedule, scheduleDesc) + // Fetch the default branch for the push trigger (dev mode only) + // Resolved here to avoid passing it through multiple layers; empty slug falls back to "main" + defaultBranch := FetchDefaultBranch(repoSlug) + // Generate the YAML content for the maintenance workflow - content := buildMaintenanceWorkflowYAML(cronSchedule, scheduleDesc, minExpiresDays, runsOnValue, actionMode, version, actionTag, resolver, configuredRunsOn) + content := buildMaintenanceWorkflowYAML(cronSchedule, scheduleDesc, minExpiresDays, runsOnValue, actionMode, version, actionTag, resolver, configuredRunsOn, defaultBranch) // Write the maintenance workflow file maintenanceFile := filepath.Join(workflowDir, "agentics-maintenance.yml") diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index be8afa2e7bf..8d550b0121a 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -150,7 +150,7 @@ func TestGenerateMaintenanceWorkflow_WithExpires(t *testing.T) { tmpDir := t.TempDir() // Call GenerateMaintenanceWorkflow - err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") // Check error expectation if tt.expectError && err == nil { @@ -239,7 +239,7 @@ func TestGenerateMaintenanceWorkflow_DeletesExistingFile(t *testing.T) { } // Call GenerateMaintenanceWorkflow - err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -271,7 +271,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -552,7 +552,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("dev mode includes push trigger on main for workflow md files", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -566,16 +566,28 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Error("Dev mode workflow should include push trigger") } if !strings.Contains(yaml, " - main") { - t.Error("Dev mode push trigger should target main branch") + t.Error("Dev mode push trigger should target main branch (fallback when slug is empty)") } if !strings.Contains(yaml, " - '.github/workflows/*.md'") { t.Error("Dev mode push trigger should target .github/workflows/*.md paths") } }) + t.Run("dev mode uses custom default branch from buildMaintenanceWorkflowYAML", func(t *testing.T) { + // Call buildMaintenanceWorkflowYAML directly to test the branch substitution + // without needing a live GitHub API call (FetchDefaultBranch falls back to "main" with no slug) + yaml := buildMaintenanceWorkflowYAML("37 */2 * * *", "Every 2 hours", 1, "ubuntu-slim", ActionModeDev, "v1.0.0", "", nil, nil, "develop") + if !strings.Contains(yaml, " - develop") { + t.Errorf("Push trigger should use the provided default branch 'develop', got:\n%s", yaml[:min(500, len(yaml))]) + } + if strings.Contains(yaml, " - main") { + t.Errorf("Push trigger should not contain hardcoded 'main' when 'develop' is specified") + } + }) + t.Run("release mode does not include push trigger", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -592,7 +604,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("close-expired-entities and secret-validation exclude push events", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -619,7 +631,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("compile-workflows runs on push events (no push exclusion)", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -654,7 +666,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { t.Run("release mode with action-tag uses remote ref", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -690,7 +702,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataListWithResolver, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataListWithResolver, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -709,7 +721,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { t.Run("dev mode ignores action-tag and uses local path", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "v0.47.4", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -822,7 +834,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("dev mode run_operation uses build from source", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -841,7 +853,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("release mode run_operation uses setup-cli action not gh extension install", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -863,7 +875,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("dev mode compile_workflows uses same codegen as run_operation", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -911,7 +923,7 @@ func TestGenerateMaintenanceWorkflow_SetupCLISHAPinning(t *testing.T) { cache.Set("github/gh-aw/actions/setup", "v1.0.0", "dddddddddddddddddddddddddddddddddddddddd") resolver := NewActionResolver(cache) - err := GenerateMaintenanceWorkflow(workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -950,7 +962,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{RunsOn: RunsOnValue{"my-custom-runner"}}, } - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg) + err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -973,7 +985,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{RunsOn: RunsOnValue{"self-hosted", "linux"}}, } - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg) + err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -995,7 +1007,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { t.Fatalf("Failed to write pre-existing file: %v", err) } cfg := &RepoConfig{MaintenanceDisabled: true} - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg) + err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1007,7 +1019,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { t.Run("maintenance disabled skips generation even with expires", func(t *testing.T) { tmpDir := t.TempDir() cfg := &RepoConfig{MaintenanceDisabled: true} - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg) + err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1029,7 +1041,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { } cfg := &RepoConfig{MaintenanceDisabled: true} // The function must succeed (no error), even though a warning is printed. - err := GenerateMaintenanceWorkflow(list, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg) + err := GenerateMaintenanceWorkflow(list, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Expected no error when maintenance is disabled with expires, got: %v", err) } @@ -1301,7 +1313,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1355,7 +1367,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1384,7 +1396,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1429,7 +1441,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1458,7 +1470,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1503,7 +1515,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index d6fd11a8e43..f7c6f49d3e3 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -20,8 +20,9 @@ func buildMaintenanceWorkflowYAML( version, actionTag string, resolver ActionSHAResolver, configuredRunsOn RunsOnValue, + defaultBranch string, ) string { - maintenanceWorkflowYAMLLog.Printf("Building maintenance workflow YAML: actionMode=%s minExpiresDays=%d cronSchedule=%q", actionMode, minExpiresDays, cronSchedule) + maintenanceWorkflowYAMLLog.Printf("Building maintenance workflow YAML: actionMode=%s minExpiresDays=%d cronSchedule=%q defaultBranch=%q", actionMode, minExpiresDays, cronSchedule, defaultBranch) var yaml strings.Builder @@ -50,7 +51,7 @@ on: if actionMode == ActionModeDev { yaml.WriteString(` push: branches: - - main + - ` + defaultBranch + ` paths: - '.github/workflows/*.md' `) diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index 7e0080ee35b..1234d422832 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -58,7 +58,7 @@ This workflow operates on a separate repository. workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-my-org-target-repo.yml") @@ -159,7 +159,7 @@ Create issues that expire after 14 days. workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-corp-infra-tools.yml") @@ -209,7 +209,7 @@ checkout: workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-acme-shared-services.yml") @@ -245,7 +245,7 @@ checkout: workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil) + err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") // No side-repo file should be created because the repository is an expression. @@ -304,7 +304,7 @@ safe-outputs: } { t.Run(tc.repo, func(t *testing.T) { wdl, tmpDir := compileSideRepoWorkflow(t, makeContent(tc.repo)) - require.NoError(t, GenerateMaintenanceWorkflow(wdl, tmpDir, "v1.0.0", ActionModeDev, "", false, nil)) + require.NoError(t, GenerateMaintenanceWorkflow(wdl, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "")) slug := sanitizeRepoForFilename(tc.repo) sideFile := filepath.Join(tmpDir, "agentics-maintenance-"+slug+".yml") From 90227ad4dedeb686b8f5d4d2ed16b491bd12baa2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:54:42 +0000 Subject: [PATCH 4/4] Add cancel-in-progress concurrency group to compile-workflows job Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6efcea04-672a-4501-a877-6c14041a8c30 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 3 +++ pkg/workflow/maintenance_workflow_test.go | 8 +++++++- pkg/workflow/maintenance_workflow_yaml.go | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index c7df6e632cc..b243a781cfe 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -557,6 +557,9 @@ jobs: compile-workflows: if: ${{ (!(github.event.repository.fork)) && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} runs-on: ubuntu-slim + concurrency: + group: ${{ github.workflow }}-compile-workflows-${{ github.repository }} + cancel-in-progress: true permissions: contents: read issues: write diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index 8d550b0121a..4c2d37f69ec 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -537,7 +537,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { - const jobSectionSearchRange = 300 + const jobSectionSearchRange = 500 workflowDataList := []*WorkflowData{ { @@ -649,6 +649,12 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { if strings.Contains(jobSection, "github.event_name != 'push'") { t.Errorf("Job compile-workflows should NOT exclude push events, but condition is:\n%s", jobSection) } + if !strings.Contains(jobSection, "cancel-in-progress: true") { + t.Errorf("Job compile-workflows should have cancel-in-progress concurrency, but got:\n%s", jobSection) + } + if !strings.Contains(jobSection, "github.workflow }}-compile-workflows-${{ github.repository") { + t.Errorf("Job compile-workflows should have a scoped concurrency group, but got:\n%s", jobSection) + } }) } diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index f7c6f49d3e3..57eaf5d996c 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -624,6 +624,9 @@ jobs: compile-workflows: if: ${{ ` + RenderCondition(buildNotForkAndScheduled()) + ` }} runs-on: ` + runsOnValue + ` + concurrency: + group: ${{ github.workflow }}-compile-workflows-${{ github.repository }} + cancel-in-progress: true permissions: contents: read issues: write