diff --git a/docs/adr/43128-thread-context-through-remote-fetch-apis.md b/docs/adr/43128-thread-context-through-remote-fetch-apis.md new file mode 100644 index 00000000000..2ab11ed111d --- /dev/null +++ b/docs/adr/43128-thread-context-through-remote-fetch-apis.md @@ -0,0 +1,48 @@ +# ADR-43128: Thread context.Context Through Public Remote Fetch APIs + +**Date**: 2026-07-03 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The 8 public APIs in `pkg/parser/remote_fetch.go` (`DownloadFileFromGitHub`, `DownloadFileFromGitHubForHost`, `ResolveRefToSHAForHost`, `ListWorkflowFiles`, `ListWorkflowFilesForHost`, `ListDirAllFilesForHost`, `ListDirAllFilesRecursivelyForHost`, `ListDirSubdirsForHost`) and all their private helpers hard-coded `context.Background()` throughout their call chains. This made it impossible for CLI commands to propagate cancellation signals: pressing Ctrl-C could not interrupt in-flight HTTP requests because no cancellable context was reachable by the HTTP layer. Go's `context.Context` idiom requires passing a caller-supplied context as the first parameter of I/O-bound functions so that the caller can cancel, time out, or add deadlines. Without this, each CLI command that fetches remote resources silently blocks until the HTTP stack times out on its own, degrading the interactive user experience. + +### Decision + +We will add `ctx context.Context` as the first parameter to all 8 public remote fetch APIs and propagate it through every private helper in the same call chain, replacing all 7 hard-coded `context.Background()` call sites. All CLI callers are updated to pass `cmd.Context()` (the Cobra command context, which is cancelled on Ctrl-C). Two remaining hard-coded `context.Background()` calls inside `downloadIncludeFromWorkflowSpec` / `resolveWorkflowSpecSHAForCache` — which sit behind `ResolveIncludePath` across 6+ files in 4 packages — are deferred to a follow-up PR because threading context through that compile-time include-resolution path requires a broader coordinated change. + +### Alternatives Considered + +#### Alternative 1: Keep context.Background() — Status Quo + +The simplest option is to leave all call sites as-is. This avoids any API surface change. It was rejected because it permanently prevents signal propagation: Ctrl-C cannot cancel HTTP calls, long-running fetches block indefinitely on poor connections, and there is no path to adding per-operation timeouts in the future without the same refactor. + +#### Alternative 2: Use a Package-Level or Global Cancellable Context + +A global or package-level `context.Context` could be set at startup and read by `remote_fetch.go` without changing any function signatures. This avoids the API churn. It was rejected because it couples the library to a mutable global state, makes behaviour non-deterministic in tests (shared state across goroutines), and is explicitly against the Go standard library's context design guidelines, which require context to flow through the call graph as an explicit argument. + +#### Alternative 3: Wrap the HTTP Client with a Default Timeout Instead + +Adding a fixed timeout (e.g., 30 s) to the `http.Client` used internally would automatically abort hung requests. This would be simpler than threading context everywhere. It was rejected because a fixed timeout cannot be adjusted by callers, cannot be cancelled early on user interrupt (Ctrl-C fires before the timeout), and does not compose with higher-level deadlines or tracing. It addresses the symptom rather than the root cause. + +### Consequences + +#### Positive +- CLI commands (e.g., `gh aw add`, `gh aw list`) now respond to Ctrl-C by cancelling in-flight HTTP requests immediately, eliminating hung-process behaviour. +- The codebase now follows standard Go context idioms throughout the remote I/O layer, making future work (per-command timeouts, distributed tracing, cancellation propagation) straightforward. +- `cmd.Context()` from Cobra is wired end-to-end, meaning any future context metadata (deadlines, trace IDs) attached by callers is automatically available to all remote fetch operations. + +#### Negative +- The change touches 15 files and ~20+ function signatures across `pkg/parser/`, `pkg/cli/`, and related packages; it is a wide refactor with high merge-conflict potential for concurrent branches. +- Two `context.Background()` call sites inside the compile-time include-resolution path (`downloadIncludeFromWorkflowSpec`, `resolveWorkflowSpecSHAForCache` via `ResolveIncludePath`) are intentionally deferred, leaving partial context propagation in that sub-system until a follow-up PR. + +#### Neutral +- Callers that previously called the public APIs without a context must now supply one; any future consumer of this library (internal or external) must comply with the updated signatures. +- The private wrapper functions in `add_package_manifest.go` (e.g., `downloadPackageFileFromGitHubForHost`, `listPackageDirFilesForHost`) gained `ctx` parameters alongside the public API changes; these are not breaking changes to exported symbols but do affect internal package coupling. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 732b2ad264a..a24de09699f 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -405,7 +405,7 @@ func compileAddedWorkflow(ctx context.Context, destFile string, workflowSpec *Wo // parse the fully merged safe-outputs configuration to discover any dispatch workflows // that originate from imported shared workflows (not visible in the raw frontmatter). if !isLocalWorkflowPath(workflowSpec.WorkflowPath) { - fetchAndSaveDispatchWorkflowsFromParsedFile(destFile, workflowSpec, githubWorkflowsDir, opts.Verbose, opts.Force, tracker) + fetchAndSaveDispatchWorkflowsFromParsedFile(ctx, destFile, workflowSpec, githubWorkflowsDir, opts.Verbose, opts.Force, tracker) } // Compile any dispatch-workflow .md dependencies that were just fetched and lack a // .lock.yml. The dispatch-workflow validator requires every .md dispatch target to be diff --git a/pkg/cli/add_package_manifest.go b/pkg/cli/add_package_manifest.go index 6ddc2f35428..fe36c7c1c9e 100644 --- a/pkg/cli/add_package_manifest.go +++ b/pkg/cli/add_package_manifest.go @@ -75,7 +75,7 @@ func (e packageRemoteNotFoundError) Unwrap() []error { return []error{errRepositoryPackageFileNotFound, e.cause} } -func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedRepositoryPackage, error) { +func resolveRepositoryPackage(ctx context.Context, repoSpec *RepoSpec, host string) (*resolvedRepositoryPackage, error) { parts := strings.SplitN(repoSpec.RepoSlug, "/", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid repository slug: %s", repoSpec.RepoSlug) @@ -104,7 +104,7 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit } packagePath := strings.Trim(repoSpec.PackagePath, "/") - manifestPath, manifestContent, err := loadRepositoryPackageManifestFile(owner, repo, packagePath, ref, host) + manifestPath, manifestContent, err := loadRepositoryPackageManifestFile(ctx, owner, repo, packagePath, ref, host) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit installationSources := normalizePackageInstallablePaths(includeInstallablePaths, packagePath) if len(installationSources) == 0 { - installationSources, err = scanRepositoryPackageInstallablePaths(owner, repo, packagePath, ref, host) + installationSources, err = scanRepositoryPackageInstallablePaths(ctx, owner, repo, packagePath, ref, host) if err != nil { return nil, err } @@ -128,7 +128,7 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit return nil, err } - docsPath, err := resolveRepositoryPackageDocsPath(owner, repo, packagePath, ref, host) + docsPath, err := resolveRepositoryPackageDocsPath(ctx, owner, repo, packagePath, ref, host) if err != nil { return nil, err } @@ -136,7 +136,7 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit // Resolve skill files: explicit from manifest or auto-scanned. explicitSkillDirs := append([]string{}, manifest.Skills...) explicitSkillDirs = append(explicitSkillDirs, includeSkillDirs...) - skillFiles, skillWarnings, err := resolvePackageSkillFiles(owner, repo, packagePath, ref, host, explicitSkillDirs) + skillFiles, skillWarnings, err := resolvePackageSkillFiles(ctx, owner, repo, packagePath, ref, host, explicitSkillDirs) if err != nil { return nil, err } @@ -145,7 +145,7 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit // Resolve agent files: explicit from manifest or auto-scanned. explicitAgentFiles := append([]string{}, manifest.Agents...) explicitAgentFiles = append(explicitAgentFiles, includeAgentFiles...) - agentFiles, agentWarnings, err := resolvePackageAgentFiles(owner, repo, packagePath, ref, host, explicitAgentFiles) + agentFiles, agentWarnings, err := resolvePackageAgentFiles(ctx, owner, repo, packagePath, ref, host, explicitAgentFiles) if err != nil { return nil, err } @@ -170,11 +170,11 @@ func resolveRepositoryPackage(repoSpec *RepoSpec, host string) (*resolvedReposit }, nil } -func loadRepositoryPackageManifestFile(owner, repo, packagePath, ref, host string) (string, []byte, error) { +func loadRepositoryPackageManifestFile(ctx context.Context, owner, repo, packagePath, ref, host string) (string, []byte, error) { manifestPath := joinRepositoryPackagePath(packagePath, repositoryPackageManifestFileName) repoSlug := owner + "/" + repo packageID := repositoryPackageIdentifier(repoSlug, packagePath) - content, err := downloadPackageFileFromGitHubForHost(owner, repo, manifestPath, ref, host) + content, err := downloadPackageFileFromGitHubForHost(ctx, owner, repo, manifestPath, ref, host) if err != nil { if !isRepositoryFileNotFound(err) { return "", nil, fmt.Errorf("failed to read manifest %q from %s/%s@%s: %w", manifestPath, owner, repo, ref, err) @@ -528,7 +528,7 @@ func agentDirectoryRoot(cleaned string) string { // skills/ directory is always auto-scanned for any additional skill subdirectories that // contain a SKILL.md file but are not already covered by the manifest. Each skill folder // is traversed recursively so that all nested files are included. -func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explicitSkillDirs []string) ([]resolvedPackageSkillFile, []string, error) { +func resolvePackageSkillFiles(ctx context.Context, owner, repo, packagePath, ref, host string, explicitSkillDirs []string) ([]resolvedPackageSkillFile, []string, error) { // seenSkillDirs tracks full skill directories already added so that auto-scanned // duplicates of manifest-specified skills are not added a second time. seenSkillDirs := make(map[string]struct{}) @@ -541,7 +541,7 @@ func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explic } // Step 2: always auto-scan and append any skills not already in the manifest. - autoScanned, err := scanPackageSkillDirs(owner, repo, packagePath, ref, host) + autoScanned, err := scanPackageSkillDirs(ctx, owner, repo, packagePath, ref, host) if err != nil { // Auto-scan is supplementary for manifest-declared skills; preserve manifest // resolution even when scan fails transiently. @@ -579,7 +579,7 @@ func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explic // exists so that typos in the manifest surface as clear warnings. if _, fromManifest := manifestSkillDirSet[skillDir]; fromManifest { markerPath := joinRepositoryPackagePath(skillDir, packageSkillMarkerFile) - if _, err := downloadPackageFileFromGitHubForHost(owner, repo, markerPath, ref, host); err != nil { + if _, err := downloadPackageFileFromGitHubForHost(ctx, owner, repo, markerPath, ref, host); err != nil { if isRepositoryFileNotFound(err) { warnings = append(warnings, fmt.Sprintf("Skill directory %q is missing required %s marker file", skillDir, packageSkillMarkerFile)) continue @@ -590,7 +590,7 @@ func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explic skillName := filepath.Base(skillDir) // Use recursive listing so that the entire skill folder (including any // subdirectories) is copied, not just the top-level files. - files, err := listPackageDirFilesRecursivelyForHost(owner, repo, ref, skillDir, host) + files, err := listPackageDirFilesRecursivelyForHost(ctx, owner, repo, ref, skillDir, host) if err != nil { if isRepositoryFileNotFound(err) { warnings = append(warnings, fmt.Sprintf("Skill directory %q not found in package, skipping", skillDir)) @@ -611,7 +611,7 @@ func resolvePackageSkillFiles(owner, repo, packagePath, ref, host string, explic // resolvePackageAgentFiles returns the list of agent file source paths for a package. // If explicitAgentFiles is non-empty it is used; otherwise the agents/ directory is // auto-scanned for .md files. -func resolvePackageAgentFiles(owner, repo, packagePath, ref, host string, explicitAgentFiles []string) ([]string, []string, error) { +func resolvePackageAgentFiles(ctx context.Context, owner, repo, packagePath, ref, host string, explicitAgentFiles []string) ([]string, []string, error) { if len(explicitAgentFiles) > 0 { var agentFiles []string for _, f := range explicitAgentFiles { @@ -623,7 +623,7 @@ func resolvePackageAgentFiles(owner, repo, packagePath, ref, host string, explic var agentFiles []string for _, root := range []string{packageAgentsDirectory, constants.GithubDir + packageAgentsDirectory} { agentsDir := joinRepositoryPackagePath(packagePath, root) - files, err := listPackageDirFilesForHost(owner, repo, ref, agentsDir, host) + files, err := listPackageDirFilesForHost(ctx, owner, repo, ref, agentsDir, host) if err != nil { if isRepositoryFileNotFound(err) { continue @@ -641,11 +641,11 @@ func resolvePackageAgentFiles(owner, repo, packagePath, ref, host string, explic // scanPackageSkillDirs auto-scans the skills/ directory of a package and returns the paths // of skill subdirectories (those that contain a SKILL.md file). -func scanPackageSkillDirs(owner, repo, packagePath, ref, host string) ([]string, error) { +func scanPackageSkillDirs(ctx context.Context, owner, repo, packagePath, ref, host string) ([]string, error) { var skillDirs []string for _, root := range []string{packageSkillsDirectory, constants.GithubDir + packageSkillsDirectory} { skillsDir := joinRepositoryPackagePath(packagePath, root) - subdirs, err := listPackageDirSubdirsForHost(owner, repo, ref, skillsDir, host) + subdirs, err := listPackageDirSubdirsForHost(ctx, owner, repo, ref, skillsDir, host) if err != nil { if isRepositoryFileNotFound(err) { continue @@ -654,7 +654,7 @@ func scanPackageSkillDirs(owner, repo, packagePath, ref, host string) ([]string, } for _, subdir := range subdirs { markerPath := joinRepositoryPackagePath(subdir, packageSkillMarkerFile) - if _, err := downloadPackageFileFromGitHubForHost(owner, repo, markerPath, ref, host); err == nil { + if _, err := downloadPackageFileFromGitHubForHost(ctx, owner, repo, markerPath, ref, host); err == nil { skillDirs = append(skillDirs, subdir) } } @@ -662,13 +662,13 @@ func scanPackageSkillDirs(owner, repo, packagePath, ref, host string) ([]string, return skillDirs, nil } -func scanRepositoryPackageInstallablePaths(owner, repo, packagePath, ref, host string) ([]string, error) { +func scanRepositoryPackageInstallablePaths(ctx context.Context, owner, repo, packagePath, ref, host string) ([]string, error) { var collected []string seen := make(map[string]struct{}) for _, sourceDir := range packageSourceDirectories { sourcePath := joinRepositoryPackagePath(packagePath, sourceDir) - files, err := listPackageWorkflowFilesForHost(owner, repo, ref, sourcePath, host) + files, err := listPackageWorkflowFilesForHost(ctx, owner, repo, ref, sourcePath, host) if err != nil { if isRepositoryFileNotFound(err) { continue @@ -699,11 +699,11 @@ func scanRepositoryPackageInstallablePaths(owner, repo, packagePath, ref, host s return collected, nil } -func resolveRepositoryPackageDocsPath(owner, repo, packagePath, ref, host string) (string, error) { +func resolveRepositoryPackageDocsPath(ctx context.Context, owner, repo, packagePath, ref, host string) (string, error) { readmePath := joinRepositoryPackagePath(packagePath, "README.md") repoSlug := owner + "/" + repo packageID := repositoryPackageIdentifier(repoSlug, packagePath) - if _, err := downloadPackageFileFromGitHubForHost(owner, repo, readmePath, ref, host); err == nil { + if _, err := downloadPackageFileFromGitHubForHost(ctx, owner, repo, readmePath, ref, host); err == nil { return readmePath, nil } else if isRepositoryFileNotFound(err) { return "", fmt.Errorf("repository %q is not a valid Agentic Workflow package: missing required README.md at %q", packageID, readmePath) @@ -877,28 +877,28 @@ func validateUniqueManifestWorkflowFilenames(paths []string, manifestPath string return nil } -func downloadRepositoryPackageFileFromGitHubForHost(owner, repo, path, ref, host string) ([]byte, error) { - content, err := parser.DownloadFileFromGitHubForHost(owner, repo, path, ref, host) +func downloadRepositoryPackageFileFromGitHubForHost(ctx context.Context, owner, repo, path, ref, host string) ([]byte, error) { + content, err := parser.DownloadFileFromGitHubForHost(ctx, owner, repo, path, ref, host) return content, normalizeRepositoryPackageRemoteError(err) } -func listRepositoryPackageWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]string, error) { - files, err := parser.ListWorkflowFilesForHost(owner, repo, ref, workflowPath, host) +func listRepositoryPackageWorkflowFilesForHost(ctx context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { + files, err := parser.ListWorkflowFilesForHost(ctx, owner, repo, ref, workflowPath, host) return files, normalizeRepositoryPackageRemoteError(err) } -func listRepositoryPackageDirFilesForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - files, err := parser.ListDirAllFilesForHost(owner, repo, ref, dirPath, host) +func listRepositoryPackageDirFilesForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + files, err := parser.ListDirAllFilesForHost(ctx, owner, repo, ref, dirPath, host) return files, normalizeRepositoryPackageRemoteError(err) } -func listRepositoryPackageDirFilesRecursivelyForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - files, err := parser.ListDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host) +func listRepositoryPackageDirFilesRecursivelyForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + files, err := parser.ListDirAllFilesRecursivelyForHost(ctx, owner, repo, ref, dirPath, host) return files, normalizeRepositoryPackageRemoteError(err) } -func listRepositoryPackageDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - dirs, err := parser.ListDirSubdirsForHost(owner, repo, ref, dirPath, host) +func listRepositoryPackageDirSubdirsForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + dirs, err := parser.ListDirSubdirsForHost(ctx, owner, repo, ref, dirPath, host) return dirs, normalizeRepositoryPackageRemoteError(err) } diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index 3dc6622e4a0..f4721b8ab90 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -42,15 +42,15 @@ func TestResolveRepositoryPackage(t *testing.T) { getRepositoryPackageLatestRelease = func(repoSlug, host string) (string, error) { return "", errors.New("no releases found") } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } t.Run("uses aw manifest files and README docs", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: Repo Assist @@ -68,12 +68,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, "aw.yml", pkg.ManifestPath) assert.Equal(t, "Repo Assist", pkg.Name) @@ -102,7 +102,7 @@ files: assert.Equal(t, "github.com", host) return "master", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { assert.Equal(t, "master", ref) switch path { case "aw.yml": @@ -113,7 +113,7 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { assert.Equal(t, "master", ref) switch workflowPath { case "workflows": @@ -125,7 +125,7 @@ files: } } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "github.com") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "github.com") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) }) @@ -146,7 +146,7 @@ files: t.Fatalf("default branch lookup should not be called when latest release is available") return "", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { assert.Equal(t, "v1.2.3", ref) switch path { case "aw.yml": @@ -157,7 +157,7 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { assert.Equal(t, "v1.2.3", ref) switch workflowPath { case "workflows": @@ -169,7 +169,7 @@ files: } } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "github/gh-aw"}, "github.com") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "github/gh-aw"}, "github.com") require.NoError(t, err) assert.Equal(t, "v1.2.3", pkg.ResolvedRef) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) @@ -192,7 +192,7 @@ files: assert.Equal(t, "github.com", host) return "main", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { assert.Equal(t, "main", ref) switch path { case "aw.yml": @@ -203,7 +203,7 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { assert.Equal(t, "main", ref) switch workflowPath { case "workflows": @@ -215,7 +215,7 @@ files: } } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "github/gh-aw"}, "github.com") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "github/gh-aw"}, "github.com") require.NoError(t, err) assert.Equal(t, "main", pkg.ResolvedRef) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) @@ -230,7 +230,7 @@ files: t.Fatalf("default branch lookup should not be called when version is provided") return "", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { assert.Equal(t, "feature/github-agentic-workflow", ref) switch path { case "agentic-workflows/aw.yml": @@ -241,12 +241,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{ + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{ RepoSlug: "owner/repo", PackagePath: "agentic-workflows", Version: "feature/github-agentic-workflow", @@ -256,7 +256,7 @@ files: }) t.Run("falls back to scanning supported workflow directories", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte("name: Repo Assist\n"), nil @@ -266,7 +266,7 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { assert.Empty(t, host) switch workflowPath { case "workflows": @@ -278,14 +278,14 @@ files: } } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, "README.md", pkg.DocsPath) assert.Equal(t, []string{"workflows/review.md", ".github/workflows/nightly-review.md"}, pkg.InstallationSource) }) t.Run("passes explicit host to scanning fallback", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": assert.Equal(t, "github.com", host) @@ -297,7 +297,7 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { assert.Equal(t, "github.com", host) switch workflowPath { case "workflows": @@ -309,50 +309,50 @@ files: } } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "github.com") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "github.com") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) }) t.Run("rejects manifest without name field", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte("description: missing name\n"), nil } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `name must be a non-empty string`) }) t.Run("requires aw manifest when only legacy alias exists", func(t *testing.T) { var requestedPaths []string - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { requestedPaths = append(requestedPaths, path) if path == "agents.yml" { return []byte("name: Legacy Alias\n"), nil } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Equal(t, []string{"aw.yml"}, requestedPaths) assert.Contains(t, err.Error(), `no aw.yml manifest found`) }) t.Run("accepts manifest-version and compatible min-version", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`manifest-version: "1" @@ -367,18 +367,18 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) }) t.Run("accepts manifest without manifest-version", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: Repo Assist @@ -391,18 +391,18 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) }) t.Run("rejects unsupported manifest-version", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`manifest-version: "2" name: Repo Assist @@ -411,13 +411,13 @@ name: Repo Assist return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `manifest-version`) }) t.Run("accepts branding field", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: Repo Assist @@ -433,18 +433,18 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, "Repo Assist", pkg.Name) }) t.Run("rejects unsupported branding icon", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist branding: @@ -455,13 +455,13 @@ branding: return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `icon`) }) t.Run("rejects docs field", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist docs: docs/overview.md @@ -470,13 +470,13 @@ docs: docs/overview.md return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `docs`) }) t.Run("rejects non-string emoji field", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist emoji: @@ -486,13 +486,13 @@ emoji: return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `emoji`) }) t.Run("rejects non-string license field", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist license: @@ -502,13 +502,13 @@ license: return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `license`) }) t.Run("rejects incompatible min-version", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`min-version: v9.9.9 name: Repo Assist @@ -517,13 +517,13 @@ name: Repo Assist return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `requires gh-aw`) }) t.Run("requires package README", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist files: @@ -532,18 +532,18 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `missing required README.md`) }) t.Run("reports nested package path when README is missing", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "packages/repo-assist/aw.yml" { return []byte(`name: Repo Assist files: @@ -552,19 +552,19 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo", PackagePath: "packages/repo-assist"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo", PackagePath: "packages/repo-assist"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `owner/repo/packages/repo-assist`) assert.Contains(t, err.Error(), `packages/repo-assist/README.md`) }) t.Run("rejects unknown manifest fields", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { return []byte(`name: Repo Assist unknown-field: true @@ -573,13 +573,13 @@ unknown-field: true return nil, createRepositoryPackageNotFoundError(path) } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), `unknown-field`) }) t.Run("resolves nested package manifests", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "packages/repo-assist/aw.yml": return []byte(`name: Repo Assist @@ -592,12 +592,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo", PackagePath: "packages/repo-assist"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo", PackagePath: "packages/repo-assist"}, "") require.NoError(t, err) assert.Equal(t, "packages/repo-assist/aw.yml", pkg.ManifestPath) assert.Equal(t, "packages/repo-assist/README.md", pkg.DocsPath) @@ -608,7 +608,7 @@ files: // When a nested bundle manifest (at "dependabot/aw.yml") lists a // ".github/workflows/" path, it must resolve to the repo-root path // ".github/workflows/foo.md" — NOT to "dependabot/.github/workflows/foo.md". - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "dependabot/aw.yml": return []byte(`name: Dependabot @@ -622,12 +622,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo", PackagePath: "dependabot"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo", PackagePath: "dependabot"}, "") require.NoError(t, err) // workflows/ path is package-relative; .github/workflows/ is repo-root-relative. assert.Equal(t, []string{ @@ -655,14 +655,14 @@ func TestResolveWorkflows_RepositoryPackage(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: Repo Assist @@ -675,7 +675,7 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } @@ -713,13 +713,13 @@ func TestResolveWorkflows_RepositoryPackageRejectsPrivateTrue(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte("name: Repo Assist\nfiles:\n - workflows/review.md\n"), nil @@ -731,7 +731,7 @@ func TestResolveWorkflows_RepositoryPackageRejectsPrivateTrue(t *testing.T) { return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } @@ -767,14 +767,14 @@ func TestResolveWorkflows_NestedRepositoryPackage(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "folder/aw.yml": return []byte(`name: Repo Assist @@ -786,7 +786,7 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } @@ -827,14 +827,14 @@ func TestResolveWorkflows_NestedRepositoryPackage_GithubWorkflowsPathIsRepoRoot( getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "dependabot/aw.yml": return []byte(`name: Dependabot Workflows @@ -846,7 +846,7 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } @@ -886,14 +886,14 @@ func TestResolveWorkflows_NestedRepositoryPackage_AutoScan(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "mypkg/aw.yml": return []byte("name: My Package\n"), nil @@ -903,7 +903,7 @@ func TestResolveWorkflows_NestedRepositoryPackage_AutoScan(t *testing.T) { return nil, createRepositoryPackageNotFoundError(path) } // Auto-scan returns full repo-root-relative paths, as the GitHub API does. - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { switch workflowPath { case "mypkg/workflows": return []string{"mypkg/workflows/pr-review.md"}, nil @@ -940,7 +940,7 @@ func TestResolveWorkflows_FallsBackToWorkflowWhenNestedManifestMissing(t *testin return "main", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { return nil, createRepositoryPackageNotFoundError(path) } fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) { @@ -1150,15 +1150,15 @@ func TestResolveRepositoryPackage_ActionWorkflowYML(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } t.Run("includes yml action workflow from files list", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: CI Pack @@ -1172,19 +1172,19 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, []string{"workflows/triage.md", ".github/workflows/ci.yml"}, pkg.InstallationSource) assert.Contains(t, strings.Join(pkg.Warnings, "\n"), "Field 'files'") }) t.Run("rejects yml files outside .github/workflows with warning", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: CI Pack @@ -1198,12 +1198,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) // Only the .md file should be accepted; the yml under workflows/ is rejected assert.Equal(t, []string{"workflows/triage.md"}, pkg.InstallationSource) @@ -1212,7 +1212,7 @@ files: }) t.Run("rejects duplicate markdown filenames across different folders", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: Duplicate Filenames @@ -1226,12 +1226,12 @@ files: return nil, createRepositoryPackageNotFoundError(path) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } - _, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.Error(t, err) assert.Contains(t, err.Error(), "duplicate workflow filename") }) @@ -1255,16 +1255,16 @@ func TestResolveWorkflows_ActionWorkflowYML(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } rawYML := []byte("name: CI\non: [push]\njobs:\n build:\n runs-on: ubuntu-latest\n steps: []\n") - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": return []byte(`name: CI Pack @@ -1277,7 +1277,7 @@ files: } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected scan of %s", workflowPath) return nil, nil } @@ -1458,7 +1458,7 @@ func TestResolveRepositoryPackage_SkillsAndAgents(t *testing.T) { } t.Run("explicit skills and agents from manifest", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1477,22 +1477,22 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/code-review" { return []string{"skills/code-review/SKILL.md", "skills/code-review/prompt.sh"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } // Auto-scan runs after manifest skills; return empty so no extras are added. - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) require.Len(t, pkg.SkillFiles, 2) @@ -1505,7 +1505,7 @@ files: }) t.Run("includes field infers workflow skill and agent types", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1522,22 +1522,22 @@ includes: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/code-review" { return []string{"skills/code-review/SKILL.md", "skills/code-review/prompt.md"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } // Auto-scan runs after manifest skills; return empty so no extras are added. - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Equal(t, []string{"workflows/review.md"}, pkg.InstallationSource) require.Len(t, pkg.SkillFiles, 2) @@ -1545,7 +1545,7 @@ includes: }) t.Run("auto-scans skills and agents when absent from manifest", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1561,32 +1561,32 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } // Agent files are listed with the non-recursive helper. - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "agents" { return []string{"agents/my-agent.md", "agents/helper.sh"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } // Skill files are listed with the recursive helper. - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/auto-skill" { return []string{"skills/auto-skill/SKILL.md"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills" { return []string{"skills/auto-skill"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) require.Len(t, pkg.SkillFiles, 1) assert.Equal(t, "skills/auto-skill/SKILL.md", pkg.SkillFiles[0].SourcePath) @@ -1596,7 +1596,7 @@ files: }) t.Run("missing skill directory produces warning not error", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1611,22 +1611,22 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } // Auto-scan runs after manifest skills; return empty so no extras are added. - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Empty(t, pkg.SkillFiles) require.NotEmpty(t, pkg.Warnings) @@ -1634,7 +1634,7 @@ files: }) t.Run("explicit skill without marker produces warning", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1649,31 +1649,31 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/no-marker" { return []string{"skills/no-marker/prompt.md"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Empty(t, pkg.SkillFiles) assert.Contains(t, strings.Join(pkg.Warnings, "\n"), "missing required SKILL.md") }) t.Run("no skills or agents when directories absent", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1686,21 +1686,21 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { // Auto-scan of agents/ returns not-found return nil, createRepositoryPackageNotFoundError(workflowPath) } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) assert.Empty(t, pkg.SkillFiles) assert.Empty(t, pkg.AgentFiles) @@ -1708,7 +1708,7 @@ files: }) t.Run("manifest skills copied first then auto-scanned additional skills appended", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1728,14 +1728,14 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { switch dirPath { case "skills/review": return []string{"skills/review/SKILL.md"}, nil @@ -1745,14 +1745,14 @@ files: return nil, createRepositoryPackageNotFoundError(dirPath) } // Auto-scan finds "triage" in addition to the manifest-specified "review". - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills" { return []string{"skills/review", "skills/triage"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) // Manifest skill "review" appears first; auto-scanned "triage" appended after. require.Len(t, pkg.SkillFiles, 3) @@ -1764,7 +1764,7 @@ files: }) t.Run("auto-scan errors are warnings when manifest skills are explicit", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1781,24 +1781,24 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/review" { return []string{"skills/review/SKILL.md", "skills/review/prompts/default.md"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, errors.New("rate limit") } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) require.Len(t, pkg.SkillFiles, 2) assert.Equal(t, "skills/review/SKILL.md", pkg.SkillFiles[0].SourcePath) @@ -1806,7 +1806,7 @@ files: }) t.Run("skill folder nested files are included recursively", func(t *testing.T) { - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: My Package @@ -1823,14 +1823,14 @@ includes: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/my-skill" { // Simulate a skill with nested files in subdirectories. return []string{ @@ -1842,11 +1842,11 @@ includes: } return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - pkg, err := resolveRepositoryPackage(&RepoSpec{RepoSlug: "owner/repo"}, "") + pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) require.Len(t, pkg.SkillFiles, 4) assert.Equal(t, "skills/my-skill/SKILL.md", pkg.SkillFiles[0].SourcePath) @@ -1884,7 +1884,7 @@ func TestResolveWorkflows_SkillsAndAgents(t *testing.T) { agentMD := []byte("# My Agent\nThis is an agent.\n") workflowMD := []byte("---\nname: Review\non: issues\n---\n") - downloadPackageFileFromGitHubForHost = func(owner, repo, filePath, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, filePath, ref, host string) ([]byte, error) { switch filePath { case "aw.yml": return []byte(`name: Full Package @@ -1903,18 +1903,18 @@ files: return nil, createRepositoryPackageNotFoundError(filePath) } } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { t.Fatalf("unexpected workflow scan of %s", workflowPath) return nil, nil } - listPackageDirFilesRecursivelyForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesRecursivelyForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { if dirPath == "skills/my-skill" { return []string{"skills/my-skill/SKILL.md"}, nil } return nil, createRepositoryPackageNotFoundError(dirPath) } // Auto-scan runs after manifest skills; return empty so no extras are added. - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) { diff --git a/pkg/cli/add_workflow_resolution.go b/pkg/cli/add_workflow_resolution.go index bb2cd0ef25e..62285a5aff3 100644 --- a/pkg/cli/add_workflow_resolution.go +++ b/pkg/cli/add_workflow_resolution.go @@ -86,7 +86,7 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R return nil, repoErr } - pkg, pkgErr := resolveRepositoryPackage(repoSpec, explicitHostForRepo(repoSpec.RepoSlug)) + pkg, pkgErr := resolveRepositoryPackage(ctx, repoSpec, explicitHostForRepo(repoSpec.RepoSlug)) if pkgErr == nil { resolutionWarnings = append(resolutionWarnings, pkg.Warnings...) parsedSpecs = appendRepositoryPackageWorkflowSpecs(parsedSpecs, repoSpec, pkg) @@ -104,7 +104,7 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R return nil, fmt.Errorf("invalid specification '%s': not a valid workflow path or repository package: %w", workflow, repoErr) } - pkg, pkgErr := resolveRepositoryPackage(repoSpec, explicitHostForRepo(repoSpec.RepoSlug)) + pkg, pkgErr := resolveRepositoryPackage(ctx, repoSpec, explicitHostForRepo(repoSpec.RepoSlug)) if pkgErr != nil { return nil, pkgErr } diff --git a/pkg/cli/dispatch.go b/pkg/cli/dispatch.go index 8a25e3fa571..ae4b3d94c87 100644 --- a/pkg/cli/dispatch.go +++ b/pkg/cli/dispatch.go @@ -61,7 +61,7 @@ func extractDispatchWorkflowNames(content string) []string { // fileDownloadFn is the type for a function that downloads a file from a GitHub repository. // It is used for dependency injection in fetchAndSaveRemoteDispatchWorkflows to allow tests // to provide a fast-failing mock instead of making real network calls. -type fileDownloadFn func(owner, repo, path, ref string) ([]byte, error) +type fileDownloadFn func(ctx context.Context, owner, repo, path, ref string) ([]byte, error) // fetchAndSaveRemoteDispatchWorkflows fetches and saves the workflow files referenced in the // safe-outputs.dispatch-workflow configuration of a remote workflow. Each listed workflow name @@ -172,14 +172,14 @@ func fetchAndSaveRemoteDispatchWorkflows(ctx context.Context, content string, sp // Download from the source repository — try .md first, then .yml as fallback // (the dispatch-workflow validator accepts either .md or .yml files locally). - workflowContent, err := downloader(owner, repo, remoteFilePath, ref) + workflowContent, err := downloader(ctx, owner, repo, remoteFilePath, ref) if err != nil { remoteWorkflowLog.Printf(".md fetch failed for dispatch workflow %s, trying .yml fallback", workflowName) // .md not found — try .yml fallback (e.g. plain GitHub Actions workflow) ymlRemotePath := path.Clean(strings.TrimSuffix(remoteFilePath, ".md") + ".yml") ymlLocalPath := filepath.Join(targetDir, filepath.Clean(workflowName+".yml")) - ymlContent, ymlErr := downloader(owner, repo, ymlRemotePath, ref) + ymlContent, ymlErr := downloader(ctx, owner, repo, ymlRemotePath, ref) if ymlErr != nil { // Neither .md nor .yml found — best-effort, continue if verbose { @@ -268,7 +268,7 @@ func fetchAndSaveRemoteDispatchWorkflows(ctx context.Context, content string, sp // intentional no-ops: this function is best-effort and must never block the add workflow flow. // Parse failures are logged at debug level so they can be investigated when needed. // Source conflicts are reported as warnings (not errors) because the main file is already written. -func fetchAndSaveDispatchWorkflowsFromParsedFile(destFile string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) { +func fetchAndSaveDispatchWorkflowsFromParsedFile(ctx context.Context, destFile string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) { remoteWorkflowLog.Printf("Fetching import-derived dispatch workflows from parsed file: %s, repo=%s", destFile, spec.RepoSlug) if spec.RepoSlug == "" { return @@ -375,13 +375,13 @@ func fetchAndSaveDispatchWorkflowsFromParsedFile(destFile string, spec *Workflow } // Download from source repository — try .md first, then .yml as fallback - workflowContent, err := parser.DownloadFileFromGitHub(owner, repo, remoteFilePath, ref) + workflowContent, err := parser.DownloadFileFromGitHub(ctx, owner, repo, remoteFilePath, ref) if err != nil { // .md not found — try .yml fallback ymlRemotePath := path.Clean(strings.TrimSuffix(remoteFilePath, ".md") + ".yml") ymlLocalPath := filepath.Join(targetDir, filepath.Clean(workflowName+".yml")) - ymlContent, ymlErr := parser.DownloadFileFromGitHub(owner, repo, ymlRemotePath, ref) + ymlContent, ymlErr := parser.DownloadFileFromGitHub(ctx, owner, repo, ymlRemotePath, ref) if ymlErr != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch dispatch workflow %s: %v", remoteFilePath, err))) diff --git a/pkg/cli/fetch.go b/pkg/cli/fetch.go index d66e61cc47b..45ea841a9ab 100644 --- a/pkg/cli/fetch.go +++ b/pkg/cli/fetch.go @@ -118,7 +118,7 @@ func fetchRemoteWorkflow(ctx context.Context, spec *WorkflowSpec, verbose bool) } // Download the workflow file from GitHub - content, err := downloadFileFromGitHubForHost(owner, repo, spec.WorkflowPath, ref, spec.Host) + content, err := downloadFileFromGitHubForHost(ctx, owner, repo, spec.WorkflowPath, ref, spec.Host) if err != nil { // Try with common workflow directory prefixes if the direct path fails. // This handles short workflow names without path separators (e.g. "my-workflow.md"). @@ -129,7 +129,7 @@ func fetchRemoteWorkflow(ctx context.Context, spec *WorkflowSpec, verbose bool) altPath += ".md" } remoteWorkflowLog.Printf("Direct path failed, trying: %s", altPath) - if altContent, altErr := downloadFileFromGitHubForHost(owner, repo, altPath, ref, spec.Host); altErr == nil { + if altContent, altErr := downloadFileFromGitHubForHost(ctx, owner, repo, altPath, ref, spec.Host); altErr == nil { remoteWorkflowLog.Printf("Downloaded workflow via alt path: %s (%d bytes)", altPath, len(altContent)) return &FetchedWorkflow{ Content: altContent, @@ -162,7 +162,7 @@ func resolveCommitSHAWithRetries(ctx context.Context, owner, repo, ref, workflow var lastErr error for attempt := 1; attempt <= attempts; attempt++ { - commitSHA, err := resolveRefToSHAForHost(owner, repo, ref, host) + commitSHA, err := resolveRefToSHAForHost(ctx, owner, repo, ref, host) if err == nil { remoteWorkflowLog.Printf("Resolved ref %s to SHA: %s", ref, commitSHA) return commitSHA, nil diff --git a/pkg/cli/includes.go b/pkg/cli/includes.go index af1a9c36583..6b4378eb986 100644 --- a/pkg/cli/includes.go +++ b/pkg/cli/includes.go @@ -26,7 +26,7 @@ var includeDirectivePattern = regexp.MustCompile(`^@include(\?)?\s+(.+)$`) // The includePath should be in the format: owner/repo/path/to/file.md[@ref] // If the includePath is a relative path, it's resolved relative to the baseSpec. // Returns: (content, section, error) where section is the #fragment from the path (e.g., "#section-name"). -func FetchIncludeFromSource(includePath string, baseSpec *WorkflowSpec, verbose bool) ([]byte, string, error) { +func FetchIncludeFromSource(ctx context.Context, includePath string, baseSpec *WorkflowSpec, verbose bool) ([]byte, string, error) { baseSpecStr := "" if baseSpec != nil { baseSpecStr = baseSpec.String() @@ -65,7 +65,7 @@ func FetchIncludeFromSource(includePath string, baseSpec *WorkflowSpec, verbose filePath := strings.Join(slashParts[2:], "/") // Download the file - content, err := parser.DownloadFileFromGitHub(owner, repo, filePath, ref) + content, err := parser.DownloadFileFromGitHub(ctx, owner, repo, filePath, ref) if err != nil { return nil, section, fmt.Errorf("failed to fetch include from %s: %w", includePath, err) } @@ -104,7 +104,7 @@ func FetchIncludeFromSource(includePath string, baseSpec *WorkflowSpec, verbose } } - content, err := parser.DownloadFileFromGitHub(owner, repo, fullPath, ref) + content, err := parser.DownloadFileFromGitHub(ctx, owner, repo, fullPath, ref) if err != nil { return nil, section, fmt.Errorf("failed to fetch include %s from %s/%s: %w", filePath, owner, repo, err) } @@ -122,7 +122,7 @@ func FetchIncludeFromSource(includePath string, baseSpec *WorkflowSpec, verbose // This is analogous to fetchAndSaveRemoteIncludes, which handles @include directives in the // markdown body; this function handles the YAML frontmatter 'imports:' field. // Import failures are non-fatal (best-effort); the compiler will report any still-missing files. -func fetchAndSaveRemoteFrontmatterImports(content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { +func fetchAndSaveRemoteFrontmatterImports(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { if spec.RepoSlug == "" { return nil } @@ -137,7 +137,7 @@ func fetchAndSaveRemoteFrontmatterImports(content string, spec *WorkflowSpec, ta ref := spec.Version if ref == "" { // Resolve the actual default branch of the source repo rather than assuming "main" - defaultBranch, err := getRepoDefaultBranch(context.Background(), spec.RepoSlug) + defaultBranch, err := getRepoDefaultBranch(ctx, spec.RepoSlug) if err != nil { remoteWorkflowLog.Printf("Failed to resolve default branch for %s, falling back to 'main': %v", spec.RepoSlug, err) ref = "main" @@ -158,7 +158,7 @@ func fetchAndSaveRemoteFrontmatterImports(content string, spec *WorkflowSpec, ta // cycles (A imports B, B imports A) are broken without infinite recursion. seen := make(map[string]struct { }) - fetchFrontmatterImportsRecursive(content, workflowBaseDir, frontmatterImportsOpts{ + fetchFrontmatterImportsRecursive(ctx, content, workflowBaseDir, frontmatterImportsOpts{ owner: owner, repo: repo, ref: ref, @@ -197,7 +197,7 @@ type frontmatterImportsOpts struct { // - originalBaseDir: directory of the top-level workflow (used to map remote paths → local paths) // - targetDir: the `.github/workflows` directory in the user's repo // - seen: shared visited set (keyed by fully-resolved remote path) — prevents cycles & duplicates -func fetchFrontmatterImportsRecursive(content, currentBaseDir string, opts frontmatterImportsOpts) { +func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseDir string, opts frontmatterImportsOpts) { result, err := parser.ExtractFrontmatterFromContent(content) if err != nil || result.Frontmatter == nil { return @@ -362,7 +362,7 @@ func fetchFrontmatterImportsRecursive(content, currentBaseDir string, opts front } // Download from the source repository - importContent, err := parser.DownloadFileFromGitHub(opts.owner, opts.repo, remoteFilePath, opts.ref) + importContent, err := parser.DownloadFileFromGitHub(ctx, opts.owner, opts.repo, remoteFilePath, opts.ref) if err != nil { remoteWorkflowLog.Printf("Failed to download import %s from %s/%s@%s: %v", remoteFilePath, opts.owner, opts.repo, opts.ref, err) if opts.verbose { @@ -403,12 +403,12 @@ func fetchFrontmatterImportsRecursive(content, currentBaseDir string, opts front // Recurse into the imported file's imports. Use the imported file's directory as // currentBaseDir so that relative paths inside it resolve correctly. importedBaseDir := path.Dir(remoteFilePath) - fetchFrontmatterImportsRecursive(string(importContent), importedBaseDir, opts) + fetchFrontmatterImportsRecursive(ctx, string(importContent), importedBaseDir, opts) } } // fetchAndSaveRemoteIncludes parses the workflow content for @include directives and fetches them from the remote source -func fetchAndSaveRemoteIncludes(content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { +func fetchAndSaveRemoteIncludes(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { remoteWorkflowLog.Printf("Fetching remote includes for workflow: %s", spec.String()) // Parse the workflow content to find @include directives @@ -440,7 +440,7 @@ func fetchAndSaveRemoteIncludes(content string, spec *WorkflowSpec, targetDir st }{} // Fetch the include file - includeContent, _, err := FetchIncludeFromSource(includePath, spec, verbose) + includeContent, _, err := FetchIncludeFromSource(ctx, includePath, spec, verbose) if err != nil { if isOptional { if verbose { @@ -502,7 +502,7 @@ func fetchAndSaveRemoteIncludes(content string, spec *WorkflowSpec, targetDir st } // Recursively fetch includes from the fetched file - if err := fetchAndSaveRemoteIncludes(string(includeContent), spec, targetDir, verbose, force, tracker); err != nil { + if err := fetchAndSaveRemoteIncludes(ctx, string(includeContent), spec, targetDir, verbose, force, tracker); err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch nested includes from %s: %v", filePath, err))) } @@ -523,7 +523,7 @@ func fetchAndSaveRemoteIncludes(content string, spec *WorkflowSpec, targetDir st func fetchAllRemoteDependencies(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { remoteWorkflowLog.Printf("Fetching all remote dependencies: spec=%s, targetDir=%s, force=%v", spec.String(), targetDir, force) // Fetch and save @include directive dependencies (best-effort: errors are not fatal). - if err := fetchAndSaveRemoteIncludes(content, spec, targetDir, verbose, force, tracker); err != nil { + if err := fetchAndSaveRemoteIncludes(ctx, content, spec, targetDir, verbose, force, tracker); err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch include dependencies: %v", err))) } @@ -532,7 +532,7 @@ func fetchAllRemoteDependencies(ctx context.Context, content string, spec *Workf // locally during compilation. Keeping these as relative paths (not workflowspecs) // ensures the compiler resolves them from disk rather than downloading from GitHub. // Best-effort: errors are not fatal. - if err := fetchAndSaveRemoteFrontmatterImports(content, spec, targetDir, verbose, force, tracker); err != nil { + if err := fetchAndSaveRemoteFrontmatterImports(ctx, content, spec, targetDir, verbose, force, tracker); err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch frontmatter import dependencies: %v", err))) } @@ -547,7 +547,7 @@ func fetchAllRemoteDependencies(ctx context.Context, content string, spec *Workf } // Fetch files listed in the 'resources:' frontmatter field (additional workflow or // action files that should be present alongside this workflow). - if err := fetchAndSaveRemoteResources(content, spec, targetDir, verbose, force, tracker); err != nil { + if err := fetchAndSaveRemoteResources(ctx, content, spec, targetDir, verbose, force, tracker); err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch resource dependencies: %v", err))) } diff --git a/pkg/cli/list_workflows_command.go b/pkg/cli/list_workflows_command.go index 148b2de748b..a33a6f214ec 100644 --- a/pkg/cli/list_workflows_command.go +++ b/pkg/cli/list_workflows_command.go @@ -1,6 +1,7 @@ package cli import ( + "context" "encoding/json" "fmt" "os" @@ -63,7 +64,7 @@ It accepts workflow IDs (basename without .md) or full filenames.`, if dir != "" && repo == "" { path = dir } - return RunListWorkflows(repo, path, pattern, verbose, jsonFlag, labelFilter) + return RunListWorkflows(cmd.Context(), repo, path, pattern, verbose, jsonFlag, labelFilter) }, } @@ -81,7 +82,7 @@ It accepts workflow IDs (basename without .md) or full filenames.`, } // RunListWorkflows lists workflows without checking GitHub status -func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool, labelFilter string) error { +func RunListWorkflows(ctx context.Context, repo, path, pattern string, verbose bool, jsonOutput bool, labelFilter string) error { listWorkflowsLog.Printf("Listing workflows: repo=%s, path=%s, pattern=%s, jsonOutput=%v, labelFilter=%s", repo, path, pattern, jsonOutput, labelFilter) var mdFiles []string @@ -94,7 +95,7 @@ func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool, if verbose && !jsonOutput { fmt.Fprintf(os.Stderr, "Listing workflow files from %s\n", repo) } - mdFiles, err = getRemoteWorkflowFiles(repo, path, verbose, jsonOutput) + mdFiles, err = getRemoteWorkflowFiles(ctx, repo, path, verbose, jsonOutput) } else { // List workflows from local repository if verbose && !jsonOutput { @@ -237,7 +238,7 @@ func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool, } // getRemoteWorkflowFiles fetches the list of workflow files from a remote repository -func getRemoteWorkflowFiles(repoSpec, workflowPath string, verbose bool, jsonOutput bool) ([]string, error) { +func getRemoteWorkflowFiles(ctx context.Context, repoSpec, workflowPath string, verbose bool, jsonOutput bool) ([]string, error) { listWorkflowsLog.Printf("Fetching remote workflow files: repoSpec=%s, path=%s", repoSpec, workflowPath) // Parse repo spec: owner/repo[@ref] var owner, repo, ref string @@ -263,7 +264,7 @@ func getRemoteWorkflowFiles(repoSpec, workflowPath string, verbose bool, jsonOut // Use the parser package to list workflow files listWorkflowsLog.Printf("Listing remote workflow files: owner=%s, repo=%s, ref=%s, path=%s", owner, repo, ref, workflowPath) - files, err := parser.ListWorkflowFiles(owner, repo, ref, workflowPath) + files, err := parser.ListWorkflowFiles(ctx, owner, repo, ref, workflowPath) if err != nil { listWorkflowsLog.Printf("Failed to list remote workflow files: %v", err) return nil, fmt.Errorf("failed to list workflow files from %s/%s: %w", owner, repo, err) diff --git a/pkg/cli/list_workflows_command_test.go b/pkg/cli/list_workflows_command_test.go index 0902ac27310..66edcec5054 100644 --- a/pkg/cli/list_workflows_command_test.go +++ b/pkg/cli/list_workflows_command_test.go @@ -28,19 +28,19 @@ func TestRunListWorkflows_JSONOutput(t *testing.T) { // Test JSON output without pattern t.Run("JSON output without pattern", func(t *testing.T) { - err := RunListWorkflows("", ".github/workflows", "", false, true, "") + err := RunListWorkflows(t.Context(), "", ".github/workflows", "", false, true, "") assert.NoError(t, err, "RunListWorkflows with JSON flag should not error") }) // Test JSON output with pattern t.Run("JSON output with pattern", func(t *testing.T) { - err := RunListWorkflows("", ".github/workflows", "smoke", false, true, "") + err := RunListWorkflows(t.Context(), "", ".github/workflows", "smoke", false, true, "") assert.NoError(t, err, "RunListWorkflows with JSON flag and pattern should not error") }) // Test JSON output with label filter t.Run("JSON output with label filter", func(t *testing.T) { - err := RunListWorkflows("", ".github/workflows", "", false, true, "test") + err := RunListWorkflows(t.Context(), "", ".github/workflows", "", false, true, "test") assert.NoError(t, err, "RunListWorkflows with JSON flag and label filter should not error") }) } @@ -96,13 +96,13 @@ func TestRunListWorkflows_TextOutput(t *testing.T) { // Test text output t.Run("Text output without pattern", func(t *testing.T) { - err := RunListWorkflows("", ".github/workflows", "", false, false, "") + err := RunListWorkflows(t.Context(), "", ".github/workflows", "", false, false, "") assert.NoError(t, err, "RunListWorkflows without JSON flag should not error") }) // Test text output with pattern t.Run("Text output with pattern", func(t *testing.T) { - err := RunListWorkflows("", ".github/workflows", "ci-", false, false, "") + err := RunListWorkflows(t.Context(), "", ".github/workflows", "ci-", false, false, "") assert.NoError(t, err, "RunListWorkflows with pattern should not error") }) } @@ -132,7 +132,7 @@ func captureListOutput(t *testing.T, dir, pattern string) string { require.NoError(t, err, "Should create pipe") os.Stdout = w - runErr := RunListWorkflows("", dir, pattern, false, true, "") + runErr := RunListWorkflows(t.Context(), "", dir, pattern, false, true, "") w.Close() os.Stdout = oldStdout diff --git a/pkg/cli/remote_workflow_test.go b/pkg/cli/remote_workflow_test.go index 9a6dd29e3d3..df379eb713a 100644 --- a/pkg/cli/remote_workflow_test.go +++ b/pkg/cli/remote_workflow_test.go @@ -119,7 +119,7 @@ func TestResolveCommitSHAWithRetries_TransientFailureThenSuccess(t *testing.T) { shaResolutionRetryDelays = testSHAResolutionRetryDelays resolveAttempts := 0 - resolveRefToSHAForHost = func(owner, repo, ref, host string) (string, error) { + resolveRefToSHAForHost = func(_ context.Context, owner, repo, ref, host string) (string, error) { resolveAttempts++ if resolveAttempts == 1 { return "", errors.New("HTTP 429: rate limit exceeded") @@ -152,7 +152,7 @@ func TestResolveCommitSHAWithRetries_PermanentFailureDoesNotRetry(t *testing.T) shaResolutionRetryDelays = testSHAResolutionRetryDelays resolveAttempts := 0 - resolveRefToSHAForHost = func(owner, repo, ref, host string) (string, error) { + resolveRefToSHAForHost = func(_ context.Context, owner, repo, ref, host string) (string, error) { resolveAttempts++ return "", errors.New("HTTP 404: Not Found") } @@ -185,7 +185,7 @@ func TestResolveCommitSHAWithRetries_TransientFailureExhaustsRetries(t *testing. shaResolutionRetryDelays = testSHAResolutionRetryDelays resolveAttempts := 0 - resolveRefToSHAForHost = func(owner, repo, ref, host string) (string, error) { + resolveRefToSHAForHost = func(_ context.Context, owner, repo, ref, host string) (string, error) { resolveAttempts++ return "", errors.New("timeout waiting for GitHub API") } @@ -215,7 +215,7 @@ func TestResolveCommitSHAWithRetries_ContextCanceledDuringBackoff(t *testing.T) }() shaResolutionRetryDelays = testSHAResolutionRetryDelays - resolveRefToSHAForHost = func(owner, repo, ref, host string) (string, error) { + resolveRefToSHAForHost = func(_ context.Context, owner, repo, ref, host string) (string, error) { return "", errors.New("HTTP 429: rate limit exceeded") } @@ -286,7 +286,7 @@ func TestFetchIncludeFromSource_WorkflowSpecParsing(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, section, err := FetchIncludeFromSource(tt.includePath, tt.baseSpec, false) + _, section, err := FetchIncludeFromSource(t.Context(), tt.includePath, tt.baseSpec, false) if tt.expectError { require.Error(t, err, "expected error") @@ -340,7 +340,7 @@ func TestFetchIncludeFromSource_SectionExtraction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We expect resolution errors in these unit tests, but section should still be extracted - _, section, _ := FetchIncludeFromSource(tt.includePath, nil, false) + _, section, _ := FetchIncludeFromSource(t.Context(), tt.includePath, nil, false) assert.Equal(t, tt.expectSection, section, "section should be correctly extracted") }) } @@ -405,7 +405,7 @@ engine: copilot } tmpDir := t.TempDir() - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error when no imports are present") // No files should have been created @@ -433,7 +433,7 @@ imports: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error for local workflow with empty RepoSlug") entries, readErr := os.ReadDir(tmpDir) @@ -462,7 +462,7 @@ imports: tmpDir := t.TempDir() // This should not attempt any network calls; already-pinned imports are skipped. - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error for workflowspec imports") entries, readErr := os.ReadDir(tmpDir) @@ -497,7 +497,7 @@ imports: tmpDir := t.TempDir() // The uses: import path is in workflowspec format so it should be skipped, // just like a string-form workflowspec import would be. - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error for uses: form workflowspec imports") entries, readErr := os.ReadDir(tmpDir) @@ -527,7 +527,7 @@ engine: copilot WorkflowPath: ".github/workflows/test.md", } - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tracker.gitRoot, false, false, tracker) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tracker.gitRoot, false, false, tracker) require.NoError(t, err) assert.Empty(t, tracker.CreatedFiles, "no files should be created when there are no imports") assert.Empty(t, tracker.ModifiedFiles, "no files should be modified when there are no imports") @@ -561,7 +561,7 @@ imports: tmpDir := t.TempDir() // No network in unit tests: the download attempt for the first import will fail silently // (verbose=false). The second import must be deduplicated without a second download. - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "section-fragment deduplication should not error") entries, readErr := os.ReadDir(tmpDir) @@ -603,7 +603,7 @@ imports: WorkflowPath: ".github/workflows/ci-coach.md", } - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, tracker) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, tracker) require.NoError(t, err) // The existing file must be untouched and not added to the tracker. @@ -649,7 +649,7 @@ imports: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "path traversal should be silently rejected, not return an error") // No file must have been written anywhere @@ -678,7 +678,7 @@ imports: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteFrontmatterImports(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteFrontmatterImports(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "invalid RepoSlug should return nil without error") entries, readErr := os.ReadDir(tmpDir) @@ -1070,7 +1070,7 @@ on: issues } tmpDir := t.TempDir() - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error when no resources field") entries, readErr := os.ReadDir(tmpDir) @@ -1097,7 +1097,7 @@ resources: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "should not error for local workflow") entries, readErr := os.ReadDir(tmpDir) @@ -1125,7 +1125,7 @@ resources: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.Error(t, err, "should error when resources contain macro syntax") assert.Contains(t, err.Error(), "${{", "error should mention the disallowed syntax") @@ -1165,7 +1165,7 @@ resources: WorkflowPath: ".github/workflows/my-workflow.md", } - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err) gotContent, readErr := os.ReadFile(existingFile) @@ -1194,7 +1194,7 @@ resources: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "path traversal should be silently rejected") entries, readErr := os.ReadDir(tmpDir) @@ -1220,7 +1220,7 @@ resources: } tmpDir := t.TempDir() - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, nil) require.NoError(t, err, "invalid RepoSlug should return nil without error") entries, readErr := os.ReadDir(tmpDir) @@ -1263,7 +1263,7 @@ resources: WorkflowPath: ".github/workflows/my-workflow.md", } - err := fetchAndSaveRemoteResources(content, spec, tmpDir, false, false, tracker) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, tmpDir, false, false, tracker) require.NoError(t, err) assert.Empty(t, tracker.CreatedFiles, "pre-existing file must not appear in CreatedFiles") assert.Empty(t, tracker.ModifiedFiles, "pre-existing file must not appear in ModifiedFiles") @@ -1284,7 +1284,7 @@ func TestFetchAndSaveDispatchWorkflowsFromParsedFile_EmptyRepoSlug(t *testing.T) WorkflowPath: ".github/workflows/my-workflow.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(filepath.Join(tmpDir, "nonexistent.md"), spec, tmpDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), filepath.Join(tmpDir, "nonexistent.md"), spec, tmpDir, false, false, nil) entries, err := os.ReadDir(tmpDir) require.NoError(t, err) @@ -1300,7 +1300,7 @@ func TestFetchAndSaveDispatchWorkflowsFromParsedFile_InvalidRepoSlug(t *testing. WorkflowPath: ".github/workflows/my-workflow.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(filepath.Join(tmpDir, "nonexistent.md"), spec, tmpDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), filepath.Join(tmpDir, "nonexistent.md"), spec, tmpDir, false, false, nil) entries, err := os.ReadDir(tmpDir) require.NoError(t, err) @@ -1317,7 +1317,7 @@ func TestFetchAndSaveDispatchWorkflowsFromParsedFile_ParseFailure(t *testing.T) } // Point to a file that does not exist — ParseWorkflowFile will fail. - fetchAndSaveDispatchWorkflowsFromParsedFile(filepath.Join(tmpDir, "does-not-exist.md"), spec, tmpDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), filepath.Join(tmpDir, "does-not-exist.md"), spec, tmpDir, false, false, nil) entries, err := os.ReadDir(tmpDir) require.NoError(t, err) @@ -1349,7 +1349,7 @@ permissions: WorkflowPath: ".github/workflows/my-workflow.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(mainPath, spec, workflowsDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), mainPath, spec, workflowsDir, false, false, nil) // Only the main workflow itself should be in the directory. entries, err := os.ReadDir(workflowsDir) @@ -1419,7 +1419,7 @@ Process incoming issues. WorkflowPath: ".github/workflows/main.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(mainPath, spec, workflowsDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), mainPath, spec, workflowsDir, false, false, nil) // The pre-existing dispatch workflow must not be modified. got, err := os.ReadFile(triagePath) @@ -1479,7 +1479,7 @@ imports: WorkflowPath: ".github/workflows/main.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(mainPath, spec, workflowsDir, false, false, tracker) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), mainPath, spec, workflowsDir, false, false, tracker) assert.Empty(t, tracker.CreatedFiles, "pre-existing dispatch workflow must not appear in CreatedFiles") assert.Empty(t, tracker.ModifiedFiles, "pre-existing dispatch workflow must not appear in ModifiedFiles") @@ -1533,7 +1533,7 @@ imports: WorkflowPath: ".github/workflows/main.md", } - fetchAndSaveDispatchWorkflowsFromParsedFile(mainPath, spec, workflowsDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), mainPath, spec, workflowsDir, false, false, nil) // No file named after the macro entry should have been created. macroFile := filepath.Join(workflowsDir, "${{ vars.DYNAMIC_WORKFLOW }}.md") @@ -1721,7 +1721,7 @@ safe-outputs: // The conflict check is bypassed; the download fails immediately via the injected // mock downloader (avoids real network calls in unit tests). // Download failures are best-effort, so the function returns nil overall. - mockDownloader := func(_, _, _, _ string) ([]byte, error) { + mockDownloader := func(_ context.Context, _, _, _, _ string) ([]byte, error) { return nil, errors.New("download not available in unit tests") } err := fetchAndSaveRemoteDispatchWorkflows(context.Background(), content, spec, dir, false, true, nil, mockDownloader) @@ -1757,7 +1757,7 @@ resources: WorkflowPath: ".github/workflows/main.md", } - err := fetchAndSaveRemoteResources(content, spec, dir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, dir, false, false, nil) require.Error(t, err, "should error when markdown resource exists from a different source") assert.Contains(t, err.Error(), "helper.md", "error should name the conflicting resource") } @@ -1782,7 +1782,7 @@ resources: WorkflowPath: ".github/workflows/main.md", } - err := fetchAndSaveRemoteResources(content, spec, dir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, dir, false, false, nil) require.Error(t, err, "should error when non-markdown resource already exists") assert.Contains(t, err.Error(), "helper.yml", "error should name the conflicting resource") } @@ -1812,7 +1812,7 @@ resources: WorkflowPath: ".github/workflows/main.md", } - err := fetchAndSaveRemoteResources(content, spec, dir, false, false, nil) + err := fetchAndSaveRemoteResources(t.Context(), content, spec, dir, false, false, nil) assert.NoError(t, err, "should not error when markdown resource is from the same source") } @@ -1858,7 +1858,7 @@ source: otherorg/other-repo/.github/workflows/triage-issue.md@v1 } // Must not panic or error — post-write is best-effort. - fetchAndSaveDispatchWorkflowsFromParsedFile(mainPath, spec, workflowsDir, false, false, nil) + fetchAndSaveDispatchWorkflowsFromParsedFile(t.Context(), mainPath, spec, workflowsDir, false, false, nil) // The conflicting file must NOT have been overwritten. got, readErr := os.ReadFile(conflictPath) diff --git a/pkg/cli/resources.go b/pkg/cli/resources.go index 08212475bf5..8611500a86f 100644 --- a/pkg/cli/resources.go +++ b/pkg/cli/resources.go @@ -67,7 +67,7 @@ func extractResources(content string) ([]string, error) { // from the same source are silently skipped. // For non-Markdown resource files: if the target already exists and force is false, an error // is returned regardless of origin (non-markdown files have no source tracking). -func fetchAndSaveRemoteResources(content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { +func fetchAndSaveRemoteResources(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error { if spec.RepoSlug == "" { return nil } @@ -79,7 +79,7 @@ func fetchAndSaveRemoteResources(content string, spec *WorkflowSpec, targetDir s owner, repo := parts[0], parts[1] ref := spec.Version if ref == "" { - defaultBranch, err := getRepoDefaultBranch(context.Background(), spec.RepoSlug) + defaultBranch, err := getRepoDefaultBranch(ctx, spec.RepoSlug) if err != nil { remoteWorkflowLog.Printf("Failed to resolve default branch for %s, falling back to 'main': %v", spec.RepoSlug, err) ref = "main" @@ -182,7 +182,7 @@ func fetchAndSaveRemoteResources(content string, spec *WorkflowSpec, targetDir s } // Download from source repository - fileContent, err := parser.DownloadFileFromGitHub(owner, repo, remoteFilePath, ref) + fileContent, err := parser.DownloadFileFromGitHub(ctx, owner, repo, remoteFilePath, ref) if err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch resource %s: %v", remoteFilePath, err))) diff --git a/pkg/cli/update_manifest.go b/pkg/cli/update_manifest.go index 18c2357c5f5..6e35878bcb2 100644 --- a/pkg/cli/update_manifest.go +++ b/pkg/cli/update_manifest.go @@ -100,7 +100,7 @@ func updateManifestWorkflowGroup(ctx context.Context, source string, grouped []* sourceFieldRef = currentRef } - currentPkg, err := resolveRepositoryPackage(&RepoSpec{ + currentPkg, err := resolveRepositoryPackage(ctx, &RepoSpec{ RepoSlug: repoSpec.RepoSlug, PackagePath: repoSpec.PackagePath, Version: currentRef, @@ -111,7 +111,7 @@ func updateManifestWorkflowGroup(ctx context.Context, source string, grouped []* } return successes, failures } - latestPkg, err := resolveRepositoryPackage(&RepoSpec{ + latestPkg, err := resolveRepositoryPackage(ctx, &RepoSpec{ RepoSlug: repoSpec.RepoSlug, PackagePath: repoSpec.PackagePath, Version: latestRef, diff --git a/pkg/cli/update_manifest_test.go b/pkg/cli/update_manifest_test.go index 2fd51e11ad3..3e6850cd24b 100644 --- a/pkg/cli/update_manifest_test.go +++ b/pkg/cli/update_manifest_test.go @@ -39,7 +39,7 @@ func TestUpdateManifestWorkflowGroup_AddsUpdatesRemoves(t *testing.T) { getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { return "main", nil } - downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": if ref == "v1.0.0" { @@ -53,14 +53,14 @@ func TestUpdateManifestWorkflowGroup_AddsUpdatesRemoves(t *testing.T) { } return nil, createRepositoryPackageNotFoundError(path) } - listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) { + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { return nil, errors.New("unexpected scan") } // Return not-found so skill/agent auto-scan skips gracefully (no real network needed) - listPackageDirSubdirsForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } - listPackageDirFilesForHost = func(owner, repo, ref, dirPath, host string) ([]string, error) { + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { return nil, createRepositoryPackageNotFoundError(dirPath) } diff --git a/pkg/parser/import_remote_nested_test.go b/pkg/parser/import_remote_nested_test.go index 3bf6de09ccb..87610be4aa3 100644 --- a/pkg/parser/import_remote_nested_test.go +++ b/pkg/parser/import_remote_nested_test.go @@ -573,7 +573,7 @@ func TestResolveRemoteSymlinksPathConstruction(t *testing.T) { // GitHub API access, which is tested in integration tests. t.Run("single component path returns error", func(t *testing.T) { - _, err := resolveRemoteSymlinks(nil, "owner", "repo", "file.md", "main") + _, err := resolveRemoteSymlinks(t.Context(), nil, "owner", "repo", "file.md", "main") assert.Error(t, err, "Single component path has no directories to resolve") }) diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index 253d2f386f5..590cd06bd3e 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -343,8 +343,12 @@ func isWorkflowSpec(path string) bool { return IsWorkflowSpec(path) } -// downloadIncludeFromWorkflowSpec downloads an include file from GitHub using workflowspec -// It first checks the cache, and only downloads if not cached +// downloadIncludeFromWorkflowSpec downloads an include file from GitHub using workflowspec. +// It first checks the cache, and only downloads if not cached. +// +// NOTE: This function is called from ResolveIncludePath which has no context.Context +// parameter. Threading ctx through ResolveIncludePath and its 6+ callers across multiple +// packages is tracked as a follow-up task; context.Background() is used in the interim. func downloadIncludeFromWorkflowSpec(spec string, cache *ImportCache) (string, error) { remoteLog.Printf("Downloading from workflowspec: %s", spec) owner, repo, filePath, ref, err := parseWorkflowSpecParts(spec) @@ -362,7 +366,7 @@ func downloadIncludeFromWorkflowSpec(spec string, cache *ImportCache) (string, e } remoteLog.Printf("Fetching file from GitHub: %s/%s/%s@%s", owner, repo, filePath, ref) - content, err := downloadFileFromGitHub(owner, repo, filePath, ref) + content, err := downloadFileFromGitHub(context.Background(), owner, repo, filePath, ref) if err != nil { return "", fmt.Errorf("failed to download include from %s: %w", spec, err) } @@ -405,7 +409,7 @@ func resolveWorkflowSpecSHAForCache(owner, repo, ref string, cache *ImportCache) if cache == nil { return "" } - resolvedSHA, err := resolveRefToSHA(owner, repo, ref, "") + resolvedSHA, err := resolveRefToSHA(context.Background(), owner, repo, ref, "") if err != nil { remoteLog.Printf("Failed to resolve ref to SHA, will skip cache: %v", err) return "" @@ -504,7 +508,7 @@ func resolveRefToSHAViaGit(owner, repo, ref, host string) (string, error) { } // resolveRefToSHA resolves a git ref (branch, tag, or SHA) to its commit SHA -func resolveRefToSHA(owner, repo, ref, host string) (string, error) { +func resolveRefToSHA(ctx context.Context, owner, repo, ref, host string) (string, error) { // If ref is already a full SHA (40 hex characters), return it as-is if len(ref) == 40 && gitutil.IsHexString(ref) { return ref, nil @@ -532,7 +536,7 @@ func resolveRefToSHA(owner, repo, ref, host string) (string, error) { if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return resolveRefToSHAViaPublicAPI(context.Background(), owner, repo, ref) + return resolveRefToSHAViaPublicAPI(ctx, owner, repo, ref) } return "", fmt.Errorf("failed to resolve ref via GitHub API (auth error) and git ls-remote: API error: %w, Git error: %w", err, gitErr) } @@ -748,14 +752,14 @@ func downloadFileViaGitClone(owner, repo, path, ref, host string) ([]byte, error // checkRemoteSymlink checks if a path in a remote GitHub repository is a symlink. // Returns the symlink target and true if it is a symlink, or empty string and false otherwise. // A nil error with false means the path is not a symlink (e.g., it's a directory or file). -func checkRemoteSymlink(client *api.RESTClient, owner, repo, dirPath, ref string) (string, bool, error) { +func checkRemoteSymlink(ctx context.Context, client *api.RESTClient, owner, repo, dirPath, ref string) (string, bool, error) { endpoint := buildContentsAPIPath(owner, repo, dirPath, ref) remoteLog.Printf("Checking if path component is symlink: %s/%s/%s@%s", owner, repo, dirPath, ref) // The Contents API returns a JSON object for files/symlinks but a JSON array for directories. // Decode into json.RawMessage first to distinguish these cases without error-driven control flow. var raw json.RawMessage - err := client.Get(endpoint, &raw) + err := client.DoWithContext(ctx, http.MethodGet, endpoint, nil, &raw) if err != nil { remoteLog.Printf("Contents API error for %s: %v", dirPath, err) return "", false, err @@ -792,7 +796,7 @@ func checkRemoteSymlink(client *api.RESTClient, owner, repo, dirPath, ref string // fetching .github/workflows/shared/elastic-tools.md returns 404. // This function walks the path components and resolves any symlinks found. // The caller must provide a REST client (already authenticated for the correct host). -func resolveRemoteSymlinks(client *api.RESTClient, owner, repo, filePath, ref string) (string, error) { +func resolveRemoteSymlinks(ctx context.Context, client *api.RESTClient, owner, repo, filePath, ref string) (string, error) { parts := strings.Split(filePath, "/") if len(parts) <= 1 { return "", fmt.Errorf("no directory components to resolve in path: %s", filePath) @@ -806,7 +810,7 @@ func resolveRemoteSymlinks(client *api.RESTClient, owner, repo, filePath, ref st for i := 1; i < len(parts); i++ { dirPath := strings.Join(parts[:i], "/") - resolvedPath, found, err := resolveRemoteSymlinkComponent(client, owner, repo, filePath, ref, parts, i, dirPath) + resolvedPath, found, err := resolveRemoteSymlinkComponent(ctx, client, owner, repo, filePath, ref, parts, i, dirPath) if err != nil { return "", err } @@ -820,13 +824,14 @@ func resolveRemoteSymlinks(client *api.RESTClient, owner, repo, filePath, ref st } func resolveRemoteSymlinkComponent( + ctx context.Context, client *api.RESTClient, owner, repo, filePath, ref string, parts []string, index int, dirPath string, ) (string, bool, error) { - target, isSymlink, err := checkRemoteSymlink(client, owner, repo, dirPath, ref) + target, isSymlink, err := checkRemoteSymlink(ctx, client, owner, repo, dirPath, ref) if err != nil { if errorutil.IsNotFoundError(err) { remoteLog.Printf("Path component %s returned 404, skipping", dirPath) @@ -873,8 +878,8 @@ func resolveAndValidateRemoteSymlinkBase(parentDir, target, dirPath string) (str // - path: Path to the file within the repository (e.g., ".github/workflows/workflow.md") // - ref: Git reference (branch, tag, or commit SHA) // Returns the file content as bytes or an error if the file cannot be retrieved. -func DownloadFileFromGitHub(owner, repo, path, ref string) ([]byte, error) { - return downloadFileFromGitHubWithDepth(owner, repo, path, ref, 0, "") +func DownloadFileFromGitHub(ctx context.Context, owner, repo, path, ref string) ([]byte, error) { + return downloadFileFromGitHubWithDepth(ctx, owner, repo, path, ref, 0, "") } // DownloadFileFromGitHubForHost downloads a file from a GitHub repository using the GitHub API, @@ -882,28 +887,28 @@ func DownloadFileFromGitHub(owner, repo, path, ref string) ([]byte, error) { // than the one configured via GH_HOST (e.g., fetching from github.com while GH_HOST is a GHE instance). // host is the hostname without scheme (e.g., "github.com", "myorg.ghe.com"). // An empty host uses the default configured host (GH_HOST or github.com). -func DownloadFileFromGitHubForHost(owner, repo, path, ref, host string) ([]byte, error) { - return downloadFileFromGitHubWithDepth(owner, repo, path, ref, 0, host) +func DownloadFileFromGitHubForHost(ctx context.Context, owner, repo, path, ref, host string) ([]byte, error) { + return downloadFileFromGitHubWithDepth(ctx, owner, repo, path, ref, 0, host) } // ResolveRefToSHAForHost resolves a git ref to its full commit SHA on a specific GitHub host. // Use this when the target repository is on a different host than the one configured via GH_HOST. // host is the hostname without scheme (e.g., "github.com", "myorg.ghe.com"). // An empty host uses the default configured host (GH_HOST or github.com). -func ResolveRefToSHAForHost(owner, repo, ref, host string) (string, error) { - return resolveRefToSHA(owner, repo, ref, host) +func ResolveRefToSHAForHost(ctx context.Context, owner, repo, ref, host string) (string, error) { + return resolveRefToSHA(ctx, owner, repo, ref, host) } -func downloadFileFromGitHub(owner, repo, path, ref string) ([]byte, error) { - return downloadFileFromGitHubWithDepth(owner, repo, path, ref, 0, "") +func downloadFileFromGitHub(ctx context.Context, owner, repo, path, ref string) ([]byte, error) { + return downloadFileFromGitHubWithDepth(ctx, owner, repo, path, ref, 0, "") } -func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth int, host string) ([]byte, error) { +func downloadFileFromGitHubWithDepth(ctx context.Context, owner, repo, path, ref string, symlinkDepth int, host string) ([]byte, error) { client, err := createRESTClientForHost(host) if err != nil { if gitutil.IsAuthError(err.Error()) { remoteLog.Printf("REST client creation failed due to auth error, attempting git fallback for %s/%s/%s@%s: %v", owner, repo, path, ref, err) - content, gitErr := downloadFileViaGit(context.Background(), owner, repo, path, ref, host) + content, gitErr := downloadFileViaGit(ctx, owner, repo, path, ref, host) if gitErr != nil { remoteLog.Printf("Git fallback also failed for %s/%s/%s@%s: %v", owner, repo, path, ref, gitErr) return nil, fmt.Errorf("failed to fetch file content: %w", err) @@ -919,15 +924,15 @@ func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth Name string `json:"name"` } - err = fetchRemoteFileContent(client, owner, repo, path, ref, &fileContent) + err = fetchRemoteFileContent(ctx, client, owner, repo, path, ref, &fileContent) if err != nil { if gitutil.IsAuthError(err.Error()) { remoteLog.Printf("GitHub API authentication failed, attempting git fallback for %s/%s/%s@%s", owner, repo, path, ref) - content, gitErr := downloadFileViaGit(context.Background(), owner, repo, path, ref, host) + content, gitErr := downloadFileViaGit(ctx, owner, repo, path, ref, host) if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s/%s@%s", owner, repo, path, ref) - return downloadFileViaPublicAPI(context.Background(), owner, repo, path, ref) + return downloadFileViaPublicAPI(ctx, owner, repo, path, ref) } return nil, fmt.Errorf("failed to fetch file content via GitHub API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -935,7 +940,7 @@ func downloadFileFromGitHubWithDepth(owner, repo, path, ref string, symlinkDepth } if errorutil.IsNotFoundError(err) && symlinkDepth < constants.MaxSymlinkDepth { - if content, handled, resolveErr := retryDownloadViaResolvedSymlink(client, owner, repo, path, ref, symlinkDepth, host); handled { + if content, handled, resolveErr := retryDownloadViaResolvedSymlink(ctx, client, owner, repo, path, ref, symlinkDepth, host); handled { return content, resolveErr } } @@ -977,8 +982,8 @@ func buildContentsAPIPath(owner, repo, path, ref string) string { ) } -func fetchRemoteFileContent(client *api.RESTClient, owner, repo, path, ref string, fileContent any) error { - return client.Get(buildContentsAPIPath(owner, repo, path, ref), fileContent) +func fetchRemoteFileContent(ctx context.Context, client *api.RESTClient, owner, repo, path, ref string, fileContent any) error { + return client.DoWithContext(ctx, http.MethodGet, buildContentsAPIPath(owner, repo, path, ref), nil, fileContent) } // downloadFileViaPublicAPI downloads a file from a public GitHub repository @@ -1010,16 +1015,17 @@ func downloadFileViaPublicAPI(ctx context.Context, owner, repo, path, ref string } func retryDownloadViaResolvedSymlink( + ctx context.Context, client *api.RESTClient, owner, repo, path, ref string, symlinkDepth int, host string, ) ([]byte, bool, error) { remoteLog.Printf("File not found at %s/%s/%s@%s, checking for symlinks in path (depth: %d)", owner, repo, path, ref, symlinkDepth) - resolvedPath, resolveErr := resolveRemoteSymlinks(client, owner, repo, path, ref) + resolvedPath, resolveErr := resolveRemoteSymlinks(ctx, client, owner, repo, path, ref) if resolveErr == nil && resolvedPath != path { remoteLog.Printf("Retrying download with symlink-resolved path: %s -> %s", path, resolvedPath) - content, err := downloadFileFromGitHubWithDepth(owner, repo, resolvedPath, ref, symlinkDepth+1, host) + content, err := downloadFileFromGitHubWithDepth(ctx, owner, repo, resolvedPath, ref, symlinkDepth+1, host) return content, true, err } return nil, false, nil @@ -1027,17 +1033,17 @@ func retryDownloadViaResolvedSymlink( // ListWorkflowFiles lists workflow files from a remote GitHub repository // Returns a list of .md files in the specified directory (excluding subdirectories) -func ListWorkflowFiles(owner, repo, ref, workflowPath string) ([]string, error) { - return listWorkflowFilesForHost(owner, repo, ref, workflowPath, "") +func ListWorkflowFiles(ctx context.Context, owner, repo, ref, workflowPath string) ([]string, error) { + return listWorkflowFilesForHost(ctx, owner, repo, ref, workflowPath, "") } // ListWorkflowFilesForHost lists workflow files from a remote GitHub repository on an explicit host. // Use this when the target repository is on a different host than the one configured via GH_HOST. -func ListWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]string, error) { - return listWorkflowFilesForHost(owner, repo, ref, workflowPath, host) +func ListWorkflowFilesForHost(ctx context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { + return listWorkflowFilesForHost(ctx, owner, repo, ref, workflowPath, host) } -func listWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]string, error) { +func listWorkflowFilesForHost(ctx context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { remoteLog.Printf("Listing workflow files for %s/%s@%s (path: %s)", owner, repo, ref, workflowPath) client, err := createRESTClientForHost(host) @@ -1055,7 +1061,7 @@ func listWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]st // Fetch directory contents from GitHub API endpoint := buildContentsAPIPath(owner, repo, workflowPath, ref) - err = client.Get(endpoint, &contents) + err = client.DoWithContext(ctx, http.MethodGet, endpoint, nil, &contents) if err != nil { errStr := err.Error() @@ -1067,7 +1073,7 @@ func listWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]st if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listWorkflowFilesViaPublicAPI(context.Background(), owner, repo, ref, workflowPath) + return listWorkflowFilesViaPublicAPI(ctx, owner, repo, ref, workflowPath) } return nil, fmt.Errorf("failed to list workflow files via GitHub API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -1092,11 +1098,11 @@ func listWorkflowFilesForHost(owner, repo, ref, workflowPath, host string) ([]st // ListDirAllFilesForHost lists all files (any extension) that are direct children of // the given directory in a remote GitHub repository. Subdirectories and their contents // are not included. This is used for skill file discovery. -func ListDirAllFilesForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - return listDirAllFilesForHost(owner, repo, ref, dirPath, host) +func ListDirAllFilesForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + return listDirAllFilesForHost(ctx, owner, repo, ref, dirPath, host) } -func listDirAllFilesForHost(owner, repo, ref, dirPath, host string) ([]string, error) { +func listDirAllFilesForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { remoteLog.Printf("Listing all files in dir for %s/%s@%s (path: %s)", owner, repo, ref, dirPath) client, err := createRESTClientForHost(host) @@ -1112,7 +1118,7 @@ func listDirAllFilesForHost(owner, repo, ref, dirPath, host string) ([]string, e } endpoint := buildContentsAPIPath(owner, repo, dirPath, ref) - err = client.Get(endpoint, &contents) + err = client.DoWithContext(ctx, http.MethodGet, endpoint, nil, &contents) if err != nil { errStr := err.Error() if gitutil.IsAuthError(errStr) { @@ -1121,7 +1127,7 @@ func listDirAllFilesForHost(owner, repo, ref, dirPath, host string) ([]string, e if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listDirAllFilesViaPublicAPI(context.Background(), owner, repo, ref, dirPath) + return listDirAllFilesViaPublicAPI(ctx, owner, repo, ref, dirPath) } return nil, fmt.Errorf("failed to list dir files via API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } @@ -1205,11 +1211,11 @@ func listDirAllFilesViaPublicAPI(ctx context.Context, owner, repo, ref, dirPath // ListDirAllFilesRecursivelyForHost lists all files (any extension) that are under the // given directory in a remote GitHub repository, including files in subdirectories at any // depth. This is used for copying entire skill folders. -func ListDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - return listDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host) +func ListDirAllFilesRecursivelyForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + return listDirAllFilesRecursivelyForHost(ctx, owner, repo, ref, dirPath, host) } -func listDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host string) ([]string, error) { +func listDirAllFilesRecursivelyForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { remoteLog.Printf("Listing all files recursively in dir for %s/%s@%s (path: %s)", owner, repo, ref, dirPath) client, err := createRESTClientForHost(host) @@ -1218,7 +1224,7 @@ func listDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host string) ( return listDirAllFilesRecursivelyViaGitForHost(owner, repo, ref, dirPath, host) } - files, err := listContentsRecursively(client, owner, repo, ref, dirPath) + files, err := listContentsRecursively(ctx, client, owner, repo, ref, dirPath) if err != nil { errStr := err.Error() if gitutil.IsAuthError(errStr) { @@ -1241,12 +1247,12 @@ func listDirAllFilesRecursivelyForHost(owner, repo, ref, dirPath, host string) ( // listContentsRecursively uses the GitHub Contents API to recursively enumerate all // files under dirPath. Each subdirectory triggers an additional API call. -func listContentsRecursively(client *api.RESTClient, owner, repo, ref, dirPath string) ([]string, error) { +func listContentsRecursively(ctx context.Context, client *api.RESTClient, owner, repo, ref, dirPath string) ([]string, error) { const maxSkillDirRecursionDepth = 10 - return listContentsRecursivelyWithDepth(client, owner, repo, ref, dirPath, 0, maxSkillDirRecursionDepth) + return listContentsRecursivelyWithDepth(ctx, client, owner, repo, ref, dirPath, 0, maxSkillDirRecursionDepth) } -func listContentsRecursivelyWithDepth(client *api.RESTClient, owner, repo, ref, dirPath string, depth, maxDepth int) ([]string, error) { +func listContentsRecursivelyWithDepth(ctx context.Context, client *api.RESTClient, owner, repo, ref, dirPath string, depth, maxDepth int) ([]string, error) { if depth > maxDepth { return nil, fmt.Errorf("maximum skill directory recursion depth exceeded at %q (max depth: %d)", dirPath, maxDepth) } @@ -1258,7 +1264,7 @@ func listContentsRecursivelyWithDepth(client *api.RESTClient, owner, repo, ref, } endpoint := buildContentsAPIPath(owner, repo, dirPath, ref) - if err := client.Get(endpoint, &contents); err != nil { + if err := client.DoWithContext(ctx, http.MethodGet, endpoint, nil, &contents); err != nil { return nil, fmt.Errorf("failed to list dir files from %s/%s (path: %s): %w", owner, repo, dirPath, err) } @@ -1268,7 +1274,7 @@ func listContentsRecursivelyWithDepth(client *api.RESTClient, owner, repo, ref, case "file": files = append(files, item.Path) case "dir": - subFiles, err := listContentsRecursivelyWithDepth(client, owner, repo, ref, item.Path, depth+1, maxDepth) + subFiles, err := listContentsRecursivelyWithDepth(ctx, client, owner, repo, ref, item.Path, depth+1, maxDepth) if err != nil { return nil, err } @@ -1351,11 +1357,11 @@ func fetchPublicGitHubContentsAPI(ctx context.Context, owner, repo, path, ref st // ListDirSubdirsForHost lists subdirectory paths that are direct children of the given // directory in a remote GitHub repository. This is used for auto-discovering skill dirs. -func ListDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, error) { - return listDirSubdirsForHost(owner, repo, ref, dirPath, host) +func ListDirSubdirsForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + return listDirSubdirsForHost(ctx, owner, repo, ref, dirPath, host) } -func listDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, error) { +func listDirSubdirsForHost(ctx context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { remoteLog.Printf("Listing subdirs in %s/%s@%s (path: %s)", owner, repo, ref, dirPath) client, err := createRESTClientForHost(host) @@ -1371,7 +1377,7 @@ func listDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, er } endpoint := buildContentsAPIPath(owner, repo, dirPath, ref) - err = client.Get(endpoint, &contents) + err = client.DoWithContext(ctx, http.MethodGet, endpoint, nil, &contents) if err != nil { errStr := err.Error() if gitutil.IsAuthError(errStr) { @@ -1380,7 +1386,7 @@ func listDirSubdirsForHost(owner, repo, ref, dirPath, host string) ([]string, er if gitErr != nil { if host == "" || host == "github.com" { remoteLog.Printf("Git fallback also failed, attempting unauthenticated API for %s/%s@%s", owner, repo, ref) - return listDirSubdirsViaPublicAPI(context.Background(), owner, repo, ref, dirPath) + return listDirSubdirsViaPublicAPI(ctx, owner, repo, ref, dirPath) } return nil, fmt.Errorf("failed to list subdirs via API (auth error) and git fallback: API error: %w, Git error: %w", err, gitErr) } diff --git a/pkg/parser/remote_fetch_integration_test.go b/pkg/parser/remote_fetch_integration_test.go index 53ec9f01610..d623aa6cbb6 100644 --- a/pkg/parser/remote_fetch_integration_test.go +++ b/pkg/parser/remote_fetch_integration_test.go @@ -21,7 +21,7 @@ func TestDownloadFileFromGitHubRESTClient(t *testing.T) { path := "Go.gitignore" ref := "main" - content, err := downloadFileFromGitHub(owner, repo, path, ref) + content, err := downloadFileFromGitHub(t.Context(), owner, repo, path, ref) if err != nil { // If we get an auth error, we can skip this test in CI environments // where GitHub tokens might not be available @@ -54,7 +54,7 @@ func TestDownloadFileFromGitHubInvalidRepo(t *testing.T) { path := "README.md" ref := "main" - _, err := downloadFileFromGitHub(owner, repo, path, ref) + _, err := downloadFileFromGitHub(t.Context(), owner, repo, path, ref) if err == nil { t.Fatal("Expected error for nonexistent repository, got nil") } @@ -79,7 +79,7 @@ func TestDownloadFileFromGitHubInvalidPath(t *testing.T) { path := "nonexistent-file-xyz123.txt" ref := "main" - _, err := downloadFileFromGitHub(owner, repo, path, ref) + _, err := downloadFileFromGitHub(t.Context(), owner, repo, path, ref) if err == nil { t.Fatal("Expected error for nonexistent file, got nil") } @@ -108,7 +108,7 @@ func TestDownloadFileFromGitHubWithSHA(t *testing.T) { // Note: This might fail if the SHA doesn't exist, but demonstrates SHA support ref := "main" // Using main instead of specific SHA to avoid brittleness - content, err := downloadFileFromGitHub(owner, repo, path, ref) + content, err := downloadFileFromGitHub(t.Context(), owner, repo, path, ref) if err != nil { if strings.Contains(err.Error(), "auth") || strings.Contains(err.Error(), "forbidden") { t.Skip("Skipping test due to authentication requirements") @@ -236,7 +236,7 @@ func TestResolveRemoteSymlinksNoSymlinks(t *testing.T) { func TestDownloadFileFromGitHubSymlinkRoute(t *testing.T) { // Use a path through a real directory but with a nonexistent file. // This triggers: 404 -> symlink resolution -> "no symlinks found" -> original error. - _, err := downloadFileFromGitHub("github", "gitignore", "Global/nonexistent-file-xyz123.gitignore", "main") + _, err := downloadFileFromGitHub(t.Context(), "github", "gitignore", "Global/nonexistent-file-xyz123.gitignore", "main") require.Error(t, err, "Expected error for nonexistent file") skipOnAuthError(t, err) @@ -294,7 +294,7 @@ func TestDownloadFileFromGitHubUnauthenticated(t *testing.T) { path := "Go.gitignore" ref := "main" - content, err := downloadFileFromGitHub(owner, repo, path, ref) + content, err := downloadFileFromGitHub(t.Context(), owner, repo, path, ref) // If the REST client unexpectedly succeeds (e.g., gh config file has a token), // that is also fine – the point is that the file is returned without error. if err != nil { diff --git a/pkg/parser/remote_fetch_test.go b/pkg/parser/remote_fetch_test.go index 0ca37a8757a..693938c8ba7 100644 --- a/pkg/parser/remote_fetch_test.go +++ b/pkg/parser/remote_fetch_test.go @@ -74,7 +74,7 @@ func TestGitFallbackRequiresNonEmptyRef(t *testing.T) { } func TestListContentsRecursivelyWithDepth_MaxDepthGuard(t *testing.T) { - _, err := listContentsRecursivelyWithDepth(nil, "owner", "repo", "main", "skills/demo/deep", 11, 10) + _, err := listContentsRecursivelyWithDepth(t.Context(), nil, "owner", "repo", "main", "skills/demo/deep", 11, 10) if err == nil { t.Fatal("expected depth limit error") }