Skip to content

Comments

compiler: show full import chain for cycle errors#16554

Merged
pelikhan merged 3 commits intomainfrom
copilot/show-full-include-chain-errors
Feb 18, 2026
Merged

compiler: show full import chain for cycle errors#16554
pelikhan merged 3 commits intomainfrom
copilot/show-full-include-chain-errors

Conversation

Copy link
Contributor

Copilot AI commented Feb 18, 2026

Import cycles were silently skipped during topological sort, forcing operators to debug blind. A 4-file cycle A→B→C→D→B would compile successfully with incomplete output.

Changes

Cycle detection

  • Extended topologicalSortImports to detect when Kahn's algorithm fails (all nodes have in-degree > 0)
  • Added DFS-based findCyclePath to construct full chain from cycle subgraph
  • Returns ImportCycleError with complete path including back-edge

Error formatting

  • New ImportCycleError type captures full chain: ["b.md", "c.md", "d.md", "b.md"]
  • FormatImportCycleError renders multiline indented output with visual hierarchy
  • Deterministic ordering via alphabetical sorting of equivalent starting points

Test coverage

  • 2-file, 4-file, and N-file cycle scenarios
  • Deterministic output verification across runs
  • Updated TestCyclicImports to expect errors instead of silent success

Example output

Import cycle detected

The following import chain creates a circular dependency:

b.md (starting point)
  ↳ imports c.md
    ↳ imports d.md
      ↳ b.md ⚠️  cycles back to b.md

To fix this issue:
1. Review the import dependencies in the files listed above
2. Remove one of the imports to break the cycle
3. Consider restructuring your workflow imports to avoid circular dependencies

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha runs/20260218-131346-16603/test-254345567/.github/workflows -run /usr/bin/bash l ./pkg/workflow/ x_amd64/vet 0284250/b359/importcfg --no�� k/gh-aw/gh-aw/pkg/cli/access_log.go k/gh-aw/gh-aw/pkg/cli/actionlint.go /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet g_.a --global /node /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha g_.a /tmp/go-build4099137013/b287/vet.cfg tartedAt,updatedAt,event,headBranch,headSha,displayTitle (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha ithub/workflows/agent-performance-analyzer.md scripts/**/*.js ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile .prettierignore (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha echo "��� JavaScript files formatted" (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v4 --jq .object.sha user.name Test User /usr/bin/git on' --ignore-patgit (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --noprofile (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha om/owner/repo.git rev-parse /usr/bin/git --noprofile .cfg x_amd64/cgo git conf�� user.email test@example.com /usr/bin/git ub/workflows --noheadings 0/x64/bin/sh git (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha --local pull.rebase ndor/bin/bash (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha HEAD (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha --write scripts/**/*.js /usr/bin/bash .prettierignore (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha 0284250/b123/_pkg_.a scripts/**/*.js 0284250/b123=> .prettierignore b/gh-aw/cmd/gh-arev-parse (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts
    • Triggering command: /usr/bin/gh gh run download 1 --dir test-logs/run-1 9137013/b225/vet.cfg ode -n1 --format=format:rev-parse cf4abeacf031ce21--show-toplevel /usr/lib/systemd/systemd-executoconfig tion�� 69 --log-level x_amd64/link --log-target journal-or-kmsg -main/dist/ripgr--show-toplevel x_amd64/link (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts
    • Triggering command: /usr/bin/gh gh run download 12345 --dir test-logs/run-12345 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts
    • Triggering command: /usr/bin/gh gh run download 12346 --dir test-logs/run-12346 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts
    • Triggering command: /usr/bin/gh gh run download 2 --dir test-logs/run-2 9137013/b223/vet.cfg x_amd64/compile -n1 --format=format:rev-parse cf4abeacf031 x_amd64/compile tion�� 9137013/b123/embedcfg (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts
    • Triggering command: /usr/bin/gh gh run download 3 --dir test-logs/run-3 (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts
    • Triggering command: /usr/bin/gh gh run download 4 --dir test-logs/run-4 9137013/b224/vet.cfg ache/node/24.13.0/x64/bin/bash --merged cf4abeacf031ce21rev-parse (http block)
  • https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts
    • Triggering command: /usr/bin/gh gh run download 5 --dir test-logs/run-5 cf4abeacf031ce210136c5b52f2ecd56-w tions/setup/js/node_modules/.bin-buildmode=exe (http block)
  • https://api.github.com/repos/github/gh-aw/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path w/js/**/*.json' --ignore-path origin x_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 er ./pkg/logger/slo/tmp/go-build2230284250/b377/_pkg_.a 64/pkg/tool/linu-trimpath --no�� th .prettierigno-p .cfg odules/npm/node_-lang=go1.25 ./pkg/logger/slogit %H %ct %D ./pkg/constants//tmp/gh-aw-test-runs/20260218-131346-16603/test-3341678157/.github/workflows ortcfg (http block)
    • Triggering command: /usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 gicalSortImportsrev-parse (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha */*.json' '!../../../pkg/workflo-test.timeout=10m0s 9137013/b279/vet.cfg x_amd64/link (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha */*.json' '!../../../pkg/workflo-p 9137013/b285/vet.cfg 64/pkg/tool/linux_amd64/link yBeforeUnloadChegit (http block)
  • https://api.github.com/repos/nonexistent/repo/actions/runs/12345
    • Triggering command: /usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion (http block)
  • https://api.github.com/repos/owner/repo/actions/workflows
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/pkg/tool/linux_amd64/vet (http block)
    • Triggering command: /usr/bin/gh gh workflow list --json name,state,path --repo owner/repo x_amd64/link (http block)
  • https://api.github.com/repos/owner/repo/contents/file.md
    • Triggering command: /tmp/go-build2230284250/b368/cli.test /tmp/go-build2230284250/b368/cli.test -test.testlogfile=/tmp/go-build2230284250/b368/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true g_adapter.go or_formatting_techeckout constants.go constants_test.go /pre�� = get && echo "******"; }; f get-errorsas = get && echo "******"; }; f get-ifaceassert 64/pkg/tool/linux_amd64/vet (http block)
  • https://api.github.com/repos/test-owner/test-repo/actions/secrets
    • Triggering command: /usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name set-url origin x_amd64/vet (http block)
  • https://api.github.com/user
    • Triggering command: /usr/bin/gh gh api user --jq .login (http block)
    • Triggering command: /usr/bin/gh gh api user --jq .login g_.a --global x_amd64/vet (http block)
    • Triggering command: `/usr/bin/gh gh api user --jq .login get r type with full chain tracking
  • Enhance topologicalSortImports to detect and report cycles
  • Add DFS-based cycle path finding with deterministic output
  • Format errors with x_amd64/vet credential.helpesh s $k => $v) echo-c` (http block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>compiler: show full include chain for import-cycle errors</issue_title>
<issue_description>Operators lose time debugging import cycles because compile errors currently show only a pair of files instead of the full include/import chain that triggered the cycle.

Why now:

  • Nested import usage has increased.
  • We are seeing recurring triage friction when cycle depth is >2.

Expected behavior:

  • Import-cycle failures report a deterministic full chain (including the first back-edge) so operators can fix the cycle in one pass.

Evidence packet:

  • Commit under test: origin/main @ 1f71659
  • Runtime: Go 1.25.7, Darwin arm64 (macOS 26 kernel 25.3.0)
  • Minimal repro:
    1. Create 4 workflows with imports A->B->C->D->B
    2. Run gh aw compile on A
    3. Observe error text
  • Expected: deterministic chain with back-edge path.
  • Actual: partial cycle context (insufficient path detail).

Acceptance / validation:

  • Unit test for a 4-file cycle verifies full deterministic chain output.
  • Error still fails closed and remains deterministic across runs.
    </issue_description>

Comments on the Issue (you are @copilot in this section)

@pelikhan We used to generate a full mermaid graph in the header @pelikhan The error message should be delightful multiline indented agent friendly.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits February 18, 2026 13:12
- Add ImportCycleError type with full chain tracking
- Enhance topologicalSortImports to detect and report cycles
- Add DFS-based cycle path finding with deterministic output
- Format errors with multiline indented agent-friendly display
- Add comprehensive tests for 2-file and 4-file cycles
- Update existing TestCyclicImports to expect error (not silent handling)
- Tests verify deterministic output and formatted error messages

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Use require.ErrorAs instead of assert.ErrorAs for error assertions
- Use assert.Len instead of assert.Equal for length checks
- All linting and tests now pass

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix import-cycle error reporting to show full include chain compiler: show full import chain for cycle errors Feb 18, 2026
Copilot AI requested a review from pelikhan February 18, 2026 13:20
@pelikhan pelikhan marked this pull request as ready for review February 18, 2026 13:23
Copilot AI review requested due to automatic review settings February 18, 2026 13:23
@pelikhan pelikhan merged commit 26a0d4f into main Feb 18, 2026
128 checks passed
@pelikhan pelikhan deleted the copilot/show-full-include-chain-errors branch February 18, 2026 13:27
Copy link
Contributor

Copilot AI left a comment

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 implements comprehensive import cycle detection and reporting for the workflow compiler, replacing silent cycle handling with detailed error messages showing the complete dependency chain.

Changes:

  • Adds cycle detection to Kahn's topological sort algorithm with DFS-based path reconstruction
  • Introduces ImportCycleError type with formatted multiline output showing full import chain and back-edge
  • Updates existing tests to expect cycle errors instead of silent success, and adds comprehensive test coverage for 2-file, 4-file, and N-file cycles with deterministic output verification

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/parser/import_error.go Defines ImportCycleError type and FormatImportCycleError function for user-friendly multiline error formatting
pkg/parser/import_processor.go Enhances topologicalSortImports to detect cycles, adds findCyclePath and dfsForCycle functions for cycle path reconstruction with deterministic ordering
pkg/parser/import_cycle_test.go Comprehensive test suite for cycle detection covering 2-file, 4-file, and multi-run deterministic scenarios, plus error formatting validation
pkg/workflow/compiler_orchestrator_engine.go Integrates ImportCycleError handling with special formatting before returning to caller
pkg/workflow/imports_recursive_test.go Updates TestCyclicImports to verify cycle detection errors instead of expecting silent success
Comments suppressed due to low confidence (1)

pkg/parser/import_error.go:43

  • The arrow symbol "→" used in the error message may not render consistently across all terminals and systems. Since the formatted error (FormatImportCycleError) uses "↳" for arrows, consider using a consistent symbol or ensuring both are ASCII-compatible for better cross-platform compatibility, especially in CI/CD environments that may have limited Unicode support.
	return fmt.Sprintf("circular import detected: %s", strings.Join(e.Chain, " → "))

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

messageBuilder.WriteString(fmt.Sprintf("%s%s (starting point)\n", indent, file))
} else if i == len(err.Chain)-1 {
// Last item is the back-edge - highlight it
messageBuilder.WriteString(fmt.Sprintf("%s↳ %s ⚠️ cycles back to %s\n", indent, file, err.Chain[0]))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The emoji character "⚠️" (warning sign) may not render consistently across all terminals and systems. Consider using a text-based alternative like "[!]" or "WARNING:" for better cross-platform compatibility, especially since this error message needs to be "agent friendly" as mentioned in the PR description.

This issue also appears on line 43 of the same file.

Suggested change
messageBuilder.WriteString(fmt.Sprintf("%s↳ %s ⚠️ cycles back to %s\n", indent, file, err.Chain[0]))
messageBuilder.WriteString(fmt.Sprintf("%s↳ %s [!] cycles back to %s\n", indent, file, err.Chain[0]))

Copilot uses AI. Check for mistakes.

// Process imports from file A - should detect cycle
frontmatter := map[string]any{
"imports": []string{"a.md"},
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The test creates a file A that imports B, and a file B that imports A, forming a cycle A→B→A. However, the frontmatter passed to ProcessImportsFromFrontmatterWithSource on line 43 uses "imports": []string{"a.md"}, which means it's importing A from A itself. This creates a self-import rather than testing the A→B→A cycle. The frontmatter should either be empty (testing from A's perspective) or should match what's actually in fileAContent (which imports B). Consider changing line 43 to use "imports": []string{"b.md"} to match the actual content of file A, or clarify the test intent.

Suggested change
"imports": []string{"a.md"},
"imports": []string{"b.md"},

Copilot uses AI. Check for mistakes.
Comment on lines +1012 to +1020
for _, imp := range imports {
found := false
for _, processed := range result {
if processed == imp {
found = true
break
}
}
if !found {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The cycle node identification uses a nested loop with O(n*m) complexity where n is the number of imports and m is the number of processed results. This could be optimized by converting the result slice to a map for O(1) lookup, reducing complexity to O(n). Consider using a map[string]bool for faster lookups, especially for large import graphs.

Suggested change
for _, imp := range imports {
found := false
for _, processed := range result {
if processed == imp {
found = true
break
}
}
if !found {
// Build a set of processed imports for O(1) membership checks
processedSet := make(map[string]bool, len(result))
for _, processed := range result {
processedSet[processed] = true
}
for _, imp := range imports {
if !processedSet[imp] {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

compiler: show full include chain for import-cycle errors

2 participants