From 39748ac3647a56e3480ef3c71f19d3cb3d7fb845 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:00:31 +0000 Subject: [PATCH 1/5] Initial plan From 621afddcb2a6a5b8a24c04429c1ec12b59345912 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:08:51 +0000 Subject: [PATCH 2/5] Fix actions-lock.json path to use git root instead of cwd Add WithGitRoot compiler option to specify git repository root Update getSharedActionResolver to use gitRoot if provided Update CLI to find and pass git root to compiler Add integration test to verify fix This ensures actions-lock.json is created at /.github/aw/ regardless of current working directory when compiling workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_compiler_setup.go | 11 ++++ pkg/cli/compile_integration_test.go | 91 +++++++++++++++++++++++++++++ pkg/workflow/compiler_types.go | 19 ++++-- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/pkg/cli/compile_compiler_setup.go b/pkg/cli/compile_compiler_setup.go index b8132a9cdf4..9615d04b787 100644 --- a/pkg/cli/compile_compiler_setup.go +++ b/pkg/cli/compile_compiler_setup.go @@ -89,11 +89,22 @@ func createAndConfigureCompiler(config CompileConfig) *workflow.Compiler { } } + // Find git repository root for action cache path + // This ensures actions-lock.json is created at the repo root regardless of CWD + gitRoot, err := findGitRoot() + if err != nil { + compileCompilerSetupLog.Printf("Could not find git root: %v, using current directory", err) + gitRoot = "" // Will fall back to cwd in compiler + } else { + compileCompilerSetupLog.Printf("Using git root for action cache: %s", gitRoot) + } + // Create compiler with auto-detected version and action mode compiler := workflow.NewCompiler( workflow.WithVerbose(config.Verbose), workflow.WithEngineOverride(config.EngineOverride), workflow.WithFailFast(config.FailFast), + workflow.WithGitRoot(gitRoot), ) compileCompilerSetupLog.Print("Created compiler instance") diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index 82d6791edb0..e5cf71e58d4 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -801,3 +801,94 @@ This workflow tests that invalid schedule strings in array format fail compilati t.Logf("Integration test passed - invalid schedule in array format correctly failed compilation\nOutput: %s", outputStr) } + +// TestCompileFromSubdirectoryCreatesActionsLockAtRoot tests that actions-lock.json +// is created at the repository root when compiling from a subdirectory +func TestCompileFromSubdirectoryCreatesActionsLockAtRoot(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + // Initialize git repository (required for git root detection) + gitInitCmd := exec.Command("git", "init") + gitInitCmd.Dir = setup.tempDir + if output, err := gitInitCmd.CombinedOutput(); err != nil { + t.Fatalf("Failed to initialize git repository: %v\nOutput: %s", err, string(output)) + } + + // Configure git user for the repository + gitConfigEmail := exec.Command("git", "config", "user.email", "test@test.com") + gitConfigEmail.Dir = setup.tempDir + if output, err := gitConfigEmail.CombinedOutput(); err != nil { + t.Fatalf("Failed to configure git user email: %v\nOutput: %s", err, string(output)) + } + + gitConfigName := exec.Command("git", "config", "user.name", "Test User") + gitConfigName.Dir = setup.tempDir + if output, err := gitConfigName.CombinedOutput(); err != nil { + t.Fatalf("Failed to configure git user name: %v\nOutput: %s", err, string(output)) + } + + // Create a simple test workflow that uses an action (to trigger actions-lock.json creation) + testWorkflow := `--- +name: Test Workflow with Action +on: + workflow_dispatch: +permissions: + contents: read +engine: copilot +--- + +# Test Workflow + +Test workflow to verify actions-lock.json creation. +` + + testWorkflowPath := filepath.Join(setup.workflowsDir, "test-action.md") + if err := os.WriteFile(testWorkflowPath, []byte(testWorkflow), 0644); err != nil { + t.Fatalf("Failed to write test workflow file: %v", err) + } + + // Change to the .github/workflows subdirectory + if err := os.Chdir(setup.workflowsDir); err != nil { + t.Fatalf("Failed to change to workflows subdirectory: %v", err) + } + + // Run the compile command from the subdirectory using a relative path + cmd := exec.Command(setup.binaryPath, "compile", "test-action.md") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("CLI compile command failed: %v\nOutput: %s", err, string(output)) + } + + // Change back to the temp directory root + if err := os.Chdir(setup.tempDir); err != nil { + t.Fatalf("Failed to change back to temp directory: %v", err) + } + + // Verify actions-lock.json is created at the repository root (.github/aw/actions-lock.json) + // NOT at .github/workflows/.github/aw/actions-lock.json + expectedLockPath := filepath.Join(setup.tempDir, ".github", "aw", "actions-lock.json") + wrongLockPath := filepath.Join(setup.workflowsDir, ".github", "aw", "actions-lock.json") + + // Check if actions-lock.json exists (it may or may not, depending on whether actions were pinned) + // The important part is that if it exists, it's in the right place + if _, err := os.Stat(expectedLockPath); err == nil { + t.Logf("actions-lock.json correctly created at repo root: %s", expectedLockPath) + } else if !os.IsNotExist(err) { + t.Fatalf("Failed to check for actions-lock.json at expected path: %v", err) + } + + // Verify actions-lock.json was NOT created in the wrong location + if _, err := os.Stat(wrongLockPath); err == nil { + t.Errorf("actions-lock.json incorrectly created at nested path: %s (should be at repo root)", wrongLockPath) + } + + // Verify the workflow lock file was created + lockFilePath := filepath.Join(setup.workflowsDir, "test-action.lock.yml") + if _, err := os.Stat(lockFilePath); os.IsNotExist(err) { + t.Fatalf("Expected lock file %s was not created", lockFilePath) + } + + t.Logf("Integration test passed - actions-lock.json created at correct location") +} + diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 07a8b3759c9..e0de8be1c74 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -72,6 +72,11 @@ func WithRepositorySlug(slug string) CompilerOption { return func(c *Compiler) { c.repositorySlug = slug } } +// WithGitRoot sets the git repository root directory for action cache path +func WithGitRoot(gitRoot string) CompilerOption { + return func(c *Compiler) { c.gitRoot = gitRoot } +} + // FileTracker interface for tracking files created during compilation type FileTracker interface { TrackCreated(filePath string) @@ -125,6 +130,7 @@ type Compiler struct { repositorySlug string // Repository slug (owner/repo) used as seed for scattering artifactManager *ArtifactManager // Tracks artifact uploads/downloads for validation scheduleFriendlyFormats map[int]string // Maps schedule item index to friendly format string for current workflow + gitRoot string // Git repository root directory (if set, used for action cache path) } // NewCompiler creates a new workflow compiler with functional options. @@ -283,11 +289,16 @@ func (c *Compiler) GetScheduleWarnings() []string { func (c *Compiler) getSharedActionResolver() (*ActionCache, *ActionResolver) { if c.actionCache == nil { // Initialize cache and resolver on first use - cwd, err := os.Getwd() - if err != nil { - cwd = "." + // Use git root if provided, otherwise fall back to current working directory + baseDir := c.gitRoot + if baseDir == "" { + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + baseDir = cwd } - c.actionCache = NewActionCache(cwd) + c.actionCache = NewActionCache(baseDir) // Load existing cache unless force refresh is enabled if !c.forceRefreshActionPins { From 58ef6cfbfac7c12915cb32a95895d3ce1d4ca531 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:11:37 +0000 Subject: [PATCH 3/5] Validate fix with formatting, linting, and full test suite Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_integration_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index e5cf71e58d4..d99d123f74c 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -891,4 +891,3 @@ Test workflow to verify actions-lock.json creation. t.Logf("Integration test passed - actions-lock.json created at correct location") } - From f463697d57bee0d4c4fa732f6a692ed6acd6479d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:12:34 +0000 Subject: [PATCH 4/5] Clarify test comment - actions-lock.json only created when needed Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_integration_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go index d99d123f74c..1797a376e75 100644 --- a/pkg/cli/compile_integration_test.go +++ b/pkg/cli/compile_integration_test.go @@ -828,9 +828,11 @@ func TestCompileFromSubdirectoryCreatesActionsLockAtRoot(t *testing.T) { t.Fatalf("Failed to configure git user name: %v\nOutput: %s", err, string(output)) } - // Create a simple test workflow that uses an action (to trigger actions-lock.json creation) + // Create a simple test workflow + // Note: actions-lock.json is only created when actions need to be pinned, + // so it may or may not exist. The test verifies it's NOT created in the wrong location. testWorkflow := `--- -name: Test Workflow with Action +name: Test Workflow on: workflow_dispatch: permissions: @@ -840,7 +842,7 @@ engine: copilot # Test Workflow -Test workflow to verify actions-lock.json creation. +Test workflow to verify actions-lock.json path handling when compiling from subdirectory. ` testWorkflowPath := filepath.Join(setup.workflowsDir, "test-action.md") From 69c18a33df68437001ef10689011975ff015e8cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:49:29 +0000 Subject: [PATCH 5/5] Move git root detection into NewCompiler() for consistency - Add findGitRoot() helper to pkg/workflow/git_helpers.go - Auto-detect git root in NewCompiler() initialization - Remove git root detection from CLI compile_compiler_setup.go - All compiler instances now automatically get git root detection - Ensures consistent behavior across all commands (compile, add, mcp, etc.) This makes the fix truly backward compatible while ensuring actions-lock.json is always created at repo root regardless of CWD. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_compiler_setup.go | 12 +----------- pkg/workflow/compiler_types.go | 5 +++++ pkg/workflow/git_helpers.go | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pkg/cli/compile_compiler_setup.go b/pkg/cli/compile_compiler_setup.go index 9615d04b787..a4afde117a8 100644 --- a/pkg/cli/compile_compiler_setup.go +++ b/pkg/cli/compile_compiler_setup.go @@ -89,22 +89,12 @@ func createAndConfigureCompiler(config CompileConfig) *workflow.Compiler { } } - // Find git repository root for action cache path - // This ensures actions-lock.json is created at the repo root regardless of CWD - gitRoot, err := findGitRoot() - if err != nil { - compileCompilerSetupLog.Printf("Could not find git root: %v, using current directory", err) - gitRoot = "" // Will fall back to cwd in compiler - } else { - compileCompilerSetupLog.Printf("Using git root for action cache: %s", gitRoot) - } - // Create compiler with auto-detected version and action mode + // Git root is now auto-detected in NewCompiler() for all compiler instances compiler := workflow.NewCompiler( workflow.WithVerbose(config.Verbose), workflow.WithEngineOverride(config.EngineOverride), workflow.WithFailFast(config.FailFast), - workflow.WithGitRoot(gitRoot), ) compileCompilerSetupLog.Print("Created compiler instance") diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index e0de8be1c74..3bed652e2eb 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -140,6 +140,10 @@ func NewCompiler(opts ...CompilerOption) *Compiler { // Get default version version := defaultVersion + // Auto-detect git repository root for action cache path resolution + // This ensures actions-lock.json is created at repo root regardless of CWD + gitRoot := findGitRoot() + // Create compiler with defaults c := &Compiler{ verbose: false, @@ -152,6 +156,7 @@ func NewCompiler(opts ...CompilerOption) *Compiler { stepOrderTracker: NewStepOrderTracker(), artifactManager: NewArtifactManager(), actionPinWarnings: make(map[string]bool), // Initialize warning cache + gitRoot: gitRoot, // Auto-detected git root } // Apply functional options diff --git a/pkg/workflow/git_helpers.go b/pkg/workflow/git_helpers.go index 42c61626903..f7662ab20dd 100644 --- a/pkg/workflow/git_helpers.go +++ b/pkg/workflow/git_helpers.go @@ -39,6 +39,22 @@ import ( var gitHelpersLog = logger.New("workflow:git_helpers") +// findGitRoot attempts to find the git repository root directory. +// Returns empty string if not in a git repository or if git command fails. +// This function is safe to call from any context and won't cause errors if git is not available. +func findGitRoot() string { + gitHelpersLog.Print("Attempting to find git root directory") + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + output, err := cmd.Output() + if err != nil { + gitHelpersLog.Printf("Could not find git root (not a git repo or git not available): %v", err) + return "" + } + gitRoot := strings.TrimSpace(string(output)) + gitHelpersLog.Printf("Found git root: %s", gitRoot) + return gitRoot +} + // GetCurrentGitTag returns the current git tag if available. // Returns empty string if not on a tag. func GetCurrentGitTag() string {