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
14 changes: 11 additions & 3 deletions .github/workflows/agentics-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -552,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
Expand Down Expand Up @@ -590,7 +598,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
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/compile_post_processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 38 additions & 16 deletions pkg/workflow/maintenance_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,53 @@ 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 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 == ”)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This condition comment uses a typographic closing quote () rather than an ASCII quote, which makes the example hard to read/copy and is inconsistent with the rest of the file. Consider changing it to operation == '' (or operation == "") to match the actual expression being built.

Suggested change
// Condition: !fork && ((event_name != 'workflow_dispatch' && event_name != 'workflow_call') || operation == )
// Condition: !fork && ((event_name != 'workflow_dispatch' && event_name != 'workflow_call') || operation == '')

Copilot uses AI. Check for mistakes.
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),
),
),
),
)
}

Expand Down
35 changes: 33 additions & 2 deletions pkg/workflow/maintenance_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +174 to +176
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

FetchDefaultBranch(repoSlug) is called unconditionally, so release-mode generation will still invoke gh api (or at least attempt to) even though the push trigger is only emitted in dev mode. This adds unnecessary network/auth dependency and can slow or flake non-dev runs. Consider only resolving the default branch when actionMode == ActionModeDev (or when the push trigger will be included), and otherwise pass an empty/default value through to buildMaintenanceWorkflowYAML.

Suggested change
// 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)
// Fetch the default branch only when the dev-mode push trigger will be included.
// Outside dev mode, leave it empty so generation does not depend on gh/api auth or network access.
defaultBranch := ""
if actionMode == ActionModeDev {
defaultBranch = FetchDefaultBranch(repoSlug)
}

Copilot uses AI. Check for mistakes.

// 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")
Expand Down
Loading
Loading