Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
73 changes: 73 additions & 0 deletions pkg/cli/add_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,3 +1059,76 @@ func TestAddWorkflowWithDispatchWorkflowFromSharedImport(t *testing.T) {
assert.Contains(t, string(lockContent), "haiku-printer",
"lock file should reference the haiku-printer dispatch-workflow target")
}

// TestAddWorkflowWithRecursiveSharedImports verifies that `gh aw add` recursively
// downloads all transitively-imported shared markdown files.
//
// daily-compiler-quality.md (at commit 8d26856) has this two-level import tree:
//
// daily-compiler-quality.md
// ├── shared/daily-audit-base.md (direct)
// │ ├── shared/daily-audit-discussion.md (nested level 2)
// │ ├── shared/reporting.md (nested level 2)
// │ └── shared/observability-otlp.md (nested level 2)
// └── shared/go-source-analysis.md (direct)
// ├── shared/mcp/serena-go.md (nested level 2)
// │ └── shared/mcp/serena.md (nested level 3, via "./serena.md")
// └── shared/reporting.md (nested level 2, shared with above)
Comment on lines +1066 to +1076
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The comment says this workflow has a "two-level import tree", but the diagram includes a nested level 3 import (serena-go.md → ./serena.md). Consider updating the wording to "multi-level" or "three-level" to avoid confusion when maintaining the pinned tree in the future.

Copilot uses AI. Check for mistakes.
//
// This test would fail without the fix to fetchFrontmatterImportsRecursive that
// resolves non-explicit relative paths (e.g. "shared/foo.md") against originalBaseDir
// rather than currentBaseDir.
//
// This test requires GitHub authentication.
func TestAddWorkflowWithRecursiveSharedImports(t *testing.T) {
authCmd := exec.Command("gh", "auth", "status")
if err := authCmd.Run(); err != nil {
t.Skip("Skipping test: GitHub authentication not available (gh auth status failed)")
}

setup := setupAddIntegrationTest(t)
defer setup.cleanup()

// Pin to commit 8d26856 so the import tree is stable and reproducible.
workflowSpec := "github/gh-aw/.github/workflows/daily-compiler-quality.md@8d26856"

cmd := exec.Command(setup.binaryPath, "add", workflowSpec, "--verbose")
cmd.Dir = setup.tempDir
output, err := cmd.CombinedOutput()
outputStr := string(output)
t.Logf("Command output:\n%s", outputStr)

require.NoError(t, err, "add command should succeed: %s", outputStr)

workflowsDir := filepath.Join(setup.tempDir, ".github", "workflows")

// 1. Main workflow must be present.
require.FileExists(t, filepath.Join(workflowsDir, "daily-compiler-quality.md"),
"main workflow daily-compiler-quality.md should exist")

// 2. Direct imports must be present.
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "daily-audit-base.md"),
"direct import shared/daily-audit-base.md should be fetched")
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "go-source-analysis.md"),
"direct import shared/go-source-analysis.md should be fetched")

// 3. Transitive imports via shared/daily-audit-base.md must be present.
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "daily-audit-discussion.md"),
"transitive import shared/daily-audit-discussion.md (via daily-audit-base) should be fetched")
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "reporting.md"),
"transitive import shared/reporting.md (via daily-audit-base) should be fetched")
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "observability-otlp.md"),
"transitive import shared/observability-otlp.md (via daily-audit-base) should be fetched")

// 4. Transitive imports via shared/go-source-analysis.md must be present.
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "mcp", "serena-go.md"),
"transitive import shared/mcp/serena-go.md (via go-source-analysis) should be fetched")
// serena-go.md imports ./serena.md (explicitly-relative), which should resolve to
// shared/mcp/serena.md and be fetched correctly.
assert.FileExists(t, filepath.Join(workflowsDir, "shared", "mcp", "serena.md"),
"deep transitive import shared/mcp/serena.md (via go-source-analysis → serena-go.md) should be fetched")

// 5. Compilation must have succeeded (lock file present).
assert.FileExists(t, filepath.Join(workflowsDir, "daily-compiler-quality.lock.yml"),
"compiled lock file daily-compiler-quality.lock.yml should exist")
}
31 changes: 27 additions & 4 deletions pkg/cli/includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,39 @@ func fetchFrontmatterImportsRecursive(content, owner, repo, ref, currentBaseDir,
continue
}

// Resolve the remote file path relative to the current file's directory.
// Resolve the remote file path to an absolute repo path.
// Use path (not filepath) because this is always a forward-slash URL/API path.
var remoteFilePath string
if rest, ok := strings.CutPrefix(filePath, "/"); ok {
// Absolute path from repo root (e.g. "/scripts/helper.md")
remoteFilePath = rest
} else if currentBaseDir != "" {
remoteFilePath = path.Join(currentBaseDir, filePath)
} else if strings.HasPrefix(filePath, "./") || strings.HasPrefix(filePath, "../") {
// Explicitly-relative path (e.g. "./serena.md"): resolve relative to the
// current importing file's directory so that sibling-file references work
// correctly regardless of nesting depth.
if currentBaseDir != "" {
remoteFilePath = path.Join(currentBaseDir, filePath)
} else {
remoteFilePath = filePath
}
} else {
remoteFilePath = filePath
// Non-explicit relative path (e.g. "shared/foo.md"): resolve relative to the
// original base directory (the top-level workflow's directory). Workflows in
// this repository write shared import paths relative to the workflow root
// (e.g. ".github/workflows"), not relative to the importing file's own
// directory. Resolving against originalBaseDir instead of currentBaseDir
// ensures that a file at ".github/workflows/shared/base.md" can import
// "shared/helper.md" and have it resolve to ".github/workflows/shared/helper.md"
// rather than the incorrect ".github/workflows/shared/shared/helper.md".
baseDir := originalBaseDir
if baseDir == "" {
baseDir = currentBaseDir
}
if baseDir != "" {
remoteFilePath = path.Join(baseDir, filePath)
} else {
remoteFilePath = filePath
}
}
remoteFilePath = path.Clean(remoteFilePath)

Expand Down
Loading