Skip to content

Commit 0e6c1f3

Browse files
authored
Add --dir flag to specify subfolder for workflow installation (#3459)
1 parent cbddeaa commit 0e6c1f3

6 files changed

Lines changed: 50 additions & 25 deletions

File tree

.github/workflows/super-linter-report.lock.yml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cli/add_command.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Examples:
3434
` + constants.CLIExtensionPrefix + ` add githubnext/agentics/ci-doctor --pr --force
3535
` + constants.CLIExtensionPrefix + ` add githubnext/agentics/*
3636
` + constants.CLIExtensionPrefix + ` add githubnext/agentics/*@v1.0.0
37+
` + constants.CLIExtensionPrefix + ` add githubnext/agentics/ci-doctor --dir shared # Add to .github/workflows/shared/
3738
3839
Workflow specifications:
3940
- Two parts: "owner/repo[@version]" (lists available workflows in the repository)
@@ -44,6 +45,7 @@ Workflow specifications:
4445
- Version can be tag, branch, or SHA
4546
4647
The -n flag allows you to specify a custom name for the workflow file (only applies to the first workflow when adding multiple).
48+
The --dir flag allows you to specify a subdirectory under .github/workflows/ where the workflow will be added.
4749
The --pr flag automatically creates a pull request with the workflow changes.
4850
The --force flag overwrites existing workflow files.`,
4951
Run: func(cmd *cobra.Command, args []string) {
@@ -56,6 +58,7 @@ The --force flag overwrites existing workflow files.`,
5658
appendText, _ := cmd.Flags().GetString("append")
5759
verbose, _ := cmd.Flags().GetBool("verbose")
5860
noGitattributes, _ := cmd.Flags().GetBool("no-gitattributes")
61+
workflowDir, _ := cmd.Flags().GetString("dir")
5962

6063
// If no arguments provided and not in CI, automatically use interactive mode
6164
if len(args) == 0 && !IsRunningInCI() {
@@ -76,12 +79,12 @@ The --force flag overwrites existing workflow files.`,
7679

7780
// Handle normal mode
7881
if prFlag {
79-
if err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, noGitattributes); err != nil {
82+
if err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, noGitattributes, workflowDir); err != nil {
8083
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
8184
os.Exit(1)
8285
}
8386
} else {
84-
if err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, noGitattributes); err != nil {
87+
if err := AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, noGitattributes, workflowDir); err != nil {
8588
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
8689
os.Exit(1)
8790
}
@@ -113,13 +116,16 @@ The --force flag overwrites existing workflow files.`,
113116
// Add no-gitattributes flag to add command
114117
cmd.Flags().Bool("no-gitattributes", false, "Skip updating .gitattributes file")
115118

119+
// Add workflow directory flag to add command
120+
cmd.Flags().StringP("dir", "d", "", "Specify subdirectory under .github/workflows/ (e.g., 'shared' for .github/workflows/shared/)")
121+
116122
return cmd
117123
}
118124

119125
// AddWorkflows adds one or more workflows from components to .github/workflows
120126
// with optional repository installation and PR creation
121-
func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, noGitattributes bool) error {
122-
addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v", len(workflows), engineOverride, createPR, noGitattributes)
127+
func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, noGitattributes bool, workflowDir string) error {
128+
addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v, workflowDir=%s", len(workflows), engineOverride, createPR, noGitattributes, workflowDir)
123129

124130
if len(workflows) == 0 {
125131
return fmt.Errorf("at least one workflow name is required")
@@ -232,12 +238,12 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
232238
// Handle PR creation workflow
233239
if createPR {
234240
addLog.Print("Creating workflow with PR")
235-
return addWorkflowsWithPR(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard)
241+
return addWorkflowsWithPR(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard, workflowDir)
236242
}
237243

238244
// Handle normal workflow addition
239245
addLog.Print("Adding workflows normally without PR")
240-
return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard)
246+
return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard, workflowDir)
241247
}
242248

243249
// handleRepoOnlySpec handles the case when user provides only owner/repo without workflow name
@@ -338,7 +344,7 @@ func displayAvailableWorkflows(repoSlug, version string, verbose bool) error {
338344
}
339345

340346
// addWorkflowsNormal handles normal workflow addition without PR creation
341-
func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool) error {
347+
func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool, workflowDir string) error {
342348
// Create file tracker for all operations
343349
tracker, err := NewFileTracker()
344350
if err != nil {
@@ -379,7 +385,7 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
379385
currentName = name
380386
}
381387

382-
if err := addWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, appendText, tracker, fromWildcard); err != nil {
388+
if err := addWorkflowWithTracking(workflow, number, verbose, engineOverride, currentName, force, appendText, tracker, fromWildcard, workflowDir); err != nil {
383389
return fmt.Errorf("failed to add workflow '%s': %w", workflow.String(), err)
384390
}
385391
}
@@ -392,7 +398,7 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
392398
}
393399

394400
// addWorkflowsWithPR handles workflow addition with PR creation
395-
func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool) error {
401+
func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool, workflowDir string) error {
396402
// Get current branch for restoration later
397403
currentBranch, err := getCurrentBranch()
398404
if err != nil {
@@ -421,7 +427,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
421427
}()
422428

423429
// Add workflows using the normal function logic
424-
if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, fromWildcard); err != nil {
430+
if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, fromWildcard, workflowDir); err != nil {
425431
// Rollback on error
426432
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
427433
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
@@ -500,7 +506,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
500506
}
501507

502508
// addWorkflowWithTracking adds a workflow from components to .github/workflows with file tracking
503-
func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool) error {
509+
func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, tracker *FileTracker, fromWildcard bool, workflowDir string) error {
504510
if verbose {
505511
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding workflow: %s", workflow.String())))
506512
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Number of copies: %d", number)))
@@ -555,10 +561,30 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e
555561
return fmt.Errorf("add workflow requires being in a git repository: %w", err)
556562
}
557563

558-
// Ensure .github/workflows directory exists relative to git root
559-
githubWorkflowsDir := filepath.Join(gitRoot, ".github/workflows")
564+
// Determine the target workflow directory
565+
var githubWorkflowsDir string
566+
if workflowDir != "" {
567+
// Validate that the path is relative
568+
if filepath.IsAbs(workflowDir) {
569+
return fmt.Errorf("workflow directory must be a relative path, got: %s", workflowDir)
570+
}
571+
// Clean the path to avoid issues with ".." or other problematic elements
572+
workflowDir = filepath.Clean(workflowDir)
573+
// Ensure the path is under .github/workflows
574+
if !strings.HasPrefix(workflowDir, ".github/workflows") {
575+
// If user provided a subdirectory name, prepend .github/workflows/
576+
githubWorkflowsDir = filepath.Join(gitRoot, ".github/workflows", workflowDir)
577+
} else {
578+
githubWorkflowsDir = filepath.Join(gitRoot, workflowDir)
579+
}
580+
} else {
581+
// Use default .github/workflows directory
582+
githubWorkflowsDir = filepath.Join(gitRoot, ".github/workflows")
583+
}
584+
585+
// Ensure the target directory exists
560586
if err := os.MkdirAll(githubWorkflowsDir, 0755); err != nil {
561-
return fmt.Errorf("failed to create .github/workflows directory: %w", err)
587+
return fmt.Errorf("failed to create workflow directory %s: %w", githubWorkflowsDir, err)
562588
}
563589

564590
// Determine the workflowName to use

pkg/cli/add_current_repo_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestAddWorkflowsFromCurrentRepository(t *testing.T) {
7676
// Clear cache before each test
7777
ClearCurrentRepoSlugCache()
7878

79-
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false)
79+
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, "")
8080

8181
if tt.expectError {
8282
if err == nil {
@@ -177,7 +177,7 @@ func TestAddWorkflowsFromCurrentRepositoryMultiple(t *testing.T) {
177177
// Clear cache before each test
178178
ClearCurrentRepoSlugCache()
179179

180-
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false)
180+
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, "")
181181

182182
if tt.expectError {
183183
if err == nil {
@@ -218,7 +218,7 @@ func TestAddWorkflowsFromCurrentRepositoryNotInGitRepo(t *testing.T) {
218218

219219
// When not in a git repo, the check should be skipped (can't determine current repo)
220220
// The function should proceed and fail for other reasons (e.g., workflow not found)
221-
err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false)
221+
err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, "")
222222

223223
// Should NOT get the "cannot add workflows from the current repository" error
224224
if err != nil && strings.Contains(err.Error(), "cannot add workflows from the current repository") {

pkg/cli/add_gitattributes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ This is a test workflow.`
8282
os.Remove(".gitattributes")
8383

8484
// Call addWorkflowsNormal with noGitattributes=false
85-
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false)
85+
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, "")
8686
if err != nil {
8787
// We expect this to fail because we don't have a full workflow setup,
8888
// but gitattributes should still be updated before the error
@@ -112,7 +112,7 @@ This is a test workflow.`
112112
os.Remove(".gitattributes")
113113

114114
// Call addWorkflowsNormal with noGitattributes=true
115-
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false)
115+
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "")
116116
if err != nil {
117117
// We expect this to fail because we don't have a full workflow setup
118118
t.Logf("Expected error during workflow addition: %v", err)
@@ -134,7 +134,7 @@ This is a test workflow.`
134134
}
135135

136136
// Call addWorkflowsNormal with noGitattributes=true
137-
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false)
137+
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "")
138138
if err != nil {
139139
// We expect this to fail because we don't have a full workflow setup
140140
t.Logf("Expected error during workflow addition: %v", err)

pkg/cli/add_wildcard_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ on: push
489489

490490
// Test 1: Non-wildcard duplicate should return error
491491
t.Run("non_wildcard_duplicate_returns_error", func(t *testing.T) {
492-
err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, false)
492+
err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, false, "")
493493
if err == nil {
494494
t.Error("Expected error for non-wildcard duplicate, got nil")
495495
}
@@ -500,15 +500,15 @@ on: push
500500

501501
// Test 2: Wildcard duplicate should return nil (skip with warning)
502502
t.Run("wildcard_duplicate_returns_nil", func(t *testing.T) {
503-
err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, true)
503+
err := addWorkflowWithTracking(spec, 1, false, "", "", false, "", nil, true, "")
504504
if err != nil {
505505
t.Errorf("Expected nil for wildcard duplicate (should skip), got error: %v", err)
506506
}
507507
})
508508

509509
// Test 3: Wildcard duplicate with force flag should succeed
510510
t.Run("wildcard_duplicate_with_force_succeeds", func(t *testing.T) {
511-
err := addWorkflowWithTracking(spec, 1, false, "", "", true, "", nil, true)
511+
err := addWorkflowWithTracking(spec, 1, false, "", "", true, "", nil, true, "")
512512
// This should succeed or return nil
513513
if err != nil && strings.Contains(err.Error(), "already exists") {
514514
t.Errorf("Expected success with force flag, got 'already exists' error: %v", err)

pkg/cli/trial_command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logica
726726
}
727727

728728
// Add the workflow from the installed package
729-
if err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false); err != nil {
729+
if err := AddWorkflows([]string{parsedSpec.String()}, 1, verbose, "", "", true, appendText, false, false, ""); err != nil {
730730
return fmt.Errorf("failed to add workflow: %w", err)
731731
}
732732
}

0 commit comments

Comments
 (0)