From 67202ab52b0e99232e09f12833e48a74b8a28c23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:26:24 +0000 Subject: [PATCH 1/6] Initial plan From d6f1415664adfb38e4d61368ddbed994f54420d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:39:04 +0000 Subject: [PATCH 2/6] Fix sparse checkout to follow symlinks for .github/agents and similar dirs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_activation_job.go | 48 +++++++++++ pkg/workflow/compiler_activation_job_test.go | 85 ++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index 557b7c1448a..44a96650130 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -2,6 +2,8 @@ package workflow import ( "fmt" + "os" + "path/filepath" "strings" "github.com/goccy/go-yaml" @@ -434,6 +436,10 @@ func (c *Compiler) generateCheckoutGitHubFolderForActivation(data *WorkflowData) } compilerActivationJobLog.Printf("Adding %d engine-specific dirs to sparse-checkout: %v", len(extraPaths), extraPaths) + // Detect symlinks for well-known .github sub-paths and add their resolved targets + // so that sparse checkout fetches the target directory, not just the symlink blob. + extraPaths = resolveSymlinkExtraPaths(extraPaths) + cm := NewCheckoutManager(nil) activationToken := c.resolveActivationToken(data) if data != nil && hasWorkflowCallTrigger(data.On) && !data.InlinedImports { @@ -531,3 +537,45 @@ func injectIfConditionAfterName(step, condition string) string { newLines = append(newLines, lines[nameLineIdx+1:]...) return strings.Join(newLines, "\n") } + +// resolveSymlinkExtraPaths inspects well-known .github sub-paths that are commonly +// symlinked (e.g. .github/agents -> ../.ai/agents). When a path is a symlink, the +// resolved target directory (relative to the repo root) is appended to extraPaths so +// that the sparse-checkout step fetches the target files, not just the dangling symlink +// blob. Symlink targets that would escape the repository root (path starts with "..") are +// silently ignored to prevent path traversal. Already-present paths are not duplicated. +func resolveSymlinkExtraPaths(extraPaths []string) []string { + candidates := []string{ + ".github/agents", + ".github/skills", + ".github/prompts", + } + existing := make(map[string]struct{}, len(extraPaths)) + for _, p := range extraPaths { + existing[p] = struct{}{} + } + for _, candidate := range candidates { + info, err := os.Lstat(candidate) + if err != nil || info.Mode()&os.ModeSymlink == 0 { + continue // not a symlink or doesn't exist + } + target, err := os.Readlink(candidate) + if err != nil { + continue + } + // Resolve relative to the parent directory of the symlink + resolved := filepath.Clean(filepath.Join(filepath.Dir(candidate), target)) + // Reject paths that escape the repository root (e.g. ../../etc) + if strings.HasPrefix(resolved, "..") { + compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q escapes the repository root", candidate, target, resolved) + continue + } + if _, alreadyPresent := existing[resolved]; alreadyPresent { + continue + } + compilerActivationJobLog.Printf("Symlink detected: %s -> %s (adding %s to sparse checkout)", candidate, target, resolved) + extraPaths = append(extraPaths, resolved) + existing[resolved] = struct{}{} + } + return extraPaths +} diff --git a/pkg/workflow/compiler_activation_job_test.go b/pkg/workflow/compiler_activation_job_test.go index 2a14bf7175f..4d8826658cf 100644 --- a/pkg/workflow/compiler_activation_job_test.go +++ b/pkg/workflow/compiler_activation_job_test.go @@ -3,6 +3,8 @@ package workflow import ( + "os" + "path/filepath" "strings" "testing" @@ -1139,3 +1141,86 @@ func TestInjectIfConditionAfterName(t *testing.T) { assert.Equal(t, step, got, "step without - name: should be returned unchanged") }) } + +// TestResolveSymlinkExtraPaths verifies that symlinks under .github/ are resolved and +// their targets appended to the extraPaths list, while non-symlinks and escape paths are ignored. +func TestResolveSymlinkExtraPaths(t *testing.T) { + // Create a temp dir that acts as the working directory for the test. + tmpDir := t.TempDir() + + // Helper: create a symlink -> inside tmpDir and chdir to tmpDir. + mkSymlink := func(t *testing.T, linkRel, target string) { + t.Helper() + linkPath := filepath.Join(tmpDir, linkRel) + if err := os.MkdirAll(filepath.Dir(linkPath), 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + if err := os.Symlink(target, linkPath); err != nil { + t.Fatalf("Symlink: %v", err) + } + } + + origDir, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { _ = os.Chdir(origDir) }) + + t.Run("symlink .github/agents resolved to inner path", func(t *testing.T) { + dir := t.TempDir() + // Create the symlink target directory so Lstat sees the symlink, not a dangling one. + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".ai", "agents"), 0o755)) + // Create .github/agents as a symlink -> ../.ai/agents + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) + require.NoError(t, os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents"))) + + require.NoError(t, os.Chdir(dir)) + result := resolveSymlinkExtraPaths(nil) + assert.Contains(t, result, ".ai/agents", "symlink target should be appended") + }) + + t.Run("non-symlink .github/agents produces no extra entry", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github", "agents"), 0o755)) + + require.NoError(t, os.Chdir(dir)) + result := resolveSymlinkExtraPaths(nil) + assert.Empty(t, result, "regular directory should not produce extra paths") + }) + + t.Run("missing .github/agents produces no extra entry", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.Chdir(dir)) + result := resolveSymlinkExtraPaths(nil) + assert.Empty(t, result, "absent path should produce no extra paths") + }) + + t.Run("symlink target escaping repo root is ignored", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) + // Symlink points outside the repo root + require.NoError(t, os.Symlink(filepath.Join("..", "..", "etc", "passwd"), filepath.Join(dir, ".github", "agents"))) + + require.NoError(t, os.Chdir(dir)) + result := resolveSymlinkExtraPaths(nil) + assert.Empty(t, result, "symlink escaping repo root should be silently ignored") + }) + + t.Run("already-present path is not duplicated", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".ai", "agents"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) + require.NoError(t, os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents"))) + + require.NoError(t, os.Chdir(dir)) + result := resolveSymlinkExtraPaths([]string{".ai/agents"}) + count := 0 + for _, p := range result { + if p == ".ai/agents" { + count++ + } + } + assert.Equal(t, 1, count, "already-present path should not be duplicated") + }) + + _ = tmpDir + _ = mkSymlink +} From 8702b965ba90f31df10d03e712d40790fa1e1ec9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 02:40:12 +0000 Subject: [PATCH 3/6] Remove unused tmpDir and mkSymlink variables from TestResolveSymlinkExtraPaths Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_activation_job_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/pkg/workflow/compiler_activation_job_test.go b/pkg/workflow/compiler_activation_job_test.go index 4d8826658cf..93e8820255a 100644 --- a/pkg/workflow/compiler_activation_job_test.go +++ b/pkg/workflow/compiler_activation_job_test.go @@ -1145,21 +1145,6 @@ func TestInjectIfConditionAfterName(t *testing.T) { // TestResolveSymlinkExtraPaths verifies that symlinks under .github/ are resolved and // their targets appended to the extraPaths list, while non-symlinks and escape paths are ignored. func TestResolveSymlinkExtraPaths(t *testing.T) { - // Create a temp dir that acts as the working directory for the test. - tmpDir := t.TempDir() - - // Helper: create a symlink -> inside tmpDir and chdir to tmpDir. - mkSymlink := func(t *testing.T, linkRel, target string) { - t.Helper() - linkPath := filepath.Join(tmpDir, linkRel) - if err := os.MkdirAll(filepath.Dir(linkPath), 0o755); err != nil { - t.Fatalf("MkdirAll: %v", err) - } - if err := os.Symlink(target, linkPath); err != nil { - t.Fatalf("Symlink: %v", err) - } - } - origDir, err := os.Getwd() require.NoError(t, err) t.Cleanup(func() { _ = os.Chdir(origDir) }) @@ -1220,7 +1205,4 @@ func TestResolveSymlinkExtraPaths(t *testing.T) { } assert.Equal(t, 1, count, "already-present path should not be duplicated") }) - - _ = tmpDir - _ = mkSymlink } From 9caa6c605d1aa6428af33dad9286baef1c2150b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 06:19:02 +0000 Subject: [PATCH 4/6] docs(adr): add draft ADR-43909 for symlink resolution in sparse checkout ADR generated by design-decision-gate to document the architectural decision made in PR #43909 to follow symlinks when generating sparse checkout for .github subdirectories. Co-Authored-By: Claude Sonnet 4.6 --- ...symlinks-sparse-checkout-github-subdirs.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/adr/43909-follow-symlinks-sparse-checkout-github-subdirs.md diff --git a/docs/adr/43909-follow-symlinks-sparse-checkout-github-subdirs.md b/docs/adr/43909-follow-symlinks-sparse-checkout-github-subdirs.md new file mode 100644 index 00000000000..27215a973cb --- /dev/null +++ b/docs/adr/43909-follow-symlinks-sparse-checkout-github-subdirs.md @@ -0,0 +1,44 @@ +# ADR-43909: Follow Symlinks in Sparse Checkout for .github Subdirectories + +**Date**: 2026-07-07 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The workflow compiler builds sparse-checkout configurations for `.github` subdirectories (`.github/agents`, `.github/skills`, `.github/prompts`) when generating activation jobs. Some repositories use git symlinks at these paths to point to directories elsewhere in the repo (e.g. `.github/agents → ../.ai/agents`). Previously, the compiler added only the symlink blob path to the sparse-checkout manifest; at runtime this produced a dangling symlink, causing any `{{#runtime-import .github/agents/.md}}` expression to fail silently. No mechanism existed to detect this case or include the symlink's target directory in the checkout. + +### Decision + +We will add a `resolveSymlinkExtraPaths` function that inspects `.github/agents`, `.github/skills`, and `.github/prompts` at compile time using `os.Lstat` and `os.Readlink`. When a candidate path is a symlink, the resolved target (relative to the repository root via `filepath.Clean(filepath.Join(dir, target))`) is appended to `extraPaths` before the sparse-checkout step is generated. Symlink targets whose resolved path begins with `..` are silently rejected to prevent path traversal. Paths already present in `extraPaths` are deduplicated via a map. + +### Alternatives Considered + +#### Alternative 1: Require explicit user configuration + +Repository owners could be required to enumerate symlink targets explicitly in a configuration file or in their sparse-checkout definition. This pushes the responsibility to every affected repository and is error-prone: users must know the internal structure of the compiled sparse-checkout and keep their configuration in sync as symlink targets change. Given that the compiler already resolves extra paths automatically for other cases, silent auto-detection is the lower-friction approach. + +#### Alternative 2: Resolve all symlinks generically across extraPaths + +Instead of inspecting a fixed set of three candidates, the compiler could recursively follow symlinks for every entry in `extraPaths`. This would cover any future well-known paths without code changes. However, unbounded symlink traversal increases the risk of accidentally including unintended directories, makes the behavior harder to reason about, and complicates path-escape validation. Limiting resolution to the three known subdirectories keeps the scope narrow and the security surface small. + +### Consequences + +#### Positive +- `runtime-import` expressions that reference `.github/agents`, `.github/skills`, or `.github/prompts` now work correctly when those paths are git symlinks. +- Path traversal is explicitly blocked: symlink targets resolving outside the repository root are ignored with a log message, preserving security guarantees. +- Deduplication ensures the generated sparse-checkout manifest does not contain redundant entries when a target is already present. + +#### Negative +- `resolveSymlinkExtraPaths` uses relative paths (`os.Lstat(".github/agents")`) and implicitly assumes the compiler's working directory is the repository root at the time of compilation. If the working directory differs, all three `os.Lstat` calls fail silently and no symlink targets are added — the bug reappears without any error surfaced to the caller. +- Only the three hard-coded candidate paths are inspected. Other symlinked `.github` subdirectories (e.g. `.github/workflows` or custom subdirectories) are not covered and would require separate changes. + +#### Neutral +- The function is append-only: it does not modify or remove existing `extraPaths` entries, preserving backward compatibility with all callers. +- The `resolveSymlinkExtraPaths` call is placed immediately before both `GenerateGitHubFolderCheckoutStep` invocations via the single call-site in `generateCheckoutGitHubFolderForActivation`, so all callers inherit the fix without further changes. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From e0676f5f84dea3655484ad0f014e36ad11753a1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 08:33:39 +0000 Subject: [PATCH 5/6] fix: resolveSymlinkExtraPaths takes repoRoot param; fix escape check; skip symlink tests on unsupported platforms Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/compiler_activation_job.go | 60 +++++++++++++++----- pkg/workflow/compiler_activation_job_test.go | 36 ++++++------ 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index 44a96650130..571a564a9e6 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -438,7 +438,14 @@ func (c *Compiler) generateCheckoutGitHubFolderForActivation(data *WorkflowData) // Detect symlinks for well-known .github sub-paths and add their resolved targets // so that sparse checkout fetches the target directory, not just the symlink blob. - extraPaths = resolveSymlinkExtraPaths(extraPaths) + // Use c.gitRoot so detection works regardless of the process CWD. + repoRoot := c.gitRoot + if repoRoot == "" { + if cwd, err := os.Getwd(); err == nil { + repoRoot = cwd + } + } + extraPaths = resolveSymlinkExtraPaths(repoRoot, extraPaths) cm := NewCheckoutManager(nil) activationToken := c.resolveActivationToken(data) @@ -542,9 +549,13 @@ func injectIfConditionAfterName(step, condition string) string { // symlinked (e.g. .github/agents -> ../.ai/agents). When a path is a symlink, the // resolved target directory (relative to the repo root) is appended to extraPaths so // that the sparse-checkout step fetches the target files, not just the dangling symlink -// blob. Symlink targets that would escape the repository root (path starts with "..") are -// silently ignored to prevent path traversal. Already-present paths are not duplicated. -func resolveSymlinkExtraPaths(extraPaths []string) []string { +// blob. Symlink targets that escape the repository root are silently ignored to prevent +// path traversal. Already-present paths are not duplicated. +// repoRoot must be an absolute path to the repository root; when empty, the function is a no-op. +func resolveSymlinkExtraPaths(repoRoot string, extraPaths []string) []string { + if repoRoot == "" { + return extraPaths + } candidates := []string{ ".github/agents", ".github/skills", @@ -555,27 +566,46 @@ func resolveSymlinkExtraPaths(extraPaths []string) []string { existing[p] = struct{}{} } for _, candidate := range candidates { - info, err := os.Lstat(candidate) + absCandidate := filepath.Join(repoRoot, candidate) + info, err := os.Lstat(absCandidate) if err != nil || info.Mode()&os.ModeSymlink == 0 { continue // not a symlink or doesn't exist } - target, err := os.Readlink(candidate) + target, err := os.Readlink(absCandidate) + if err != nil { + continue + } + // Resolve target to an absolute path. + // When the symlink target is absolute (e.g. /etc/passwd), use it directly. + // When relative, resolve it against the directory that contains the symlink. + var absResolved string + if filepath.IsAbs(target) { + absResolved = filepath.Clean(target) + } else { + absResolved = filepath.Clean(filepath.Join(filepath.Dir(absCandidate), target)) + } + rel, err := filepath.Rel(repoRoot, absResolved) if err != nil { continue } - // Resolve relative to the parent directory of the symlink - resolved := filepath.Clean(filepath.Join(filepath.Dir(candidate), target)) - // Reject paths that escape the repository root (e.g. ../../etc) - if strings.HasPrefix(resolved, "..") { - compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q escapes the repository root", candidate, target, resolved) + // Reject paths that escape the repository root using a segment-aware check. + // strings.HasPrefix would incorrectly reject names like "..foo". + firstSeg := rel + if i := strings.IndexByte(rel, filepath.Separator); i >= 0 { + firstSeg = rel[:i] + } + if firstSeg == ".." { + compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q escapes the repository root", candidate, target, rel) continue } - if _, alreadyPresent := existing[resolved]; alreadyPresent { + // Normalise to forward slashes for use in YAML sparse-checkout paths. + rel = filepath.ToSlash(rel) + if _, alreadyPresent := existing[rel]; alreadyPresent { continue } - compilerActivationJobLog.Printf("Symlink detected: %s -> %s (adding %s to sparse checkout)", candidate, target, resolved) - extraPaths = append(extraPaths, resolved) - existing[resolved] = struct{}{} + compilerActivationJobLog.Printf("Symlink detected: %s -> %s (adding %s to sparse checkout)", candidate, target, rel) + extraPaths = append(extraPaths, rel) + existing[rel] = struct{}{} } return extraPaths } diff --git a/pkg/workflow/compiler_activation_job_test.go b/pkg/workflow/compiler_activation_job_test.go index 93e8820255a..e122c25a040 100644 --- a/pkg/workflow/compiler_activation_job_test.go +++ b/pkg/workflow/compiler_activation_job_test.go @@ -1145,20 +1145,17 @@ func TestInjectIfConditionAfterName(t *testing.T) { // TestResolveSymlinkExtraPaths verifies that symlinks under .github/ are resolved and // their targets appended to the extraPaths list, while non-symlinks and escape paths are ignored. func TestResolveSymlinkExtraPaths(t *testing.T) { - origDir, err := os.Getwd() - require.NoError(t, err) - t.Cleanup(func() { _ = os.Chdir(origDir) }) - t.Run("symlink .github/agents resolved to inner path", func(t *testing.T) { dir := t.TempDir() // Create the symlink target directory so Lstat sees the symlink, not a dangling one. require.NoError(t, os.MkdirAll(filepath.Join(dir, ".ai", "agents"), 0o755)) // Create .github/agents as a symlink -> ../.ai/agents require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) - require.NoError(t, os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents"))) + if err := os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents")); err != nil { + t.Skipf("symlinks not supported: %v", err) + } - require.NoError(t, os.Chdir(dir)) - result := resolveSymlinkExtraPaths(nil) + result := resolveSymlinkExtraPaths(dir, nil) assert.Contains(t, result, ".ai/agents", "symlink target should be appended") }) @@ -1166,26 +1163,30 @@ func TestResolveSymlinkExtraPaths(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github", "agents"), 0o755)) - require.NoError(t, os.Chdir(dir)) - result := resolveSymlinkExtraPaths(nil) + result := resolveSymlinkExtraPaths(dir, nil) assert.Empty(t, result, "regular directory should not produce extra paths") }) t.Run("missing .github/agents produces no extra entry", func(t *testing.T) { dir := t.TempDir() - require.NoError(t, os.Chdir(dir)) - result := resolveSymlinkExtraPaths(nil) + result := resolveSymlinkExtraPaths(dir, nil) assert.Empty(t, result, "absent path should produce no extra paths") }) + t.Run("empty repoRoot is a no-op", func(t *testing.T) { + result := resolveSymlinkExtraPaths("", []string{"existing"}) + assert.Equal(t, []string{"existing"}, result, "empty repoRoot should return extraPaths unchanged") + }) + t.Run("symlink target escaping repo root is ignored", func(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) // Symlink points outside the repo root - require.NoError(t, os.Symlink(filepath.Join("..", "..", "etc", "passwd"), filepath.Join(dir, ".github", "agents"))) + if err := os.Symlink(filepath.Join("..", "..", "etc", "passwd"), filepath.Join(dir, ".github", "agents")); err != nil { + t.Skipf("symlinks not supported: %v", err) + } - require.NoError(t, os.Chdir(dir)) - result := resolveSymlinkExtraPaths(nil) + result := resolveSymlinkExtraPaths(dir, nil) assert.Empty(t, result, "symlink escaping repo root should be silently ignored") }) @@ -1193,10 +1194,11 @@ func TestResolveSymlinkExtraPaths(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, ".ai", "agents"), 0o755)) require.NoError(t, os.MkdirAll(filepath.Join(dir, ".github"), 0o755)) - require.NoError(t, os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents"))) + if err := os.Symlink(filepath.Join("..", ".ai", "agents"), filepath.Join(dir, ".github", "agents")); err != nil { + t.Skipf("symlinks not supported: %v", err) + } - require.NoError(t, os.Chdir(dir)) - result := resolveSymlinkExtraPaths([]string{".ai/agents"}) + result := resolveSymlinkExtraPaths(dir, []string{".ai/agents"}) count := 0 for _, p := range result { if p == ".ai/agents" { From edcac7a7b174736ddb2beaaad609601ee265afd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 08:34:50 +0000 Subject: [PATCH 6/6] fix: normalize spelling and add nil extraPaths test for empty repoRoot Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/compiler_activation_job.go | 2 +- pkg/workflow/compiler_activation_job_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index 571a564a9e6..a37fbf769ac 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -598,7 +598,7 @@ func resolveSymlinkExtraPaths(repoRoot string, extraPaths []string) []string { compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q escapes the repository root", candidate, target, rel) continue } - // Normalise to forward slashes for use in YAML sparse-checkout paths. + // Normalize to forward slashes for use in YAML sparse-checkout paths. rel = filepath.ToSlash(rel) if _, alreadyPresent := existing[rel]; alreadyPresent { continue diff --git a/pkg/workflow/compiler_activation_job_test.go b/pkg/workflow/compiler_activation_job_test.go index e122c25a040..22818354f90 100644 --- a/pkg/workflow/compiler_activation_job_test.go +++ b/pkg/workflow/compiler_activation_job_test.go @@ -1176,6 +1176,8 @@ func TestResolveSymlinkExtraPaths(t *testing.T) { t.Run("empty repoRoot is a no-op", func(t *testing.T) { result := resolveSymlinkExtraPaths("", []string{"existing"}) assert.Equal(t, []string{"existing"}, result, "empty repoRoot should return extraPaths unchanged") + result = resolveSymlinkExtraPaths("", nil) + assert.Nil(t, result, "empty repoRoot with nil extraPaths should return nil") }) t.Run("symlink target escaping repo root is ignored", func(t *testing.T) {