From 7ed34586415004d38d01c6058d2d57b6d1a8630c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:48:58 +0000 Subject: [PATCH] feat: add codemod disable flags Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/fix_codemods.go | 51 ++++++++++++++++++++++++++++++++- pkg/cli/fix_codemods_test.go | 21 ++++++++++++++ pkg/cli/fix_command.go | 24 ++++++++++------ pkg/cli/fix_command_test.go | 36 ++++++++++++++++++++++- pkg/cli/upgrade_command.go | 34 +++++++++++++--------- pkg/cli/upgrade_command_test.go | 5 ++++ 6 files changed, 146 insertions(+), 25 deletions(-) diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go index 82a32cb477e..c8237218da5 100644 --- a/pkg/cli/fix_codemods.go +++ b/pkg/cli/fix_codemods.go @@ -1,6 +1,12 @@ package cli -import "github.com/github/gh-aw/pkg/logger" +import ( + "fmt" + "slices" + "strings" + + "github.com/github/gh-aw/pkg/logger" +) var fixCodemodsLog = logger.New("cli:fix_codemods") @@ -85,3 +91,46 @@ func GetAllCodemods() []Codemod { fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods)) return codemods } + +// GetCodemods returns all codemods except any explicitly disabled by ID. +func GetCodemods(disabledIDs []string) ([]Codemod, error) { + codemods := GetAllCodemods() + if len(disabledIDs) == 0 { + return codemods, nil + } + + disabledSet := make(map[string]struct{}, len(disabledIDs)) + for _, id := range disabledIDs { + if id == "" { + continue + } + disabledSet[id] = struct{}{} + } + + if len(disabledSet) == 0 { + return codemods, nil + } + + knownIDs := make([]string, 0, len(codemods)) + filtered := make([]Codemod, 0, len(codemods)) + for _, codemod := range codemods { + knownIDs = append(knownIDs, codemod.ID) + if _, disabled := disabledSet[codemod.ID]; disabled { + continue + } + filtered = append(filtered, codemod) + } + + var unknown []string + for id := range disabledSet { + if !slices.Contains(knownIDs, id) { + unknown = append(unknown, id) + } + } + if len(unknown) > 0 { + slices.Sort(unknown) + return nil, fmt.Errorf("unknown codemod ID(s): %s", strings.Join(unknown, ", ")) + } + + return filtered, nil +} diff --git a/pkg/cli/fix_codemods_test.go b/pkg/cli/fix_codemods_test.go index 7cdc93c8c28..6e7baa9c343 100644 --- a/pkg/cli/fix_codemods_test.go +++ b/pkg/cli/fix_codemods_test.go @@ -124,6 +124,27 @@ func TestGetAllCodemods_NoduplicateIDs(t *testing.T) { } } +func TestGetCodemods_DisablesRequestedCodemods(t *testing.T) { + codemods, err := GetCodemods([]string{"timeout-minutes-migration", "network-firewall-migration"}) + require.NoError(t, err) + + var ids []string + for _, codemod := range codemods { + ids = append(ids, codemod.ID) + } + + assert.NotContains(t, ids, "timeout-minutes-migration") + assert.NotContains(t, ids, "network-firewall-migration") + assert.Contains(t, ids, "command-to-slash-command-migration") +} + +func TestGetCodemods_UnknownDisabledCodemodReturnsError(t *testing.T) { + codemods, err := GetCodemods([]string{"not-a-real-codemod"}) + require.Error(t, err) + assert.Nil(t, codemods) + assert.Contains(t, err.Error(), "unknown codemod ID(s): not-a-real-codemod") +} + func TestGetAllCodemods_InExpectedOrder(t *testing.T) { codemods := GetAllCodemods() diff --git a/pkg/cli/fix_command.go b/pkg/cli/fix_command.go index 61d77507378..6351e2d5d8a 100644 --- a/pkg/cli/fix_command.go +++ b/pkg/cli/fix_command.go @@ -18,15 +18,16 @@ var fixLog = logger.New("cli:fix_command") // FixConfig contains configuration for the fix command type FixConfig struct { - WorkflowIDs []string - Write bool - Verbose bool - WorkflowDir string // Custom workflow directory + WorkflowIDs []string + Write bool + Verbose bool + WorkflowDir string // Custom workflow directory + DisabledCodemodIDs []string // Codemod IDs to skip } // RunFix runs the fix command with the given configuration func RunFix(config FixConfig) error { - return runFixCommand(config.WorkflowIDs, config.Write, config.Verbose, config.WorkflowDir) + return runFixCommand(config.WorkflowIDs, config.Write, config.Verbose, config.WorkflowDir, config.DisabledCodemodIDs) } // NewFixCommand creates the fix command @@ -69,18 +70,20 @@ Examples: write, _ := cmd.Flags().GetBool("write") verbose, _ := cmd.Flags().GetBool("verbose") dir, _ := cmd.Flags().GetString("dir") + disabledCodemods, _ := cmd.Flags().GetStringSlice("disable-codemod") if listCodemods { return listAvailableCodemods() } - return runFixCommand(args, write, verbose, dir) + return runFixCommand(args, write, verbose, dir, disabledCodemods) }, } cmd.Flags().Bool("write", false, "Write changes to files (without this flag, no changes are made)") cmd.Flags().Bool("list-codemods", false, "List all available codemods and exit") cmd.Flags().StringP("dir", "d", "", "Workflow directory (default: .github/workflows)") + cmd.Flags().StringSlice("disable-codemod", nil, "Disable specific codemod IDs (repeatable)") // Register completions cmd.ValidArgsFunction = CompleteWorkflowNames @@ -110,8 +113,8 @@ func listAvailableCodemods() error { } // runFixCommand runs the fix command on specified or all workflows -func runFixCommand(workflowIDs []string, write bool, verbose bool, workflowDir string) error { - fixLog.Printf("Running fix command: workflowIDs=%v, write=%v, verbose=%v, workflowDir=%s", workflowIDs, write, verbose, workflowDir) +func runFixCommand(workflowIDs []string, write bool, verbose bool, workflowDir string, disabledCodemodIDs []string) error { + fixLog.Printf("Running fix command: workflowIDs=%v, write=%v, verbose=%v, workflowDir=%s, disabledCodemodIDs=%v", workflowIDs, write, verbose, workflowDir, disabledCodemodIDs) // Set up workflow directory (using default if not specified) if workflowDir == "" { @@ -149,7 +152,10 @@ func runFixCommand(workflowIDs []string, write bool, verbose bool, workflowDir s } // Load all codemods - codemods := GetAllCodemods() + codemods, err := GetCodemods(disabledCodemodIDs) + if err != nil { + return err + } fixLog.Printf("Loaded %d codemods", len(codemods)) // Process each file diff --git a/pkg/cli/fix_command_test.go b/pkg/cli/fix_command_test.go index e148898ca08..117c48cd9ab 100644 --- a/pkg/cli/fix_command_test.go +++ b/pkg/cli/fix_command_test.go @@ -482,7 +482,6 @@ func TestGetAllCodemods(t *testing.T) { if len(codemods) == 0 { t.Fatal("Expected at least one codemod, got none") } - // Check for required codemods expectedIDs := []string{ "timeout-minutes-migration", @@ -517,6 +516,41 @@ func TestGetAllCodemods(t *testing.T) { } } +func TestNewFixCommand_HasDisableCodemodFlag(t *testing.T) { + cmd := NewFixCommand() + require.NotNil(t, cmd) + + flag := cmd.Flags().Lookup("disable-codemod") + require.NotNil(t, flag, "fix command should register --disable-codemod") + assert.Equal(t, "stringSlice", flag.Value.Type()) + assert.Contains(t, flag.Usage, "Disable specific codemod IDs") +} + +func TestRunFix_DisabledCodemodSkipsMatchingFix(t *testing.T) { + tmpDir := t.TempDir() + workflowFile := filepath.Join(tmpDir, "test.md") + + content := `--- +on: workflow_dispatch +timeout_minutes: 30 +--- +# Test Workflow +` + require.NoError(t, os.WriteFile(workflowFile, []byte(content), 0644)) + + err := RunFix(FixConfig{ + Write: true, + WorkflowDir: tmpDir, + DisabledCodemodIDs: []string{"timeout-minutes-migration"}, + }) + require.NoError(t, err) + + updatedContent, err := os.ReadFile(workflowFile) + require.NoError(t, err) + assert.Contains(t, string(updatedContent), "timeout_minutes: 30") + assert.NotContains(t, string(updatedContent), "timeout-minutes: 30") +} + func TestFixCommand_CommandToSlashCommandMigration(t *testing.T) { // Create a temporary directory for test files tmpDir := t.TempDir() diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go index d0d6fdaecbc..cf4bdd4f46a 100644 --- a/pkg/cli/upgrade_command.go +++ b/pkg/cli/upgrade_command.go @@ -19,14 +19,15 @@ var upgradeLog = logger.New("cli:upgrade_command") // UpgradeConfig contains configuration for the upgrade command type UpgradeConfig struct { - Verbose bool - WorkflowDir string - NoFix bool - NoCompile bool - CreatePR bool - NoActions bool - Audit bool - JSON bool + Verbose bool + WorkflowDir string + NoFix bool + NoCompile bool + CreatePR bool + NoActions bool + Audit bool + JSON bool + DisabledCodemodIDs []string } // NewUpgradeCommand creates the upgrade command @@ -79,6 +80,7 @@ Examples: noCompile, _ := cmd.Flags().GetBool("no-compile") auditFlag, _ := cmd.Flags().GetBool("audit") jsonOutput, _ := cmd.Flags().GetBool("json") + disabledCodemods, _ := cmd.Flags().GetStringSlice("disable-codemod") skipExtensionUpgrade, _ := cmd.Flags().GetBool("skip-extension-upgrade") approveUpgrade, _ := cmd.Flags().GetBool("approve") preReleases, _ := cmd.Flags().GetBool("pre-releases") @@ -101,6 +103,7 @@ Examples: noFix: noFix, noCompile: noCompile, noActions: noActions, + disabledCodemodIDs: disabledCodemods, skipExtensionUpgrade: skipExtensionUpgrade, approve: approveUpgrade, preReleases: preReleases, @@ -123,6 +126,7 @@ Examples: cmd.Flags().Bool("no-fix", false, "Skip codemods, action version updates, and workflow compilation (only update agent files)") cmd.Flags().Bool("no-actions", false, "Skip updating GitHub Actions versions (ignored when --no-fix is set)") cmd.Flags().Bool("no-compile", false, "Skip recompiling workflows (do not modify lock files; ignored when --no-fix is set)") + cmd.Flags().StringSlice("disable-codemod", nil, "Disable specific codemod IDs during the fix step (repeatable)") cmd.Flags().Bool("create-pull-request", false, "Create a pull request with the upgrade changes") cmd.Flags().Bool("pr", false, "Alias for --create-pull-request") _ = cmd.Flags().MarkHidden("pr") // Hide the short alias from help output @@ -166,6 +170,7 @@ type upgradeOptions struct { noFix bool noCompile bool noActions bool + disabledCodemodIDs []string skipExtensionUpgrade bool approve bool preReleases bool @@ -173,8 +178,8 @@ type upgradeOptions struct { // runUpgradeCommand executes the upgrade process func runUpgradeCommand(opts upgradeOptions) error { - upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, skipExtensionUpgrade=%v", - opts.verbose, opts.workflowDir, opts.noFix, opts.noCompile, opts.noActions, opts.skipExtensionUpgrade) + upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, disabledCodemodIDs=%v, skipExtensionUpgrade=%v", + opts.verbose, opts.workflowDir, opts.noFix, opts.noCompile, opts.noActions, opts.disabledCodemodIDs, opts.skipExtensionUpgrade) // Step 0b: Ensure gh-aw extension is on the latest version. // If the extension was just upgraded, re-launch the freshly-installed binary @@ -221,10 +226,11 @@ func runUpgradeCommand(opts upgradeOptions) error { upgradeLog.Print("Applying codemods to all workflows") fixConfig := FixConfig{ - WorkflowIDs: nil, // nil means all workflows - Write: true, - Verbose: opts.verbose, - WorkflowDir: opts.workflowDir, + WorkflowIDs: nil, // nil means all workflows + Write: true, + Verbose: opts.verbose, + WorkflowDir: opts.workflowDir, + DisabledCodemodIDs: opts.disabledCodemodIDs, } if err := RunFix(fixConfig); err != nil { diff --git a/pkg/cli/upgrade_command_test.go b/pkg/cli/upgrade_command_test.go index 51d3dc19199..a6383a1888d 100644 --- a/pkg/cli/upgrade_command_test.go +++ b/pkg/cli/upgrade_command_test.go @@ -24,4 +24,9 @@ func TestUpgradeCommandHelpTextConsistency(t *testing.T) { assert.Contains(t, preReleasesFlag.Usage, "Include pre-release versions", "--pre-releases description should mention pre-release upgrades") assert.Contains(t, preReleasesFlag.Usage, "installed by exact tag", "--pre-releases description should explain prerelease pinning") assert.Contains(t, cmd.Long, "stable releases are the default", "help text should distinguish stable releases from prereleases") + + disableCodemodFlag := cmd.Flags().Lookup("disable-codemod") + require.NotNil(t, disableCodemodFlag, "--disable-codemod flag should exist") + assert.Equal(t, "stringSlice", disableCodemodFlag.Value.Type()) + assert.Contains(t, disableCodemodFlag.Usage, "Disable specific codemod IDs", "--disable-codemod usage should describe codemod exclusion") }