新增:流水线接口代码

This commit is contained in:
yystopf 2024-03-04 11:21:57 +08:00
parent 520306d632
commit 0333de46ce
2 changed files with 466 additions and 0 deletions

View File

@ -29,6 +29,7 @@ import (
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/admin"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/org"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/repo/actions"
"code.gitlink.org.cn/Gitlink/gitea_hat.git/routers/hat/user"
"github.com/go-chi/cors"
)
@ -122,6 +123,12 @@ func Routers() *web.Route {
m.Post("/create_pr_version", bind(gitea_api.PullRequestPayload{}), repo.CreatePrVersion)
m.Group("/repos", func() {
m.Group("/{username}/{reponame}", func() {
m.Group("/actions", func() {
m.Get("", context.ReferencesGitRepo(), actions.ListActions)
m.Post("/disable", reqAdmin(), actions.DisableWorkflowFile)
m.Post("/enable", reqAdmin(), actions.EnableWorkflowFile)
m.Post("/runs/{run}/jobs/{job}", context.ReferencesGitRepo(), bind(actions.ViewRequest{}), actions.ListJobs)
})
m.Post("/transfer", reqOwner(), bind(gitea_api.TransferRepoOption{}), repo.Transfer)
m.Get("/branch_name_set", context.ReferencesGitRepo(), repo.BranchNameSet)
m.Get("/tag_name_set", context.ReferencesGitRepo(), repo.TagNameSet)

View File

@ -0,0 +1,459 @@
package actions
import (
"bytes"
"errors"
"fmt"
"net/http"
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model"
)
type Workflow struct {
Name string
ErrMsg string
}
type ResponseAction struct {
Workflows []Workflow
CurWorkflow string
ActionsConfig *repo_model.ActionsConfig
AllowDisableOrEnableWorkflow bool
CurWorkflowDisabled bool
CurActor int64
CurStatus int
IsFiltered bool
Runs actions_model.RunList
Actors []*user.User
StatusInfoList []actions_model.StatusInfo
}
func ListActions(ctx *context.APIContext) {
workflow := ctx.FormString("workflow")
actorID := ctx.FormInt64("actor")
status := ctx.FormInt("status")
var workflows []Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
ctx.Error(http.StatusInternalServerError, "IsEmpty", err.Error())
return
} else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err.Error())
return
}
entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListWorkflows", err.Error())
return
}
opts := actions_model.FindRunnerOptions{
RepoID: ctx.Repo.Repository.ID,
WithAvailable: true,
}
runners, err := actions_model.FindRunners(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindRunners", err.Error())
}
allRunnerLabels := make(container.Set[string])
for _, r := range runners {
allRunnerLabels.AddMultiple(r.AgentLabels...)
}
workflows = make([]Workflow, 0, len(entries))
for _, entry := range entries {
workflow := Workflow{Name: entry.Name()}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetContentFromEntry", err.Error())
return
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
// Check whether have matching runner
for _, j := range wf.Jobs {
runsOnList := j.RunsOn()
for _, ro := range runsOnList {
if strings.Contains(ro, "${{") {
// Skip if it contains expressions.
// The expressions could be very complex and could not be evaluated here,
// so just skip it, it's OK since it's just a tooltip message.
continue
}
if !allRunnerLabels.Contains(ro) {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_runner_helper", ro)
break
}
}
if workflow.ErrMsg != "" {
break
}
}
workflows = append(workflows, workflow)
}
}
responseAction := ResponseAction{}
responseAction.Workflows = workflows
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
responseAction.CurWorkflow = workflow
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
responseAction.ActionsConfig = actionsConfig
if len(workflow) > 0 && ctx.Repo.IsAdmin() {
responseAction.AllowDisableOrEnableWorkflow = true
responseAction.CurWorkflowDisabled = actionsConfig.IsWorkflowDisabled(workflow)
}
responseAction.CurActor = actorID
responseAction.CurStatus = status
if actorID > 0 && status > int(actions_model.StatusUnknown) {
responseAction.IsFiltered = true
}
opts := actions_model.FindRunOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
},
RepoID: ctx.Repo.Repository.ID,
WorkflowID: workflow,
TriggerUserID: actorID,
}
if actions_model.Status(status) != actions_model.StatusUnknown {
opts.Status = []actions_model.Status{actions_model.Status(status)}
}
runs, total, err := actions_model.FindRuns(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindRuns", err.Error())
return
}
for _, run := range runs {
run.Repo = ctx.Repo.Repository
}
if err := runs.LoadTriggerUser(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadTriggerUser", err.Error())
return
}
responseAction.Runs = runs
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetActors", err.Error())
return
}
responseAction.Actors = repo.MakeSelfOnTop(ctx.Doer, actors)
responseAction.StatusInfoList = actions_model.GetStatusInfoList(ctx)
ctx.SetLinkHeader(int(total), ctx.FormInt("limit"))
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, responseAction)
}
type ViewRequest struct {
LogCursors []struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
Expanded bool `json:"expanded"`
} `json:"logCursors"`
}
type ViewResponse struct {
State struct {
Run struct {
Link string `json:"link"`
Title string `json:"title"`
Status string `json:"status"`
CanCancel bool `json:"canCancel"`
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
CanRerun bool `json:"canRerun"`
Done bool `json:"done"`
Jobs []*ViewJob `json:"jobs"`
Commit ViewCommit `json:"commit"`
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"`
} `json:"currentJob"`
} `json:"state"`
Logs struct {
StepsLog []*ViewStepLog `json:"stepsLog"`
} `json:"logs"`
}
type ViewJob struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
CanRerun bool `json:"canRerun"`
Duration string `json:"duration"`
}
type ViewCommit struct {
LocaleCommit string `json:"localeCommit"`
LocalePushedBy string `json:"localePushedBy"`
ShortSha string `json:"shortSHA"`
Link string `json:"link"`
Pusher ViewUser `json:"pusher"`
Branch ViewBranch `json:"branch"`
}
type ViewUser struct {
DisplayName string `json:"displayName"`
Link string `json:"link"`
}
type ViewBranch struct {
Name string `json:"name"`
Link string `json:"link"`
}
type ViewJobStep struct {
Summary string `json:"summary"`
Duration string `json:"duration"`
Status string `json:"status"`
}
type ViewStepLog struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
Lines []*ViewStepLogLine `json:"lines"`
Started int64 `json:"started"`
}
type ViewStepLogLine struct {
Index int64 `json:"index"`
Message string `json:"message"`
Timestamp float64 `json:"timestamp"`
}
func ListJobs(ctx *context.APIContext) {
req := web.GetForm(ctx).(*ViewRequest)
runIndex := ctx.ParamsInt64("run")
jobIndex := ctx.ParamsInt64("job")
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
if ctx.Written() {
return
}
run := current.Run
if err := run.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err.Error())
return
}
resp := &ViewResponse{}
resp.State.Run.Title = run.Title
resp.State.Run.Link = run.Link()
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
resp.State.Run.Status = run.Status.String()
for _, v := range jobs {
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
ID: v.ID,
Name: v.Name,
Status: v.Status.String(),
CanRerun: v.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions),
Duration: v.Duration().String(),
})
}
pusher := ViewUser{
DisplayName: run.TriggerUser.GetDisplayName(),
Link: run.TriggerUser.HomeLink(),
}
branch := ViewBranch{
Name: run.PrettyRef(),
Link: run.RefLink(),
}
resp.State.Run.Commit = ViewCommit{
LocaleCommit: ctx.Tr("actions.runs.commit"),
LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher,
Branch: branch,
}
var task *actions_model.ActionTask
if current.TaskID > 0 {
var err error
task, err = actions_model.GetTaskByID(ctx, current.TaskID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTaskByID", err.Error())
return
}
task.Job = current
if err := task.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err.Error())
return
}
}
resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
if run.NeedApproval {
resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
}
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
if task != nil {
steps := actions.FullSteps(task)
for _, v := range steps {
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
Summary: v.Name,
Duration: v.Duration().String(),
Status: v.Status.String(),
})
}
for _, cursor := range req.LogCursors {
if !cursor.Expanded {
continue
}
step := steps[cursor.Step]
logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
index := step.LogIndex + cursor.Cursor
validCursor := cursor.Cursor >= 0 &&
// !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
// So return the same cursor and empty lines to let the frontend retry.
cursor.Cursor < step.LogLength &&
// !(index < task.LogIndexes[index]) when task data is older than step data.
// It can be fixed by making sure write/read tasks and steps in the same transaction,
// but it's easier to just treat it as fetching the next line before it's ready.
index < int64(len(task.LogIndexes))
if validCursor {
length := step.LogLength - cursor.Cursor
offset := task.LogIndexes[index]
var err error
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ReadLogs", err.Error())
return
}
for i, row := range logRows {
logLines = append(logLines, &ViewStepLogLine{
Index: cursor.Cursor + int64(i) + 1, // start at 1
Message: row.Content,
Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
})
}
}
resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
Step: cursor.Step,
Cursor: cursor.Cursor + int64(len(logLines)),
Lines: logLines,
Started: int64(step.Started),
})
}
}
ctx.JSON(http.StatusOK, resp)
}
func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetRunByIndex", err.Error())
return nil, nil
}
ctx.Error(http.StatusInternalServerError, "GetRunByIndex", err.Error())
return nil, nil
}
run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetRunJobsByRunID", err.Error())
return nil, nil
}
if len(jobs) == 0 {
ctx.Error(http.StatusNotFound, "", err.Error())
return nil, nil
}
for _, v := range jobs {
v.Run = run
}
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
return jobs[jobIndex], jobs
}
return jobs[0], jobs
}
func DisableWorkflowFile(ctx *context.APIContext) {
disableOrEnableWorkflowFile(ctx, false)
}
func EnableWorkflowFile(ctx *context.APIContext) {
disableOrEnableWorkflowFile(ctx, true)
}
func disableOrEnableWorkflowFile(ctx *context.APIContext, isEnable bool) {
workflow := ctx.FormString("workflow")
if len(workflow) == 0 {
ctx.ServerError("workflow", nil)
return
}
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if isEnable {
cfg.EnableWorkflow(workflow)
} else {
cfg.DisableWorkflow(workflow)
}
if err := repo_model.UpdateRepoUnit(cfgUnit); err != nil {
ctx.ServerError("UpdateRepoUnit", err)
return
}
ctx.Status(http.StatusNoContent)
}