2025-12-08 11:16:35 +01:00
package cmd
import (
"fmt"
"os"
"strconv"
"text/tabwriter"
"time"
"code.gitea.io/sdk/gitea"
"github.com/spf13/cobra"
"codeberg.org/romaintb/fgj/internal/api"
"codeberg.org/romaintb/fgj/internal/config"
)
// ActionRun represents a workflow run
type ActionRun struct {
2026-01-18 11:48:08 +01:00
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" `
2025-12-08 11:16:35 +01:00
}
// 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 {
2026-01-18 11:48:08 +01:00
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" `
2025-12-08 11:16:35 +01:00
}
// 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" `
}
2025-12-08 11:16:35 +01:00
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." ,
}
// Run commands (compatible with gh run)
var runCmd = & cobra . Command {
Use : "run" ,
Short : "View and manage workflow runs" ,
Long : "List, view, and manage workflow runs." ,
}
var runListCmd = & cobra . Command {
Use : "list" ,
Short : "List recent workflow runs" ,
Long : "List recent workflow runs for a repository." ,
RunE : runRunList ,
}
var runViewCmd = & 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 ,
}
2026-01-16 10:51:37 +01:00
// Workflow commands
var workflowCmd = & cobra . Command {
Use : "workflow" ,
Short : "Manage workflows" ,
Long : "List, view, and run workflows." ,
}
var workflowListCmd = & cobra . Command {
Use : "list" ,
Short : "List workflows" ,
Long : "List all workflows in a repository." ,
RunE : runWorkflowList ,
}
var workflowViewCmd = & 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 ,
}
var workflowRunCmd = & 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 ,
}
2025-12-08 11:16:35 +01:00
// 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." ,
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." ,
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." ,
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." ,
RunE : runActionsVariableList ,
}
var actionsVariableGetCmd = & cobra . Command {
Use : "get <name>" ,
Short : "Get a repository variable" ,
Long : "Get the value of a specific repository variable." ,
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." ,
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." ,
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." ,
Args : cobra . ExactArgs ( 1 ) ,
RunE : runActionsVariableDelete ,
}
func init ( ) {
rootCmd . AddCommand ( actionsCmd )
// Add run commands (gh run compatible)
actionsCmd . AddCommand ( runCmd )
runCmd . AddCommand ( runListCmd )
runCmd . AddCommand ( runViewCmd )
2026-01-16 10:51:37 +01:00
// Add workflow commands (gh workflow compatible)
actionsCmd . AddCommand ( workflowCmd )
workflowCmd . AddCommand ( workflowListCmd )
workflowCmd . AddCommand ( workflowViewCmd )
workflowCmd . AddCommand ( workflowRunCmd )
2025-12-08 11:16:35 +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 run commands
addRepoFlags ( runListCmd )
runListCmd . Flags ( ) . IntP ( "limit" , "L" , 20 , "Maximum number of runs to list" )
2026-01-18 11:48:08 +01:00
runListCmd . Flags ( ) . Bool ( "json" , false , "Output workflow runs as JSON" )
2025-12-08 11:16:35 +01:00
addRepoFlags ( runViewCmd )
2025-12-09 13:41:08 +01:00
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" )
2026-01-18 11:48:08 +01:00
runViewCmd . Flags ( ) . Bool ( "json" , false , "Output workflow run as JSON" )
2025-12-08 11:16:35 +01:00
2026-01-16 10:51:37 +01:00
// Add flags for workflow commands
addRepoFlags ( workflowListCmd )
workflowListCmd . Flags ( ) . IntP ( "limit" , "L" , 20 , "Maximum number of workflows to list" )
2026-01-18 11:48:08 +01:00
workflowListCmd . Flags ( ) . Bool ( "json" , false , "Output workflows as JSON" )
2026-01-16 10:51:37 +01:00
addRepoFlags ( workflowViewCmd )
2026-01-18 11:48:08 +01:00
workflowViewCmd . Flags ( ) . Bool ( "json" , false , "Output workflow as JSON" )
2026-01-16 10:51:37 +01:00
addRepoFlags ( workflowRunCmd )
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)" )
2025-12-08 11:16:35 +01:00
// Add flags for secret commands
addRepoFlags ( actionsSecretListCmd )
addRepoFlags ( actionsSecretCreateCmd )
addRepoFlags ( actionsSecretDeleteCmd )
// Add flags for variable commands
addRepoFlags ( actionsVariableListCmd )
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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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" )
// 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 )
}
2026-01-18 11:48:08 +01:00
if jsonOutput , _ := cmd . Flags ( ) . GetBool ( "json" ) ; jsonOutput {
return writeJSON ( runList . WorkflowRuns )
}
2025-12-08 11:16:35 +01:00
if len ( runList . WorkflowRuns ) == 0 {
fmt . Println ( "No workflow runs found" )
return nil
}
w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
if _ , err := fmt . Fprintln ( w , "STATUS\tTITLE\tWORKFLOW\tEVENT\tID\tCREATED" ) ; err != nil {
return fmt . Errorf ( "failed to write header: %w" , err )
}
for _ , run := range runList . WorkflowRuns {
createdTime , err := time . Parse ( time . RFC3339 , run . Created )
if err != nil {
createdTime = time . Now ( )
}
timeStr := formatTimeSince ( createdTime )
if _ , err := fmt . Fprintf ( w , "%s\t%s\t%s\t%s\t%d\t%s\n" ,
formatStatus ( run . Status ) , run . Title , run . WorkflowID , run . Event , run . ID , timeStr ) ; err != nil {
return fmt . Errorf ( "failed to write run: %w" , err )
}
}
if err := w . Flush ( ) ; err != nil {
return fmt . Errorf ( "failed to flush output: %w" , err )
}
return nil
}
func runRunView ( cmd * cobra . Command , args [ ] string ) error {
cfg , err := config . Load ( )
if err != nil {
return fmt . Errorf ( "failed to load config: %w" , err )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
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" )
2026-01-18 11:48:08 +01:00
jsonOutput , _ := cmd . Flags ( ) . GetBool ( "json" )
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 )
}
}
2025-12-08 11:16:35 +01:00
2026-01-18 11:48:08 +01:00
if jsonOutput && ( showLog || showLogFailed ) {
return fmt . Errorf ( "--json cannot be used with --log or --log-failed" )
}
2025-12-08 11:16:35 +01:00
// 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 )
}
2026-01-18 11:48:08 +01:00
needsJobs := verbose || showLog || showLogFailed || jobID > 0
if jsonOutput {
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 writeJSON ( payload )
}
2025-12-08 11:16:35 +01:00
// Display run information
fmt . Printf ( "Title: %s\n" , run . Title )
fmt . Printf ( "Workflow: %s\n" , run . WorkflowID )
fmt . Printf ( "Run: #%d\n" , run . IndexInRepo )
fmt . Printf ( "Status: %s\n" , formatStatus ( run . Status ) )
fmt . Printf ( "Event: %s\n" , run . Event )
fmt . Printf ( "Ref: %s\n" , run . PrettyRef )
commit := run . CommitSHA
if len ( commit ) > 8 {
commit = commit [ : 8 ]
}
fmt . Printf ( "Commit: %s\n" , commit )
if createdTime , err := time . Parse ( time . RFC3339 , run . Created ) ; err == nil {
fmt . Printf ( "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 . Printf ( "Started: %s\n" , startedTime . Format ( "2006-01-02 15:04:05" ) )
}
}
if updatedTime , err := time . Parse ( time . RFC3339 , run . Updated ) ; err == nil {
fmt . Printf ( "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-08 11:16:35 +01:00
}
2025-12-09 13:41:08 +01:00
}
if len ( runTasks ) == 0 {
fmt . Println ( "\nNo jobs found for this run" )
return nil
}
2025-12-08 11:16:35 +01:00
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-08 11:16:35 +01:00
}
}
2025-12-09 13:41:08 +01:00
if ! found {
return fmt . Errorf ( "job %d not found in this run" , jobID )
2025-12-08 11:16:35 +01:00
}
2025-12-09 13:41:08 +01:00
}
2025-12-08 11:16:35 +01:00
2025-12-09 13:41:08 +01:00
// Case 1: --verbose (show job steps/details without logs)
if verbose && ! showLog && ! showLogFailed {
fmt . Println ( "\nJobs:" )
for _ , task := range runTasks {
fmt . Printf ( "\n %s - %s (ID: %d)\n" , formatStatus ( task . Status ) , task . Name , task . ID )
if startedTime , err := time . Parse ( time . RFC3339 , task . RunStartedAt ) ; err == nil {
fmt . Printf ( " Started: %s\n" , startedTime . Format ( "2006-01-02 15:04:05" ) )
}
if updatedTime , err := time . Parse ( time . RFC3339 , task . UpdatedAt ) ; err == nil {
fmt . Printf ( " Updated: %s\n" , updatedTime . Format ( "2006-01-02 15:04:05" ) )
2025-12-08 11:16:35 +01:00
}
}
2025-12-09 13:41:08 +01:00
return nil
}
2025-12-08 11:16:35 +01:00
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 , run . IndexInRepo , task , showLogFailed ) ; err != nil {
fmt . Printf ( "\nError fetching log for job %s: %v\n" , task . Name , err )
2025-12-08 11:16:35 +01:00
}
2025-12-09 13:41:08 +01:00
}
return nil
}
2025-12-08 11:16:35 +01:00
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 . Println ( "\nJob Details:" )
fmt . Printf ( " Name: %s\n" , task . Name )
fmt . Printf ( " ID: %d\n" , task . ID )
fmt . Printf ( " Status: %s\n" , formatStatus ( task . Status ) )
if startedTime , err := time . Parse ( time . RFC3339 , task . RunStartedAt ) ; err == nil {
fmt . Printf ( " Started: %s\n" , startedTime . Format ( "2006-01-02 15:04:05" ) )
}
if updatedTime , err := time . Parse ( time . RFC3339 , task . UpdatedAt ) ; err == nil {
fmt . Printf ( " Updated: %s\n" , updatedTime . Format ( "2006-01-02 15:04:05" ) )
2025-12-08 11:16:35 +01:00
}
}
return nil
}
2025-12-09 13:41:08 +01:00
func showJobLog ( client * api . Client , owner , name string , runNumber int64 , task ActionTask , logFailed bool ) error {
2025-12-08 11:16:35 +01:00
// Fetch log from /repos/{owner}/{repo}/actions/runs/{run_number}/jobs/{job_id}/logs
logURL := fmt . Sprintf ( "https://%s/%s/%s/actions/runs/%d/jobs/%d/logs" ,
client . Hostname ( ) , owner , name , runNumber , task . ID )
fmt . Printf ( "\n========================================\n" )
fmt . Printf ( "Job: %s (ID: %d)\n" , task . Name , task . ID )
fmt . Printf ( "Status: %s\n" , formatStatus ( task . Status ) )
fmt . Printf ( "========================================\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 . Println ( "Note: --log-failed filtering not yet implemented, showing all logs" )
}
2025-12-08 11:16:35 +01:00
fmt . Print ( logContent )
fmt . Println ( )
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 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 ( ) )
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 {
2026-01-18 11:48:08 +01:00
if jsonOutput , _ := cmd . Flags ( ) . GetBool ( "json" ) ; jsonOutput {
return writeJSON ( allWorkflows )
}
2026-01-16 10:51:37 +01:00
fmt . Println ( "No workflows found" )
return nil
}
2026-01-18 11:48:08 +01:00
if jsonOutput , _ := cmd . Flags ( ) . GetBool ( "json" ) ; jsonOutput {
return writeJSON ( allWorkflows )
}
2026-01-16 10:51:37 +01:00
w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
if _ , err := fmt . Fprintln ( w , "NAME\tSTATE\tPATH" ) ; err != nil {
return fmt . Errorf ( "failed to write header: %w" , err )
}
for _ , workflow := range allWorkflows {
if _ , err := fmt . Fprintf ( w , "%s\t%s\t%s\n" ,
workflow . Name , workflow . State , workflow . Path ) ; err != nil {
return fmt . Errorf ( "failed to write workflow: %w" , err )
}
}
if err := w . Flush ( ) ; err != nil {
return fmt . Errorf ( "failed to flush output: %w" , err )
}
return nil
}
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 ( ) )
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 ]
// Find the workflow by listing from both .gitea/workflows and .forgejo/workflows
var workflow * 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" && ( content . Name == workflowIdentifier || content . Path == workflowIdentifier ) {
workflow = & Workflow {
Name : content . Name ,
Path : content . Path ,
State : "active" ,
}
break
}
}
if workflow != nil {
break
}
}
if workflow == nil {
return fmt . Errorf ( "workflow '%s' not found" , workflowIdentifier )
}
2026-01-18 11:48:08 +01:00
jsonOutput , _ := cmd . Flags ( ) . GetBool ( "json" )
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
2026-01-18 11:48:08 +01:00
if err := client . GetJSON ( runsEndpoint , & runList ) ; err == nil && len ( runList . WorkflowRuns ) > 0 {
latestRun = & runList . WorkflowRuns [ 0 ]
2026-01-16 10:51:37 +01:00
}
2026-01-18 11:48:08 +01:00
if jsonOutput {
payload := struct {
Workflow * Workflow ` json:"workflow" `
LatestRun * ActionRun ` json:"latest_run,omitempty" `
} {
Workflow : workflow ,
LatestRun : latestRun ,
}
return writeJSON ( payload )
}
// Display workflow information
fmt . Printf ( "Name: %s\n" , workflow . Name )
fmt . Printf ( "Path: %s\n" , workflow . Path )
fmt . Printf ( "State: %s\n" , workflow . State )
if latestRun != nil {
2026-01-16 10:51:37 +01:00
fmt . Printf ( "\nLatest run:\n" )
2026-01-18 11:48:08 +01:00
fmt . Printf ( " Status: %s\n" , formatStatus ( latestRun . Status ) )
fmt . Printf ( " Event: %s\n" , latestRun . Event )
fmt . Printf ( " Ref: %s\n" , latestRun . PrettyRef )
if createdTime , err := time . Parse ( time . RFC3339 , latestRun . Created ) ; err == nil {
2026-01-16 10:51:37 +01:00
fmt . Printf ( " Created: %s\n" , formatTimeSince ( createdTime ) )
}
}
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 ( ) )
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 . Printf ( "✓ Workflow '%s' triggered successfully\n" , workflowIdentifier )
fmt . Printf ( " Branch/Tag: %s\n" , ref )
if len ( inputs ) > 0 {
fmt . Println ( " Inputs:" )
for key , value := range inputs {
fmt . Printf ( " %s: %s\n" , key , value )
}
}
return nil
}
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 : ] }
}
2025-12-08 11:16:35 +01:00
// 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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
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 . Println ( "No secrets found" )
return nil
}
w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
if _ , err := fmt . Fprintln ( w , "NAME\tCREATED" ) ; err != nil {
return fmt . Errorf ( "failed to write header: %w" , err )
}
for _ , secret := range secrets {
if _ , err := fmt . Fprintf ( w , "%s\t%s\n" , secret . Name , secret . Created . Format ( "2006-01-02 15:04:05" ) ) ; err != nil {
return fmt . Errorf ( "failed to write secret: %w" , err )
}
}
if err := w . Flush ( ) ; err != nil {
return fmt . Errorf ( "failed to flush output: %w" , err )
}
return nil
}
func runActionsSecretCreate ( cmd * cobra . Command , args [ ] string ) error {
cfg , err := config . Load ( )
if err != nil {
return fmt . Errorf ( "failed to load config: %w" , err )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
secretName := args [ 0 ]
// Read secret value from stdin
fmt . Print ( "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 . Printf ( "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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
secretName := args [ 0 ]
_ , err = client . DeleteRepoActionSecret ( owner , name , secretName )
if err != nil {
return fmt . Errorf ( "failed to delete secret: %w" , err )
}
fmt . Printf ( "Secret '%s' deleted successfully\n" , secretName )
return nil
}
// Variable command implementations
func runActionsVariableList ( cmd * cobra . Command , args [ ] string ) error {
// Note: The SDK doesn't have a ListRepoActionVariable method yet
return fmt . Errorf ( "listing variables is not yet supported in the SDK" )
}
func runActionsVariableGet ( cmd * cobra . Command , args [ ] string ) error {
cfg , err := config . Load ( )
if err != nil {
return fmt . Errorf ( "failed to load config: %w" , err )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
variableName := args [ 0 ]
variable , _ , err := client . GetRepoActionVariable ( owner , name , variableName )
if err != nil {
return fmt . Errorf ( "failed to get variable: %w" , err )
}
fmt . Printf ( "%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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
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 . Printf ( "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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
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 . Printf ( "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 )
}
2026-01-05 12:47:28 +01:00
client , err := api . NewClientFromConfig ( cfg , "" , getDetectedHost ( ) )
2025-12-08 11:16:35 +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
}
variableName := args [ 0 ]
_ , err = client . DeleteRepoActionVariable ( owner , name , variableName )
if err != nil {
return fmt . Errorf ( "failed to delete variable: %w" , err )
}
fmt . Printf ( "Variable '%s' deleted successfully\n" , variableName )
return nil
}