aboutsummaryrefslogtreecommitdiffstats
path: root/models/actions
diff options
context:
space:
mode:
Diffstat (limited to 'models/actions')
-rw-r--r--models/actions/artifact.go50
-rw-r--r--models/actions/run.go55
-rw-r--r--models/actions/run_job.go33
-rw-r--r--models/actions/run_job_list.go60
-rw-r--r--models/actions/run_job_status_test.go6
-rw-r--r--models/actions/run_list.go38
-rw-r--r--models/actions/runner.go66
-rw-r--r--models/actions/runner_token_test.go6
-rw-r--r--models/actions/schedule.go22
-rw-r--r--models/actions/schedule_spec_list.go2
-rw-r--r--models/actions/status.go9
-rw-r--r--models/actions/task.go17
-rw-r--r--models/actions/task_list.go4
-rw-r--r--models/actions/utils.go19
-rw-r--r--models/actions/variable.go67
15 files changed, 338 insertions, 116 deletions
diff --git a/models/actions/artifact.go b/models/actions/artifact.go
index 0bc66ba24e..757bd13acd 100644
--- a/models/actions/artifact.go
+++ b/models/actions/artifact.go
@@ -30,6 +30,25 @@ const (
ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted
)
+func (status ArtifactStatus) ToString() string {
+ switch status {
+ case ArtifactStatusUploadPending:
+ return "upload is not yet completed"
+ case ArtifactStatusUploadConfirmed:
+ return "upload is completed"
+ case ArtifactStatusUploadError:
+ return "upload failed"
+ case ArtifactStatusExpired:
+ return "expired"
+ case ArtifactStatusPendingDeletion:
+ return "pending deletion"
+ case ArtifactStatusDeleted:
+ return "deleted"
+ default:
+ return "unknown"
+ }
+}
+
func init() {
db.RegisterModel(new(ActionArtifact))
}
@@ -48,7 +67,7 @@ type ActionArtifact struct {
ContentEncoding string // The content encoding of the artifact
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
- Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
+ Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
@@ -68,7 +87,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
RepoID: t.RepoID,
OwnerID: t.OwnerID,
CommitSHA: t.CommitSHA,
- Status: int64(ArtifactStatusUploadPending),
+ Status: ArtifactStatusUploadPending,
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
}
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
@@ -108,12 +127,19 @@ func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) erro
type FindArtifactsOptions struct {
db.ListOptions
- RepoID int64
- RunID int64
- ArtifactName string
- Status int
+ RepoID int64
+ RunID int64
+ ArtifactName string
+ Status int
+ FinalizedArtifactsV4 bool
}
+func (opts FindArtifactsOptions) ToOrders() string {
+ return "id"
+}
+
+var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
+
func (opts FindArtifactsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
@@ -128,11 +154,15 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
if opts.Status > 0 {
cond = cond.And(builder.Eq{"status": opts.Status})
}
+ if opts.FinalizedArtifactsV4 {
+ cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired}))
+ cond = cond.And(builder.Eq{"content_encoding": "application/zip"})
+ }
return cond
}
-// ActionArtifactMeta is the meta data of an artifact
+// ActionArtifactMeta is the meta-data of an artifact
type ActionArtifactMeta struct {
ArtifactName string
FileSize int64
@@ -166,18 +196,18 @@ func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifa
// SetArtifactExpired sets an artifact to expired
func SetArtifactExpired(ctx context.Context, artifactID int64) error {
- _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)})
+ _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired})
return err
}
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
- _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)})
+ _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err
}
// SetArtifactDeleted sets an artifact to deleted
func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
- _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)})
+ _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
return err
}
diff --git a/models/actions/run.go b/models/actions/run.go
index 60fbbcd323..f0ab61b200 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -5,6 +5,7 @@ package actions
import (
"context"
+ "errors"
"fmt"
"slices"
"strings"
@@ -15,6 +16,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -164,12 +166,24 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
}
+func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) {
+ if run.Event == webhook_module.HookEventWorkflowRun {
+ var payload api.WorkflowRunPayload
+ if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
+ return nil, err
+ }
+ return &payload, nil
+ }
+ return nil, fmt.Errorf("event %s is not a workflow run event", run.Event)
+}
+
func (run *ActionRun) IsSchedule() bool {
return run.ScheduleID > 0
}
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID).
+ NoAutoTime().
SetExpr("num_action_runs",
builder.Select("count(*)").From("action_run").
Where(builder.Eq{"repo_id": repo.ID}),
@@ -194,7 +208,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
-func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID,
@@ -204,14 +218,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
})
if err != nil {
- return err
+ return nil, err
}
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 {
- return nil
+ return nil, nil
}
+ cancelledJobs := make([]*ActionRunJob, 0, total)
+
// Iterate over each found run and cancel its associated jobs.
for _, run := range runs {
// Find all jobs associated with the current run.
@@ -219,7 +235,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
RunID: run.ID,
})
if err != nil {
- return err
+ return cancelledJobs, err
}
// Iterate over each job and attempt to cancel it.
@@ -238,27 +254,29 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
// Update the job's status and stopped time in the database.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
- return err
+ return cancelledJobs, err
}
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
if n == 0 {
- return fmt.Errorf("job has changed, try again")
+ return cancelledJobs, errors.New("job has changed, try again")
}
+ cancelledJobs = append(cancelledJobs, job)
// Continue with the next job.
continue
}
// If the job has an associated task, try to stop the task, effectively cancelling the job.
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
- return err
+ return cancelledJobs, err
}
+ cancelledJobs = append(cancelledJobs, job)
}
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
- return nil
+ return cancelledJobs, nil
}
// InsertRun inserts a run
@@ -337,13 +355,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
return committer.Commit()
}
-func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
+func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
var run ActionRun
- has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
+ has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
if err != nil {
return nil, err
} else if !has {
- return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist)
+ return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist)
}
return &run, nil
@@ -408,23 +426,16 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
return err
}
if affected == 0 {
- return fmt.Errorf("run has changed")
+ return errors.New("run has changed")
// It's impossible that the run is not found, since Gitea never deletes runs.
}
if run.Status != 0 || slices.Contains(cols, "status") {
if run.RepoID == 0 {
- run, err = GetRunByID(ctx, run.ID)
- if err != nil {
- return err
- }
+ setting.PanicInDevOrTesting("RepoID should not be 0")
}
- if run.Repo == nil {
- repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
- if err != nil {
- return err
- }
- run.Repo = repo
+ if err = run.LoadRepo(ctx); err != nil {
+ return err
}
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
return err
diff --git a/models/actions/run_job.go b/models/actions/run_job.go
index de4b6aab66..bad895036d 100644
--- a/models/actions/run_job.go
+++ b/models/actions/run_job.go
@@ -10,6 +10,7 @@ import (
"time"
"code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -19,11 +20,12 @@ import (
// ActionRunJob represents a job of a run
type ActionRunJob struct {
ID int64
- RunID int64 `xorm:"index"`
- Run *ActionRun `xorm:"-"`
- RepoID int64 `xorm:"index"`
- OwnerID int64 `xorm:"index"`
- CommitSHA string `xorm:"index"`
+ RunID int64 `xorm:"index"`
+ Run *ActionRun `xorm:"-"`
+ RepoID int64 `xorm:"index"`
+ Repo *repo_model.Repository `xorm:"-"`
+ OwnerID int64 `xorm:"index"`
+ CommitSHA string `xorm:"index"`
IsForkPullRequest bool
Name string `xorm:"VARCHAR(255)"`
Attempt int64
@@ -49,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration {
func (job *ActionRunJob) LoadRun(ctx context.Context) error {
if job.Run == nil {
- run, err := GetRunByID(ctx, job.RunID)
+ run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
if err != nil {
return err
}
@@ -58,6 +60,17 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error {
return nil
}
+func (job *ActionRunJob) LoadRepo(ctx context.Context) error {
+ if job.Repo == nil {
+ repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID)
+ if err != nil {
+ return err
+ }
+ job.Repo = repo
+ }
+ return nil
+}
+
// LoadAttributes load Run if not loaded
func (job *ActionRunJob) LoadAttributes(ctx context.Context) error {
if job == nil {
@@ -83,7 +96,7 @@ func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
return &job, nil
}
-func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) {
+func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) {
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil {
return nil, err
@@ -129,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
{
// Other goroutines may aggregate the status of the run and update it too.
// So we need load the run and its jobs before updating the run.
- run, err := GetRunByID(ctx, job.RunID)
+ run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID)
if err != nil {
return 0, err
}
@@ -172,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
return StatusSuccess
case hasCancelled:
return StatusCancelled
- case hasFailure:
- return StatusFailure
case hasRunning:
return StatusRunning
+ case hasFailure:
+ return StatusFailure
case hasWaiting:
return StatusWaiting
case hasBlocked:
diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go
index 6c5d3b3252..5f7bb62878 100644
--- a/models/actions/run_job_list.go
+++ b/models/actions/run_job_list.go
@@ -7,6 +7,7 @@ import (
"context"
"code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/timeutil"
@@ -21,7 +22,33 @@ func (jobs ActionJobList) GetRunIDs() []int64 {
})
}
+func (jobs ActionJobList) LoadRepos(ctx context.Context) error {
+ repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) {
+ return j.RepoID, j.RepoID != 0 && j.Repo == nil
+ })
+ if len(repoIDs) == 0 {
+ return nil
+ }
+
+ repos := make(map[int64]*repo_model.Repository, len(repoIDs))
+ if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil {
+ return err
+ }
+ for _, j := range jobs {
+ if j.RepoID > 0 && j.Repo == nil {
+ j.Repo = repos[j.RepoID]
+ }
+ }
+ return nil
+}
+
func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error {
+ if withRepo {
+ if err := jobs.LoadRepos(ctx); err != nil {
+ return err
+ }
+ }
+
runIDs := jobs.GetRunIDs()
runs := make(map[int64]*ActionRun, len(runIDs))
if err := db.GetEngine(ctx).In("id", runIDs).Find(&runs); err != nil {
@@ -30,15 +57,9 @@ func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error {
for _, j := range jobs {
if j.RunID > 0 && j.Run == nil {
j.Run = runs[j.RunID]
+ j.Run.Repo = j.Repo
}
}
- if withRepo {
- var runsList RunList = make([]*ActionRun, 0, len(runs))
- for _, r := range runs {
- runsList = append(runsList, r)
- }
- return runsList.LoadRepos(ctx)
- }
return nil
}
@@ -59,22 +80,31 @@ type FindRunJobOptions struct {
func (opts FindRunJobOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RunID > 0 {
- cond = cond.And(builder.Eq{"run_id": opts.RunID})
+ cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID})
}
if opts.RepoID > 0 {
- cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
- }
- if opts.OwnerID > 0 {
- cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+ cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
}
if opts.CommitSHA != "" {
- cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA})
+ cond = cond.And(builder.Eq{"`action_run_job`.commit_sha": opts.CommitSHA})
}
if len(opts.Statuses) > 0 {
- cond = cond.And(builder.In("status", opts.Statuses))
+ cond = cond.And(builder.In("`action_run_job`.status", opts.Statuses))
}
if opts.UpdatedBefore > 0 {
- cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore})
+ cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore})
}
return cond
}
+
+func (opts FindRunJobOptions) ToJoins() []db.JoinFunc {
+ if opts.OwnerID > 0 {
+ return []db.JoinFunc{
+ func(sess db.Engine) error {
+ sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID)
+ return nil
+ },
+ }
+ }
+ return nil
+}
diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go
index 523d38327e..2a5eb00a6f 100644
--- a/models/actions/run_job_status_test.go
+++ b/models/actions/run_job_status_test.go
@@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
- // failure with other status, fail fast
- // Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
+ // failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
+ // another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
{[]Status{StatusFailure}, StatusFailure},
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
- {[]Status{StatusFailure, StatusRunning}, StatusFailure},
+ {[]Status{StatusFailure, StatusRunning}, StatusRunning},
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
// skipped with other status
diff --git a/models/actions/run_list.go b/models/actions/run_list.go
index 4046c7d369..12c55e538e 100644
--- a/models/actions/run_list.go
+++ b/models/actions/run_list.go
@@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/translation"
webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/builder"
@@ -71,39 +72,50 @@ type FindRunOptions struct {
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true
Status []Status
+ CommitSHA string
}
func (opts FindRunOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
- cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
- }
- if opts.OwnerID > 0 {
- cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+ cond = cond.And(builder.Eq{"`action_run`.repo_id": opts.RepoID})
}
if opts.WorkflowID != "" {
- cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID})
+ cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID})
}
if opts.TriggerUserID > 0 {
- cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID})
+ cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID})
}
if opts.Approved {
- cond = cond.And(builder.Gt{"approved_by": 0})
+ cond = cond.And(builder.Gt{"`action_run`.approved_by": 0})
}
if len(opts.Status) > 0 {
- cond = cond.And(builder.In("status", opts.Status))
+ cond = cond.And(builder.In("`action_run`.status", opts.Status))
}
if opts.Ref != "" {
- cond = cond.And(builder.Eq{"ref": opts.Ref})
+ cond = cond.And(builder.Eq{"`action_run`.ref": opts.Ref})
}
if opts.TriggerEvent != "" {
- cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
+ cond = cond.And(builder.Eq{"`action_run`.trigger_event": opts.TriggerEvent})
+ }
+ if opts.CommitSHA != "" {
+ cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
}
return cond
}
+func (opts FindRunOptions) ToJoins() []db.JoinFunc {
+ if opts.OwnerID > 0 {
+ return []db.JoinFunc{func(sess db.Engine) error {
+ sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID)
+ return nil
+ }}
+ }
+ return nil
+}
+
func (opts FindRunOptions) ToOrders() string {
- return "`id` DESC"
+ return "`action_run`.`id` DESC"
}
type StatusInfo struct {
@@ -112,14 +124,14 @@ type StatusInfo struct {
}
// GetStatusInfoList returns a slice of StatusInfo
-func GetStatusInfoList(ctx context.Context) []StatusInfo {
+func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
// same as those in aggregateJobStatus
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning}
statusInfoList := make([]StatusInfo, 0, 4)
for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{
Status: int(s),
- DisplayedStatus: s.String(),
+ DisplayedStatus: s.LocaleString(lang),
})
}
return statusInfoList
diff --git a/models/actions/runner.go b/models/actions/runner.go
index 0d5464a5be..81d4249ae0 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -5,6 +5,7 @@ package actions
import (
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -14,6 +15,7 @@ import (
"code.gitea.io/gitea/models/shared/types"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@@ -57,6 +59,8 @@ type ActionRunner struct {
// Store labels defined in state file (default: .runner file) of `act_runner`
AgentLabels []string `xorm:"TEXT"`
+ // Store if this is a runner that only ever get one single job assigned
+ Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
@@ -84,9 +88,10 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType {
return types.OwnerTypeRepository
}
if r.OwnerID != 0 {
- if r.Owner.Type == user_model.UserTypeOrganization {
+ switch r.Owner.Type {
+ case user_model.UserTypeOrganization:
return types.OwnerTypeOrganization
- } else if r.Owner.Type == user_model.UserTypeIndividual {
+ case user_model.UserTypeIndividual:
return types.OwnerTypeIndividual
}
}
@@ -120,8 +125,15 @@ func (r *ActionRunner) IsOnline() bool {
return false
}
-// Editable checks if the runner is editable by the user
-func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
+// EditableInContext checks if the runner is editable by the "context" owner/repo
+// ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited
+// ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited
+// ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited
+// ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it
+func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool {
+ if ownerID != 0 && repoID != 0 {
+ setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
+ }
if ownerID == 0 && repoID == 0 {
return true
}
@@ -165,8 +177,15 @@ func init() {
db.RegisterModel(&ActionRunner{})
}
+// FindRunnerOptions
+// ownerID == 0 and repoID == 0 means any runner including global runners
+// repoID != 0 and WithAvailable == false means any runner for the given repo
+// repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners
+// ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org
+// ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners
type FindRunnerOptions struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Sort string
@@ -178,6 +197,14 @@ type FindRunnerOptions struct {
func (opts FindRunnerOptions) ToConds() builder.Cond {
cond := builder.NewCond()
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
if opts.RepoID > 0 {
c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
if opts.WithAvailable {
@@ -272,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
return err
}
+// DeleteEphemeralRunner deletes a ephemeral runner by given ID.
+func DeleteEphemeralRunner(ctx context.Context, id int64) error {
+ runner, err := GetRunnerByID(ctx, id)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ return nil
+ }
+ return err
+ }
+ if !runner.Ephemeral {
+ return nil
+ }
+
+ _, err = db.DeleteByID[ActionRunner](ctx, id)
+ return err
+}
+
// CreateRunner creates new runner.
func CreateRunner(ctx context.Context, t *ActionRunner) error {
if t.OwnerID != 0 && t.RepoID != 0 {
@@ -328,3 +372,17 @@ func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) {
}
return res.RowsAffected()
}
+
+func CountWrongRepoLevelRunners(ctx context.Context) (int64, error) {
+ var result int64
+ _, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_runner` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result)
+ return result, err
+}
+
+func UpdateWrongRepoLevelRunners(ctx context.Context) (int64, error) {
+ result, err := db.GetEngine(ctx).Exec("UPDATE `action_runner` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0")
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected()
+}
diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go
index 159805e5f7..21614b7086 100644
--- a/models/actions/runner_token_test.go
+++ b/models/actions/runner_token_test.go
@@ -17,7 +17,7 @@ func TestGetLatestRunnerToken(t *testing.T) {
token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3})
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err)
- assert.EqualValues(t, expectedToken, token)
+ assert.Equal(t, expectedToken, token)
}
func TestNewRunnerToken(t *testing.T) {
@@ -26,7 +26,7 @@ func TestNewRunnerToken(t *testing.T) {
assert.NoError(t, err)
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err)
- assert.EqualValues(t, expectedToken, token)
+ assert.Equal(t, expectedToken, token)
}
func TestUpdateRunnerToken(t *testing.T) {
@@ -36,5 +36,5 @@ func TestUpdateRunnerToken(t *testing.T) {
assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token))
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err)
- assert.EqualValues(t, expectedToken, token)
+ assert.Equal(t, expectedToken, token)
}
diff --git a/models/actions/schedule.go b/models/actions/schedule.go
index e2cc32eedc..2edf483fe0 100644
--- a/models/actions/schedule.go
+++ b/models/actions/schedule.go
@@ -43,15 +43,12 @@ func init() {
// GetSchedulesMapByIDs returns the schedules by given id slice.
func GetSchedulesMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionSchedule, error) {
schedules := make(map[int64]*ActionSchedule, len(ids))
+ if len(ids) == 0 {
+ return schedules, nil
+ }
return schedules, db.GetEngine(ctx).In("id", ids).Find(&schedules)
}
-// GetReposMapByIDs returns the repos by given id slice.
-func GetReposMapByIDs(ctx context.Context, ids []int64) (map[int64]*repo_model.Repository, error) {
- repos := make(map[int64]*repo_model.Repository, len(ids))
- return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
-}
-
// CreateScheduleTask creates new schedule task.
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
// Return early if there are no rows to insert
@@ -120,21 +117,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit()
}
-func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
+func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([]*ActionRunJob, error) {
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
// There is no other place we can do this because the app.ini will be changed manually
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
- return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
+ return nil, fmt.Errorf("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := CancelPreviousJobs(
+ jobs, err := CancelPreviousJobs(
ctx,
repo.ID,
repo.DefaultBranch,
"",
webhook_module.HookEventSchedule,
- ); err != nil {
- return fmt.Errorf("CancelPreviousJobs: %v", err)
+ )
+ if err != nil {
+ return jobs, fmt.Errorf("CancelPreviousJobs: %v", err)
}
- return nil
+ return jobs, nil
}
diff --git a/models/actions/schedule_spec_list.go b/models/actions/schedule_spec_list.go
index f7dac72f8b..e26b2c1120 100644
--- a/models/actions/schedule_spec_list.go
+++ b/models/actions/schedule_spec_list.go
@@ -32,7 +32,7 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error {
}
repoIDs := specs.GetRepoIDs()
- repos, err := GetReposMapByIDs(ctx, repoIDs)
+ repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil {
return err
}
diff --git a/models/actions/status.go b/models/actions/status.go
index eda2234137..2b1d70613c 100644
--- a/models/actions/status.go
+++ b/models/actions/status.go
@@ -4,6 +4,8 @@
package actions
import (
+ "slices"
+
"code.gitea.io/gitea/modules/translation"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
@@ -88,12 +90,7 @@ func (s Status) IsBlocked() bool {
// In returns whether s is one of the given statuses
func (s Status) In(statuses ...Status) bool {
- for _, v := range statuses {
- if s == v {
- return true
- }
- }
- return false
+ return slices.Contains(statuses, s)
}
func (s Status) AsResult() runnerv1.Result {
diff --git a/models/actions/task.go b/models/actions/task.go
index 9f13ff94c9..e0756b10c2 100644
--- a/models/actions/task.go
+++ b/models/actions/task.go
@@ -6,6 +6,7 @@ package actions
import (
"context"
"crypto/subtle"
+ "errors"
"fmt"
"time"
@@ -277,14 +278,13 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
return nil, false, err
}
- var workflowJob *jobparser.Job
- if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil {
+ parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload)
+ if err != nil {
return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err)
- } else if len(gots) != 1 {
+ } else if len(parsedWorkflows) != 1 {
return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID)
- } else { //nolint:revive
- _, workflowJob = gots[0].Job()
}
+ _, workflowJob := parsedWorkflows[0].Job()
if _, err := e.Insert(task); err != nil {
return nil, false, err
@@ -335,6 +335,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
sess.Cols(cols...)
}
_, err := sess.Update(task)
+
+ // Automatically delete the ephemeral runner if the task is done
+ if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") {
+ return DeleteEphemeralRunner(ctx, task.RunnerID)
+ }
return err
}
@@ -361,7 +366,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task
} else if !has {
return nil, util.ErrNotExist
} else if runnerID != task.RunnerID {
- return nil, fmt.Errorf("invalid runner for task")
+ return nil, errors.New("invalid runner for task")
}
if task.Status.IsDone() {
diff --git a/models/actions/task_list.go b/models/actions/task_list.go
index df4b43c5ef..0c80397899 100644
--- a/models/actions/task_list.go
+++ b/models/actions/task_list.go
@@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error {
type FindTaskOptions struct {
db.ListOptions
RepoID int64
+ JobID int64
OwnerID int64
CommitSHA string
Status Status
@@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
}
+ if opts.JobID > 0 {
+ cond = cond.And(builder.Eq{"job_id": opts.JobID})
+ }
if opts.OwnerID > 0 {
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
}
diff --git a/models/actions/utils.go b/models/actions/utils.go
index 12657942fc..f6ba661ae3 100644
--- a/models/actions/utils.go
+++ b/models/actions/utils.go
@@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.
}
return timeSince(s).Truncate(time.Second)
}
+
+// best effort function to convert an action schedule to action run, to be used in GenerateGiteaContext
+func (s *ActionSchedule) ToActionRun() *ActionRun {
+ return &ActionRun{
+ Title: s.Title,
+ RepoID: s.RepoID,
+ Repo: s.Repo,
+ OwnerID: s.OwnerID,
+ WorkflowID: s.WorkflowID,
+ TriggerUserID: s.TriggerUserID,
+ TriggerUser: s.TriggerUser,
+ Ref: s.Ref,
+ CommitSHA: s.CommitSHA,
+ Event: s.Event,
+ EventPayload: s.EventPayload,
+ Created: s.Created,
+ Updated: s.Updated,
+ }
+}
diff --git a/models/actions/variable.go b/models/actions/variable.go
index d0f917d923..7154843c17 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -6,10 +6,12 @@ package actions
import (
"context"
"strings"
+ "unicode/utf8"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
@@ -32,32 +34,46 @@ type ActionVariable struct {
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
Data string `xorm:"LONGTEXT NOT NULL"`
+ Description string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
+const (
+ VariableDataMaxLength = 65536
+ VariableDescriptionMaxLength = 4096
+)
+
func init() {
db.RegisterModel(new(ActionVariable))
}
-func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
+func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) {
if ownerID != 0 && repoID != 0 {
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
ownerID = 0
}
+ if utf8.RuneCountInString(data) > VariableDataMaxLength {
+ return nil, util.NewInvalidArgumentErrorf("data too long")
+ }
+
+ description = util.TruncateRunes(description, VariableDescriptionMaxLength)
+
variable := &ActionVariable{
- OwnerID: ownerID,
- RepoID: repoID,
- Name: strings.ToUpper(name),
- Data: data,
+ OwnerID: ownerID,
+ RepoID: repoID,
+ Name: strings.ToUpper(name),
+ Data: data,
+ Description: description,
}
return variable, db.Insert(ctx, variable)
}
type FindVariablesOpts struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Name string
@@ -65,6 +81,15 @@ type FindVariablesOpts struct {
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
+
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
// Since we now support instance-level variables,
// there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
@@ -85,12 +110,18 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
return db.Find[ActionVariable](ctx, opts)
}
-func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
- count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
- Update(&ActionVariable{
- Name: variable.Name,
- Data: variable.Data,
- })
+func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
+ if utf8.RuneCountInString(variable.Data) > VariableDataMaxLength {
+ return false, util.NewInvalidArgumentErrorf("data too long")
+ }
+
+ variable.Description = util.TruncateRunes(variable.Description, VariableDescriptionMaxLength)
+
+ variable.Name = strings.ToUpper(variable.Name)
+ count, err := db.GetEngine(ctx).
+ ID(variable.ID).
+ Cols(cols...).
+ Update(variable)
return count != 0, err
}
@@ -137,3 +168,17 @@ func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string,
return variables, nil
}
+
+func CountWrongRepoLevelVariables(ctx context.Context) (int64, error) {
+ var result int64
+ _, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_variable` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result)
+ return result, err
+}
+
+func UpdateWrongRepoLevelVariables(ctx context.Context) (int64, error) {
+ result, err := db.GetEngine(ctx).Exec("UPDATE `action_variable` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0")
+ if err != nil {
+ return 0, err
+ }
+ return result.RowsAffected()
+}