mirror of
https://gitea.com/gitea/tea.git
synced 2026-04-25 17:53:37 +02:00
feat(workflows): add dispatch, view, enable and disable subcommands (#952)
## Summary - Add `tea actions workflows dispatch` to trigger `workflow_dispatch` events with `--ref`, `--input key=value`, and `--follow` for log tailing - Add `tea actions workflows view` to show workflow details - Add `tea actions workflows enable` and `disable` to toggle workflow state - Rewrite `workflows list` to use the Workflow API instead of file listing - Remove dead `WorkflowsList` print function that used `ContentsResponse` - Update `CLI.md` and `example-workflows.md` with usage documentation and examples ## Motivation Enable re-triggering specific workflows from the CLI, which is essential for AI-driven PR flows where a specific workflow needs to be re-run after pushing changes. Leverages the 5 workflow API endpoints already supported by the Go SDK (v0.24.1) from go-gitea/gitea#33545: - `ListRepoActionWorkflows` - `GetRepoActionWorkflow` - `DispatchRepoActionWorkflow` (with `returnRunDetails` support) - `EnableRepoActionWorkflow` - `DisableRepoActionWorkflow` ## New commands \`\`\` tea actions workflows ├── list (rewritten to use Workflow API) ├── view <id> (new) ├── dispatch <id> (new) ├── enable <id> (new) └── disable <id> (new) \`\`\` ### Usage examples \`\`\`bash # Dispatch workflow on current branch tea actions workflows dispatch deploy.yml # Dispatch with specific ref and inputs tea actions workflows dispatch deploy.yml --ref main --input env=staging --input version=1.2.3 # Dispatch and follow logs tea actions workflows dispatch ci.yml --ref feature/my-pr --follow # View workflow details tea actions workflows view deploy.yml # Enable/disable workflows tea actions workflows enable deploy.yml tea actions workflows disable deploy.yml --confirm \`\`\` ## Test plan - [x] `go build ./...` passes - [x] `go test ./...` passes - [x] `go vet ./...` passes - [x] `make lint` — 0 issues - [x] `make docs-check` — CLI.md is up to date - [x] Manual test: `tea actions workflows list` shows workflows from API - [x] Manual test: `tea actions workflows dispatch <workflow> --ref main` triggers a run - [x] Manual test: `tea actions workflows view <workflow>` shows details --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-on: https://gitea.com/gitea/tea/pulls/952 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com> Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
@@ -20,6 +20,10 @@ var CmdActionsWorkflows = cli.Command{
|
|||||||
Action: runWorkflowsDefault,
|
Action: runWorkflowsDefault,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
&workflows.CmdWorkflowsList,
|
&workflows.CmdWorkflowsList,
|
||||||
|
&workflows.CmdWorkflowsView,
|
||||||
|
&workflows.CmdWorkflowsDispatch,
|
||||||
|
&workflows.CmdWorkflowsEnable,
|
||||||
|
&workflows.CmdWorkflowsDisable,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
cmd/actions/workflows/disable.go
Normal file
65
cmd/actions/workflows/disable.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDisable represents a sub command to disable a workflow
|
||||||
|
var CmdWorkflowsDisable = cli.Command{
|
||||||
|
Name: "disable",
|
||||||
|
Usage: "Disable a workflow",
|
||||||
|
Description: "Disable a workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDisable,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Aliases: []string{"y"},
|
||||||
|
Usage: "confirm disable without prompting",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDisable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
if !cmd.Bool("confirm") {
|
||||||
|
fmt.Printf("Are you sure you want to disable workflow %s? [y/N] ", workflowID)
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
if response != "y" && response != "Y" && response != "yes" {
|
||||||
|
fmt.Println("Disable canceled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.DisableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to disable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s disabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
174
cmd/actions/workflows/dispatch.go
Normal file
174
cmd/actions/workflows/dispatch.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsDispatch represents a sub command to dispatch a workflow
|
||||||
|
var CmdWorkflowsDispatch = cli.Command{
|
||||||
|
Name: "dispatch",
|
||||||
|
Aliases: []string{"trigger", "run"},
|
||||||
|
Usage: "Dispatch a workflow run",
|
||||||
|
Description: "Trigger a workflow_dispatch event for a workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsDispatch,
|
||||||
|
Flags: append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "ref",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "branch or tag to dispatch on (default: current branch)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "input",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "workflow input in key=value format (can be specified multiple times)",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "follow",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "follow log output after dispatching",
|
||||||
|
},
|
||||||
|
}, flags.AllDefaultFlags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsDispatch(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
|
||||||
|
ref := cmd.String("ref")
|
||||||
|
if ref == "" {
|
||||||
|
if c.LocalRepo != nil {
|
||||||
|
branchName, _, localErr := c.LocalRepo.TeaGetCurrentBranchNameAndSHA()
|
||||||
|
if localErr == nil && branchName != "" {
|
||||||
|
ref = branchName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref == "" {
|
||||||
|
return fmt.Errorf("--ref is required (no local branch detected)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make(map[string]string)
|
||||||
|
for _, input := range cmd.StringSlice("input") {
|
||||||
|
key, value, ok := strings.Cut(input, "=")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid input format %q, expected key=value", input)
|
||||||
|
}
|
||||||
|
inputs[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := gitea.CreateActionWorkflowDispatchOption{
|
||||||
|
Ref: ref,
|
||||||
|
Inputs: inputs,
|
||||||
|
}
|
||||||
|
|
||||||
|
details, _, err := client.DispatchRepoActionWorkflow(c.Owner, c.Repo, workflowID, opt, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to dispatch workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDispatchResult(details)
|
||||||
|
|
||||||
|
if cmd.Bool("follow") && details != nil && details.WorkflowRunID > 0 {
|
||||||
|
return followDispatchedRun(client, c, details.WorkflowRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
followPollInterval = 2 * time.Second
|
||||||
|
followMaxDuration = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// followDispatchedRun waits for the dispatched run to start, then follows its logs
|
||||||
|
func followDispatchedRun(client *gitea.Client, c *context.TeaContext, runID int64) error {
|
||||||
|
fmt.Printf("\nWaiting for run %d to start...\n", runID)
|
||||||
|
|
||||||
|
var jobs *gitea.ActionWorkflowJobsResponse
|
||||||
|
for range 30 {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
jobs, _, err = client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get jobs: %w", err)
|
||||||
|
}
|
||||||
|
if len(jobs.Jobs) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||||
|
return fmt.Errorf("timed out waiting for jobs to appear")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := jobs.Jobs[0].ID
|
||||||
|
jobName := jobs.Jobs[0].Name
|
||||||
|
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
|
||||||
|
fmt.Println("---")
|
||||||
|
|
||||||
|
deadline := time.Now().Add(followMaxDuration)
|
||||||
|
var lastLogLength int
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
job, _, err := client.GetRepoActionJob(c.Owner, c.Repo, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
|
||||||
|
|
||||||
|
logs, _, logErr := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||||
|
if logErr != nil && isRunning {
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if logErr == nil && len(logs) > lastLogLength {
|
||||||
|
fmt.Print(string(logs[lastLogLength:]))
|
||||||
|
lastLogLength = len(logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRunning {
|
||||||
|
if logErr != nil {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s (failed to fetch final logs: %v)\n", job.Status, logErr)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(followPollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return fmt.Errorf("timed out after %s following logs", followMaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
cmd/actions/workflows/enable.go
Normal file
48
cmd/actions/workflows/enable.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsEnable represents a sub command to enable a workflow
|
||||||
|
var CmdWorkflowsEnable = cli.Command{
|
||||||
|
Name: "enable",
|
||||||
|
Usage: "Enable a workflow",
|
||||||
|
Description: "Enable a disabled workflow in the repository",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsEnable,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsEnable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
_, err = client.EnableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Workflow %s enabled successfully\n", workflowID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ package workflows
|
|||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
@@ -22,15 +20,12 @@ var CmdWorkflowsList = cli.Command{
|
|||||||
Name: "list",
|
Name: "list",
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Usage: "List repository workflows",
|
Usage: "List repository workflows",
|
||||||
Description: "List workflow files in the repository with active/inactive status",
|
Description: "List workflows in the repository with their status",
|
||||||
Action: RunWorkflowsList,
|
Action: RunWorkflowsList,
|
||||||
Flags: append([]cli.Flag{
|
Flags: flags.AllDefaultFlags,
|
||||||
&flags.PaginationPageFlag,
|
|
||||||
&flags.PaginationLimitFlag,
|
|
||||||
}, flags.AllDefaultFlags...),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunWorkflowsList lists workflow files in the repository
|
// RunWorkflowsList lists workflows in the repository using the workflow API
|
||||||
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
c, err := context.InitCommand(cmd)
|
c, err := context.InitCommand(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,51 +36,15 @@ func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
client := c.Login.Client()
|
client := c.Login.Client()
|
||||||
|
|
||||||
// Try to list workflow files from .gitea/workflows directory
|
resp, _, err := client.ListRepoActionWorkflows(c.Owner, c.Repo)
|
||||||
var workflows []*gitea.ContentsResponse
|
|
||||||
|
|
||||||
// Try .gitea/workflows first, then .github/workflows
|
|
||||||
workflowDir := ".gitea/workflows"
|
|
||||||
contents, _, err := client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
workflowDir = ".github/workflows"
|
return fmt.Errorf("failed to list workflows: %w", err)
|
||||||
contents, _, err = client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("No workflow files found\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for workflow files (.yml and .yaml)
|
var workflows []*gitea.ActionWorkflow
|
||||||
for _, content := range contents {
|
if resp != nil {
|
||||||
if content.Type == "file" {
|
workflows = resp.Workflows
|
||||||
ext := strings.ToLower(filepath.Ext(content.Name))
|
|
||||||
if ext == ".yml" || ext == ".yaml" {
|
|
||||||
content.Path = workflowDir + "/" + content.Name
|
|
||||||
workflows = append(workflows, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(workflows) == 0 {
|
return print.ActionWorkflowsList(workflows, c.Output)
|
||||||
fmt.Printf("No workflow files found\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which workflows have runs to determine active status
|
|
||||||
workflowStatus := make(map[string]bool)
|
|
||||||
|
|
||||||
// Get recent runs to check activity
|
|
||||||
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
|
|
||||||
ListOptions: flags.GetListOptions(cmd),
|
|
||||||
})
|
|
||||||
if err == nil && runs != nil {
|
|
||||||
for _, run := range runs.WorkflowRuns {
|
|
||||||
// Extract workflow file name from path
|
|
||||||
workflowFile := filepath.Base(run.Path)
|
|
||||||
workflowStatus[workflowFile] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return print.WorkflowsList(workflows, workflowStatus, c.Output)
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
cmd/actions/workflows/view.go
Normal file
50
cmd/actions/workflows/view.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package workflows
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdWorkflowsView represents a sub command to view workflow details
|
||||||
|
var CmdWorkflowsView = cli.Command{
|
||||||
|
Name: "view",
|
||||||
|
Aliases: []string{"show", "get"},
|
||||||
|
Usage: "View workflow details",
|
||||||
|
Description: "View details of a specific workflow",
|
||||||
|
ArgsUsage: "<workflow-id>",
|
||||||
|
Action: runWorkflowsView,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkflowsView(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
if cmd.Args().Len() == 0 {
|
||||||
|
return fmt.Errorf("workflow ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := c.Login.Client()
|
||||||
|
|
||||||
|
workflowID := cmd.Args().First()
|
||||||
|
wf, _, err := client.GetRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get workflow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.ActionWorkflowDetails(wf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
docs/CLI.md
56
docs/CLI.md
@@ -1573,13 +1573,65 @@ Manage repository workflows
|
|||||||
|
|
||||||
List repository workflows
|
List repository workflows
|
||||||
|
|
||||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
**--login, -l**="": Use a different Gitea Login. Optional
|
||||||
|
|
||||||
|
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||||
|
|
||||||
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
#### view, show, get
|
||||||
|
|
||||||
|
View workflow details
|
||||||
|
|
||||||
**--login, -l**="": Use a different Gitea Login. Optional
|
**--login, -l**="": Use a different Gitea Login. Optional
|
||||||
|
|
||||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||||
|
|
||||||
**--page, -p**="": specify page (default: 1)
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
#### dispatch, trigger, run
|
||||||
|
|
||||||
|
Dispatch a workflow run
|
||||||
|
|
||||||
|
**--follow, -f**: follow log output after dispatching
|
||||||
|
|
||||||
|
**--input, -i**="": workflow input in key=value format (can be specified multiple times)
|
||||||
|
|
||||||
|
**--login, -l**="": Use a different Gitea Login. Optional
|
||||||
|
|
||||||
|
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||||
|
|
||||||
|
**--ref, -r**="": branch or tag to dispatch on (default: current branch)
|
||||||
|
|
||||||
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
#### enable
|
||||||
|
|
||||||
|
Enable a workflow
|
||||||
|
|
||||||
|
**--login, -l**="": Use a different Gitea Login. Optional
|
||||||
|
|
||||||
|
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||||
|
|
||||||
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
#### disable
|
||||||
|
|
||||||
|
Disable a workflow
|
||||||
|
|
||||||
|
**--confirm, -y**: confirm disable without prompting
|
||||||
|
|
||||||
|
**--login, -l**="": Use a different Gitea Login. Optional
|
||||||
|
|
||||||
|
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||||
|
|
||||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,93 @@
|
|||||||
# Gitea actions workflows
|
# Gitea actions workflows
|
||||||
|
|
||||||
|
## Workflow management with tea
|
||||||
|
|
||||||
|
### List workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all workflows in the repository
|
||||||
|
tea actions workflows list
|
||||||
|
```
|
||||||
|
|
||||||
|
### View workflow details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View details of a specific workflow by ID or filename
|
||||||
|
tea actions workflows view deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dispatch (trigger) a workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dispatch a workflow on the current branch
|
||||||
|
tea actions workflows dispatch deploy.yml
|
||||||
|
|
||||||
|
# Dispatch on a specific branch
|
||||||
|
tea actions workflows dispatch deploy.yml --ref main
|
||||||
|
|
||||||
|
# Dispatch with workflow inputs
|
||||||
|
tea actions workflows dispatch deploy.yml --ref main --input env=staging --input version=1.2.3
|
||||||
|
|
||||||
|
# Dispatch and follow log output
|
||||||
|
tea actions workflows dispatch ci.yml --ref feature/my-pr --follow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable / disable workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Disable a workflow
|
||||||
|
tea actions workflows disable deploy.yml --confirm
|
||||||
|
|
||||||
|
# Enable a workflow
|
||||||
|
tea actions workflows enable deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Re-trigger CI from an AI-driven PR flow
|
||||||
|
|
||||||
|
Use `tea actions workflows dispatch` to re-run a specific workflow after
|
||||||
|
pushing changes in an automated PR workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push changes to a feature branch, then re-trigger CI
|
||||||
|
git push origin feature/auto-fix
|
||||||
|
tea actions workflows dispatch check-and-test --ref feature/auto-fix --follow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Dispatch a workflow with `workflow_dispatch` trigger
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
env:
|
||||||
|
description: "Target environment"
|
||||||
|
required: true
|
||||||
|
default: "staging"
|
||||||
|
version:
|
||||||
|
description: "Version to deploy"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
echo "Deploying version ${{ gitea.event.inputs.version }} to ${{ gitea.event.inputs.env }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Trigger this workflow from the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea actions workflows dispatch deploy.yml --ref main --input env=production --input version=2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
## Merge Pull request on approval
|
## Merge Pull request on approval
|
||||||
|
|
||||||
``` Yaml
|
```yaml
|
||||||
---
|
---
|
||||||
name: Pull request
|
name: Pull request
|
||||||
on:
|
on:
|
||||||
|
|||||||
@@ -154,27 +154,23 @@ func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) erro
|
|||||||
return t.print(output)
|
return t.print(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkflowsList prints a list of workflow files with active status
|
// ActionWorkflowsList prints a list of workflows from the workflow API
|
||||||
func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]bool, output string) error {
|
func ActionWorkflowsList(workflows []*gitea.ActionWorkflow, output string) error {
|
||||||
t := table{
|
t := table{
|
||||||
headers: []string{
|
headers: []string{
|
||||||
"Active",
|
"ID",
|
||||||
"Name",
|
"Name",
|
||||||
"Path",
|
"Path",
|
||||||
|
"State",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
machineReadable := isMachineReadable(output)
|
for _, wf := range workflows {
|
||||||
|
|
||||||
for _, workflow := range workflows {
|
|
||||||
// Check if this workflow file is active (has runs)
|
|
||||||
isActive := activeStatus[workflow.Name]
|
|
||||||
activeIndicator := formatBoolean(isActive, !machineReadable)
|
|
||||||
|
|
||||||
t.addRow(
|
t.addRow(
|
||||||
activeIndicator,
|
wf.ID,
|
||||||
workflow.Name,
|
wf.Name,
|
||||||
workflow.Path,
|
wf.Path,
|
||||||
|
wf.State,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,3 +182,34 @@ func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]
|
|||||||
t.sort(1, true) // Sort by name column
|
t.sort(1, true) // Sort by name column
|
||||||
return t.print(output)
|
return t.print(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionWorkflowDetails prints detailed information about a workflow
|
||||||
|
func ActionWorkflowDetails(wf *gitea.ActionWorkflow) {
|
||||||
|
fmt.Printf("ID: %s\n", wf.ID)
|
||||||
|
fmt.Printf("Name: %s\n", wf.Name)
|
||||||
|
fmt.Printf("Path: %s\n", wf.Path)
|
||||||
|
fmt.Printf("State: %s\n", wf.State)
|
||||||
|
if wf.HTMLURL != "" {
|
||||||
|
fmt.Printf("URL: %s\n", wf.HTMLURL)
|
||||||
|
}
|
||||||
|
if wf.BadgeURL != "" {
|
||||||
|
fmt.Printf("Badge: %s\n", wf.BadgeURL)
|
||||||
|
}
|
||||||
|
if !wf.CreatedAt.IsZero() {
|
||||||
|
fmt.Printf("Created: %s\n", FormatTime(wf.CreatedAt, false))
|
||||||
|
}
|
||||||
|
if !wf.UpdatedAt.IsZero() {
|
||||||
|
fmt.Printf("Updated: %s\n", FormatTime(wf.UpdatedAt, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionWorkflowDispatchResult prints the result of a workflow dispatch
|
||||||
|
func ActionWorkflowDispatchResult(details *gitea.RunDetails) {
|
||||||
|
fmt.Printf("Workflow dispatched successfully\n")
|
||||||
|
if details != nil {
|
||||||
|
fmt.Printf("Run ID: %d\n", details.WorkflowRunID)
|
||||||
|
if details.HTMLURL != "" {
|
||||||
|
fmt.Printf("URL: %s\n", details.HTMLURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,6 +123,87 @@ func TestActionWorkflowJobsListWithData(t *testing.T) {
|
|||||||
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionWorkflowsListEmpty(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("ActionWorkflowsList panicked with empty list: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.NoError(t, ActionWorkflowsList([]*gitea.ActionWorkflow{}, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionWorkflowsListWithData(t *testing.T) {
|
||||||
|
workflows := []*gitea.ActionWorkflow{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Name: "CI",
|
||||||
|
Path: ".gitea/workflows/ci.yml",
|
||||||
|
State: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Name: "Deploy",
|
||||||
|
Path: ".gitea/workflows/deploy.yml",
|
||||||
|
State: "disabled_manually",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("ActionWorkflowsList panicked with data: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.NoError(t, ActionWorkflowsList(workflows, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionWorkflowDetails(t *testing.T) {
|
||||||
|
wf := &gitea.ActionWorkflow{
|
||||||
|
ID: "1",
|
||||||
|
Name: "CI Pipeline",
|
||||||
|
Path: ".gitea/workflows/ci.yml",
|
||||||
|
State: "active",
|
||||||
|
HTMLURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml",
|
||||||
|
BadgeURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml/badge.svg",
|
||||||
|
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||||
|
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("ActionWorkflowDetails panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ActionWorkflowDetails(wf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionWorkflowDispatchResult(t *testing.T) {
|
||||||
|
details := &gitea.RunDetails{
|
||||||
|
WorkflowRunID: 42,
|
||||||
|
HTMLURL: "https://gitea.example.com/owner/repo/actions/runs/42",
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("ActionWorkflowDispatchResult panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ActionWorkflowDispatchResult(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActionWorkflowDispatchResultNil(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("ActionWorkflowDispatchResult panicked with nil: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ActionWorkflowDispatchResult(nil)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatDurationMinutes(t *testing.T) {
|
func TestFormatDurationMinutes(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user