forked from gitea/gitea
		
	Currently, Gitea will run actions automatically which are triggered by fork pull request. It's a security risk, people can create a PR and modify the workflow yamls to execute a malicious script. So we should require approval for first-time contributors, which is the default strategy of a public repo on GitHub, see [Approving workflow runs from public forks](https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks). Current strategy: - don't need approval if it's not a fork PR; - always need approval if the user is restricted; - don't need approval if the user can write; - don't need approval if the user has been approved before; - otherwise, need approval. https://user-images.githubusercontent.com/9418365/217207121-badf50a8-826c-4425-bef1-d82d1979bc81.mov GitHub has an option for that, you can see that at `/<owner>/<repo>/settings/actions`, and we can support that later. <img width="835" alt="image" src="https://user-images.githubusercontent.com/9418365/217199990-2967e68b-e693-4e59-8186-ab33a1314a16.png"> --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
		
			
				
	
	
		
			337 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2022 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package actions
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"time"
 | |
| 
 | |
| 	actions_model "code.gitea.io/gitea/models/actions"
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/unit"
 | |
| 	"code.gitea.io/gitea/modules/actions"
 | |
| 	context_module "code.gitea.io/gitea/modules/context"
 | |
| 	"code.gitea.io/gitea/modules/timeutil"
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| 	"code.gitea.io/gitea/modules/web"
 | |
| 	actions_service "code.gitea.io/gitea/services/actions"
 | |
| 
 | |
| 	"xorm.io/builder"
 | |
| )
 | |
| 
 | |
| func View(ctx *context_module.Context) {
 | |
| 	ctx.Data["PageIsActions"] = true
 | |
| 	runIndex := ctx.ParamsInt64("run")
 | |
| 	jobIndex := ctx.ParamsInt64("job")
 | |
| 	ctx.Data["RunIndex"] = runIndex
 | |
| 	ctx.Data["JobIndex"] = jobIndex
 | |
| 	ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
 | |
| 
 | |
| 	if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ctx.HTML(http.StatusOK, tplViewActions)
 | |
| }
 | |
| 
 | |
| 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"`
 | |
| 			CanCancel  bool       `json:"canCancel"`
 | |
| 			CanApprove bool       `json:"canApprove"` // the run needs an approval and the doer has permission to approve
 | |
| 			Done       bool       `json:"done"`
 | |
| 			Jobs       []*ViewJob `json:"jobs"`
 | |
| 		} `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"`
 | |
| }
 | |
| 
 | |
| 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"`
 | |
| }
 | |
| 
 | |
| type ViewStepLogLine struct {
 | |
| 	Index     int64   `json:"index"`
 | |
| 	Message   string  `json:"message"`
 | |
| 	Timestamp float64 `json:"timestamp"`
 | |
| }
 | |
| 
 | |
| func ViewPost(ctx *context_module.Context) {
 | |
| 	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
 | |
| 
 | |
| 	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.Done = run.Status.IsDone()
 | |
| 	resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
 | |
| 	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),
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	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, err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 		task.Job = current
 | |
| 		if err := task.LoadAttributes(ctx); err != nil {
 | |
| 			ctx.Error(http.StatusInternalServerError, 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
 | |
| 			if c := cursor.Cursor; c < step.LogLength && c >= 0 {
 | |
| 				index := step.LogIndex + c
 | |
| 				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, 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,
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	ctx.JSON(http.StatusOK, resp)
 | |
| }
 | |
| 
 | |
| func Rerun(ctx *context_module.Context) {
 | |
| 	runIndex := ctx.ParamsInt64("run")
 | |
| 	jobIndex := ctx.ParamsInt64("job")
 | |
| 
 | |
| 	job, _ := getRunJobs(ctx, runIndex, jobIndex)
 | |
| 	if ctx.Written() {
 | |
| 		return
 | |
| 	}
 | |
| 	status := job.Status
 | |
| 	if !status.IsDone() {
 | |
| 		ctx.JSON(http.StatusOK, struct{}{})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	job.TaskID = 0
 | |
| 	job.Status = actions_model.StatusWaiting
 | |
| 	job.Started = 0
 | |
| 	job.Stopped = 0
 | |
| 
 | |
| 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		if _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped"); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		return actions_service.CreateCommitStatus(ctx, job)
 | |
| 	}); err != nil {
 | |
| 		ctx.Error(http.StatusInternalServerError, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ctx.JSON(http.StatusOK, struct{}{})
 | |
| }
 | |
| 
 | |
| func Cancel(ctx *context_module.Context) {
 | |
| 	runIndex := ctx.ParamsInt64("run")
 | |
| 
 | |
| 	_, jobs := getRunJobs(ctx, runIndex, -1)
 | |
| 	if ctx.Written() {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		for _, job := range jobs {
 | |
| 			status := job.Status
 | |
| 			if status.IsDone() {
 | |
| 				continue
 | |
| 			}
 | |
| 			if job.TaskID == 0 {
 | |
| 				job.Status = actions_model.StatusCancelled
 | |
| 				job.Stopped = timeutil.TimeStampNow()
 | |
| 				n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				if n == 0 {
 | |
| 					return fmt.Errorf("job has changed, try again")
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			if err := actions_service.CreateCommitStatus(ctx, job); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	}); err != nil {
 | |
| 		ctx.Error(http.StatusInternalServerError, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ctx.JSON(http.StatusOK, struct{}{})
 | |
| }
 | |
| 
 | |
| func Approve(ctx *context_module.Context) {
 | |
| 	runIndex := ctx.ParamsInt64("run")
 | |
| 
 | |
| 	current, jobs := getRunJobs(ctx, runIndex, -1)
 | |
| 	if ctx.Written() {
 | |
| 		return
 | |
| 	}
 | |
| 	run := current.Run
 | |
| 	doer := ctx.Doer
 | |
| 
 | |
| 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 | |
| 		run.NeedApproval = false
 | |
| 		run.ApprovedBy = doer.ID
 | |
| 		if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		for _, job := range jobs {
 | |
| 			if len(job.Needs) == 0 && job.Status.IsBlocked() {
 | |
| 				job.Status = actions_model.StatusWaiting
 | |
| 				_, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return nil
 | |
| 	}); err != nil {
 | |
| 		ctx.Error(http.StatusInternalServerError, err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	ctx.JSON(http.StatusOK, struct{}{})
 | |
| }
 | |
| 
 | |
| // getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
 | |
| // Any error will be written to the ctx.
 | |
| // It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
 | |
| func getRunJobs(ctx *context_module.Context, 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, err.Error())
 | |
| 			return nil, nil
 | |
| 		}
 | |
| 		ctx.Error(http.StatusInternalServerError, 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, 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
 | |
| }
 |