Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Disable setup-uv Actions caching
Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
  • Loading branch information
Copilot and SivaKesava1 authored Oct 8, 2026
commit 7a8dc00ee3bccdfcff69b96f25827989fb69ac81
5 changes: 4 additions & 1 deletion pkg/workflow/compiler_yaml_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
}

// Phase 2: Runtime detection, custom steps, and workspace setup
customStepsContainCheckout := c.generateRuntimeAndWorkspaceSetupSteps(yaml, data, needsCheckout)
customStepsContainCheckout, err := c.generateRuntimeAndWorkspaceSetupSteps(yaml, data, needsCheckout)
if err != nil {
return err
}
needsGitConfig := needsCheckout || customStepsContainCheckout

// Phase 3: Engine installation, MCP setup, and pre-agent preparation
Expand Down
40 changes: 32 additions & 8 deletions pkg/workflow/compiler_yaml_runtime_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
Expand All @@ -19,10 +20,13 @@ import (
// turn.
// It mutates data.CustomSteps (via deduplication) and returns whether the custom steps
// themselves contain a checkout action (used by the caller to compute needsGitConfig).
func (c *Compiler) generateRuntimeAndWorkspaceSetupSteps(yaml *strings.Builder, data *WorkflowData, needsCheckout bool) bool {
runtimeSetupSteps, customStepsContainCheckout := c.prepareRuntimeSetupAndCheckoutInfo(data)
compilerYamlLog.Printf("Custom steps contain checkout: %t (len(customSteps)=%d)", customStepsContainCheckout, len(data.CustomSteps))
func (c *Compiler) generateRuntimeAndWorkspaceSetupSteps(yaml *strings.Builder, data *WorkflowData, needsCheckout bool) (bool, error) {
runtimeSetupSteps, customStepsContainCheckout, err := c.prepareRuntimeSetupAndCheckoutInfo(data)
if err != nil {
return false, err
}

compilerYamlLog.Printf("Custom steps contain checkout: %t (len(customSteps)=%d)", customStepsContainCheckout, len(data.CustomSteps))
c.emitRuntimeSetupPrelude(yaml, data, needsCheckout, customStepsContainCheckout, runtimeSetupSteps)

// Create /tmp/gh-aw/ base directory for all temporary files
Expand All @@ -37,8 +41,7 @@ func (c *Compiler) generateRuntimeAndWorkspaceSetupSteps(yaml *strings.Builder,
// 3. For GHE/GHES: authenticates gh CLI with the enterprise host and sets
// GH_HOST=<host> in GITHUB_ENV so every subsequent step in this job
// picks up the correct host without manual per-step configuration.
// Must run after the setup action (so the script is available at ${RUNNER_TEMP}/gh-aw/actions/)
// and before any custom steps that invoke gh CLI commands.
// Run after setup so its script is available, before custom steps that invoke gh CLI commands.
yaml.WriteString(" - name: Configure gh CLI for GitHub Enterprise\n")
yaml.WriteString(" run: bash \"${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh\"\n")
yaml.WriteString(" env:\n")
Expand Down Expand Up @@ -77,7 +80,7 @@ func (c *Compiler) generateRuntimeAndWorkspaceSetupSteps(yaml *strings.Builder,
compilerYamlLog.Printf("Generating cache steps for workflow")
generateCacheSteps(yaml, data, c.verbose)

return customStepsContainCheckout
return customStepsContainCheckout, nil
}

func (c *Compiler) generateWorkQueueIntentOriginStep(yaml *strings.Builder, data *WorkflowData) {
Expand All @@ -92,7 +95,7 @@ func (c *Compiler) generateWorkQueueIntentOriginStep(yaml *strings.Builder, data
yaml.WriteString(generateGitHubScriptWithRequire("capture_work_queue_intent_origin.cjs"))
}

func (c *Compiler) prepareRuntimeSetupAndCheckoutInfo(data *WorkflowData) ([]GitHubActionStep, bool) {
func (c *Compiler) prepareRuntimeSetupAndCheckoutInfo(data *WorkflowData) ([]GitHubActionStep, bool, error) {
// Add automatic runtime setup steps if needed
// This detects runtimes from custom steps and MCP configs
runtimeRequirements := detectRuntimeRequirementsCached(data)
Expand All @@ -111,6 +114,10 @@ func (c *Compiler) prepareRuntimeSetupAndCheckoutInfo(data *WorkflowData) ([]Git
}
}

if err := c.validateCustomSetupUVCache(data.CustomSteps); err != nil {
return nil, false, err
}

// Generate runtime setup steps (after filtering out user-customized ones)
runtimeSetupSteps := GenerateRuntimeSetupSteps(runtimeRequirements, data)
compilerYamlLog.Printf("Detected runtime requirements: %d runtimes, %d setup steps", len(runtimeRequirements), len(runtimeSetupSteps))
Expand All @@ -123,7 +130,24 @@ func (c *Compiler) prepareRuntimeSetupAndCheckoutInfo(data *WorkflowData) ([]Git
// sanitization.
customStepsContainCheckout := data.CustomSteps != "" && ContainsCheckout(data.CustomSteps)

return runtimeSetupSteps, customStepsContainCheckout
return runtimeSetupSteps, customStepsContainCheckout, nil
}

func (c *Compiler) validateCustomSetupUVCache(customSteps string) error {
warnings := customSetupUVCacheWarnings(customSteps)
if len(warnings) == 0 {
return nil
}

if c.strictMode {
return errors.New("strict mode: " + strings.Join(warnings, "; "))
}

for _, warning := range warnings {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(warning))
c.IncrementWarningCount()
}
return nil
}

func (c *Compiler) emitRuntimeSetupPrelude(yaml *strings.Builder, data *WorkflowData, needsCheckout bool, customStepsContainCheckout bool, runtimeSetupSteps []GitHubActionStep) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/runtime_deduplication.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ func DeduplicateRuntimeSetupStepsFromCustomSteps(customSteps string, runtimeRequ
if req.Runtime.ID == "go" && (key == "go-version-file" || key == "cache") {
continue
}
if req.Runtime.ID == "uv" && key == "enable-cache" {
continue
}
// Carry over any other fields
req.ExtraFields[key] = value
runtimeDeduplicationLog.Printf(" Capturing extra field from setup step: %s = %v", key, value)
Expand Down
17 changes: 10 additions & 7 deletions pkg/workflow/runtime_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,16 @@ var knownRuntimes = []*Runtime{
ManifestFiles: []string{"Gemfile", "Gemfile.lock"},
},
{
ID: "uv",
Name: "uv",
ActionRepo: "astral-sh/setup-uv",
ActionVersion: "v5",
VersionField: "version",
DefaultVersion: "", // Uses latest
Commands: []string{"uv", "uvx"},
ID: "uv",
Name: "uv",
ActionRepo: "astral-sh/setup-uv",
ActionVersion: "v5",
VersionField: "version",
DefaultVersion: "", // Uses latest
Commands: []string{"uv", "uvx"},
ExtraWithFields: map[string]string{
"enable-cache": "false",
},
ManifestFiles: []string{"pyproject.toml", "uv.lock"},
UnverifiedCreator: true, // astral-sh org is not GitHub-verified on the Marketplace
},
Expand Down
4 changes: 4 additions & 0 deletions pkg/workflow/runtime_step_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func mergeRuntimeWithFields(req *RuntimeRequirement) map[string]string {
allExtraFields[k] = formatYAMLValue(v)
}

if req.Runtime.ID == "uv" {
allExtraFields["enable-cache"] = "false"
}

return allExtraFields
}

Expand Down
148 changes: 148 additions & 0 deletions pkg/workflow/runtime_uv_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package workflow

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerateRuntimeSetupSteps_UVDisablesActionsCache(t *testing.T) {
steps := GenerateRuntimeSetupSteps([]RuntimeRequirement{{
Runtime: findRuntimeByID("uv"),
ExtraFields: map[string]any{
"enable-cache": true,
"python-downloads": false,
},
}}, nil)
require.Len(t, steps, 1)

content := strings.Join(steps[0], "\n")
assert.Equal(t, 1, strings.Count(content, "enable-cache:"))
assert.Contains(t, content, "enable-cache: false")
assert.NotContains(t, content, "enable-cache: true")
assert.Contains(t, content, "python-downloads: false")
}

func TestDeduplicateRuntimeSetupSteps_DoesNotCarryUVCacheSetting(t *testing.T) {
customSteps := `steps:
- name: Setup uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
python-downloads: false
- name: Check uv
run: uv --version`

requirements := []RuntimeRequirement{{
Runtime: findRuntimeByID("uv"),
}}
deduplicatedSteps, filteredRequirements, err := DeduplicateRuntimeSetupStepsFromCustomSteps(customSteps, requirements)
require.NoError(t, err)
require.Len(t, filteredRequirements, 1)
assert.NotContains(t, filteredRequirements[0].ExtraFields, "enable-cache")
assert.Equal(t, false, filteredRequirements[0].ExtraFields["python-downloads"])
assert.NotContains(t, deduplicatedSteps, "Setup uv")

generatedSteps := GenerateRuntimeSetupSteps(filteredRequirements, nil)
require.Len(t, generatedSteps, 1)
content := strings.Join(generatedSteps[0], "\n")
assert.Contains(t, content, "enable-cache: false")
assert.NotContains(t, content, "enable-cache: true")
assert.Contains(t, content, "python-downloads: false")
}

func TestPrepareRuntimeSetupAndCheckoutInfo_UVCacheValidation(t *testing.T) {
tests := []struct {
name string
with string
strictMode bool
expectWarning bool
expectError bool
}{
{
name: "omitted cache setting warns",
with: " version: '0.8.0'\n",
expectWarning: true,
},
{
name: "enabled cache setting warns",
with: " version: '0.8.0'\n enable-cache: true\n",
expectWarning: true,
},
{
name: "omitted cache setting errors in strict mode",
with: " version: '0.8.0'\n",
strictMode: true,
expectError: true,
},
{
name: "enabled cache setting errors in strict mode",
with: " version: '0.8.0'\n enable-cache: true\n",
strictMode: true,
expectError: true,
},
{
name: "disabled cache setting is valid",
with: " version: '0.8.0'\n enable-cache: false\n python-downloads: false\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := &WorkflowData{
CustomSteps: "steps:\n - name: Custom uv setup\n uses: astral-sh/setup-uv@v5\n with:\n" + tt.with,
}
compiler := &Compiler{strictMode: tt.strictMode}

_, _, err := compiler.prepareRuntimeSetupAndCheckoutInfo(data)
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), `step "Custom uv setup"`)
assert.Contains(t, err.Error(), "enable-cache: false")
} else {
require.NoError(t, err)
}

if tt.expectWarning {
assert.Equal(t, 1, compiler.GetWarningCount())
} else {
assert.Zero(t, compiler.GetWarningCount())
}
assert.Contains(t, data.CustomSteps, "version: '0.8.0'")
if tt.name == "disabled cache setting is valid" {
assert.Contains(t, data.CustomSteps, "enable-cache: false")
assert.Contains(t, data.CustomSteps, "python-downloads: false")
}
})
}
}

func TestPrepareRuntimeSetupAndCheckoutInfo_UVCacheValidationWithoutUVCommand(t *testing.T) {
data := &WorkflowData{
CustomSteps: `steps:
- name: Custom uv setup
uses: astral-sh/setup-uv@v5
with:
version: '0.8.0'
`,
}
compiler := &Compiler{}

runtimeSteps, _, err := compiler.prepareRuntimeSetupAndCheckoutInfo(data)
require.NoError(t, err)
assert.Empty(t, runtimeSteps)
assert.Equal(t, 1, compiler.GetWarningCount())
assert.Contains(t, data.CustomSteps, "version: '0.8.0'")
}

func TestCustomSetupUVCacheWarningsAcceptsExplicitFalse(t *testing.T) {
warnings := customSetupUVCacheWarnings(`steps:
- name: Safe uv setup
uses: astral-sh/setup-uv@v5
with:
enable-cache: "false"
`)
assert.Empty(t, warnings)
}
51 changes: 51 additions & 0 deletions pkg/workflow/runtime_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/goccy/go-yaml"
)

var runtimeValidationLog = logger.New("workflow:runtime_validation")
Expand Down Expand Up @@ -308,3 +309,53 @@ func (c *Compiler) validateRuntimePackages(workflowData *WorkflowData) error {
runtimeValidationLog.Print("Runtime package validation passed")
return nil
}

func customSetupUVCacheWarnings(customSteps string) []string {
var stepsWrapper map[string]any
if err := yaml.Unmarshal([]byte(customSteps), &stepsWrapper); err != nil {
return nil
}

steps, ok := stepsWrapper["steps"].([]any)
if !ok {
return nil
}

var warnings []string
for index, stepValue := range steps {
step, ok := stepValue.(map[string]any)
if !ok {
continue
}
uses, ok := step["uses"].(string)
if !ok || !strings.EqualFold(extractActionRepo(uses), "astral-sh/setup-uv") {
continue
}

withFields, hasWithFields := step["with"].(map[string]any)
if value, hasCacheSetting := withFields["enable-cache"]; hasWithFields && hasCacheSetting && isExplicitlyFalse(value) {
continue
}

stepDescription := fmt.Sprintf("step %d", index+1)
if name, ok := step["name"].(string); ok && strings.TrimSpace(name) != "" {
stepDescription = fmt.Sprintf("step %q", name)
}
warnings = append(warnings, fmt.Sprintf(
"custom setup-uv %s does not explicitly disable Actions caching; set `with.enable-cache: false` to disable caching",
stepDescription,
))
}
return warnings
}

func isExplicitlyFalse(value any) bool {
switch value := value.(type) {
case bool:
return !value
case string:
return strings.EqualFold(strings.TrimSpace(value), "false")
default:
return false
}
}
Loading