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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion pkg/cli/fix_codemods.go
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions pkg/cli/fix_codemods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
24 changes: 15 additions & 9 deletions pkg/cli/fix_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion pkg/cli/fix_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand Down
34 changes: 20 additions & 14 deletions pkg/cli/upgrade_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -101,6 +103,7 @@ Examples:
noFix: noFix,
noCompile: noCompile,
noActions: noActions,
disabledCodemodIDs: disabledCodemods,
skipExtensionUpgrade: skipExtensionUpgrade,
approve: approveUpgrade,
preReleases: preReleases,
Expand All @@ -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
Expand Down Expand Up @@ -166,15 +170,16 @@ type upgradeOptions struct {
noFix bool
noCompile bool
noActions bool
disabledCodemodIDs []string
skipExtensionUpgrade bool
approve bool
preReleases bool
}

// 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
Expand Down Expand Up @@ -221,10 +226,11 @@ func runUpgradeCommand(opts upgradeOptions) error {
upgradeLog.Print("Applying codemods to all workflows")

fixConfig := FixConfig{
Comment thread
pelikhan marked this conversation as resolved.
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 {
Expand Down
5 changes: 5 additions & 0 deletions pkg/cli/upgrade_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Loading