fj/cmd/actions.go

1508 lines
42 KiB
Go
Raw Normal View History

package cmd
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"github.com/spf13/cobra"
"forgejo.zerova.net/public/fj/internal/api"
"forgejo.zerova.net/public/fj/internal/config"
)
// ActionRun represents a workflow run
type ActionRun struct {
ID int64 `json:"id"`
Title string `json:"title"`
WorkflowID string `json:"workflow_id"`
IndexInRepo int64 `json:"index_in_repo"`
Event string `json:"event"`
Status string `json:"status"`
CommitSHA string `json:"commit_sha"`
PrettyRef string `json:"prettyref"`
Created string `json:"created"`
Updated string `json:"updated"`
Started string `json:"started"`
}
// ActionRunList represents a list of workflow runs
type ActionRunList struct {
TotalCount int `json:"total_count"`
WorkflowRuns []ActionRun `json:"workflow_runs"`
}
// ActionTask represents a job/task within a workflow run
type ActionTask struct {
ID int64 `json:"id"`
Name string `json:"name"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
DisplayTitle string `json:"display_title"`
Status string `json:"status"`
WorkflowID string `json:"workflow_id"`
URL string `json:"url"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
RunStartedAt string `json:"run_started_at"`
}
// ActionTaskList represents a list of tasks/jobs
type ActionTaskList struct {
WorkflowRuns []ActionTask `json:"workflow_runs"`
TotalCount int `json:"total_count"`
}
2026-01-16 10:51:37 +01:00
// Workflow represents a workflow definition
type Workflow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
State string `json:"state"`
}
// WorkflowList represents a list of workflows
type WorkflowList struct {
Workflows []Workflow `json:"workflows"`
TotalCount int `json:"total_count"`
}
// ContentsResponse represents a file/directory in the repository
type ContentsResponse struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Size int64 `json:"size"`
}
var actionsCmd = &cobra.Command{
Use: "actions",
Aliases: []string{"action"},
Short: "Manage Forgejo Actions",
Long: "View and manage workflows, runs, secrets, and variables for Forgejo Actions in your repositories.",
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
// 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" + 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
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
func newRunListCmd() *cobra.Command {
c := &cobra.Command{
Use: "list",
Short: "List recent workflow runs",
Long: "List recent workflow runs for a repository.",
Example: ` # List recent workflow runs
fj actions run list
# List runs with a custom limit
fj actions run list -L 50
# Output as JSON
fj actions run list --json`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
func newRunViewCmd() *cobra.Command {
c := &cobra.Command{
Use: "view <run-id>",
Short: "View a workflow run",
Long: "View details about a specific workflow run.",
Example: ` # View a workflow run
fj actions run view 123
# View with job details
fj actions run view 123 -v
# View logs for a specific job
fj actions run view 123 --job 456 --log
# View only failed logs
fj actions run view 123 --log-failed`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
func newRunWatchCmd() *cobra.Command {
c := &cobra.Command{
Use: "watch <run-id>",
Short: "Watch a workflow run",
Long: "Poll a workflow run until it completes.",
Example: ` # Watch a run until it completes
fj actions run watch 123
# Watch with a custom polling interval
fj actions run watch 123 -i 10s`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runRunWatch,
}
addRepoFlags(c)
c.Flags().DurationP("interval", "i", 5*time.Second, "Polling interval")
return c
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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.",
Example: ` # Rerun a failed workflow run
fj actions run rerun 123`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runRunRerun,
}
addRepoFlags(c)
return c
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
func newRunCancelCmd() *cobra.Command {
c := &cobra.Command{
Use: "cancel <run-id>",
Short: "Cancel a workflow run",
Long: "Cancel a running workflow run.",
Example: ` # Cancel a running workflow
fj actions run cancel 123`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runRunCancel,
}
addRepoFlags(c)
return c
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
// 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" + 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
2026-01-16 10:51:37 +01:00
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
func newWorkflowListCmd() *cobra.Command {
c := &cobra.Command{
Use: "list",
Short: "List workflows",
Long: "List all workflows in a repository.",
Example: ` # List all workflows
fj actions workflow list
# List workflows as JSON
fj actions workflow list --json
# List workflows for a specific repo
fj actions workflow list -R owner/repo`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
RunE: runWorkflowList,
}
addRepoFlags(c)
c.Flags().IntP("limit", "L", 20, "Maximum number of workflows to list")
addJSONFlags(c, "Output workflows as JSON")
return c
2026-01-16 10:51:37 +01:00
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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.",
Example: ` # View a workflow by filename
fj actions workflow view ci.yml
# View as JSON
fj actions workflow view ci.yml --json`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runWorkflowView,
}
addRepoFlags(c)
addJSONFlags(c, "Output workflow as JSON")
return c
2026-01-16 10:51:37 +01:00
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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.",
Example: ` # Trigger a workflow on the default branch
fj actions workflow run deploy.yml
# Trigger on a specific branch with input parameters
fj actions workflow run deploy.yml -r staging -f environment=staging -f version=1.2.3`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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
2026-01-16 10:51:37 +01:00
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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.",
Example: ` # Enable a workflow
fj actions workflow enable ci.yml`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runWorkflowEnable,
}
addRepoFlags(c)
return c
}
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
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.",
Example: ` # Disable a workflow
fj actions workflow disable ci.yml`,
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
Args: cobra.ExactArgs(1),
RunE: runWorkflowDisable,
}
addRepoFlags(c)
return c
}
// Secret commands
var actionsSecretCmd = &cobra.Command{
Use: "secret",
Short: "Manage repository secrets",
Long: "List, create, and delete secrets for Forgejo Actions.",
}
var actionsSecretListCmd = &cobra.Command{
Use: "list",
Short: "List repository secrets",
Long: "List all secrets for a repository.",
Example: ` # List all secrets
fj actions secret list
# List secrets for a specific repo
fj actions secret list -R owner/repo`,
RunE: runActionsSecretList,
}
var actionsSecretCreateCmd = &cobra.Command{
Use: "create <name>",
Short: "Create or update a repository secret",
Long: "Create or update a secret for Forgejo Actions. The secret value will be read from stdin.",
Example: ` # Create a secret (will prompt for value)
fj actions secret create DEPLOY_TOKEN
# Create a secret for a specific repo
fj actions secret create API_KEY -R owner/repo`,
Args: cobra.ExactArgs(1),
RunE: runActionsSecretCreate,
}
var actionsSecretDeleteCmd = &cobra.Command{
Use: "delete <name>",
Short: "Delete a repository secret",
Long: "Delete a secret from Forgejo Actions.",
Example: ` # Delete a secret
fj actions secret delete DEPLOY_TOKEN`,
Args: cobra.ExactArgs(1),
RunE: runActionsSecretDelete,
}
// Variable commands
var actionsVariableCmd = &cobra.Command{
Use: "variable",
Short: "Manage repository variables",
Long: "List, get, create, update, and delete variables for Forgejo Actions.",
}
var actionsVariableListCmd = &cobra.Command{
Use: "list",
Short: "List repository variables",
Long: "List all variables for a repository.",
Example: ` # List all variables
fj actions variable list
# List variables for a specific repo
fj actions variable list -R owner/repo`,
RunE: runActionsVariableList,
}
var actionsVariableGetCmd = &cobra.Command{
Use: "get <name>",
Short: "Get a repository variable",
Long: "Get the value of a specific repository variable.",
Example: ` # Get a variable value
fj actions variable get ENVIRONMENT`,
Args: cobra.ExactArgs(1),
RunE: runActionsVariableGet,
}
var actionsVariableCreateCmd = &cobra.Command{
Use: "create <name> <value>",
Short: "Create a repository variable",
Long: "Create a new variable for Forgejo Actions.",
Example: ` # Create a variable
fj actions variable create ENVIRONMENT production
# Create a variable for a specific repo
fj actions variable create NODE_VERSION 20 -R owner/repo`,
Args: cobra.ExactArgs(2),
RunE: runActionsVariableCreate,
}
var actionsVariableUpdateCmd = &cobra.Command{
Use: "update <name> <value>",
Short: "Update a repository variable",
Long: "Update an existing variable for Forgejo Actions.",
Example: ` # Update a variable
fj actions variable update ENVIRONMENT staging`,
Args: cobra.ExactArgs(2),
RunE: runActionsVariableUpdate,
}
var actionsVariableDeleteCmd = &cobra.Command{
Use: "delete <name>",
Short: "Delete a repository variable",
Long: "Delete a variable from Forgejo Actions.",
Example: ` # Delete a variable
fj actions variable delete ENVIRONMENT`,
Args: cobra.ExactArgs(1),
RunE: runActionsVariableDelete,
}
func init() {
rootCmd.AddCommand(actionsCmd)
refactor(cmd): unify actions/aliases command trees via factory functions 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.
2026-05-02 15:56:58 -06:00
// 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(""))
2026-01-16 10:51:37 +01:00
// Add secret commands
actionsCmd.AddCommand(actionsSecretCmd)
actionsSecretCmd.AddCommand(actionsSecretListCmd)
actionsSecretCmd.AddCommand(actionsSecretCreateCmd)
actionsSecretCmd.AddCommand(actionsSecretDeleteCmd)
// Add variable commands
actionsCmd.AddCommand(actionsVariableCmd)
actionsVariableCmd.AddCommand(actionsVariableListCmd)
actionsVariableCmd.AddCommand(actionsVariableGetCmd)
actionsVariableCmd.AddCommand(actionsVariableCreateCmd)
actionsVariableCmd.AddCommand(actionsVariableUpdateCmd)
actionsVariableCmd.AddCommand(actionsVariableDeleteCmd)
// Add flags for secret commands
addRepoFlags(actionsSecretListCmd)
addRepoFlags(actionsSecretCreateCmd)
addRepoFlags(actionsSecretDeleteCmd)
// Add flags for variable commands
addRepoFlags(actionsVariableListCmd)
addJSONFlags(actionsVariableListCmd, "Output variables as JSON")
addRepoFlags(actionsVariableGetCmd)
addRepoFlags(actionsVariableCreateCmd)
addRepoFlags(actionsVariableUpdateCmd)
addRepoFlags(actionsVariableDeleteCmd)
}
func addRepoFlags(cmd *cobra.Command) {
cmd.Flags().StringP("repo", "R", "", "Repository in owner/name format (auto-detected from git if not specified)")
}
// Run command implementations
func runRunList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
limit, _ := cmd.Flags().GetInt("limit")
// Call the API endpoint directly since SDK doesn't have it yet
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?limit=%d", owner, name, limit)
var runList ActionRunList
if err := client.GetJSON(endpoint, &runList); err != nil {
return fmt.Errorf("failed to list runs: %w", err)
}
if wantJSON(cmd) {
return outputJSON(cmd, runList.WorkflowRuns)
}
if len(runList.WorkflowRuns) == 0 {
fmt.Fprintln(ios.Out, "No workflow runs found")
return nil
}
tp := ios.NewTablePrinter()
tp.AddHeader("STATUS", "TITLE", "WORKFLOW", "EVENT", "ID", "CREATED")
for _, run := range runList.WorkflowRuns {
createdTime, err := time.Parse(time.RFC3339, run.Created)
if err != nil {
createdTime = time.Now()
}
timeStr := formatTimeSince(createdTime)
tp.AddRow(formatStatus(run.Status), run.Title, run.WorkflowID, run.Event, fmt.Sprintf("%d", run.ID), timeStr)
}
return tp.Render()
}
func runRunView(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
runID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %w", err)
}
verbose, _ := cmd.Flags().GetBool("verbose")
showLog, _ := cmd.Flags().GetBool("log")
2025-12-09 13:41:08 +01:00
jobIDStr, _ := cmd.Flags().GetString("job")
showLogFailed, _ := cmd.Flags().GetBool("log-failed")
jsonRequested := wantJSON(cmd)
2025-12-09 13:41:08 +01:00
var jobID int64
if jobIDStr != "" {
var err error
jobID, err = strconv.ParseInt(jobIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid job ID: %w", err)
}
}
if jsonRequested && (showLog || showLogFailed) {
return fmt.Errorf("--json cannot be used with --log or --log-failed")
}
// Call the API endpoint directly since SDK doesn't have it yet
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
var run ActionRun
if err := client.GetJSON(endpoint, &run); err != nil {
return fmt.Errorf("failed to get run: %w", err)
}
needsJobs := verbose || showLog || showLogFailed || jobID > 0
if jsonRequested {
var runTasks []ActionTask
if needsJobs {
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
var taskList ActionTaskList
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
for _, task := range taskList.WorkflowRuns {
if task.RunNumber == run.IndexInRepo {
runTasks = append(runTasks, task)
}
}
if jobID > 0 {
var filtered []ActionTask
for _, task := range runTasks {
if task.ID == jobID {
filtered = append(filtered, task)
break
}
}
if len(filtered) == 0 {
return fmt.Errorf("job %d not found in this run", jobID)
}
runTasks = filtered
}
}
payload := struct {
Run ActionRun `json:"run"`
Tasks []ActionTask `json:"tasks,omitempty"`
}{
Run: run,
Tasks: runTasks,
}
return outputJSON(cmd, payload)
}
// Display run information
fmt.Fprintf(ios.Out, "Title: %s\n", run.Title)
fmt.Fprintf(ios.Out, "Workflow: %s\n", run.WorkflowID)
fmt.Fprintf(ios.Out, "Run: #%d\n", run.IndexInRepo)
fmt.Fprintf(ios.Out, "Status: %s\n", formatStatus(run.Status))
fmt.Fprintf(ios.Out, "Event: %s\n", run.Event)
fmt.Fprintf(ios.Out, "Ref: %s\n", run.PrettyRef)
commit := run.CommitSHA
if len(commit) > 8 {
commit = commit[:8]
}
fmt.Fprintf(ios.Out, "Commit: %s\n", commit)
if createdTime, err := time.Parse(time.RFC3339, run.Created); err == nil {
fmt.Fprintf(ios.Out, "Created: %s\n", createdTime.Format("2006-01-02 15:04:05"))
}
if run.Started != "" {
if startedTime, err := time.Parse(time.RFC3339, run.Started); err == nil {
fmt.Fprintf(ios.Out, "Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
}
}
if updatedTime, err := time.Parse(time.RFC3339, run.Updated); err == nil {
fmt.Fprintf(ios.Out, "Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
}
2025-12-09 13:41:08 +01:00
// Fetch jobs if needed for verbose, log, or job-specific views
if !needsJobs {
return nil
}
tasksEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", owner, name)
var taskList ActionTaskList
if err := client.GetJSON(tasksEndpoint, &taskList); err != nil {
return fmt.Errorf("failed to get tasks: %w", err)
}
// Filter tasks for this run number
var runTasks []ActionTask
for _, task := range taskList.WorkflowRuns {
if task.RunNumber == run.IndexInRepo {
runTasks = append(runTasks, task)
}
2025-12-09 13:41:08 +01:00
}
if len(runTasks) == 0 {
fmt.Fprintln(ios.Out, "\nNo jobs found for this run")
2025-12-09 13:41:08 +01:00
return nil
}
2025-12-09 13:41:08 +01:00
// If --job is specified, filter to that job
if jobID > 0 {
var found bool
for _, task := range runTasks {
if task.ID == jobID {
runTasks = []ActionTask{task}
found = true
break
}
}
2025-12-09 13:41:08 +01:00
if !found {
return fmt.Errorf("job %d not found in this run", jobID)
}
2025-12-09 13:41:08 +01:00
}
2025-12-09 13:41:08 +01:00
// Case 1: --verbose (show job steps/details without logs)
if verbose && !showLog && !showLogFailed {
fmt.Fprintln(ios.Out, "\nJobs:")
2025-12-09 13:41:08 +01:00
for _, task := range runTasks {
fmt.Fprintf(ios.Out, "\n %s - %s (ID: %d)\n", formatStatus(task.Status), task.Name, task.ID)
2025-12-09 13:41:08 +01:00
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
fmt.Fprintf(ios.Out, " Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
2025-12-09 13:41:08 +01:00
}
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
fmt.Fprintf(ios.Out, " Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
}
}
2025-12-09 13:41:08 +01:00
return nil
}
2025-12-09 13:41:08 +01:00
// Case 2: --log or --log-failed (show logs)
if showLog || showLogFailed {
for _, task := range runTasks {
if err := showJobLog(client, owner, name, task, showLogFailed); err != nil {
fmt.Fprintf(ios.Out, "\nError fetching log for job %s: %v\n", task.Name, err)
}
2025-12-09 13:41:08 +01:00
}
return nil
}
2025-12-09 13:41:08 +01:00
// Case 3: --job without --log or --verbose (show job details only)
if jobID > 0 {
task := runTasks[0]
fmt.Fprintln(ios.Out, "\nJob Details:")
fmt.Fprintf(ios.Out, " Name: %s\n", task.Name)
fmt.Fprintf(ios.Out, " ID: %d\n", task.ID)
fmt.Fprintf(ios.Out, " Status: %s\n", formatStatus(task.Status))
2025-12-09 13:41:08 +01:00
if startedTime, err := time.Parse(time.RFC3339, task.RunStartedAt); err == nil {
fmt.Fprintf(ios.Out, " Started: %s\n", startedTime.Format("2006-01-02 15:04:05"))
2025-12-09 13:41:08 +01:00
}
if updatedTime, err := time.Parse(time.RFC3339, task.UpdatedAt); err == nil {
fmt.Fprintf(ios.Out, " Updated: %s\n", updatedTime.Format("2006-01-02 15:04:05"))
}
}
return nil
}
func runRunWatch(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
runID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %w", err)
}
interval, _ := cmd.Flags().GetDuration("interval")
if interval <= 0 {
return fmt.Errorf("interval must be greater than 0")
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", owner, name, runID)
var lastStatus string
for {
var run ActionRun
if err := client.GetJSON(endpoint, &run); err != nil {
return fmt.Errorf("failed to get run: %w", err)
}
if run.Status != lastStatus {
fmt.Fprintf(ios.Out, "Status: %s\n", formatStatus(run.Status))
lastStatus = run.Status
}
if isRunComplete(run.Status) {
fmt.Fprintf(ios.Out, "Run #%d completed with status %s\n", run.IndexInRepo, formatStatus(run.Status))
return nil
}
time.Sleep(interval)
}
}
func runRunRerun(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
runID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %w", err)
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/rerun", owner, name, runID)
if err := client.PostJSON(endpoint, nil, nil); err != nil {
return fmt.Errorf("failed to rerun workflow: %w", err)
}
fmt.Fprintf(ios.Out, "✓ Rerun requested for run %d\n", runID)
return nil
}
func runRunCancel(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
runID, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %w", err)
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/cancel", owner, name, runID)
if err := client.PostJSON(endpoint, nil, nil); err != nil {
return fmt.Errorf("failed to cancel workflow run: %w", err)
}
fmt.Fprintf(ios.Out, "✓ Cancel requested for run %d\n", runID)
return nil
}
func showJobLog(client *api.Client, owner, name string, task ActionTask, logFailed bool) error {
// Fetch log from API: GET /api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs
logURL := fmt.Sprintf("https://%s/api/v1/repos/%s/%s/actions/jobs/%d/logs",
client.Hostname(), owner, name, task.ID)
fmt.Fprintf(ios.Out, "\n========================================\n")
fmt.Fprintf(ios.Out, "Job: %s (ID: %d)\n", task.Name, task.ID)
fmt.Fprintf(ios.Out, "Status: %s\n", formatStatus(task.Status))
fmt.Fprintf(ios.Out, "========================================\n\n")
// Use GetRawLog helper
logContent, err := client.GetRawLog(logURL)
if err != nil {
return err
}
2025-12-09 13:41:08 +01:00
// If --log-failed, filter to only show failed steps
// For now, just show all logs (filtering failed steps would require parsing the log format)
if logFailed {
// TODO: Implement filtering for failed steps only
// This would require parsing the log format and identifying failed step markers
fmt.Fprintln(ios.Out, "Note: --log-failed filtering not yet implemented, showing all logs")
2025-12-09 13:41:08 +01:00
}
fmt.Fprint(ios.Out, logContent)
fmt.Fprintln(ios.Out)
return nil
}
func formatStatus(status string) string {
switch status {
case "success":
return "✓ success"
case "failure":
return "✗ failure"
case "cancelled":
return "- cancelled"
case "skipped":
return "○ skipped"
case "in_progress", "running":
return "● in progress"
case "queued", "waiting":
return "○ queued"
default:
return status
}
}
func isRunComplete(status string) bool {
switch status {
case "success", "failure", "cancelled", "skipped":
return true
default:
return false
}
}
func formatTimeSince(t time.Time) string {
duration := time.Since(t)
if duration < time.Minute {
return "just now"
} else if duration < time.Hour {
mins := int(duration.Minutes())
if mins == 1 {
return "1 minute ago"
}
return fmt.Sprintf("%d minutes ago", mins)
} else if duration < 24*time.Hour {
hours := int(duration.Hours())
if hours == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", hours)
}
days := int(duration.Hours() / 24)
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
}
2026-01-16 10:51:37 +01:00
// Workflow command implementations
func runWorkflowList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
2026-01-16 10:51:37 +01:00
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
limit, _ := cmd.Flags().GetInt("limit")
// List workflows from both .gitea/workflows and .forgejo/workflows
var allWorkflows []Workflow
for _, dir := range []string{".gitea/workflows", ".forgejo/workflows"} {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, name, dir)
var contents []ContentsResponse
if err := client.GetJSON(endpoint, &contents); err != nil {
// Directory might not exist, continue
continue
}
for _, content := range contents {
if content.Type == "file" && (len(content.Name) > 4 && (content.Name[len(content.Name)-4:] == ".yml" || content.Name[len(content.Name)-5:] == ".yaml")) {
workflow := Workflow{
Name: content.Name,
Path: content.Path,
State: "active",
}
allWorkflows = append(allWorkflows, workflow)
if len(allWorkflows) >= limit {
break
}
}
}
if len(allWorkflows) >= limit {
break
}
}
if len(allWorkflows) == 0 {
if wantJSON(cmd) {
return outputJSON(cmd, allWorkflows)
}
fmt.Fprintln(ios.Out, "No workflows found")
2026-01-16 10:51:37 +01:00
return nil
}
if wantJSON(cmd) {
return outputJSON(cmd, allWorkflows)
}
tp := ios.NewTablePrinter()
tp.AddHeader("NAME", "STATE", "PATH")
2026-01-16 10:51:37 +01:00
for _, workflow := range allWorkflows {
tp.AddRow(workflow.Name, workflow.State, workflow.Path)
2026-01-16 10:51:37 +01:00
}
return tp.Render()
2026-01-16 10:51:37 +01:00
}
func runWorkflowView(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
2026-01-16 10:51:37 +01:00
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
workflowIdentifier := args[0]
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
if err != nil {
return err
2026-01-16 10:51:37 +01:00
}
var latestRun *ActionRun
2026-01-16 10:51:37 +01:00
// Get the latest run for this workflow
runsEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs?workflow_id=%s&limit=1", owner, name, workflow.Path)
var runList ActionRunList
if err := client.GetJSON(runsEndpoint, &runList); err == nil && len(runList.WorkflowRuns) > 0 {
latestRun = &runList.WorkflowRuns[0]
2026-01-16 10:51:37 +01:00
}
if wantJSON(cmd) {
payload := struct {
Workflow *Workflow `json:"workflow"`
LatestRun *ActionRun `json:"latest_run,omitempty"`
}{
Workflow: workflow,
LatestRun: latestRun,
}
return outputJSON(cmd, payload)
}
// Display workflow information
fmt.Fprintf(ios.Out, "Name: %s\n", workflow.Name)
fmt.Fprintf(ios.Out, "Path: %s\n", workflow.Path)
fmt.Fprintf(ios.Out, "State: %s\n", workflow.State)
if latestRun != nil {
fmt.Fprintf(ios.Out, "\nLatest run:\n")
fmt.Fprintf(ios.Out, " Status: %s\n", formatStatus(latestRun.Status))
fmt.Fprintf(ios.Out, " Event: %s\n", latestRun.Event)
fmt.Fprintf(ios.Out, " Ref: %s\n", latestRun.PrettyRef)
if createdTime, err := time.Parse(time.RFC3339, latestRun.Created); err == nil {
fmt.Fprintf(ios.Out, " Created: %s\n", formatTimeSince(createdTime))
2026-01-16 10:51:37 +01:00
}
}
return nil
}
func runWorkflowRun(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
2026-01-16 10:51:37 +01:00
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
workflowIdentifier := args[0]
ref, _ := cmd.Flags().GetString("ref")
fields, _ := cmd.Flags().GetStringSlice("field")
rawFields, _ := cmd.Flags().GetStringSlice("raw-field")
// If no ref is specified, get the repository's default branch
if ref == "" {
repoInfo, _, err := client.GetRepo(owner, name)
if err != nil {
return fmt.Errorf("failed to get repository info: %w", err)
}
ref = repoInfo.DefaultBranch
}
// Build the inputs map
inputs := make(map[string]string)
// Process -f/--field flags
for _, field := range fields {
parts := splitKeyValue(field)
if len(parts) == 2 {
inputs[parts[0]] = parts[1]
}
}
// Process -F/--raw-field flags (same as field for now, file reading can be added later)
for _, field := range rawFields {
parts := splitKeyValue(field)
if len(parts) == 2 {
inputs[parts[0]] = parts[1]
}
}
// Prepare the dispatch request
dispatchReq := map[string]any{
"ref": ref,
}
if len(inputs) > 0 {
dispatchReq["inputs"] = inputs
}
// Trigger the workflow
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, name, workflowIdentifier)
if err := client.PostJSON(endpoint, dispatchReq, nil); err != nil {
return fmt.Errorf("failed to trigger workflow: %w", err)
}
fmt.Fprintf(ios.Out, "✓ Workflow '%s' triggered successfully\n", workflowIdentifier)
fmt.Fprintf(ios.Out, " Branch/Tag: %s\n", ref)
2026-01-16 10:51:37 +01:00
if len(inputs) > 0 {
fmt.Fprintln(ios.Out, " Inputs:")
2026-01-16 10:51:37 +01:00
for key, value := range inputs {
fmt.Fprintf(ios.Out, " %s: %s\n", key, value)
2026-01-16 10:51:37 +01:00
}
}
return nil
}
func runWorkflowEnable(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
workflowIdentifier := args[0]
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
if err != nil {
return err
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/enable", owner, name, workflow.Name)
// Try PUT first (correct method per GitHub/Gitea API spec)
status, err := client.DoJSON(http.MethodPut, endpoint, nil, nil)
if err != nil && (status == http.StatusNotFound || status == http.StatusMethodNotAllowed) {
// Fall back to POST for older versions
status, err = client.DoJSON(http.MethodPost, endpoint, nil, nil)
}
if err != nil {
if status == http.StatusNotFound && strings.Contains(err.Error(), "404") {
2026-01-28 14:29:59 +01:00
return fmt.Errorf("failed to enable workflow: this feature requires Forgejo 15.0+ or Gitea 1.24+. " +
"Your instance does not support the workflow enable/disable API endpoints yet. " +
"You can enable workflows via the web UI instead")
}
return fmt.Errorf("failed to enable workflow: %w", err)
}
fmt.Fprintf(ios.Out, "✓ Workflow '%s' enabled\n", workflow.Name)
return nil
}
func runWorkflowDisable(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
workflowIdentifier := args[0]
workflow, err := findWorkflow(client, owner, name, workflowIdentifier)
if err != nil {
return err
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/disable", owner, name, workflow.Name)
// Try PUT first (correct method per GitHub/Gitea API spec)
status, err := client.DoJSON(http.MethodPut, endpoint, nil, nil)
if err != nil && (status == http.StatusNotFound || status == http.StatusMethodNotAllowed) {
// Fall back to POST for older versions
status, err = client.DoJSON(http.MethodPost, endpoint, nil, nil)
}
if err != nil {
if status == http.StatusNotFound && strings.Contains(err.Error(), "404") {
2026-01-28 14:29:59 +01:00
return fmt.Errorf("failed to disable workflow: this feature requires Forgejo 15.0+ or Gitea 1.24+. " +
"Your instance does not support the workflow enable/disable API endpoints yet. " +
"You can disable workflows via the web UI instead")
}
return fmt.Errorf("failed to disable workflow: %w", err)
}
fmt.Fprintf(ios.Out, "✓ Workflow '%s' disabled\n", workflow.Name)
return nil
}
2026-01-16 10:51:37 +01:00
func splitKeyValue(s string) []string {
idx := -1
for i, c := range s {
if c == '=' {
idx = i
break
}
}
if idx == -1 {
return []string{s}
}
return []string{s[:idx], s[idx+1:]}
}
func findWorkflow(client *api.Client, owner, name, workflowIdentifier string) (*Workflow, error) {
for _, dir := range []string{".gitea/workflows", ".forgejo/workflows"} {
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, name, dir)
var contents []ContentsResponse
if err := client.GetJSON(endpoint, &contents); err != nil {
continue
}
for _, content := range contents {
if content.Type == "file" && (content.Name == workflowIdentifier || content.Path == workflowIdentifier) {
return &Workflow{
Name: content.Name,
Path: content.Path,
State: "active",
}, nil
}
}
}
return nil, fmt.Errorf("workflow '%s' not found", workflowIdentifier)
}
// Secret command implementations
func runActionsSecretList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
secrets, _, err := client.ListRepoActionSecret(owner, name, gitea.ListRepoActionSecretOption{})
if err != nil {
return fmt.Errorf("failed to list secrets: %w", err)
}
if len(secrets) == 0 {
fmt.Fprintln(ios.Out, "No secrets found")
return nil
}
tp := ios.NewTablePrinter()
tp.AddHeader("NAME", "CREATED")
for _, secret := range secrets {
tp.AddRow(secret.Name, secret.Created.Format("2006-01-02 15:04:05"))
}
return tp.Render()
}
func runActionsSecretCreate(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
secretName := args[0]
// Read secret value from stdin
fmt.Fprint(ios.ErrOut, "Enter secret value: ")
var secretValue string
_, err = fmt.Scanln(&secretValue)
if err != nil {
return fmt.Errorf("failed to read secret value: %w", err)
}
opt := gitea.CreateSecretOption{
Name: secretName,
Data: secretValue,
}
_, err = client.CreateRepoActionSecret(owner, name, opt)
if err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}
fmt.Fprintf(ios.Out, "Secret '%s' created successfully\n", secretName)
return nil
}
func runActionsSecretDelete(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
secretName := args[0]
_, err = client.DeleteRepoActionSecret(owner, name, secretName)
if err != nil {
return fmt.Errorf("failed to delete secret: %w", err)
}
fmt.Fprintf(ios.Out, "Secret '%s' deleted successfully\n", secretName)
return nil
}
// Variable command implementations
// ActionVariable represents a repository action variable from the API
type ActionVariable struct {
Name string `json:"name"`
Value string `json:"data"`
}
func runActionsVariableList(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
endpoint := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, name)
var variables []ActionVariable
if err := client.GetJSON(endpoint, &variables); err != nil {
return fmt.Errorf("failed to list variables: %w", err)
}
if wantJSON(cmd) {
return outputJSON(cmd, variables)
}
if len(variables) == 0 {
fmt.Fprintln(ios.Out, "No variables found")
return nil
}
tp := ios.NewTablePrinter()
tp.AddHeader("NAME", "VALUE")
for _, v := range variables {
tp.AddRow(v.Name, v.Value)
}
return tp.Render()
}
func runActionsVariableGet(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
variableName := args[0]
variable, _, err := client.GetRepoActionVariable(owner, name, variableName)
if err != nil {
return fmt.Errorf("failed to get variable: %w", err)
}
fmt.Fprintf(ios.Out, "%s=%s\n", variable.Name, variable.Value)
return nil
}
func runActionsVariableCreate(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
variableName := args[0]
variableValue := args[1]
_, err = client.CreateRepoActionVariable(owner, name, variableName, variableValue)
if err != nil {
return fmt.Errorf("failed to create variable: %w", err)
}
fmt.Fprintf(ios.Out, "Variable '%s' created successfully\n", variableName)
return nil
}
func runActionsVariableUpdate(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
variableName := args[0]
variableValue := args[1]
_, err = client.UpdateRepoActionVariable(owner, name, variableName, variableValue)
if err != nil {
return fmt.Errorf("failed to update variable: %w", err)
}
fmt.Fprintf(ios.Out, "Variable '%s' updated successfully\n", variableName)
return nil
}
func runActionsVariableDelete(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
variableName := args[0]
_, err = client.DeleteRepoActionVariable(owner, name, variableName)
if err != nil {
return fmt.Errorf("failed to delete variable: %w", err)
}
fmt.Fprintf(ios.Out, "Variable '%s' deleted successfully\n", variableName)
return nil
}