mirror of
https://gitea.com/gitea/tea.git
synced 2026-09-13 08:56:35 -04:00
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.
222 lines
6.4 KiB
Go
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
|
|
}
|