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,14 +87,30 @@ var actionsCmd = &cobra.Command{
Long: "View and manage workflows, runs, secrets, and variables for Forgejo Actions in your repositories.",
}
// Run commands (compatible with gh run)
var runCmd = &cobra.Command{
// Run and Workflow command trees are built via factory functions
// (newRunCmd / newWorkflowCmd) so cmd/aliases.go can build an identical
// top-level tree under rootCmd without duplicating Use/Short/Long/Example/
// flag declarations. Single source of truth — drift impossible.
// 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",
Long: "List, view, and manage workflow runs.",
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 {
c := &cobra.Command{
Use: "list",
Short: "List recent workflow runs",
Long: "List recent workflow runs for a repository.",
@ -107,9 +123,15 @@ var runListCmd = &cobra.Command{
# Output as JSON
fj actions run list --json`,
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 {
c := &cobra.Command{
Use: "view <run-id>",
Short: "View a workflow run",
Long: "View details about a specific workflow run.",
@ -126,9 +148,18 @@ var runViewCmd = &cobra.Command{
fj actions run view 123 --log-failed`,
Args: cobra.ExactArgs(1),
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 {
c := &cobra.Command{
Use: "watch <run-id>",
Short: "Watch a workflow run",
Long: "Poll a workflow run until it completes.",
@ -139,9 +170,14 @@ var runWatchCmd = &cobra.Command{
fj actions run watch 123 -i 10s`,
Args: cobra.ExactArgs(1),
RunE: runRunWatch,
}
addRepoFlags(c)
c.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
return c
}
var runRerunCmd = &cobra.Command{
func newRunRerunCmd() *cobra.Command {
c := &cobra.Command{
Use: "rerun <run-id>",
Short: "Rerun a workflow run",
Long: "Trigger a rerun for a specific workflow run.",
@ -149,9 +185,13 @@ var runRerunCmd = &cobra.Command{
fj actions run rerun 123`,
Args: cobra.ExactArgs(1),
RunE: runRunRerun,
}
addRepoFlags(c)
return c
}
var runCancelCmd = &cobra.Command{
func newRunCancelCmd() *cobra.Command {
c := &cobra.Command{
Use: "cancel <run-id>",
Short: "Cancel a workflow run",
Long: "Cancel a running workflow run.",
@ -159,16 +199,29 @@ var runCancelCmd = &cobra.Command{
fj actions run cancel 123`,
Args: cobra.ExactArgs(1),
RunE: runRunCancel,
}
addRepoFlags(c)
return c
}
// Workflow commands
var workflowCmd = &cobra.Command{
// newWorkflowCmd builds the `workflow` subtree. parentLabel is interpolated
// the same way as newRunCmd's, so the alias variant can self-identify.
func newWorkflowCmd(parentLabel string) *cobra.Command {
cmd := &cobra.Command{
Use: "workflow",
Short: "Manage workflows",
Long: "List, view, and run workflows.",
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 {
c := &cobra.Command{
Use: "list",
Short: "List workflows",
Long: "List all workflows in a repository.",
@ -181,9 +234,15 @@ var workflowListCmd = &cobra.Command{
# List workflows for a specific repo
fj actions workflow list -R owner/repo`,
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 {
c := &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.",
@ -194,9 +253,14 @@ var workflowViewCmd = &cobra.Command{
fj actions workflow view ci.yml --json`,
Args: cobra.ExactArgs(1),
RunE: runWorkflowView,
}
addRepoFlags(c)
addJSONFlags(c, "Output workflow as JSON")
return c
}
var workflowRunCmd = &cobra.Command{
func newWorkflowRunCmd() *cobra.Command {
c := &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.",
@ -207,9 +271,16 @@ var workflowRunCmd = &cobra.Command{
fj actions workflow run deploy.yml -r staging -f environment=staging -f version=1.2.3`,
Args: cobra.ExactArgs(1),
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 {
c := &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.",
@ -217,9 +288,13 @@ var workflowEnableCmd = &cobra.Command{
fj actions workflow enable ci.yml`,
Args: cobra.ExactArgs(1),
RunE: runWorkflowEnable,
}
addRepoFlags(c)
return c
}
var workflowDisableCmd = &cobra.Command{
func newWorkflowDisableCmd() *cobra.Command {
c := &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.",
@ -227,6 +302,9 @@ var workflowDisableCmd = &cobra.Command{
fj actions workflow disable ci.yml`,
Args: cobra.ExactArgs(1),
RunE: runWorkflowDisable,
}
addRepoFlags(c)
return c
}
// Secret commands
@ -336,21 +414,10 @@ var actionsVariableDeleteCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(actionsCmd)
// Add run commands (gh run compatible)
actionsCmd.AddCommand(runCmd)
runCmd.AddCommand(runListCmd)
runCmd.AddCommand(runViewCmd)
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)
// Run and Workflow trees come from the factory functions defined above
// so cmd/aliases.go can build identical top-level trees under rootCmd.
actionsCmd.AddCommand(newRunCmd(""))
actionsCmd.AddCommand(newWorkflowCmd(""))
// Add secret commands
actionsCmd.AddCommand(actionsSecretCmd)
@ -366,34 +433,6 @@ func init() {
actionsVariableCmd.AddCommand(actionsVariableUpdateCmd)
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
addRepoFlags(actionsSecretListCmd)
addRepoFlags(actionsSecretCreateCmd)

View file

@ -1,142 +1,16 @@
package cmd
import (
"time"
"github.com/spf13/cobra"
)
// Top-level aliases for "actions run" and "actions workflow" commands,
// matching gh CLI's command structure (e.g., "fj run list" instead of "fj actions run list").
// Top-level aliases for "actions run" and "actions workflow" — matches gh
// CLI's ergonomics so users can type `fj run list` and `fj workflow list`
// instead of `fj actions run list`.
//
// Both trees are built from the same factory functions defined in
// `cmd/actions.go` (newRunCmd / newWorkflowCmd), which means flags and
// help text are guaranteed identical between the two paths. Previously
// this file rebuilt parallel trees by hand and silently drifted (the
// `--json` Bool/string mismatch was the symptom that surfaced).
func init() {
// --- run alias ---
runAliasCmd := &cobra.Command{
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)
rootCmd.AddCommand(newRunCmd(" (alias for 'actions run')"))
rootCmd.AddCommand(newWorkflowCmd(" (alias for 'actions workflow')"))
}