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
12 changes: 1 addition & 11 deletions .github/workflows/ai-moderator.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/workflows/ai-moderator.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ safe-outputs:
max: 5
allowed-reasons: [spam]
threat-detection: false
checkout: false
---

# AI Moderator
Expand Down
7 changes: 6 additions & 1 deletion pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7718,7 +7718,7 @@
"additionalProperties": false
},
"checkout": {
"description": "Checkout configuration for the agent job. Controls how actions/checkout is invoked. Can be a single checkout configuration or an array for multiple checkouts.",
"description": "Checkout configuration for the agent job. Controls how actions/checkout is invoked. Can be a single checkout configuration, an array for multiple checkouts, or false to disable the default checkout step entirely (dev-mode checkouts are unaffected).",
"oneOf": [
{
"$ref": "#/$defs/checkoutConfig",
Expand All @@ -7730,6 +7730,11 @@
"items": {
"$ref": "#/$defs/checkoutConfig"
}
},
{
"type": "boolean",
"enum": [false],
"description": "Set to false to disable the default checkout step. The agent job will not check out any repository (dev-mode checkouts are unaffected)."
}
]
},
Expand Down
103 changes: 103 additions & 0 deletions pkg/workflow/checkout_disabled_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//go:build integration

package workflow

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/github/gh-aw/pkg/stringutil"
"github.com/github/gh-aw/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCheckoutDisabled(t *testing.T) {
tests := []struct {
name string
frontmatter string
expectedHasDefaultCheckout bool
expectedHasDevModeCheckout bool
description string
}{
{
name: "checkout: false disables agent job default checkout",
frontmatter: `---
on:
issues:
types: [opened]
permissions:
contents: read
issues: read
pull-requests: read
tools:
github:
toolsets: [issues]
engine: claude
checkout: false
strict: false
---`,
expectedHasDefaultCheckout: false,
expectedHasDevModeCheckout: true,
description: "checkout: false should disable the default repository checkout step but leave dev-mode checkouts intact",
},
{
name: "checkout absent still adds checkout by default",
frontmatter: `---
on:
issues:
types: [opened]
permissions:
contents: read
issues: read
pull-requests: read
tools:
github:
toolsets: [issues]
engine: claude
strict: false
---`,
expectedHasDefaultCheckout: true,
expectedHasDevModeCheckout: true,
description: "When checkout is not set, the default checkout step is included",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := testutil.TempDir(t, "checkout-disabled-test")

testContent := tt.frontmatter + "\n\n# Test Workflow\n\nThis is a test workflow.\n"
testFile := filepath.Join(tmpDir, "test-workflow.md")
require.NoError(t, os.WriteFile(testFile, []byte(testContent), 0644), "should write test file")

compiler := NewCompiler()
compiler.SetActionMode(ActionModeDev)
require.NoError(t, compiler.CompileWorkflow(testFile), "should compile workflow")

lockFile := stringutil.MarkdownToLockFile(testFile)
lockContent, err := os.ReadFile(lockFile)
require.NoError(t, err, "should read lock file")

lockContentStr := string(lockContent)

// Find the agent job section
agentJobStart := strings.Index(lockContentStr, "\n agent:")
require.NotEqual(t, -1, agentJobStart, "agent job not found in compiled workflow")

// Extract from agent job to end
agentSection := lockContentStr[agentJobStart:]

// The default workspace checkout is identified by "name: Checkout repository"
hasDefaultCheckout := strings.Contains(agentSection, "name: Checkout repository")

// Dev-mode checkouts (e.g. "Checkout actions folder") should always be present
hasDevModeCheckout := strings.Contains(agentSection, "name: Checkout actions folder")

Comment on lines +86 to +98
assert.Equal(t, tt.expectedHasDefaultCheckout, hasDefaultCheckout, "%s: default checkout presence mismatch", tt.description)
assert.Equal(t, tt.expectedHasDevModeCheckout, hasDevModeCheckout, "%s: dev-mode checkout should not be affected", tt.description)
})
}
}
7 changes: 7 additions & 0 deletions pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,9 +746,16 @@ func (c *Compiler) buildCustomJobs(data *WorkflowData, activationJobCreated bool
//
// The checkout step is only skipped when:
// - Custom steps already contain a checkout action
// - checkout: false is set in the workflow frontmatter
//
// Otherwise, checkout is always added to ensure the agent has access to the repository.
func (c *Compiler) shouldAddCheckoutStep(data *WorkflowData) bool {
// If checkout was explicitly disabled via checkout: false, skip it
if data.CheckoutDisabled {
log.Print("Skipping checkout step: checkout disabled via checkout: false")
return false
}

// If custom steps already contain checkout, don't add another one
if data.CustomSteps != "" && ContainsCheckout(data.CustomSteps) {
log.Print("Skipping checkout step: custom steps already contain checkout")
Expand Down
5 changes: 4 additions & 1 deletion pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,11 @@ func (c *Compiler) buildInitialWorkflowData(
// (e.g. due to unrecognised tool config shapes like bash: ["*"]).
if toolsResult.parsedFrontmatter != nil {
workflowData.CheckoutConfigs = toolsResult.parsedFrontmatter.CheckoutConfigs
workflowData.CheckoutDisabled = toolsResult.parsedFrontmatter.CheckoutDisabled
} else if rawCheckout, ok := result.Frontmatter["checkout"]; ok {
if configs, err := ParseCheckoutConfigs(rawCheckout); err == nil {
if checkoutValue, ok := rawCheckout.(bool); ok && !checkoutValue {
workflowData.CheckoutDisabled = true
} else if configs, err := ParseCheckoutConfigs(rawCheckout); err == nil {
workflowData.CheckoutConfigs = configs
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ type WorkflowData struct {
HasExplicitGitHubTool bool // true if tools.github was explicitly configured in frontmatter
InlinedImports bool // if true, inline all imports at compile time (from inlined-imports frontmatter field)
CheckoutConfigs []*CheckoutConfig // user-configured checkout settings from frontmatter
CheckoutDisabled bool // true when checkout: false is set in frontmatter
HasDispatchItemNumber bool // true when workflow_dispatch has item_number input (generated by label trigger shorthand)
ConcurrencyJobDiscriminator string // optional discriminator expression appended to job-level concurrency groups (from concurrency.job-discriminator)
IsDetectionRun bool // true when this WorkflowData is used for inline threat detection (not the main agent run)
Expand Down
21 changes: 14 additions & 7 deletions pkg/workflow/frontmatter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ type FrontmatterConfig struct {
// Checkout configuration for the agent job.
// Controls how actions/checkout is invoked.
// Can be a single CheckoutConfig object or an array of CheckoutConfig objects.
Checkout any `json:"checkout,omitempty"` // Raw value (object or array)
CheckoutConfigs []*CheckoutConfig `json:"-"` // Parsed checkout configs (not in JSON)
// Set to false to disable the default checkout step entirely.
Checkout any `json:"checkout,omitempty"` // Raw value (object, array, or false)
CheckoutConfigs []*CheckoutConfig `json:"-"` // Parsed checkout configs (not in JSON)
CheckoutDisabled bool `json:"-"` // true when checkout: false is set in frontmatter
}

// ParseFrontmatterConfig creates a FrontmatterConfig from a raw frontmatter map
Expand Down Expand Up @@ -231,12 +233,17 @@ func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, err
}
}

// Parse checkout field - supports single object or array of objects
// Parse checkout field - supports single object, array of objects, or false to disable
if config.Checkout != nil {
checkoutConfigs, err := ParseCheckoutConfigs(config.Checkout)
if err == nil {
config.CheckoutConfigs = checkoutConfigs
frontmatterTypesLog.Printf("Parsed checkout config: %d entries", len(checkoutConfigs))
if checkoutValue, ok := config.Checkout.(bool); ok && !checkoutValue {
config.CheckoutDisabled = true
frontmatterTypesLog.Print("Checkout disabled via checkout: false")
} else {
checkoutConfigs, err := ParseCheckoutConfigs(config.Checkout)
if err == nil {
config.CheckoutConfigs = checkoutConfigs
frontmatterTypesLog.Printf("Parsed checkout config: %d entries", len(checkoutConfigs))
}
}
}

Expand Down
59 changes: 59 additions & 0 deletions pkg/workflow/frontmatter_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1390,3 +1390,62 @@ func TestRuntimesConfigToMapWithIfCondition(t *testing.T) {
t.Error("node should not have if condition in map")
}
}

func TestParseFrontmatterConfigCheckoutDisabled(t *testing.T) {
t.Run("checkout: false sets CheckoutDisabled", func(t *testing.T) {
frontmatter := map[string]any{
"name": "test-workflow",
"engine": "claude",
"checkout": false,
}

config, err := ParseFrontmatterConfig(frontmatter)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !config.CheckoutDisabled {
t.Error("CheckoutDisabled should be true when checkout: false is set")
}
if len(config.CheckoutConfigs) != 0 {
t.Errorf("CheckoutConfigs should be empty when checkout: false is set, got %d entries", len(config.CheckoutConfigs))
}
})

t.Run("checkout: true is not a valid value (treated as object)", func(t *testing.T) {
frontmatter := map[string]any{
"name": "test-workflow",
"engine": "claude",
"checkout": true,
}

config, err := ParseFrontmatterConfig(frontmatter)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// checkout: true is not recognized as a disable flag; CheckoutDisabled stays false
if config.CheckoutDisabled {
t.Error("CheckoutDisabled should be false when checkout: true is set")
}
Comment on lines +1414 to +1428
})

t.Run("checkout object leaves CheckoutDisabled false", func(t *testing.T) {
frontmatter := map[string]any{
"name": "test-workflow",
"engine": "claude",
"checkout": map[string]any{
"fetch-depth": 0,
},
}

config, err := ParseFrontmatterConfig(frontmatter)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if config.CheckoutDisabled {
t.Error("CheckoutDisabled should be false when checkout is an object")
}
if len(config.CheckoutConfigs) == 0 {
t.Error("CheckoutConfigs should be populated when checkout is an object")
}
})
}
Loading