Skip to content

fix: follow symlinks when generating sparse checkout for .github subdirs#43909

Merged
pelikhan merged 7 commits into
mainfrom
copilot/fix-sparse-checkout-symlinks
Jul 7, 2026
Merged

fix: follow symlinks when generating sparse checkout for .github subdirs#43909
pelikhan merged 7 commits into
mainfrom
copilot/fix-sparse-checkout-symlinks

Conversation

Copilot AI commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

When .github/agents (or .github/skills/.github/prompts) is a git symlink to a directory outside .github/ (e.g. ../.ai/agents), the compiled sparse checkout only fetched the symlink blob — leaving a dangling symlink at runtime and breaking any {{#runtime-import .github/agents/<agent>.md}} expression.

Changes

  • pkg/workflow/compiler_activation_job.go — adds resolveSymlinkExtraPaths, called just before both GenerateGitHubFolderCheckoutStep invocations in generateCheckoutGitHubFolderForActivation. For each of .github/agents, .github/skills, .github/prompts, it uses os.Lstat/os.Readlink to detect symlinks, resolves the target relative to the repo root via filepath.Clean, rejects targets that escape the root (..-prefixed paths), deduplicates, and appends qualifying paths to extraPaths.

  • pkg/workflow/compiler_activation_job_test.go — adds TestResolveSymlinkExtraPaths covering: symlink resolved to inner path, regular directory, missing path, escape-root symlink ignored, and deduplication.

Example

Given .github/agents → ../.ai/agents, the generated sparse checkout becomes:

sparse-checkout: |
  .github
  .agents
  .ai/agents   # resolved symlink target, added automatically

Generated by 👨‍🍳 PR Sous Chef · 10.8 AIC · ⌖ 4.38 AIC · ⊞ 7.1K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 7, 2026 02:39
… dirs

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…xtraPaths

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix sparse checkout to include symlink targets for agents fix: follow symlinks when generating sparse checkout for .github subdirs Jul 7, 2026
Copilot AI requested a review from pelikhan July 7, 2026 02:40
@pelikhan pelikhan marked this pull request as ready for review July 7, 2026 06:08
Copilot AI review requested due to automatic review settings July 7, 2026 06:08
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a sparse-checkout edge case in activation-job generation: when well-known .github/* directories (agents/skills/prompts) are symlinks that point elsewhere in the repo (e.g. ../.ai/agents), the generated sparse checkout should include the symlink target directory so runtime imports don’t break.

Changes:

  • Add resolveSymlinkExtraPaths to detect symlinks under .github/ and append their resolved repo-relative targets to the activation sparse-checkout extra paths.
  • Call resolveSymlinkExtraPaths during activation checkout step generation.
  • Add unit tests for symlink resolution, escape handling, missing paths, and deduplication.
Show a summary per file
File Description
pkg/workflow/compiler_activation_job.go Adds symlink target discovery and appends resolved paths to activation-job sparse checkout.
pkg/workflow/compiler_activation_job_test.go Adds tests validating symlink resolution behavior and deduplication.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 5
  • Review effort level: Low

Comment thread pkg/workflow/compiler_activation_job.go Outdated
Comment on lines +566 to +572
// 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
}
Comment on lines +557 to +560
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
Comment thread pkg/workflow/compiler_activation_job_test.go Outdated
Comment thread pkg/workflow/compiler_activation_job_test.go Outdated
Comment thread pkg/workflow/compiler_activation_job_test.go Outdated
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 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (115 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/43909-follow-symlinks-sparse-checkout-github-subdirs.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-43909: Follow Symlinks in Sparse Checkout for .github Subdirectories

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 39.1 AIC · ⌖ 11.4 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 94/100 — Excellent

Analyzed 5 test scenario(s) across 1 new test function: 5 design, 0 implementation, 0 violation(s).

📊 Metrics (5 scenarios in 1 function)
Metric Value
Analyzed 1 function, 5 t.Run scenarios (Go: 5, JS: 0)
✅ Design 5 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 4 (80%)
Duplicate clusters 0
Inflation No (1.39:1)
🚨 Violations 0
Test File Classification Issues
TestResolveSymlinkExtraPaths/symlink .github/agents resolved to inner path compiler_activation_job_test.go:1152 design_test / high_value None
TestResolveSymlinkExtraPaths/non-symlink .github/agents produces no extra entry compiler_activation_job_test.go:1165 design_test / high_value None
TestResolveSymlinkExtraPaths/missing .github/agents produces no extra entry compiler_activation_job_test.go:1174 design_test / high_value None
TestResolveSymlinkExtraPaths/symlink target escaping repo root is ignored compiler_activation_job_test.go:1181 design_test / high_value None — security invariant, excellent
TestResolveSymlinkExtraPaths/already-present path is not duplicated compiler_activation_job_test.go:1192 design_test / high_value None

Verdict

Passed. 0% implementation tests (threshold: 30%). The single new test function is a well-structured table-driven suite covering the happy path, two no-op conditions (regular dir and missing path), a security boundary (symlink escape), and an idempotency invariant. All assertions carry descriptive failure messages. No violations detected.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 110.3 AIC · ⌖ 11.5 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

\u2705 Test Quality Sentinel: 94/100. 0% implementation tests (threshold: 30%). All 5 new test scenarios are high-value design tests covering the core symlink-resolution behavioral contract, including a security boundary (escape detection) and idempotency invariant.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on a correctness gap in the escape check and test fragility.

📋 Key Themes & Highlights

Key Themes

  • Absolute symlink targets not handled (line 567): Go's filepath.Join does not treat absolute second args as absolute — an absolute symlink target like /etc/passwd resolves to .github/etc/passwd, passes the ".." escape check, and gets added to sparse checkout as a wrong path. A filepath.IsAbs(target) guard is needed before the join.

  • Escape-root check is fragile (line 569): strings.HasPrefix(resolved, "..") misses the edge case where resolved == ".." exactly (which filepath.Clean produces for a one-level escape). resolved == ".." || strings.HasPrefix(resolved, "../") is more precise.

  • CWD-implicit behaviour (line 558): os.Lstat(candidate) silently skips all candidates if the process CWD is not the repo root. Accepting a repoRoot parameter would make this explicit and testable without os.Chdir.

  • Test fragility (line 1150): os.Chdir is process-global; the single parent-level cleanup is not resilient to sub-test panics. Per-sub-test cleanups (or t.Chdir in Go 1.24+) are safer.

Positive Highlights

  • ✅ Root cause is correctly identified and the fix is precise: os.Lstat/os.Readlink rather than blindly resolving every path.
  • ✅ Deduplication via the existing map is clean and efficient.
  • ✅ Five test cases cover the main scenarios well; the test structure is clear.
  • ✅ Logging at each decision point makes debugging future failures straightforward.
  • ✅ Candidates list is kept small and explicit — easy to extend.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 90.5 AIC · ⌖ 8.39 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/workflow/compiler_activation_job.go Outdated
continue
}
// Resolve relative to the parent directory of the symlink
resolved := filepath.Clean(filepath.Join(filepath.Dir(candidate), target))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] Absolute symlink targets are silently mishandled — filepath.Join(".github", "/abs/path") returns .github/abs/path in Go (it does not honour the absolute second arg), so the strings.HasPrefix(resolved, "..") escape check passes and an incorrect path gets appended to the sparse checkout instead of resolving the actual dangling symlink.

💡 Suggested fix

Add an explicit guard for absolute targets before the join:

if filepath.IsAbs(target) {
    compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: absolute target not supported", candidate, target)
    continue
}
resolved := filepath.Clean(filepath.Join(filepath.Dir(candidate), target))

Most git symlinks use relative targets for portability, so rejecting absolute ones is the safest default.

@copilot please address this.

Comment thread pkg/workflow/compiler_activation_job.go Outdated
existing[p] = struct{}{}
}
for _, candidate := range candidates {
info, err := os.Lstat(candidate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] os.Lstat(candidate) uses a relative path, so it is implicitly CWD-dependent — the function works only if the compiler process is already at the repo root. If cwd ever differs (e.g., during testing or if the compiler is invoked from a sub-directory), all candidates are silently skipped rather than raising an error.

💡 Suggested improvement

Accept a repoRoot string parameter (or derive it from the caller) and use filepath.Join(repoRoot, candidate) for all os.Lstat/os.Readlink calls. This makes the dependency explicit and easier to test without os.Chdir:

func resolveSymlinkExtraPaths(repoRoot string, extraPaths []string) []string {
    ...
    info, err := os.Lstat(filepath.Join(repoRoot, candidate))
    ...
    target, err := os.Readlink(filepath.Join(repoRoot, candidate))
    ...
}

@copilot please address this.

Comment thread pkg/workflow/compiler_activation_job_test.go Outdated
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")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The "symlink resolved" test only asserts .ai/agents is in the result, but does not assert the full slice length. If a second, unexpected path were also added (e.g., due to a bug), the test would still pass.

💡 Suggested addition

Add a length check after the Contains assertion to make the test a precise specification:

result := resolveSymlinkExtraPaths(nil)
assert.Contains(t, result, ".ai/agents", "symlink target should be appended")
assert.Len(t, result, 1, "only the resolved target should be in the result")

@copilot please address this.

Comment thread pkg/workflow/compiler_activation_job.go Outdated
// 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, "..") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The escape-root check strings.HasPrefix(resolved, "..") is fragile — it only catches paths that literally begin with .., but Windows-style paths or normalisation edge cases (e.g., after clean) could produce paths like ../foo on some hosts but just foo on others. More importantly, a symlink target of exactly .. (resolves to .) would pass the check and add . to the sparse checkout list, which is likely unintended.

💡 Suggested improvement

Use a more robust check: verify the resolved path does not escape the current working directory by confirming it is within a known root:

// Use strings.HasPrefix with slash to avoid false negatives on paths like "..foo"
if resolved == ".." || strings.HasPrefix(resolved, "../") {
    ...
    continue
}

Or if repoRoot is passed as a parameter, use filepath.Rel to confirm containment.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two correctness issues block merge

The symlink-resolution idea is sound, but two problems need to be fixed.

1. Implicit CWD contract (high)resolveSymlinkExtraPaths calls os.Lstat(".github/agents") with a bare relative path, silently no-oppping whenever the process CWD is not the repo root. The tests reveal this by calling os.Chdir before every case. The function should accept the repo root as an explicit parameter.

2. Absolute symlink targets bypass the escape guard (medium)strings.HasPrefix(resolved, "..") only stops relative traversal. When a symlink target is an absolute path, Go silently strips the leading / in filepath.Join, so the guard passes and a wrong path enters the sparse-checkout list. Add filepath.IsAbs(resolved) to the rejection condition.

🔎 Code quality review by PR Code Quality Reviewer · 158.5 AIC · ⌖ 5.43 AIC · ⊞ 5.5K
Comment /review to run again

for _, p := range extraPaths {
existing[p] = struct{}{}
}
for _, candidate := range candidates {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Silent no-op when CWD ≠ repo root: os.Lstat(candidate) resolves the bare relative path against the process working directory. If the compiler is invoked from any directory other than the repo root, symlink detection silently skips every candidate and the fix never fires.

💡 Suggested fix

Pass the repo root as an explicit parameter instead of relying on CWD:

func resolveSymlinkExtraPaths(repoRoot string, extraPaths []string) []string {
    // ...
    for _, candidate := range candidates {
        fullPath := filepath.Join(repoRoot, candidate)
        info, err := os.Lstat(fullPath)
        if err != nil || info.Mode()&os.ModeSymlink == 0 {
            continue
        }
        target, err := os.Readlink(fullPath)
        if err != nil {
            continue
        }
        resolved := filepath.Clean(filepath.Join(filepath.Dir(fullPath), target))
        // Reject anything that escapes repoRoot
        if !strings.HasPrefix(resolved, filepath.Clean(repoRoot)+string(os.PathSeparator)) {
            continue
        }
        rel, _ := filepath.Rel(repoRoot, resolved)
        // ...
    }
}

The caller already has access to the repo path; the hidden CWD contract makes the function fragile in library/test/embedded invocations. The tests themselves are forced to call os.Chdir as a workaround, which is the clearest sign this contract needs to be explicit.

target, err := os.Readlink(candidate)
if err != nil {
continue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Absolute symlink target bypasses escape check: strings.HasPrefix(resolved, "..") only guards against relative traversal. If the symlink points to an absolute path (e.g. /data/shared-agents), Go's filepath.Join(".github", "/data/shared-agents") produces .github/data/shared-agents — it silently strips the leading / — so the resolved path doesn't start with .." and passes the guard, appending a meaningless path to extraPaths`.

💡 Suggested fix

Tighten the guard to cover both relative and absolute escape:

// After resolving:
resolved := filepath.Clean(filepath.Join(filepath.Dir(candidate), target))

// Reject absolute paths and relative-traversal paths
if filepath.IsAbs(resolved) || strings.HasPrefix(resolved, "..") {
    compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q is outside the repository root", candidate, target, resolved)
    continue
}

Note: even without this fix the impact is low (a wrong path in sparse-checkout is a non-operation), but it's an inconsistency between the documented invariant ("rejects paths that escape the root") and the actual behavior.

// 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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use t.Chdir instead of manual os.Chdir/t.Cleanup: the manual save/restore pattern is process-global and unsafe if any test in the package ever adopts t.Parallel(). Each subtest calls os.Chdir(dir) with no mutual exclusion between subtests.

💡 Suggested fix

t.Chdir (Go 1.24+) handles per-subtest cleanup automatically and is already the established pattern in this package (see compiler_shared_cache_test.go and resolve_test.go):

// Replace this:
origDir, err := os.Getwd()
require.NoError(t, err)
t.Cleanup(func() { _ = os.Chdir(origDir) })
// ...
require.NoError(t, os.Chdir(dir))

// With this (in each subtest):
t.Chdir(dir)

This is a low-effort change that removes the fragile manual pattern and aligns with the rest of the test file.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: fix: follow symlinks when generating sparse checkout for .github subdirs

The fix correctly solves the dangling-symlink problem by resolving .github/agents, .github/skills, and .github/prompts symlinks and adding their targets to extraPaths before the sparse-checkout step is generated. The approach is sound and well-tested.

One blocking issue was found in the escape guard:

Absolute symlink targets bypass the ..-prefix guard

Go's filepath.Join(base, absPath) on Linux does not discard base when absPath is absolute. For example:

filepath.Join(".github", "/etc/passwd") → ".github/etc/passwd"

That resolved path doesn't start with .., so the guard on line 569 silently passes and ".github/etc/passwd" is appended to extraPaths. This is practically harmless (the path won't exist in the checkout), but it breaks the documented security contract.

Fix: add filepath.IsAbs(resolved) to the guard, and add a test case with an absolute-path symlink target.

See inline comment for the exact change needed.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 99.5 AIC · ⌖ 6.24 AIC · ⊞ 4.9K

Comment thread pkg/workflow/compiler_activation_job.go Outdated
// 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, "..") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Absolute symlink targets bypass the escape guard.

Go's filepath.Join does not discard the base when the second argument is an absolute path on Linux: filepath.Join(".github", "/etc/passwd")".github/etc/passwd". That resolved path does not start with .., so the guard on this line passes silently, and ".github/etc/passwd" would be appended to extraPaths.

This is benign in practice (the path just won't exist in the sparse checkout), but it violates the security contract the comment on line 568 promises.

Suggested fix:

if filepath.IsAbs(resolved) || strings.HasPrefix(resolved, "..") {
    compilerActivationJobLog.Printf("Ignoring symlink %s -> %s: resolved path %q is unsafe", candidate, target, resolved)
    continue
}

Also add a test case that uses an absolute-path symlink target (e.g. os.Symlink("/etc/passwd", ...)) to cover this branch.

@copilot please address this.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage — §28848998303

Field Value
Category bug
Risk medium
Score 55/100 (impact:25 urgency:18 quality:12)
Action ⏸️ defer

Fix: follow symlinks for sparse checkout of .github subdirs (+159 lines, 3 files). Bot CHANGES_REQUESTED ×3. CI no data yet. Needs agent to address review feedback before re-evaluation.

Generated by 🔧 PR Triage Agent · 84.1 AIC · ⌖ 6.65 AIC · ⊞ 5.4K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, confirm the branch is up to date with the base branch, and complete any remaining PR follow-up before rerunning checks.

Generated by 👨‍🍳 PR Sous Chef · 10.8 AIC · ⌖ 4.38 AIC · ⊞ 7.1K ·
Comment /souschef to run again

Copilot AI and others added 2 commits July 7, 2026 08:33
… skip symlink tests on unsupported platforms

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, confirm the branch is up to date with the base branch, and complete any remaining PR follow-up before rerunning checks.

...

Done. The following changes were made in commits fix: resolveSymlinkExtraPaths... and fix: normalize spelling...:

  • repoRoot parameterresolveSymlinkExtraPaths now takes an explicit repoRoot string; the call site passes c.gitRoot (with os.Getwd() fallback), so detection works regardless of process CWD.
  • Security / escape check — replaced strings.HasPrefix(resolved, "..") with filepath.Rel(repoRoot, absResolved) + a segment-aware firstSeg == ".." check. Absolute symlink targets (/etc/passwd) are resolved correctly via filepath.IsAbs(target) before joining, and filepath.Rel will return ../../… which the segment check catches and rejects.
  • Forward-slash normalizationfilepath.ToSlash(rel) applied before appending to the sparse-checkout list.
  • Test portability — all three subtests that call os.Symlink now use t.Skipf("symlinks not supported: %v", err) instead of require.NoError, matching the pattern in pkg/fileutil/fileutil_test.go. The os.Chdir dependency is also removed since the function no longer relies on CWD.

Local make fmt, make lint, and make test-unit all pass. CI will need a human re-trigger to confirm.

Copilot AI requested a review from gh-aw-bot July 7, 2026 08:35
@pelikhan pelikhan merged commit eba6a73 into main Jul 7, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/fix-sparse-checkout-symlinks branch July 7, 2026 09:28
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: sparse checkout does not follow symlinks for .github/agents, causing runtime-import failures

4 participants