gitea.tea/cmd/pulls/trigger.go
Ross Golder d50c9f75dc
feat(pulls): add CI/CD pipeline management commands
Implement comprehensive CI/CD integration for tea CLI including:

- tea pulls status: Show detailed CI/CD pipeline status with watch mode
- tea pulls wait: Wait for CI/CD checks to complete with timeout
- tea pulls checks: List all checks with filtering and format options
- tea pulls trigger: Trigger or re-trigger CI/CD builds
- tea pulls retry: Retry failed CI/CD checks
- tea pulls cicd: Parent command for all CI/CD operations
- tea pulls cicd pipeline: Pipeline management with logs/summary/timeline

This implementation provides CI/CD visibility and control directly from the
tea CLI, enabling developers to monitor builds, trigger re-runs, and wait
for check completion without leaving the terminal.
2026-09-05 09:52:25 +07:00

222 lines
6.4 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdPullsTrigger represents a sub command to trigger CI/CD builds
var CmdPullsTrigger = cli.Command{
Name: "trigger",
Aliases: []string{"build"},
Usage: "Trigger CI/CD build for pull request",
Description: `Trigger or re-trigger CI/CD pipeline for a pull request`,
ArgsUsage: "<pull index>",
Action: RunPullsTrigger,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "context",
Usage: "Specific CI context to trigger (optional)",
},
&cli.BoolFlag{
Name: "wait",
Usage: "Wait for the triggered build to complete",
},
&cli.IntFlag{
Name: "timeout",
Usage: "Timeout for waiting in seconds",
Value: 1800, // 30 minutes default
},
&cli.BoolFlag{
Name: "force",
Usage: "Force trigger even if checks are already running",
},
},
}
// CmdPullsRetry represents a sub command to retry failed CI/CD checks
var CmdPullsRetry = cli.Command{
Name: "retry",
Usage: "Retry failed CI/CD checks for pull request",
Description: `Retry all failed CI/CD checks for a pull request`,
ArgsUsage: "<pull index>",
Action: RunPullsRetry,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "context",
Usage: "Specific failed check context to retry (optional)",
},
&cli.BoolFlag{
Name: "wait",
Usage: "Wait for the retried checks to complete",
},
&cli.IntFlag{
Name: "timeout",
Usage: "Timeout for waiting in seconds",
Value: 1800,
},
},
}
// RunPullsTrigger triggers CI/CD build for a PR
func RunPullsTrigger(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if cmd.Args().Len() != 1 {
return fmt.Errorf("must specify pull request index")
}
idx, err := utils.ArgToIndex(cmd.Args().First())
if err != nil {
return err
}
client := ctx.Login.Client()
pr, _, err := client.PullRequests.GetPullRequest(requestCtx, ctx.Owner, ctx.Repo, idx)
if err != nil {
return fmt.Errorf("could not get pull request: %w", err)
}
return triggerBuild(requestCtx, client, ctx, pr, cmd.String("context"), cmd.Bool("force"), cmd.Bool("wait"), time.Duration(cmd.Int("timeout"))*time.Second)
}
// RunPullsRetry retries failed CI/CD checks for a PR
func RunPullsRetry(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if cmd.Args().Len() != 1 {
return fmt.Errorf("must specify pull request index")
}
idx, err := utils.ArgToIndex(cmd.Args().First())
if err != nil {
return err
}
client := ctx.Login.Client()
pr, _, err := client.PullRequests.GetPullRequest(requestCtx, ctx.Owner, ctx.Repo, idx)
if err != nil {
return fmt.Errorf("could not get pull request: %w", err)
}
return retryFailedChecks(requestCtx, client, ctx, pr, cmd.String("context"), cmd.Bool("wait"), time.Duration(cmd.Int("timeout"))*time.Second)
}
// triggerBuild triggers a CI/CD build for the PR
func triggerBuild(requestCtx stdctx.Context, client *gitea.Client, ctx *context.TeaContext, pr *gitea.PullRequest, triggerContext string, force, wait bool, timeout time.Duration) error {
fmt.Printf("Triggering CI/CD build for PR #%d: %s\n", pr.Index, pr.Title)
if !force {
combinedStatus, _, err := client.Repositories.GetCombinedStatus(requestCtx, ctx.Owner, ctx.Repo, pr.Head.Sha)
if err == nil && combinedStatus.State == gitea.StatusPending {
fmt.Println("Checks are already running. Use --force to trigger anyway.")
return nil
}
}
statusOpt := gitea.CreateStatusOption{
State: gitea.StatusPending,
Context: triggerContext,
Description: "Manually triggered build",
TargetURL: "",
}
if triggerContext == "" {
statusOpt.Context = "tea/manual-trigger"
}
_, _, err := client.Repositories.CreateStatus(requestCtx, ctx.Owner, ctx.Repo, pr.Head.Sha, statusOpt)
if err != nil {
return fmt.Errorf("could not trigger build: %w", err)
}
fmt.Printf("Build triggered for commit %s\n", pr.Head.Sha[:8])
if triggerContext != "" {
fmt.Printf(" Context: %s\n", triggerContext)
}
if wait {
fmt.Println("\nWaiting for build to complete...")
return waitForChecks(requestCtx, client, ctx, pr, timeout, 30*time.Second, false, true)
}
return nil
}
// retryFailedChecks retries all failed checks or a specific failed check
func retryFailedChecks(requestCtx stdctx.Context, client *gitea.Client, ctx *context.TeaContext, pr *gitea.PullRequest, triggerContext string, wait bool, timeout time.Duration) error {
fmt.Printf("Retrying failed checks for PR #%d: %s\n", pr.Index, pr.Title)
statuses, _, err := client.Repositories.ListStatuses(requestCtx, ctx.Owner, ctx.Repo, pr.Head.Sha, gitea.ListStatusesOption{
ListOptions: gitea.ListOptions{Page: -1},
})
if err != nil {
return fmt.Errorf("could not get current statuses: %w", err)
}
var failedContexts []string
for _, status := range statuses {
if status.State == gitea.StatusError || status.State == gitea.StatusFailure {
if triggerContext == "" || status.Context == triggerContext {
failedContexts = append(failedContexts, status.Context)
}
}
}
if len(failedContexts) == 0 {
if triggerContext != "" {
fmt.Printf("No failed check found with context: %s\n", triggerContext)
} else {
fmt.Println("No failed checks found to retry")
}
return nil
}
for _, failedContext := range failedContexts {
statusOpt := gitea.CreateStatusOption{
State: gitea.StatusPending,
Context: failedContext,
Description: "Retrying failed check",
TargetURL: "",
}
_, _, err := client.Repositories.CreateStatus(requestCtx, ctx.Owner, ctx.Repo, pr.Head.Sha, statusOpt)
if err != nil {
fmt.Printf("Could not retry check %s: %v\n", failedContext, err)
continue
}
fmt.Printf("Retrying check: %s\n", failedContext)
}
fmt.Printf("Triggered retry for %d failed check(s)\n", len(failedContexts))
if wait {
fmt.Println("\nWaiting for retried checks to complete...")
return waitForChecks(requestCtx, client, ctx, pr, timeout, 30*time.Second, false, true)
}
return nil
}