Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/content/docs/setup/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Add workflows from The Agentics collection or other repositories to `.github/wor
```bash wrap
gh aw add githubnext/agentics/ci-doctor # Add single workflow
gh aw add githubnext/agentics/ci-doctor@v1.0.0 # Add specific version
gh aw add my-org/my-repo/agentic-workflows/pr-review.md@feature/github-agentic-workflows
gh aw add githubnext/agentics/ci-doctor --dir shared # Organize in subdirectory
gh aw add githubnext/agentics/ci-doctor --create-pull-request # Create PR instead of commit
gh aw add https://example.com/workflows/my-workflow.md # Arbitrary HTTPS URL (markdown)
Expand Down
3 changes: 2 additions & 1 deletion pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Examples:
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/packages/repo-assist # Add package from nested aw.yml
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor@v1.0.0 # Add with version
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/workflows/ci-doctor.md@main
` + string(constants.CLIExtensionPrefix) + ` add my-org/my-repo/agentic-workflows/pr-review.md@feature/github-agentic-workflows
` + string(constants.CLIExtensionPrefix) + ` add https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor.md
` + string(constants.CLIExtensionPrefix) + ` add https://example.com/my-workflow.md # Add workflow from any HTTPS URL
` + string(constants.CLIExtensionPrefix) + ` add https://example.com/workflow.json # Import JSON workflow definition
Expand All @@ -80,7 +81,7 @@ Workflow specifications:
- application/json → converted from a JSON workflow definition
- Local file: "./path/to/workflow.md" (adds a workflow from local filesystem)
- Local wildcard: "./*.md" or "./dir/*.md" (adds all .md files matching pattern)
- Version can be tag, branch, or SHA (for remote workflows)
- Version can be a tag, branch (including branches with slashes like feature/my-branch), or commit SHA (for remote workflows)

The -n flag allows you to specify a custom name for the workflow file (not allowed when adding multiple workflows at once).
The --dir flag allows you to specify the workflow directory (default: .github/workflows).
Expand Down
125 changes: 125 additions & 0 deletions pkg/cli/add_package_manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,57 @@ files:
assert.Equal(t, ".github/workflows/nightly-review.md", resolved.Workflows[1].Spec.WorkflowPath)
}

func TestResolveWorkflows_RepositoryPackageWithRef(t *testing.T) {
originalFetchFn := fetchWorkflowFromSourceWithContextFn
originalDownload := downloadPackageFileFromGitHubForHost
originalList := listPackageWorkflowFilesForHost
originalDefaultBranch := getRepositoryPackageDefaultBranch
t.Cleanup(func() {
fetchWorkflowFromSourceWithContextFn = originalFetchFn
downloadPackageFileFromGitHubForHost = originalDownload
listPackageWorkflowFilesForHost = originalList
getRepositoryPackageDefaultBranch = originalDefaultBranch
})
getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) {
t.Fatalf("default branch lookup should not be used when ref is explicitly provided (repoSlug=%q host=%q)", repoSlug, host)
return "", nil
}

const expectedRef = "feature/github-agentic-workflows"
downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) {
assert.Equal(t, expectedRef, ref)
switch path {
case "aw.yml":
return []byte(`name: Repo Assist
files:
- workflows/review.md
`), nil
case "README.md":
return []byte("# Repo Assist\n"), nil
}
return nil, createRepositoryPackageNotFoundError(path)
}
listPackageWorkflowFilesForHost = func(owner, repo, ref, workflowPath, host string) ([]string, error) {
t.Fatalf("unexpected scan of %q (owner=%q repo=%q ref=%q host=%q)", workflowPath, owner, repo, ref, host)
return nil, nil
}
fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) {
assert.Equal(t, expectedRef, spec.Version)
return &FetchedWorkflow{
Content: []byte("---\nname: Test\non: push\n---\n"),
CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
IsLocal: false,
SourcePath: spec.WorkflowPath,
}, nil
}

resolved, err := ResolveWorkflows(context.Background(), []string{"owner/repo@" + expectedRef}, false)
require.NoError(t, err)
require.Len(t, resolved.Workflows, 1)
assert.Equal(t, "workflows/review.md", resolved.Workflows[0].Spec.WorkflowPath)
assert.Equal(t, expectedRef, resolved.Workflows[0].Spec.Version)
}

func TestResolveWorkflows_NestedRepositoryPackage(t *testing.T) {
originalFetchFn := fetchWorkflowFromSourceWithContextFn
originalDownload := downloadPackageFileFromGitHubForHost
Expand Down Expand Up @@ -494,6 +545,66 @@ func TestResolveWorkflows_FallsBackToWorkflowWhenNestedManifestMissing(t *testin
assert.Equal(t, "workflows/review.md", resolved.Workflows[0].Spec.WorkflowPath)
}

func TestResolveWorkflows_FallsBackToWorkflowWithRefWhenNestedManifestMissing(t *testing.T) {
originalFetchFn := fetchWorkflowFromSourceWithContextFn
originalDownload := downloadPackageFileFromGitHubForHost
originalDefaultBranch := getRepositoryPackageDefaultBranch
t.Cleanup(func() {
fetchWorkflowFromSourceWithContextFn = originalFetchFn
downloadPackageFileFromGitHubForHost = originalDownload
getRepositoryPackageDefaultBranch = originalDefaultBranch
})
getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) {
return "main", nil
}

downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) {
return nil, createRepositoryPackageNotFoundError(path)
}
fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) {
return &FetchedWorkflow{
Content: []byte("---\nname: Test\non: push\n---\n"),
CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
IsLocal: false,
SourcePath: spec.WorkflowPath,
}, nil
}

resolved, err := ResolveWorkflows(context.Background(), []string{"owner/repo/review@feature/github-agentic-workflows"}, false)
require.NoError(t, err)
require.Len(t, resolved.Workflows, 1)
assert.Equal(t, "workflows/review.md", resolved.Workflows[0].Spec.WorkflowPath)
assert.Equal(t, "feature/github-agentic-workflows", resolved.Workflows[0].Spec.Version)
}

func TestResolveWorkflows_DirectWorkflowPathWithBranchRef(t *testing.T) {
originalFetchFn := fetchWorkflowFromSourceWithContextFn
originalDownload := downloadPackageFileFromGitHubForHost
t.Cleanup(func() {
fetchWorkflowFromSourceWithContextFn = originalFetchFn
downloadPackageFileFromGitHubForHost = originalDownload
})

downloadPackageFileFromGitHubForHost = func(owner, repo, path, ref, host string) ([]byte, error) {
t.Fatalf("direct workflow path must not be resolved as repository package (unexpected aw.yml lookup: %s)", path)
return nil, nil
}
fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) {
return &FetchedWorkflow{
Content: []byte("---\nname: Test\non: push\n---\n"),
CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
IsLocal: false,
SourcePath: spec.WorkflowPath,
}, nil
}

resolved, err := ResolveWorkflows(context.Background(), []string{"owner/repo/agentic-workflows/pr-review.md@feature/github-agentic-workflows"}, false)
require.NoError(t, err)
require.Len(t, resolved.Workflows, 1)
assert.Equal(t, "agentic-workflows/pr-review.md", resolved.Workflows[0].Spec.WorkflowPath)
assert.Equal(t, "feature/github-agentic-workflows", resolved.Workflows[0].Spec.Version)
}

func TestParseRepositoryPackageSpec(t *testing.T) {
tests := []struct {
name string
Expand All @@ -502,13 +613,21 @@ func TestParseRepositoryPackageSpec(t *testing.T) {
wantErr string
wantRepoSlug string
wantPackagePath string
wantVersion string
}{
{
name: "repo only package",
spec: "owner/repo",
wantOK: true,
wantRepoSlug: "owner/repo",
},
{
name: "repo only package with ref",
spec: "owner/repo@feature/github-agentic-workflows",
wantOK: true,
wantRepoSlug: "owner/repo",
wantVersion: "feature/github-agentic-workflows",
},
{
name: "nested package path",
spec: "owner/repo/packages/repo-assist",
Expand All @@ -521,6 +640,11 @@ func TestParseRepositoryPackageSpec(t *testing.T) {
spec: "owner/repo/workflows/review.md",
wantOK: false,
},
{
name: "workflow path with branch ref is not package",
spec: "owner/repo/agentic-workflows/pr-review.md@feature/github-agentic-workflows",
wantOK: false,
},
{
name: "url is not package",
spec: "https://github.com/owner/repo",
Expand Down Expand Up @@ -551,6 +675,7 @@ func TestParseRepositoryPackageSpec(t *testing.T) {
require.NotNil(t, repoSpec)
assert.Equal(t, tt.wantRepoSlug, repoSpec.RepoSlug)
assert.Equal(t, tt.wantPackagePath, repoSpec.PackagePath)
assert.Equal(t, tt.wantVersion, repoSpec.Version)
})
}
}
Expand Down
Loading