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
41 changes: 37 additions & 4 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,33 @@ The --dependabot flag generates dependency manifests when dependencies are detec
- Cannot be used with specific workflow files or custom --dir
- Only processes workflows in the default .github/workflows directory

Action mode controls how gh-aw action scripts are referenced in compiled workflows.
Three flags govern this. --gh-aw-ref is mutually exclusive with the other two;
--action-tag and --action-mode may be combined (e.g. --action-mode action --action-tag v1.2.3):

--action-mode <mode>
Explicit mode selection. Values:
dev Local paths (./actions/...). For developing inside the gh-aw repo.
release SHA-pinned refs from github/gh-aw (e.g. github/gh-aw/actions/setup@<sha>).
The SHA is derived from the binary version or from --action-tag.
action SHA-pinned refs from the github/gh-aw-actions repository.
Used by release binaries. Can be combined with --action-tag to pin a version.
Auto-detected from the binary build type when not set.

--action-tag <sha-or-tag>
Pin to a specific SHA or version tag (e.g. v1, v1.2.3, <full-sha>).
Implies --action-mode release unless --action-mode action is also specified.
The value is used as-is; branch names are not resolved. Use --gh-aw-ref to
pin to a branch by resolving it to its current commit SHA first.

--gh-aw-ref <branch-tag-or-sha>
Resolve a branch name, tag, or SHA from github/gh-aw to its full commit SHA
at compile time and pin the compiled workflow to that immutable SHA.
Equivalent to --action-mode release --action-tag <resolved-sha>.
Branch and tag names are resolved via the GitHub API.
Cannot be combined with --action-tag or --action-mode.
Use this when E2E-testing compiled workflows against a specific gh-aw revision.

Examples:
` + string(constants.CLIExtensionPrefix) + ` compile # Compile all Markdown files
` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor # Compile a specific workflow
Expand All @@ -259,7 +286,9 @@ Examples:
` + string(constants.CLIExtensionPrefix) + ` compile --watch ci-doctor # Watch and auto-compile
` + string(constants.CLIExtensionPrefix) + ` compile --trial --logical-repo owner/repo # Compile for trial mode
` + string(constants.CLIExtensionPrefix) + ` compile --dependabot # Generate Dependabot manifests
` + string(constants.CLIExtensionPrefix) + ` compile --dependabot --force # Force overwrite existing dependabot.yml`,
` + string(constants.CLIExtensionPrefix) + ` compile --dependabot --force # Force overwrite existing dependabot.yml
` + string(constants.CLIExtensionPrefix) + ` compile --gh-aw-ref main # Pin workflows to current HEAD of github/gh-aw main
` + string(constants.CLIExtensionPrefix) + ` compile --action-tag v1.2.3 # Pin workflows to a specific release tag`,
RunE: func(cmd *cobra.Command, args []string) error {
engineOverride, _ := cmd.Flags().GetString("engine")
actionMode, _ := cmd.Flags().GetString("action-mode")
Expand Down Expand Up @@ -697,10 +726,10 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all

// Add AI flag to compile and add commands
compileCmd.Flags().StringP("engine", "e", "", "Override AI engine (copilot, claude, codex, gemini, crush)")
compileCmd.Flags().String("action-mode", "", "Action script inlining mode (inline, dev, release). Auto-detected if not specified")
compileCmd.Flags().String("action-tag", "", "Override action SHA or tag for actions/setup (overrides action-mode to release). Accepts full SHA or tag name")
compileCmd.Flags().String("action-mode", "", "How gh-aw action scripts are referenced in compiled workflows: 'dev' uses local paths (for developing gh-aw itself), 'release' emits SHA-pinned remote refs from github/gh-aw, 'action' uses the github/gh-aw-actions repository. Auto-detected from the binary build type if not specified")
compileCmd.Flags().String("action-tag", "", "Pin compiled workflows to a specific version of gh-aw actions. Accepts a full commit SHA or a version tag (e.g. v1, v1.2.3). Sets --action-mode to 'release' unless --action-mode action is also specified. Cannot be combined with --gh-aw-ref; use --gh-aw-ref when you want to resolve a branch or tag name to its current SHA")
compileCmd.Flags().String("actions-repo", "", "Override the external actions repository used in action mode (default: github/gh-aw-actions)")
compileCmd.Flags().String("gh-aw-ref", "", "Compile workflows to reference github/gh-aw at the given branch, tag, or SHA (e.g. main, my-feature, abc123). Branch and tag names are resolved to their commit SHA at compile time so the baked-in ref is immutable. Convenience alias for --action-mode release --action-tag <sha>. Use this to E2E-test workflows compiled by an external repo against a specific gh-aw revision.")
compileCmd.Flags().String("gh-aw-ref", "", "Pin compiled workflows to a specific branch, tag, or commit SHA of github/gh-aw (e.g. main, my-feature, abc123). Branch and tag names are resolved to their full commit SHA at compile time so the baked-in ref is immutable. Equivalent to --action-mode release --action-tag <resolved-sha>. Cannot be combined with --action-tag or --action-mode. Use this to E2E-test workflows against a specific gh-aw revision")
compileCmd.Flags().Bool("validate", false, "Enable GitHub Actions workflow schema validation, container image validation, and action SHA validation")
compileCmd.Flags().BoolP("watch", "w", false, "Watch for changes to workflow files and recompile automatically")
compileCmd.Flags().StringP("dir", "d", "", "Workflow directory (default: .github/workflows)")
Expand Down Expand Up @@ -739,6 +768,10 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
_ = err
}
compileCmd.MarkFlagsMutuallyExclusive("dir", "workflows-dir")
// --gh-aw-ref is a convenience alias for --action-mode release --action-tag <sha>;
// combining it with either of those flags leads to one silently overwriting the other.
compileCmd.MarkFlagsMutuallyExclusive("gh-aw-ref", "action-tag")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No regression test for the mutual exclusivity enforcement — the core behavior fix has zero test coverage.

💡 Suggested approach

A unit test in cmd/gh-aw/main_entry_test.go (pattern already used there: rootCmd.SetArgs(...) + Execute()) could cover both error cases:

func TestCompileGhAwRefMutuallyExclusive(t *testing.T) {
    cases := []struct {
        name string
        args []string
    }{
        {"gh-aw-ref+action-tag", []string{"compile", "--gh-aw-ref", "main", "--action-tag", "v1.2.3"}},
        {"gh-aw-ref+action-mode", []string{"compile", "--gh-aw-ref", "main", "--action-mode", "dev"}},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            rootCmd.SetArgs(tc.args)
            err := rootCmd.Execute()
            if err == nil {
                t.Fatalf("expected mutual-exclusivity error, got nil")
            }
        })
    }
}

Without this, a future refactor removing MarkFlagsMutuallyExclusive would produce no CI signal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d11d8c0: added TestCompileGhAwRefMutuallyExclusiveFlags in cmd/gh-aw/main_help_text_test.go to cover both --gh-aw-ref conflict cases.

compileCmd.MarkFlagsMutuallyExclusive("gh-aw-ref", "action-mode")

// Register completions for compile command
compileCmd.ValidArgsFunction = cli.CompleteWorkflowNames
Expand Down
29 changes: 29 additions & 0 deletions cmd/gh-aw/main_help_text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,32 @@ func TestCompileShowAllFlagHelpText(t *testing.T) {
require.NotNil(t, showAllFlag, "compile command should define --show-all")
assert.Equal(t, "Display all prioritized compilation errors instead of the default top five", showAllFlag.Usage)
}

func TestCompileGhAwRefMutuallyExclusiveFlags(t *testing.T) {
tests := []struct {
name string
args []string
}{
{
name: "gh-aw-ref with action-tag",
args: []string{"compile", "--gh-aw-ref", "main", "--action-tag", "v1.2.3"},
},
{
name: "gh-aw-ref with action-mode",
args: []string{"compile", "--gh-aw-ref", "main", "--action-mode", "dev"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rootCmd.SetArgs(tt.args)
t.Cleanup(func() {
rootCmd.SetArgs([]string{})
})

err := rootCmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "if any flags in the group", "expected mutually exclusive flag-group error")
})
}
}
20 changes: 13 additions & 7 deletions pkg/cli/compile_compiler_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//
// Configuration:
// - configureCompilerFlags() - Sets validation, strict mode, trial mode flags
// - setupActionMode() - Configures action script inlining mode
// - setupActionMode() - Configures how gh-aw action scripts are referenced (--action-mode / --action-tag / --gh-aw-ref)
// - setupRepositoryContext() - Sets repository slug for schedule scattering
//
// These functions abstract compiler setup, allowing the main compile
Expand Down Expand Up @@ -195,22 +195,28 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
}
}

// setupActionMode configures the action script inlining mode
// setupActionMode configures how gh-aw action scripts are referenced in compiled workflows.
// Priority order (after --gh-aw-ref resolution):
// - actionTag != "" → pin to that SHA/tag; use ActionModeRelease unless actionMode is explicitly "action"
// - actionTag == "" && actionMode != "" → honour the explicit mode
// - neither set → auto-detect from the binary build type via DetectActionMode
func setupActionMode(compiler *workflow.Compiler, actionMode string, actionTag string) {
compileCompilerSetupLog.Printf("Setting up action mode: %s, actionTag: %s", actionMode, actionTag)

// If actionTag is specified, it pins the version used in action/release references.
// When --action-mode action is explicitly set alongside --action-tag, honour the explicit
// action mode so that the external actions repo (--actions-repo) is also respected.
// Without an explicit action mode, --action-tag still defaults to release mode (original behaviour).
// --action-tag pins the version used in action/release references.
// When --action-mode action is also explicitly set, honour it so that the
// external actions repo (--actions-repo) is respected too.
// Otherwise --action-tag implies release mode.
// Note: --gh-aw-ref is mutually exclusive with both flags and resolves to a
// full SHA before calling this function, so there is no double-set risk here.
if actionTag != "" {
compiler.SetActionTag(actionTag)
if actionMode == string(workflow.ActionModeAction) {
compileCompilerSetupLog.Printf("--action-tag specified (%s) with --action-mode action, using action mode", actionTag)
compiler.SetActionMode(workflow.ActionModeAction)
compileCompilerSetupLog.Printf("Action mode set to: action with tag/SHA: %s", actionTag)
} else {
compileCompilerSetupLog.Printf("--action-tag specified (%s), overriding to release mode", actionTag)
compileCompilerSetupLog.Printf("--action-tag specified (%s), defaulting to release mode", actionTag)
compiler.SetActionMode(workflow.ActionModeRelease)
compileCompilerSetupLog.Printf("Action mode set to: release with tag/SHA: %s", actionTag)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ type CompileConfig struct {
RunnerGuard bool // Run runner-guard taint analysis scanner on generated .lock.yml files
JSONOutput bool // Output validation results as JSON
ShowAllErrors bool // Display all prioritized errors instead of the default top five
ActionMode string // Action script inlining mode: inline, dev, or release
ActionTag string // Override action SHA or tag for actions/setup (overrides action-mode to release)
ActionMode string // How action scripts are referenced: dev, release, or action. Auto-detected if empty.
ActionTag string // Pin action refs to this SHA or version tag (e.g. v1, <full-sha>). Sets release mode unless ActionMode is already "action". Mutually exclusive with GHAwRef at the CLI layer.
ActionsRepo string // Override the external actions repository (default: github/gh-aw-actions)
Stats bool // Display statistics table sorted by file size
FailFast bool // Stop at first error instead of collecting all errors
Expand Down
4 changes: 3 additions & 1 deletion pkg/cli/compile_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ func validateActionModeConfig(actionMode string) error {

mode := workflow.ActionMode(actionMode)
if !mode.IsValid() {
return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', 'script', or 'action'", actionMode)
// ActionModeScript is intentionally excluded from this user-facing error:
// it remains internal and is not advertised as a CLI-supported mode.
return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', or 'action'", actionMode)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

script mode is silently accepted by IsValid() but now absent from all help and error guidance — a user passing --action-mode script gets no validation error but nothing tells them the mode exists or is supported.

💡 Details

ActionModeScript is still included in IsValid() in pkg/workflow/action_mode.go:

func (m ActionMode) IsValid() bool {
    return m == ActionModeDev || m == ActionModeRelease || m == ActionModeScript || m == ActionModeAction
}

So --action-mode script passes validation silently. But this PR simultaneously removes script from:

  • this error message
  • the --action-mode flag description
  • the Long help text

This creates a hidden, undocumented but functional mode with no discoverability path. Two clean resolutions:

  1. Retire it: remove ActionModeScript from IsValid() so the mode is truly invalid at the CLI layer.
  2. Keep it internal: add a // Internal: or // Deprecated: godoc to ActionModeScript in action_mode.go, and leave a comment in validateActionModeConfig explaining why it is intentionally excluded from the user-facing error message.

Without one of these, a future developer will find a working hidden mode with no rationale for why it exists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d11d8c0: added an explicit comment in validateActionModeConfig documenting that script is intentionally internal and excluded from user-facing error guidance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] ActionModeScript.IsValid() still returns true (see action_mode.go:36), so --action-mode script silently passes validation even though the error message no longer lists it as valid. Decide whether script should be accepted at the CLI layer:

💡 Options

Option A — keep it as an undocumented internal escape hatch (current state), but add a comment here to make the gap intentional:

// Note: ActionModeScript is intentionally excluded — it is an internal mode
// not meant to be set directly by end users via the CLI.
return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', or 'action'", actionMode)

Option B — explicitly block it at the CLI layer so the error message and accepted set agree:

if mode == workflow.ActionModeScript || !mode.IsValid() {
    return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', or 'action'", actionMode)
}

Option A is lower-risk if internal automation relies on script being passable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d11d8c0: documented in validateActionModeConfig that ActionModeScript is intentionally internal and omitted from user-facing CLI mode guidance.

}

return nil
Expand Down
Loading