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
2 changes: 1 addition & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
prCmd := cli.NewPRCommand()
secretsCmd := cli.NewSecretsCommand()
fixCmd := cli.NewFixCommand()
upgradeCmd := cli.NewUpgradeCommand()
upgradeCmd := cli.NewUpgradeCommand(validateEngine)
completionCmd := cli.NewCompletionCommand()
hashCmd := cli.NewHashCommand()
projectCmd := cli.NewProjectCommand()
Expand Down
57 changes: 57 additions & 0 deletions docs/adr/45573-cli-flag-consistency-upgrade-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR-45573: Add --engine, --repo, and --approve Flags to upgrade and update Commands for CLI Consistency

**Date**: 2026-07-15
**Status**: Draft
**Deciders**: Unknown

---

### Context

The `gh aw` CLI exposes several workflow management commands — `compile`, `validate`, `update`, and `upgrade` — that share an underlying compilation pipeline. A CLI consistency audit identified four high-severity flag gaps: the `upgrade` command was missing `--engine/-e` (AI engine override) and `--repo/-r` (target repository), while the `update` command was missing `--approve` (bypass strict-mode enforcement). Sibling commands already supported these flags, creating asymmetry that forced users to use different commands for equivalent operations or to work around missing controls.

The `--approve` flag controls whether the compiler allows action additions/removals and restricted secrets not already present in the `gh-aw-manifest` during strict-mode compilation. The `--engine` flag allows overriding the AI engine used during workflow compilation. The `--repo` flag dispatches compilation work to a remote repository.

### Decision

We will add `--engine/-e` and `--repo/-r` to `upgrade` and `--approve` to `update`, threading the new values through to the shared compilation pipeline (`compileWorkflowWithRefresh`, `UpdateActionsInWorkflowFiles`, `recompileAllWorkflows`). We will also enforce `--repo`/`--org` mutual exclusion on `upgrade`, matching the existing guard on `update`. The `--no-security-scanner` gap on `compile`/`validate` (F3) is intentionally skipped because those commands operate exclusively on local files where the scanner is a no-op.

### Alternatives Considered

#### Alternative 1: Document the Asymmetry and Provide Workarounds

Accept that `upgrade` and `update` expose fewer controls than `compile`/`validate`, and add documentation explaining which flags are available on which commands. Users needing `--engine` during upgrade would run a separate `compile` pass afterward.

This was rejected because it degrades usability: users running upgrade in CI cannot override the engine mid-pipeline without invoking an extra command, and the asymmetry is not obvious from `--help` output alone.

#### Alternative 2: Add Flag Declarations Without Wiring Them to the Pipeline

Register the new flags on `upgrade`/`update` to satisfy the consistency checker but silently ignore their values, deferring the pipeline integration to a later PR.

This was rejected because it would introduce misleading no-op flags — a worse UX than missing flags — and any CI automation that relied on the flags would silently get wrong behavior.

#### Alternative 3: Unify Commands Behind a Single Orchestration Layer

Refactor `upgrade` and `update` to delegate to `compile` internally, so any flag supported by `compile` is automatically available to callers.

This was considered but rejected as out of scope for a consistency fix. It would require a larger architectural refactor that risks regressions across the entire compilation pipeline.

### Consequences

#### Positive
- `upgrade` and `update` now accept the same engine, repo, and approval controls as their sibling commands, removing user-visible asymmetry.
- Users can override the AI engine during upgrade without a separate `compile` pass — important in CI pipelines where compilation must use a specific engine.
- The `--repo`/`--org` mutual exclusion guard on `upgrade` matches `update` behavior, preventing ambiguous invocations.
- Flag coverage is verified by new unit tests for registration, description consistency, and mutual exclusion.

#### Negative
- `compileWorkflowWithRefresh` and several internal helpers now take an additional `approve bool` parameter, growing the function signatures. Seven call sites required updates.
- The `approve` value is threaded through multiple layers (`update_command.go` → `update_manifest.go`, `update_workflows.go`, `update_actions.go`), increasing the surface area for future parameter drift if the compilation interface changes.

#### Neutral
- F3 (`--no-security-scanner` on `compile`/`validate`) is explicitly deferred and documented as a no-op case; no code or flag was added.
- Documentation in `docs/src/content/docs/setup/cli.md` for both `update` and `upgrade` is updated to reflect the new flags.

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
6 changes: 4 additions & 2 deletions docs/src/content/docs/setup/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ gh aw update --create-pull-request # Update and open a pull request
gh aw update --org my-org --create-issue --yes # Auto-accept per-repo confirmations (required in CI)
```

**Options:** `--dir/-d`, `--no-merge`, `--major`, `--force/-f`, `--engine/-e`, `--no-stop-after`, `--stop-after`, `--no-release-bump`, `--no-security-scanner`, `--create-pull-request`, `--create-issue`, `--org`, `--repos`, `--yes/-y`, `--no-compile`, `--no-redirect`, `--cool-down`, `--repo/-r`
**Options:** `--dir/-d`, `--no-merge`, `--major`, `--force/-f`, `--engine/-e`, `--no-stop-after`, `--stop-after`, `--no-release-bump`, `--no-security-scanner`, `--approve`, `--create-pull-request`, `--create-issue`, `--org`, `--repos`, `--yes/-y`, `--no-compile`, `--no-redirect`, `--cool-down`, `--repo/-r`

Org mode (`--org`) previews or creates workflow update pull requests across every repository in an organization. Use `--repos` to limit org mode to repositories matching one or more glob patterns, `--create-issue` to open an issue in each repository that has pending updates (requires `--org`), and `--yes/-y` to auto-accept per-repository confirmations (required in CI).

Expand Down Expand Up @@ -739,12 +739,14 @@ Upgrade repository with latest agent files and apply codemods to all workflows.
gh aw upgrade # Upgrade repository agent files and all workflows
gh aw upgrade --no-fix # Update agent files only (skip codemods, actions, and compilation)
gh aw upgrade --create-pull-request # Upgrade and open a pull request
gh aw upgrade --engine claude # Override AI engine for compilation
gh aw upgrade --repo owner/repo # Upgrade workflows in another repository
gh aw upgrade --audit # Run dependency health audit
gh aw upgrade --audit --json # Dependency audit in JSON format
gh aw upgrade --org my-org --create-issue --yes # Auto-accept per-repo confirmations (required in CI)
```

**Options:** `--dir/-d`, `--no-fix`, `--no-actions`, `--no-compile`, `--disable-codemod`, `--create-pull-request`, `--create-issue`, `--org`, `--repos`, `--yes/-y`, `--audit`, `--json/-j`, `--approve`, `--pre-releases`
**Options:** `--dir/-d`, `--engine/-e`, `--repo/-r`, `--no-fix`, `--no-actions`, `--no-compile`, `--disable-codemod`, `--create-pull-request`, `--create-issue`, `--org`, `--repos`, `--yes/-y`, `--audit`, `--json/-j`, `--approve`, `--pre-releases`

Org mode (`--org`) previews or creates upgrade pull requests across every repository in an organization. Use `--repos` to limit org mode to repositories matching one or more glob patterns, `--create-issue` to open an issue in each org repository with agentic workflows (requires `--org`), and `--yes/-y` to auto-accept org-mode upgrade confirmations (required in CI).

Expand Down
7 changes: 4 additions & 3 deletions pkg/cli/add_workflow_compilation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ var addWorkflowCompilationLog = logger.New("cli:add_workflow_compilation")
// compileWorkflow compiles a workflow file without refreshing stop time.
// This is a convenience wrapper around compileWorkflowWithRefresh.
func compileWorkflow(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string) error {
return compileWorkflowWithRefresh(ctx, filePath, verbose, quiet, engineOverride, false)
return compileWorkflowWithRefresh(ctx, filePath, verbose, quiet, engineOverride, false, false)
}

// compileWorkflowWithRefresh compiles a workflow file with optional stop time refresh.
// This function handles the compilation process and ensures .gitattributes is updated.
func compileWorkflowWithRefresh(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool) error {
addWorkflowCompilationLog.Printf("Compiling workflow: file=%s, refresh_stop_time=%v, engine=%s", filePath, refreshStopTime, engineOverride)
func compileWorkflowWithRefresh(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool, approve bool) error {
addWorkflowCompilationLog.Printf("Compiling workflow: file=%s, refresh_stop_time=%v, engine=%s, approve=%v", filePath, refreshStopTime, engineOverride, approve)

// Create compiler with auto-detected version and action mode
compiler := workflow.NewCompiler(
Expand All @@ -34,6 +34,7 @@ func compileWorkflowWithRefresh(ctx context.Context, filePath string, verbose bo
)

compiler.SetRefreshStopTime(refreshStopTime)
compiler.SetApprove(approve)
compiler.SetQuiet(quiet)
if err := CompileWorkflowWithValidation(ctx, compiler, filePath, CompileValidationOptions{Verbose: verbose}); err != nil {
addWorkflowCompilationLog.Printf("Compilation failed: %v", err)
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/update_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,11 @@ type latestReleaseResult struct {
// major version. Updated files are recompiled. By default all actions are updated to
// the latest major version; pass disableReleaseBump=true to only update core
// (actions/*) references.
func UpdateActionsInWorkflowFiles(ctx context.Context, workflowsDir, engineOverride string, verbose, disableReleaseBump bool, noCompile bool, coolDown time.Duration) error {
return updateActionsInWorkflowFiles(ctx, defaultActionUpdateDeps(), workflowsDir, engineOverride, verbose, disableReleaseBump, noCompile, coolDown)
func UpdateActionsInWorkflowFiles(ctx context.Context, workflowsDir, engineOverride string, verbose, disableReleaseBump bool, noCompile bool, coolDown time.Duration, approve bool) error {
return updateActionsInWorkflowFiles(ctx, defaultActionUpdateDeps(), workflowsDir, engineOverride, verbose, disableReleaseBump, noCompile, coolDown, approve)
}

func updateActionsInWorkflowFiles(ctx context.Context, deps actionUpdateDeps, workflowsDir, engineOverride string, verbose, disableReleaseBump bool, noCompile bool, coolDown time.Duration) error {
func updateActionsInWorkflowFiles(ctx context.Context, deps actionUpdateDeps, workflowsDir, engineOverride string, verbose, disableReleaseBump bool, noCompile bool, coolDown time.Duration, approve bool) error {
if workflowsDir == "" {
workflowsDir = getWorkflowsDir()
}
Expand Down Expand Up @@ -664,7 +664,7 @@ func updateActionsInWorkflowFiles(ctx context.Context, deps actionUpdateDeps, wo

// Recompile the updated workflow (unless --no-compile is set)
if !noCompile {
if err := compileWorkflowWithRefresh(ctx, path, verbose, false, engineOverride, false); err != nil {
if err := compileWorkflowWithRefresh(ctx, path, verbose, false, engineOverride, false, approve); err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to recompile %s: %v", path, err)))
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/update_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ func TestUpdateActionsInWorkflowFiles_UpdatesUsesReferences(t *testing.T) {
t.Fatalf("failed to write workflow file: %v", err)
}

if err := updateActionsInWorkflowFiles(context.Background(), deps, workflowsDir, "", false, false, true, 0); err != nil {
if err := updateActionsInWorkflowFiles(context.Background(), deps, workflowsDir, "", false, false, true, 0, false); err != nil {
t.Fatalf("UpdateActionsInWorkflowFiles() error = %v", err)
}

Expand Down Expand Up @@ -872,7 +872,7 @@ func TestUpdateActionsInWorkflowFiles_NeverDowngrades(t *testing.T) {
t.Fatalf("failed to write workflow file: %v", err)
}

if err := updateActionsInWorkflowFiles(context.Background(), deps, workflowsDir, "", false, false, true, 0); err != nil {
if err := updateActionsInWorkflowFiles(context.Background(), deps, workflowsDir, "", false, false, true, 0, false); err != nil {
t.Fatalf("UpdateActionsInWorkflowFiles() error = %v", err)
}

Expand Down
11 changes: 7 additions & 4 deletions pkg/cli/update_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Note: In GitHub Enterprise repos, shorthand source specs resolve on your enterpr
disableSecurityScanner, _ := cmd.Flags().GetBool("no-security-scanner")
disableSecurityScannerLegacy, _ := cmd.Flags().GetBool("disable-security-scanner")
disableSecurityScanner = disableSecurityScanner || disableSecurityScannerLegacy
approveFlag, _ := cmd.Flags().GetBool("approve")
createPRFlag, _ := cmd.Flags().GetBool("create-pull-request")
prFlagAlias, _ := cmd.Flags().GetBool("pr")
createPR := createPRFlag || prFlagAlias
Expand Down Expand Up @@ -136,6 +137,7 @@ Note: In GitHub Enterprise repos, shorthand source specs resolve on your enterpr
NoRedirect: noRedirect,
DisableSecurityScanner: disableSecurityScanner,
CoolDown: coolDown,
Approve: approveFlag,
}

if targetRepo != "" {
Expand Down Expand Up @@ -173,6 +175,7 @@ Note: In GitHub Enterprise repos, shorthand source specs resolve on your enterpr
cmd.Flags().Bool("no-security-scanner", false, "Skip security scanning of workflow markdown content")
cmd.Flags().Bool("disable-security-scanner", false, "Skip security scanning of workflow markdown content")
_ = cmd.Flags().MarkDeprecated("disable-security-scanner", "use --no-security-scanner instead")
cmd.Flags().Bool("approve", false, "Approve all safe update changes. When strict mode is active (the default), the compiler emits warnings for new restricted secrets or unapproved action additions/removals not present in the existing gh-aw-manifest. Use this flag to approve and skip safe update enforcement")
cmd.Flags().Bool("no-compile", false, "Skip recompiling workflows during update (do not modify lock files)")
cmd.Flags().Bool("no-redirect", false, "Refuse updates when redirect frontmatter is present")
cmd.Flags().String("org", "", "Preview or create workflow update pull requests across an organization")
Expand Down Expand Up @@ -216,7 +219,7 @@ func RunUpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error
// Update action references in user-provided steps within workflow .md files.
// By default all org/repo@version references are updated to the latest major version.
updateLog.Print("Updating action references in workflow .md files")
if err := UpdateActionsInWorkflowFiles(ctx, opts.WorkflowsDir, opts.EngineOverride, opts.Verbose, opts.DisableReleaseBump, opts.NoCompile, opts.CoolDown); err != nil {
if err := UpdateActionsInWorkflowFiles(ctx, opts.WorkflowsDir, opts.EngineOverride, opts.Verbose, opts.DisableReleaseBump, opts.NoCompile, opts.CoolDown, opts.Approve); err != nil {
// Non-fatal: warn but don't fail the update
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update action references in workflow files: %v", err)))
}
Expand All @@ -237,7 +240,7 @@ func RunUpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error
if newContainerPins && !opts.NoCompile {
updateLog.Print("Recompiling workflows to embed new container digest pins")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Recompiling workflows to embed container digest pins..."))
recompileErr := recompileAllWorkflows(ctx, opts.WorkflowsDir, opts.EngineOverride, opts.Verbose)
recompileErr := recompileAllWorkflows(ctx, opts.WorkflowsDir, opts.EngineOverride, opts.Verbose, opts.Approve)
if recompileErr != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to recompile workflows after container pin update: %v", recompileErr)))
}
Expand All @@ -250,7 +253,7 @@ func RunUpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error
// recompileAllWorkflows recompiles all .md workflow files in the given directory.
// This is used after container pin updates to embed digest-pinned image references
// in the generated lock files.
func recompileAllWorkflows(ctx context.Context, workflowsDir, engineOverride string, verbose bool) error {
func recompileAllWorkflows(ctx context.Context, workflowsDir, engineOverride string, verbose bool, approve bool) error {
if workflowsDir == "" {
workflowsDir = getWorkflowsDir()
}
Expand All @@ -265,7 +268,7 @@ func recompileAllWorkflows(ctx context.Context, workflowsDir, engineOverride str
continue
}
path := filepath.Join(workflowsDir, entry.Name())
if err := compileWorkflowWithRefresh(ctx, path, verbose, true, engineOverride, false); err != nil {
if err := compileWorkflowWithRefresh(ctx, path, verbose, true, engineOverride, false, approve); err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to recompile %s: %v", entry.Name(), err)))
}
Expand Down
13 changes: 11 additions & 2 deletions pkg/cli/update_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ func TestNewUpdateCommand_MentionsEnterpriseSourceResolution(t *testing.T) {
assert.Contains(t, cmd.Long, "Use full https://github.com/... source URLs for other public github.com workflows.")
}

func TestNewUpdateCommand_HasApproveFlag(t *testing.T) {
cmd := NewUpdateCommand(func(string) error { return nil })
require.NotNil(t, cmd, "update command should be created")

flag := cmd.Flags().Lookup("approve")
require.NotNil(t, flag, "update command should register --approve flag")
assert.Contains(t, flag.Usage, "When strict mode is active", "--approve description should match compile/upgrade semantics")
}

// TestMergeWorkflowContent_WithConflicts tests a merge with conflicts
func TestMergeWorkflowContent_WithConflicts(t *testing.T) {
base := `---
Expand Down Expand Up @@ -760,7 +769,7 @@ This is a test workflow.

// Test with refreshStopTime=false (should preserve existing stop time if lock exists)
t.Run("compileWorkflowWithRefresh false", func(t *testing.T) {
err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", false)
err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", false, false)
if err != nil {
t.Logf("Compilation failed (expected in test environment): %v", err)
// In a test environment without full setup, compilation may fail,
Expand All @@ -770,7 +779,7 @@ This is a test workflow.

// Test with refreshStopTime=true (should regenerate stop time)
t.Run("compileWorkflowWithRefresh true", func(t *testing.T) {
err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", true)
err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", true, false)
if err != nil {
t.Logf("Compilation failed (expected in test environment): %v", err)
// In a test environment without full setup, compilation may fail,
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/update_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ func updateManifestManagedWorkflow(ctx context.Context, update manifestManagedWo
}
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Updated %s from %s to %s", update.wf.Name, shortRef(update.currentRef), shortRef(update.latestRef))))
if !opts.NoCompile {
if err := compileWorkflowWithRefresh(ctx, update.wf.Path, opts.Verbose, false, opts.EngineOverride, true); err != nil {
if err := compileWorkflowWithRefresh(ctx, update.wf.Path, opts.Verbose, false, opts.EngineOverride, true, opts.Approve); err != nil {
return fmt.Errorf("failed to compile updated workflow: %w", err)
}
}
Expand Down Expand Up @@ -318,7 +318,7 @@ func addManifestManagedWorkflow(ctx context.Context, targetDir, name, repo, late
}
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Added new workflow from manifest: "+filepath.Base(destPath)))
if !opts.NoCompile {
if err := compileWorkflowWithRefresh(ctx, destPath, opts.Verbose, false, opts.EngineOverride, true); err != nil {
if err := compileWorkflowWithRefresh(ctx, destPath, opts.Verbose, false, opts.EngineOverride, true, opts.Approve); err != nil {
return fmt.Errorf("failed to compile new manifest workflow: %w", err)
}
}
Expand Down
Loading
Loading