refactor(cmd): unify actions/aliases command trees via factory functions
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / functional (push) Blocked by required conditions

The remaining audit finding: cmd/aliases.go rebuilt parallel `run *` and
`workflow *` command subtrees by hand to expose them at top level
(matching gh CLI's `gh run list` ergonomics). That duplication is what
let the `--json` Bool/string mismatch fixed in 0c181df slip in — the
flag was registered correctly under `actions run list` but Bool-typed
under the top-level `run list`, and `wantJSON` silently swallowed the
type-error return.

Switch each `run *` and `workflow *` command from a package-level
`var xxxCmd = &cobra.Command{...}` declaration to a factory function
`newXxxCmd() *cobra.Command` that returns a fully-configured Command
(struct, examples, args, RunE, AND its own flag registrations).

Each parent factory (newRunCmd, newWorkflowCmd) takes a `parentLabel`
string that's appended to the parent's Short/Long, so the alias-tree
variant says "(alias for 'actions run')" without the children diverging.

actions.go init() now does:
  actionsCmd.AddCommand(newRunCmd(""))
  actionsCmd.AddCommand(newWorkflowCmd(""))

aliases.go shrinks from 142 lines to 17 lines:
  rootCmd.AddCommand(newRunCmd(" (alias for 'actions run')"))
  rootCmd.AddCommand(newWorkflowCmd(" (alias for 'actions workflow')"))

Verified: `diff` of `fj run list --help` flags vs `fj actions run list
--help` flags is empty. Both trees produce IDENTICAL surfaces. Future
flag changes touch one factory and propagate to both paths.

Note: secret/variable subcommands aren't aliased so they keep the
package-level var pattern. Only the run/workflow subtrees moved.
This commit is contained in:
sid 2026-05-02 15:56:58 -06:00
parent 155ddb97ba
commit 373c769d2c
2 changed files with 171 additions and 258 deletions

View file

@ -87,18 +87,34 @@ var actionsCmd = &cobra.Command{
Long: "View and manage workflows, runs, secrets, and variables for Forgejo Actions in your repositories.", Long: "View and manage workflows, runs, secrets, and variables for Forgejo Actions in your repositories.",
} }
// Run commands (compatible with gh run) // Run and Workflow command trees are built via factory functions
var runCmd = &cobra.Command{ // (newRunCmd / newWorkflowCmd) so cmd/aliases.go can build an identical
Use: "run", // top-level tree under rootCmd without duplicating Use/Short/Long/Example/
Short: "View and manage workflow runs", // flag declarations. Single source of truth — drift impossible.
Long: "List, view, and manage workflow runs.",
// newRunCmd builds the `run` subtree. parentLabel is interpolated into the
// parent's Short/Long so the alias-tree variant can advertise itself as
// "alias for 'actions run'" without diverging on the children.
func newRunCmd(parentLabel string) *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "View and manage workflow runs" + parentLabel,
Long: "List, view, and manage workflow runs." + parentLabel,
}
cmd.AddCommand(newRunListCmd())
cmd.AddCommand(newRunViewCmd())
cmd.AddCommand(newRunWatchCmd())
cmd.AddCommand(newRunRerunCmd())
cmd.AddCommand(newRunCancelCmd())
return cmd
} }
var runListCmd = &cobra.Command{ func newRunListCmd() *cobra.Command {
Use: "list", c := &cobra.Command{
Short: "List recent workflow runs", Use: "list",
Long: "List recent workflow runs for a repository.", Short: "List recent workflow runs",
Example: ` # List recent workflow runs Long: "List recent workflow runs for a repository.",
Example: ` # List recent workflow runs
fj actions run list fj actions run list
# List runs with a custom limit # List runs with a custom limit
@ -106,14 +122,20 @@ var runListCmd = &cobra.Command{
# Output as JSON # Output as JSON
fj actions run list --json`, fj actions run list --json`,
RunE: runRunList, RunE: runRunList,
}
addRepoFlags(c)
c.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
addJSONFlags(c, "Output workflow runs as JSON")
return c
} }
var runViewCmd = &cobra.Command{ func newRunViewCmd() *cobra.Command {
Use: "view <run-id>", c := &cobra.Command{
Short: "View a workflow run", Use: "view <run-id>",
Long: "View details about a specific workflow run.", Short: "View a workflow run",
Example: ` # View a workflow run Long: "View details about a specific workflow run.",
Example: ` # View a workflow run
fj actions run view 123 fj actions run view 123
# View with job details # View with job details
@ -124,55 +146,86 @@ var runViewCmd = &cobra.Command{
# View only failed logs # View only failed logs
fj actions run view 123 --log-failed`, fj actions run view 123 --log-failed`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runRunView, RunE: runRunView,
}
addRepoFlags(c)
c.Flags().BoolP("verbose", "v", false, "Show job steps")
c.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
c.Flags().StringP("job", "j", "", "View a specific job ID from a run")
c.Flags().BoolP("log-failed", "", false, "View the log for any failed steps in a run or specific job")
addJSONFlags(c, "Output workflow run as JSON")
return c
} }
var runWatchCmd = &cobra.Command{ func newRunWatchCmd() *cobra.Command {
Use: "watch <run-id>", c := &cobra.Command{
Short: "Watch a workflow run", Use: "watch <run-id>",
Long: "Poll a workflow run until it completes.", Short: "Watch a workflow run",
Example: ` # Watch a run until it completes Long: "Poll a workflow run until it completes.",
Example: ` # Watch a run until it completes
fj actions run watch 123 fj actions run watch 123
# Watch with a custom polling interval # Watch with a custom polling interval
fj actions run watch 123 -i 10s`, fj actions run watch 123 -i 10s`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runRunWatch, RunE: runRunWatch,
}
addRepoFlags(c)
c.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
return c
} }
var runRerunCmd = &cobra.Command{ func newRunRerunCmd() *cobra.Command {
Use: "rerun <run-id>", c := &cobra.Command{
Short: "Rerun a workflow run", Use: "rerun <run-id>",
Long: "Trigger a rerun for a specific workflow run.", Short: "Rerun a workflow run",
Example: ` # Rerun a failed workflow run Long: "Trigger a rerun for a specific workflow run.",
Example: ` # Rerun a failed workflow run
fj actions run rerun 123`, fj actions run rerun 123`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runRunRerun, RunE: runRunRerun,
}
addRepoFlags(c)
return c
} }
var runCancelCmd = &cobra.Command{ func newRunCancelCmd() *cobra.Command {
Use: "cancel <run-id>", c := &cobra.Command{
Short: "Cancel a workflow run", Use: "cancel <run-id>",
Long: "Cancel a running workflow run.", Short: "Cancel a workflow run",
Example: ` # Cancel a running workflow Long: "Cancel a running workflow run.",
Example: ` # Cancel a running workflow
fj actions run cancel 123`, fj actions run cancel 123`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runRunCancel, RunE: runRunCancel,
}
addRepoFlags(c)
return c
} }
// Workflow commands // newWorkflowCmd builds the `workflow` subtree. parentLabel is interpolated
var workflowCmd = &cobra.Command{ // the same way as newRunCmd's, so the alias variant can self-identify.
Use: "workflow", func newWorkflowCmd(parentLabel string) *cobra.Command {
Short: "Manage workflows", cmd := &cobra.Command{
Long: "List, view, and run workflows.", Use: "workflow",
Short: "Manage workflows" + parentLabel,
Long: "List, view, and run workflows." + parentLabel,
}
cmd.AddCommand(newWorkflowListCmd())
cmd.AddCommand(newWorkflowViewCmd())
cmd.AddCommand(newWorkflowRunCmd())
cmd.AddCommand(newWorkflowEnableCmd())
cmd.AddCommand(newWorkflowDisableCmd())
return cmd
} }
var workflowListCmd = &cobra.Command{ func newWorkflowListCmd() *cobra.Command {
Use: "list", c := &cobra.Command{
Short: "List workflows", Use: "list",
Long: "List all workflows in a repository.", Short: "List workflows",
Example: ` # List all workflows Long: "List all workflows in a repository.",
Example: ` # List all workflows
fj actions workflow list fj actions workflow list
# List workflows as JSON # List workflows as JSON
@ -180,53 +233,78 @@ var workflowListCmd = &cobra.Command{
# List workflows for a specific repo # List workflows for a specific repo
fj actions workflow list -R owner/repo`, fj actions workflow list -R owner/repo`,
RunE: runWorkflowList, RunE: runWorkflowList,
}
addRepoFlags(c)
c.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
addJSONFlags(c, "Output workflows as JSON")
return c
} }
var workflowViewCmd = &cobra.Command{ func newWorkflowViewCmd() *cobra.Command {
Use: "view <workflow>", c := &cobra.Command{
Short: "View a workflow", Use: "view <workflow>",
Long: "View details about a specific workflow. You can specify the workflow by name or filename.", Short: "View a workflow",
Example: ` # View a workflow by filename Long: "View details about a specific workflow. You can specify the workflow by name or filename.",
Example: ` # View a workflow by filename
fj actions workflow view ci.yml fj actions workflow view ci.yml
# View as JSON # View as JSON
fj actions workflow view ci.yml --json`, fj actions workflow view ci.yml --json`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWorkflowView, RunE: runWorkflowView,
}
addRepoFlags(c)
addJSONFlags(c, "Output workflow as JSON")
return c
} }
var workflowRunCmd = &cobra.Command{ func newWorkflowRunCmd() *cobra.Command {
Use: "run <workflow>", c := &cobra.Command{
Short: "Run a workflow", Use: "run <workflow>",
Long: "Trigger a workflow_dispatch event for a workflow. The workflow must support the workflow_dispatch trigger.", Short: "Run a workflow",
Example: ` # Trigger a workflow on the default branch Long: "Trigger a workflow_dispatch event for a workflow. The workflow must support the workflow_dispatch trigger.",
Example: ` # Trigger a workflow on the default branch
fj actions workflow run deploy.yml fj actions workflow run deploy.yml
# Trigger on a specific branch with input parameters # Trigger on a specific branch with input parameters
fj actions workflow run deploy.yml -r staging -f environment=staging -f version=1.2.3`, fj actions workflow run deploy.yml -r staging -f environment=staging -f version=1.2.3`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWorkflowRun, RunE: runWorkflowRun,
}
addRepoFlags(c)
c.Flags().StringP("ref", "r", "", "Branch or tag name to run the workflow on (defaults to repository's default branch)")
c.Flags().StringSliceP("field", "f", nil, "Add a string parameter in key=value format (can be used multiple times)")
c.Flags().StringSliceP("raw-field", "F", nil, "Add a string parameter in key=value format, reading from file if value starts with @ (can be used multiple times)")
return c
} }
var workflowEnableCmd = &cobra.Command{ func newWorkflowEnableCmd() *cobra.Command {
Use: "enable <workflow>", c := &cobra.Command{
Short: "Enable a workflow", Use: "enable <workflow>",
Long: "Enable a workflow so it can be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to enable workflows.", Short: "Enable a workflow",
Example: ` # Enable a workflow Long: "Enable a workflow so it can be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to enable workflows.",
Example: ` # Enable a workflow
fj actions workflow enable ci.yml`, fj actions workflow enable ci.yml`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWorkflowEnable, RunE: runWorkflowEnable,
}
addRepoFlags(c)
return c
} }
var workflowDisableCmd = &cobra.Command{ func newWorkflowDisableCmd() *cobra.Command {
Use: "disable <workflow>", c := &cobra.Command{
Short: "Disable a workflow", Use: "disable <workflow>",
Long: "Disable a workflow so it cannot be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to disable workflows.", Short: "Disable a workflow",
Example: ` # Disable a workflow Long: "Disable a workflow so it cannot be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to disable workflows.",
Example: ` # Disable a workflow
fj actions workflow disable ci.yml`, fj actions workflow disable ci.yml`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runWorkflowDisable, RunE: runWorkflowDisable,
}
addRepoFlags(c)
return c
} }
// Secret commands // Secret commands
@ -336,21 +414,10 @@ var actionsVariableDeleteCmd = &cobra.Command{
func init() { func init() {
rootCmd.AddCommand(actionsCmd) rootCmd.AddCommand(actionsCmd)
// Add run commands (gh run compatible) // Run and Workflow trees come from the factory functions defined above
actionsCmd.AddCommand(runCmd) // so cmd/aliases.go can build identical top-level trees under rootCmd.
runCmd.AddCommand(runListCmd) actionsCmd.AddCommand(newRunCmd(""))
runCmd.AddCommand(runViewCmd) actionsCmd.AddCommand(newWorkflowCmd(""))
runCmd.AddCommand(runWatchCmd)
runCmd.AddCommand(runRerunCmd)
runCmd.AddCommand(runCancelCmd)
// Add workflow commands (gh workflow compatible)
actionsCmd.AddCommand(workflowCmd)
workflowCmd.AddCommand(workflowListCmd)
workflowCmd.AddCommand(workflowViewCmd)
workflowCmd.AddCommand(workflowRunCmd)
workflowCmd.AddCommand(workflowEnableCmd)
workflowCmd.AddCommand(workflowDisableCmd)
// Add secret commands // Add secret commands
actionsCmd.AddCommand(actionsSecretCmd) actionsCmd.AddCommand(actionsSecretCmd)
@ -366,34 +433,6 @@ func init() {
actionsVariableCmd.AddCommand(actionsVariableUpdateCmd) actionsVariableCmd.AddCommand(actionsVariableUpdateCmd)
actionsVariableCmd.AddCommand(actionsVariableDeleteCmd) actionsVariableCmd.AddCommand(actionsVariableDeleteCmd)
// Add flags for run commands
addRepoFlags(runListCmd)
runListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
addJSONFlags(runListCmd, "Output workflow runs as JSON")
addRepoFlags(runViewCmd)
runViewCmd.Flags().BoolP("verbose", "v", false, "Show job steps")
runViewCmd.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
runViewCmd.Flags().StringP("job", "j", "", "View a specific job ID from a run")
runViewCmd.Flags().BoolP("log-failed", "", false, "View the log for any failed steps in a run or specific job")
addJSONFlags(runViewCmd, "Output workflow run as JSON")
addRepoFlags(runWatchCmd)
runWatchCmd.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
addRepoFlags(runRerunCmd)
addRepoFlags(runCancelCmd)
// Add flags for workflow commands
addRepoFlags(workflowListCmd)
workflowListCmd.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
addJSONFlags(workflowListCmd, "Output workflows as JSON")
addRepoFlags(workflowViewCmd)
addJSONFlags(workflowViewCmd, "Output workflow as JSON")
addRepoFlags(workflowRunCmd)
addRepoFlags(workflowEnableCmd)
addRepoFlags(workflowDisableCmd)
workflowRunCmd.Flags().StringP("ref", "r", "", "Branch or tag name to run the workflow on (defaults to repository's default branch)")
workflowRunCmd.Flags().StringSliceP("field", "f", nil, "Add a string parameter in key=value format (can be used multiple times)")
workflowRunCmd.Flags().StringSliceP("raw-field", "F", nil, "Add a string parameter in key=value format, reading from file if value starts with @ (can be used multiple times)")
// Add flags for secret commands // Add flags for secret commands
addRepoFlags(actionsSecretListCmd) addRepoFlags(actionsSecretListCmd)
addRepoFlags(actionsSecretCreateCmd) addRepoFlags(actionsSecretCreateCmd)

View file

@ -1,142 +1,16 @@
package cmd package cmd
import ( // Top-level aliases for "actions run" and "actions workflow" — matches gh
"time" // CLI's ergonomics so users can type `fj run list` and `fj workflow list`
// instead of `fj actions run list`.
"github.com/spf13/cobra" //
) // Both trees are built from the same factory functions defined in
// `cmd/actions.go` (newRunCmd / newWorkflowCmd), which means flags and
// Top-level aliases for "actions run" and "actions workflow" commands, // help text are guaranteed identical between the two paths. Previously
// matching gh CLI's command structure (e.g., "fj run list" instead of "fj actions run list"). // this file rebuilt parallel trees by hand and silently drifted (the
// `--json` Bool/string mismatch was the symptom that surfaced).
func init() { func init() {
// --- run alias --- rootCmd.AddCommand(newRunCmd(" (alias for 'actions run')"))
runAliasCmd := &cobra.Command{ rootCmd.AddCommand(newWorkflowCmd(" (alias for 'actions workflow')"))
Use: "run",
Short: "View and manage workflow runs (alias for 'actions run')",
Long: "List, view, and manage workflow runs.\n\nThis is a top-level alias for 'actions run'.",
}
runAliasListCmd := &cobra.Command{
Use: "list",
Short: "List recent workflow runs",
Long: "List recent workflow runs for a repository.",
RunE: runRunList,
}
addRepoFlags(runAliasListCmd)
runAliasListCmd.Flags().IntP("limit", "L", 20, "Maximum number of runs to list")
addJSONFlags(runAliasListCmd, "Output workflow runs as JSON")
runAliasViewCmd := &cobra.Command{
Use: "view <run-id>",
Short: "View a workflow run",
Long: "View details about a specific workflow run.",
Args: cobra.ExactArgs(1),
RunE: runRunView,
}
addRepoFlags(runAliasViewCmd)
runAliasViewCmd.Flags().BoolP("verbose", "v", false, "Show job steps")
runAliasViewCmd.Flags().BoolP("log", "", false, "View full log for either a run or specific job")
runAliasViewCmd.Flags().StringP("job", "j", "", "View a specific job ID from a run")
runAliasViewCmd.Flags().BoolP("log-failed", "", false, "View the log for any failed steps in a run or specific job")
addJSONFlags(runAliasViewCmd, "Output workflow run as JSON")
runAliasWatchCmd := &cobra.Command{
Use: "watch <run-id>",
Short: "Watch a workflow run",
Long: "Poll a workflow run until it completes.",
Args: cobra.ExactArgs(1),
RunE: runRunWatch,
}
addRepoFlags(runAliasWatchCmd)
runAliasWatchCmd.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
runAliasRerunCmd := &cobra.Command{
Use: "rerun <run-id>",
Short: "Rerun a workflow run",
Long: "Trigger a rerun for a specific workflow run.",
Args: cobra.ExactArgs(1),
RunE: runRunRerun,
}
addRepoFlags(runAliasRerunCmd)
runAliasCancelCmd := &cobra.Command{
Use: "cancel <run-id>",
Short: "Cancel a workflow run",
Long: "Cancel a running workflow run.",
Args: cobra.ExactArgs(1),
RunE: runRunCancel,
}
addRepoFlags(runAliasCancelCmd)
runAliasCmd.AddCommand(runAliasListCmd)
runAliasCmd.AddCommand(runAliasViewCmd)
runAliasCmd.AddCommand(runAliasWatchCmd)
runAliasCmd.AddCommand(runAliasRerunCmd)
runAliasCmd.AddCommand(runAliasCancelCmd)
rootCmd.AddCommand(runAliasCmd)
// --- workflow alias ---
workflowAliasCmd := &cobra.Command{
Use: "workflow",
Short: "Manage workflows (alias for 'actions workflow')",
Long: "List, view, and run workflows.\n\nThis is a top-level alias for 'actions workflow'.",
}
workflowAliasListCmd := &cobra.Command{
Use: "list",
Short: "List workflows",
Long: "List all workflows in a repository.",
RunE: runWorkflowList,
}
addRepoFlags(workflowAliasListCmd)
workflowAliasListCmd.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
addJSONFlags(workflowAliasListCmd, "Output workflows as JSON")
workflowAliasViewCmd := &cobra.Command{
Use: "view <workflow>",
Short: "View a workflow",
Long: "View details about a specific workflow. You can specify the workflow by name or filename.",
Args: cobra.ExactArgs(1),
RunE: runWorkflowView,
}
addRepoFlags(workflowAliasViewCmd)
addJSONFlags(workflowAliasViewCmd, "Output workflow as JSON")
workflowAliasRunCmd := &cobra.Command{
Use: "run <workflow>",
Short: "Run a workflow",
Long: "Trigger a workflow_dispatch event for a workflow. The workflow must support the workflow_dispatch trigger.",
Args: cobra.ExactArgs(1),
RunE: runWorkflowRun,
}
addRepoFlags(workflowAliasRunCmd)
workflowAliasRunCmd.Flags().StringP("ref", "r", "", "Branch or tag name to run the workflow on (defaults to repository's default branch)")
workflowAliasRunCmd.Flags().StringSliceP("field", "f", nil, "Add a string parameter in key=value format (can be used multiple times)")
workflowAliasRunCmd.Flags().StringSliceP("raw-field", "F", nil, "Add a string parameter in key=value format, reading from file if value starts with @ (can be used multiple times)")
workflowAliasEnableCmd := &cobra.Command{
Use: "enable <workflow>",
Short: "Enable a workflow",
Long: "Enable a workflow so it can be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to enable workflows.",
Args: cobra.ExactArgs(1),
RunE: runWorkflowEnable,
}
addRepoFlags(workflowAliasEnableCmd)
workflowAliasDisableCmd := &cobra.Command{
Use: "disable <workflow>",
Short: "Disable a workflow",
Long: "Disable a workflow so it cannot be triggered.\n\nNote: This feature requires Forgejo 15.0+ or Gitea 1.24+.\nFor older versions, use the web UI to disable workflows.",
Args: cobra.ExactArgs(1),
RunE: runWorkflowDisable,
}
addRepoFlags(workflowAliasDisableCmd)
workflowAliasCmd.AddCommand(workflowAliasListCmd)
workflowAliasCmd.AddCommand(workflowAliasViewCmd)
workflowAliasCmd.AddCommand(workflowAliasRunCmd)
workflowAliasCmd.AddCommand(workflowAliasEnableCmd)
workflowAliasCmd.AddCommand(workflowAliasDisableCmd)
rootCmd.AddCommand(workflowAliasCmd)
} }