mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-14 01:25:06 +02:00
Support Actions concurrency
syntax (#32751)
Fix #24769 Fix #32662 Fix #33260 Depends on https://gitea.com/gitea/act/pulls/124 - https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency ## ⚠️ BREAKING ⚠️ This PR removes the auto-cancellation feature added by #25716. Users need to manually add `concurrency` to workflows to control concurrent workflows or jobs. --------- Signed-off-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Christopher Homberger <christopher.homberger@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
327d0a7fdd
commit
40f71bcd4c
@ -16,13 +16,13 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ import (
|
|||||||
type ActionRun struct {
|
type ActionRun struct {
|
||||||
ID int64
|
ID int64
|
||||||
Title string
|
Title string
|
||||||
RepoID int64 `xorm:"index unique(repo_index)"`
|
RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
OwnerID int64 `xorm:"index"`
|
OwnerID int64 `xorm:"index"`
|
||||||
WorkflowID string `xorm:"index"` // the name of workflow file
|
WorkflowID string `xorm:"index"` // the name of workflow file
|
||||||
@ -49,6 +49,9 @@ type ActionRun struct {
|
|||||||
TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow
|
TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow
|
||||||
Status Status `xorm:"index"`
|
Status Status `xorm:"index"`
|
||||||
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
|
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
|
||||||
|
RawConcurrency string // raw concurrency
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0
|
// Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0
|
||||||
Started timeutil.TimeStamp
|
Started timeutil.TimeStamp
|
||||||
Stopped timeutil.TimeStamp
|
Stopped timeutil.TimeStamp
|
||||||
@ -190,7 +193,7 @@ func (run *ActionRun) IsSchedule() bool {
|
|||||||
return run.ScheduleID > 0
|
return run.ScheduleID > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
||||||
_, err := db.GetEngine(ctx).ID(repo.ID).
|
_, err := db.GetEngine(ctx).ID(repo.ID).
|
||||||
NoAutoTime().
|
NoAutoTime().
|
||||||
SetExpr("num_action_runs",
|
SetExpr("num_action_runs",
|
||||||
@ -247,6 +250,19 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
|||||||
return cancelledJobs, err
|
return cancelledJobs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cjs, err := CancelJobs(ctx, jobs)
|
||||||
|
if err != nil {
|
||||||
|
return cancelledJobs, err
|
||||||
|
}
|
||||||
|
cancelledJobs = append(cancelledJobs, cjs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil to indicate successful cancellation of all running and waiting jobs.
|
||||||
|
return cancelledJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) {
|
||||||
|
cancelledJobs := make([]*ActionRunJob, 0, len(jobs))
|
||||||
// Iterate over each job and attempt to cancel it.
|
// Iterate over each job and attempt to cancel it.
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
|
// Skip jobs that are already in a terminal state (completed, cancelled, etc.).
|
||||||
@ -266,9 +282,10 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
|||||||
return cancelledJobs, 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 the update affected 0 rows, it means the job has changed in the meantime
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return cancelledJobs, errors.New("job has changed, try again")
|
log.Error("Failed to cancel job %d because it has changed", job.ID)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelledJobs = append(cancelledJobs, job)
|
cancelledJobs = append(cancelledJobs, job)
|
||||||
@ -280,85 +297,17 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
|||||||
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
|
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
|
||||||
return cancelledJobs, err
|
return cancelledJobs, err
|
||||||
}
|
}
|
||||||
cancelledJobs = append(cancelledJobs, job)
|
updatedJob, err := GetRunJobByID(ctx, job.ID)
|
||||||
|
if err != nil {
|
||||||
|
return cancelledJobs, fmt.Errorf("get job: %w", err)
|
||||||
}
|
}
|
||||||
|
cancelledJobs = append(cancelledJobs, updatedJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return nil to indicate successful cancellation of all running and waiting jobs.
|
// Return nil to indicate successful cancellation of all running and waiting jobs.
|
||||||
return cancelledJobs, nil
|
return cancelledJobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertRun inserts a run
|
|
||||||
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
|
||||||
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
run.Index = index
|
|
||||||
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
|
||||||
|
|
||||||
if err := db.Insert(ctx, run); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if run.Repo == nil {
|
|
||||||
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
run.Repo = repo
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
runJobs := make([]*ActionRunJob, 0, len(jobs))
|
|
||||||
var hasWaiting bool
|
|
||||||
for _, v := range jobs {
|
|
||||||
id, job := v.Job()
|
|
||||||
needs := job.Needs()
|
|
||||||
if err := v.SetJob(id, job.EraseNeeds()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
payload, _ := v.Marshal()
|
|
||||||
status := StatusWaiting
|
|
||||||
if len(needs) > 0 || run.NeedApproval {
|
|
||||||
status = StatusBlocked
|
|
||||||
} else {
|
|
||||||
hasWaiting = true
|
|
||||||
}
|
|
||||||
job.Name = util.EllipsisDisplayString(job.Name, 255)
|
|
||||||
runJobs = append(runJobs, &ActionRunJob{
|
|
||||||
RunID: run.ID,
|
|
||||||
RepoID: run.RepoID,
|
|
||||||
OwnerID: run.OwnerID,
|
|
||||||
CommitSHA: run.CommitSHA,
|
|
||||||
IsForkPullRequest: run.IsForkPullRequest,
|
|
||||||
Name: job.Name,
|
|
||||||
WorkflowPayload: payload,
|
|
||||||
JobID: id,
|
|
||||||
Needs: needs,
|
|
||||||
RunsOn: job.RunsOn(),
|
|
||||||
Status: status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := db.Insert(ctx, runJobs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a job in the waiting status, increase tasks version.
|
|
||||||
if hasWaiting {
|
|
||||||
if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
|
func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) {
|
||||||
var run ActionRun
|
var run ActionRun
|
||||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
|
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run)
|
||||||
@ -441,7 +390,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
if err = run.LoadRepo(ctx); err != nil {
|
if err = run.LoadRepo(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
if err := UpdateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,3 +399,59 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActionRunIndex db.ResourceIndex
|
type ActionRunIndex db.ResourceIndex
|
||||||
|
|
||||||
|
func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) {
|
||||||
|
runs, err := db.Find[ActionRun](ctx, &FindRunOptions{
|
||||||
|
RepoID: repoID,
|
||||||
|
ConcurrencyGroup: concurrencyGroup,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("find runs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{
|
||||||
|
RepoID: repoID,
|
||||||
|
ConcurrencyGroup: concurrencyGroup,
|
||||||
|
Statuses: status,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("find jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runs, jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) {
|
||||||
|
if actionRun.ConcurrencyGroup == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobsToCancel []*ActionRunJob
|
||||||
|
|
||||||
|
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
||||||
|
if actionRun.ConcurrencyCancel {
|
||||||
|
statusFindOption = append(statusFindOption, StatusRunning)
|
||||||
|
}
|
||||||
|
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
|
}
|
||||||
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
|
||||||
|
// cancel runs in the same concurrency group
|
||||||
|
for _, run := range runs {
|
||||||
|
if run.ID == actionRun.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
|
||||||
|
RunID: run.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err)
|
||||||
|
}
|
||||||
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CancelJobs(ctx, jobsToCancel)
|
||||||
|
}
|
||||||
|
@ -22,19 +22,34 @@ type ActionRunJob struct {
|
|||||||
ID int64
|
ID int64
|
||||||
RunID int64 `xorm:"index"`
|
RunID int64 `xorm:"index"`
|
||||||
Run *ActionRun `xorm:"-"`
|
Run *ActionRun `xorm:"-"`
|
||||||
RepoID int64 `xorm:"index"`
|
RepoID int64 `xorm:"index(repo_concurrency)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
OwnerID int64 `xorm:"index"`
|
OwnerID int64 `xorm:"index"`
|
||||||
CommitSHA string `xorm:"index"`
|
CommitSHA string `xorm:"index"`
|
||||||
IsForkPullRequest bool
|
IsForkPullRequest bool
|
||||||
Name string `xorm:"VARCHAR(255)"`
|
Name string `xorm:"VARCHAR(255)"`
|
||||||
Attempt int64
|
Attempt int64
|
||||||
|
|
||||||
|
// WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse
|
||||||
|
// it should contain exactly one job with global workflow fields for this model
|
||||||
WorkflowPayload []byte
|
WorkflowPayload []byte
|
||||||
|
|
||||||
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
|
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
|
||||||
Needs []string `xorm:"JSON TEXT"`
|
Needs []string `xorm:"JSON TEXT"`
|
||||||
RunsOn []string `xorm:"JSON TEXT"`
|
RunsOn []string `xorm:"JSON TEXT"`
|
||||||
TaskID int64 // the latest task of the job
|
TaskID int64 // the latest task of the job
|
||||||
Status Status `xorm:"index"`
|
Status Status `xorm:"index"`
|
||||||
|
|
||||||
|
RawConcurrency string // raw concurrency from job YAML's "concurrency" section
|
||||||
|
|
||||||
|
// IsConcurrencyEvaluated is only valid/needed when this job's RawConcurrency is not empty.
|
||||||
|
// If RawConcurrency can't be evaluated (e.g. depend on other job's outputs or have errors), this field will be false.
|
||||||
|
// If RawConcurrency has been successfully evaluated, this field will be true, ConcurrencyGroup and ConcurrencyCancel are also set.
|
||||||
|
IsConcurrencyEvaluated bool
|
||||||
|
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress
|
||||||
|
|
||||||
Started timeutil.TimeStamp
|
Started timeutil.TimeStamp
|
||||||
Stopped timeutil.TimeStamp
|
Stopped timeutil.TimeStamp
|
||||||
Created timeutil.TimeStamp `xorm:"created"`
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
@ -125,7 +140,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
|||||||
return affected, nil
|
return affected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() {
|
if slices.Contains(cols, "status") && job.Status.IsWaiting() {
|
||||||
// if the status of job changes to waiting again, increase tasks version.
|
// if the status of job changes to waiting again, increase tasks version.
|
||||||
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
|
if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -197,3 +212,39 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
|||||||
return StatusUnknown // it shouldn't happen
|
return StatusUnknown // it shouldn't happen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) {
|
||||||
|
if job.RawConcurrency == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if !job.IsConcurrencyEvaluated {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if job.ConcurrencyGroup == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statusFindOption := []Status{StatusWaiting, StatusBlocked}
|
||||||
|
if job.ConcurrencyCancel {
|
||||||
|
statusFindOption = append(statusFindOption, StatusRunning)
|
||||||
|
}
|
||||||
|
runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
|
}
|
||||||
|
jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID })
|
||||||
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
|
||||||
|
// cancel runs in the same concurrency group
|
||||||
|
for _, run := range runs {
|
||||||
|
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
|
||||||
|
RunID: run.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err)
|
||||||
|
}
|
||||||
|
jobsToCancel = append(jobsToCancel, jobs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CancelJobs(ctx, jobsToCancel)
|
||||||
|
}
|
||||||
|
@ -75,6 +75,7 @@ type FindRunJobOptions struct {
|
|||||||
CommitSHA string
|
CommitSHA string
|
||||||
Statuses []Status
|
Statuses []Status
|
||||||
UpdatedBefore timeutil.TimeStamp
|
UpdatedBefore timeutil.TimeStamp
|
||||||
|
ConcurrencyGroup string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindRunJobOptions) ToConds() builder.Cond {
|
func (opts FindRunJobOptions) ToConds() builder.Cond {
|
||||||
@ -94,6 +95,12 @@ func (opts FindRunJobOptions) ToConds() builder.Cond {
|
|||||||
if opts.UpdatedBefore > 0 {
|
if opts.UpdatedBefore > 0 {
|
||||||
cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore})
|
cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore})
|
||||||
}
|
}
|
||||||
|
if opts.ConcurrencyGroup != "" {
|
||||||
|
if opts.RepoID == 0 {
|
||||||
|
panic("Invalid FindRunJobOptions: repo_id is required")
|
||||||
|
}
|
||||||
|
cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup})
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ type FindRunOptions struct {
|
|||||||
TriggerEvent webhook_module.HookEventType
|
TriggerEvent webhook_module.HookEventType
|
||||||
Approved bool // not util.OptionalBool, it works only when it's true
|
Approved bool // not util.OptionalBool, it works only when it's true
|
||||||
Status []Status
|
Status []Status
|
||||||
|
ConcurrencyGroup string
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +102,12 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
|||||||
if opts.CommitSHA != "" {
|
if opts.CommitSHA != "" {
|
||||||
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA})
|
||||||
}
|
}
|
||||||
|
if len(opts.ConcurrencyGroup) > 0 {
|
||||||
|
if opts.RepoID == 0 {
|
||||||
|
panic("Invalid FindRunOptions: repo_id is required")
|
||||||
|
}
|
||||||
|
cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup})
|
||||||
|
}
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,6 +394,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
// Gitea 1.24.0 ends at database version 321
|
// Gitea 1.24.0 ends at database version 321
|
||||||
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
|
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
|
||||||
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
|
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
|
||||||
|
newMigration(323, "Add support for actions concurrency", v1_25.AddActionsConcurrency),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
43
models/migrations/v1_25/v323.go
Normal file
43
models/migrations/v1_25/v323.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_25
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddActionsConcurrency(x *xorm.Engine) error {
|
||||||
|
type ActionRun struct {
|
||||||
|
RepoID int64 `xorm:"index(repo_concurrency)"`
|
||||||
|
RawConcurrency string
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(ActionRun)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync(new(ActionRun)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionRunJob struct {
|
||||||
|
RepoID int64 `xorm:"index(repo_concurrency)"`
|
||||||
|
RawConcurrency string
|
||||||
|
IsConcurrencyEvaluated bool
|
||||||
|
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
|
||||||
|
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(ActionRunJob)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -227,9 +227,12 @@ func (s *Service) UpdateTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
|
||||||
if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil {
|
if err := actions_service.EmitJobsIfReadyByRun(task.Job.RunID); err != nil {
|
||||||
log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err)
|
log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err)
|
||||||
}
|
}
|
||||||
|
if task.Job.Run.Status.IsDone() {
|
||||||
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(&runnerv1.UpdateTaskResponse{
|
return connect.NewResponse(&runnerv1.UpdateTaskResponse{
|
||||||
|
@ -27,7 +27,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
@ -36,6 +35,7 @@ import (
|
|||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -420,12 +420,45 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check run (workflow-level) concurrency
|
||||||
|
|
||||||
|
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// reset run's start and stop time when it is done
|
// reset run's start and stop time when it is done
|
||||||
if run.Status.IsDone() {
|
if run.Status.IsDone() {
|
||||||
run.PreviousDuration = run.Duration()
|
run.PreviousDuration = run.Duration()
|
||||||
run.Started = 0
|
run.Started = 0
|
||||||
run.Stopped = 0
|
run.Stopped = 0
|
||||||
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
|
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if run.RawConcurrency != "" {
|
||||||
|
var rawConcurrency model.RawConcurrency
|
||||||
|
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
|
||||||
|
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
|
||||||
ctx.ServerError("UpdateRun", err)
|
ctx.ServerError("UpdateRun", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -437,16 +470,12 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||||
}
|
}
|
||||||
|
|
||||||
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
isRunBlocked := run.Status == actions_model.StatusBlocked
|
||||||
if ctx.Written() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if jobIndexStr == "" { // rerun all jobs
|
if jobIndexStr == "" { // rerun all jobs
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
||||||
shouldBlock := len(j.Needs) > 0
|
shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
|
||||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
|
||||||
ctx.ServerError("RerunJob", err)
|
ctx.ServerError("RerunJob", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -459,8 +488,8 @@ func Rerun(ctx *context_module.Context) {
|
|||||||
|
|
||||||
for _, j := range rerunJobs {
|
for _, j := range rerunJobs {
|
||||||
// jobs other than the specified one should be set to "blocked" status
|
// jobs other than the specified one should be set to "blocked" status
|
||||||
shouldBlock := j.JobID != job.JobID
|
shouldBlockJob := j.JobID != job.JobID || isRunBlocked
|
||||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
|
||||||
ctx.ServerError("RerunJob", err)
|
ctx.ServerError("RerunJob", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -476,15 +505,37 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
|
|||||||
}
|
}
|
||||||
|
|
||||||
job.TaskID = 0
|
job.TaskID = 0
|
||||||
job.Status = actions_model.StatusWaiting
|
job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
|
||||||
if shouldBlock {
|
|
||||||
job.Status = actions_model.StatusBlocked
|
|
||||||
}
|
|
||||||
job.Started = 0
|
job.Started = 0
|
||||||
job.Stopped = 0
|
job.Stopped = 0
|
||||||
|
|
||||||
|
job.ConcurrencyGroup = ""
|
||||||
|
job.ConcurrencyCancel = false
|
||||||
|
job.IsConcurrencyEvaluated = false
|
||||||
|
if err := job.LoadRun(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if job.RawConcurrency != "" && !shouldBlock {
|
||||||
|
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
|
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
|
||||||
|
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -523,33 +574,14 @@ func Cancel(ctx *context_module.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedjobs []*actions_model.ActionRunJob
|
var updatedJobs []*actions_model.ActionRunJob
|
||||||
|
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
for _, job := range jobs {
|
cancelledJobs, err := actions_model.CancelJobs(ctx, 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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("cancel jobs: %w", err)
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
return errors.New("job has changed, try again")
|
|
||||||
}
|
|
||||||
if n > 0 {
|
|
||||||
updatedjobs = append(updatedjobs, job)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
updatedJobs = append(updatedJobs, cancelledJobs...)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("StopTask", err)
|
ctx.ServerError("StopTask", err)
|
||||||
@ -557,13 +589,14 @@ func Cancel(ctx *context_module.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actions_service.CreateCommitStatus(ctx, jobs...)
|
actions_service.CreateCommitStatus(ctx, jobs...)
|
||||||
|
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
|
||||||
|
|
||||||
for _, job := range updatedjobs {
|
for _, job := range updatedJobs {
|
||||||
_ = job.LoadAttributes(ctx)
|
_ = job.LoadAttributes(ctx)
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
if len(updatedjobs) > 0 {
|
if len(updatedJobs) > 0 {
|
||||||
job := updatedjobs[0]
|
job := updatedJobs[0]
|
||||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
}
|
}
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
@ -579,40 +612,44 @@ func Approve(ctx *context_module.Context) {
|
|||||||
run := current.Run
|
run := current.Run
|
||||||
doer := ctx.Doer
|
doer := ctx.Doer
|
||||||
|
|
||||||
var updatedjobs []*actions_model.ActionRunJob
|
var updatedJobs []*actions_model.ActionRunJob
|
||||||
|
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||||
run.NeedApproval = false
|
run.NeedApproval = false
|
||||||
run.ApprovedBy = doer.ID
|
run.ApprovedBy = doer.ID
|
||||||
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
|
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
if len(job.Needs) == 0 && job.Status.IsBlocked() {
|
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
|
||||||
job.Status = actions_model.StatusWaiting
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if job.Status == actions_model.StatusWaiting {
|
||||||
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
|
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
updatedjobs = append(updatedjobs, job)
|
updatedJobs = append(updatedJobs, job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
|
if err != nil {
|
||||||
ctx.ServerError("UpdateRunJob", err)
|
ctx.ServerError("UpdateRunJob", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
actions_service.CreateCommitStatus(ctx, jobs...)
|
actions_service.CreateCommitStatus(ctx, jobs...)
|
||||||
|
|
||||||
if len(updatedjobs) > 0 {
|
if len(updatedJobs) > 0 {
|
||||||
job := updatedjobs[0]
|
job := updatedJobs[0]
|
||||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range updatedjobs {
|
for _, job := range updatedJobs {
|
||||||
_ = job.LoadAttributes(ctx)
|
_ = job.LoadAttributes(ctx)
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
)
|
)
|
||||||
@ -50,15 +51,84 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac
|
|||||||
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) error {
|
||||||
jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event)
|
jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event)
|
||||||
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
||||||
|
EmitJobsIfReadyByJobs(jobs)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
|
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
|
||||||
jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo)
|
jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo)
|
||||||
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
||||||
|
EmitJobsIfReadyByJobs(jobs)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldBlockJobByConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (bool, error) {
|
||||||
|
if job.RawConcurrency != "" && !job.IsConcurrencyEvaluated {
|
||||||
|
// when the job depends on other jobs, we cannot evaluate its concurrency, so it should be blocked and will be evaluated again when its dependencies are done
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if job.ConcurrencyGroup == "" || job.ConcurrencyCancel {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("GetConcurrentRunsAndJobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runs) > 0 || len(jobs) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareToStartJobWithConcurrency prepares a job to start by its evaluated concurrency group and cancelling previous jobs if necessary.
|
||||||
|
// It returns the new status of the job (either StatusBlocked or StatusWaiting) and any error encountered during the process.
|
||||||
|
func PrepareToStartJobWithConcurrency(ctx context.Context, job *actions_model.ActionRunJob) (actions_model.Status, error) {
|
||||||
|
shouldBlock, err := shouldBlockJobByConcurrency(ctx, job)
|
||||||
|
if err != nil {
|
||||||
|
return actions_model.StatusBlocked, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// even if the current job is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group
|
||||||
|
jobs, err := actions_model.CancelPreviousJobsByJobConcurrency(ctx, job)
|
||||||
|
if err != nil {
|
||||||
|
return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByJobConcurrency: %w", err)
|
||||||
|
}
|
||||||
|
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
||||||
|
|
||||||
|
return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldBlockRunByConcurrency(ctx context.Context, actionRun *actions_model.ActionRun) (bool, error) {
|
||||||
|
if actionRun.ConcurrencyGroup == "" || actionRun.ConcurrencyCancel {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runs, jobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, []actions_model.Status{actions_model.StatusRunning})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(runs) > 0 || len(jobs) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareToStartRunWithConcurrency prepares a run to start by its evaluated concurrency group and cancelling previous jobs if necessary.
|
||||||
|
// It returns the new status of the run (either StatusBlocked or StatusWaiting) and any error encountered during the process.
|
||||||
|
func PrepareToStartRunWithConcurrency(ctx context.Context, run *actions_model.ActionRun) (actions_model.Status, error) {
|
||||||
|
shouldBlock, err := shouldBlockRunByConcurrency(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return actions_model.StatusBlocked, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// even if the current run is blocked, we still need to cancel previous "waiting/blocked" jobs in the same concurrency group
|
||||||
|
jobs, err := actions_model.CancelPreviousJobsByRunConcurrency(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return actions_model.StatusBlocked, fmt.Errorf("CancelPreviousJobsByRunConcurrency: %w", err)
|
||||||
|
}
|
||||||
|
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
||||||
|
|
||||||
|
return util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting), nil
|
||||||
|
}
|
||||||
|
|
||||||
func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
||||||
tasks, err := db.Find[actions_model.ActionTask](ctx, opts)
|
tasks, err := db.Find[actions_model.ActionTask](ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -95,6 +165,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
notifyWorkflowJobStatusUpdate(ctx, jobs)
|
||||||
|
EmitJobsIfReadyByJobs(jobs)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -103,7 +174,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
|
|||||||
func CancelAbandonedJobs(ctx context.Context) error {
|
func CancelAbandonedJobs(ctx context.Context) error {
|
||||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
|
||||||
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
|
Statuses: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusBlocked},
|
||||||
UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.AbandonedJobTimeout).Unix()),
|
UpdatedBefore: timeutil.TimeStampNow().AddDuration(-setting.Actions.AbandonedJobTimeout),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("find abandoned tasks: %v", err)
|
log.Warn("find abandoned tasks: %v", err)
|
||||||
@ -114,6 +185,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
|
|||||||
|
|
||||||
// Collect one job per run to send workflow run status update
|
// Collect one job per run to send workflow run status update
|
||||||
updatedRuns := map[int64]*actions_model.ActionRunJob{}
|
updatedRuns := map[int64]*actions_model.ActionRunJob{}
|
||||||
|
updatedJobs := []*actions_model.ActionRunJob{}
|
||||||
|
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
job.Status = actions_model.StatusCancelled
|
job.Status = actions_model.StatusCancelled
|
||||||
@ -138,6 +210,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
CreateCommitStatus(ctx, job)
|
CreateCommitStatus(ctx, job)
|
||||||
if updated {
|
if updated {
|
||||||
|
updatedJobs = append(updatedJobs, job)
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,6 +218,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
|
|||||||
for _, job := range updatedRuns {
|
for _, job := range updatedRuns {
|
||||||
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
|
||||||
}
|
}
|
||||||
|
EmitJobsIfReadyByJobs(updatedJobs)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
121
services/actions/concurrency.go
Normal file
121
services/actions/concurrency.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
act_model "github.com/nektos/act/pkg/model"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EvaluateRunConcurrencyFillModel evaluates the expressions in a run-level (workflow) concurrency,
|
||||||
|
// and fills the run's model fields with `concurrency.group` and `concurrency.cancel-in-progress`.
|
||||||
|
// Workflow-level concurrency doesn't depend on the job outputs, so it can always be evaluated if there is no syntax error.
|
||||||
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency
|
||||||
|
func EvaluateRunConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, wfRawConcurrency *act_model.RawConcurrency, vars map[string]string) error {
|
||||||
|
if err := run.LoadAttributes(ctx); err != nil {
|
||||||
|
return fmt.Errorf("run LoadAttributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsRunCtx := GenerateGiteaContext(run, nil)
|
||||||
|
jobResults := map[string]*jobparser.JobResult{"": {}}
|
||||||
|
inputs, err := getInputsFromRun(run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get inputs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConcurrency, err := yaml.Marshal(wfRawConcurrency)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal raw concurrency: %w", err)
|
||||||
|
}
|
||||||
|
run.RawConcurrency = string(rawConcurrency)
|
||||||
|
run.ConcurrencyGroup, run.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(wfRawConcurrency, "", nil, actionsRunCtx, jobResults, vars, inputs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate concurrency: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findJobNeedsAndFillJobResults(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*jobparser.JobResult, error) {
|
||||||
|
taskNeeds, err := FindTaskNeeds(ctx, job)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find task needs: %w", err)
|
||||||
|
}
|
||||||
|
jobResults := make(map[string]*jobparser.JobResult, len(taskNeeds))
|
||||||
|
for jobID, taskNeed := range taskNeeds {
|
||||||
|
jobResult := &jobparser.JobResult{
|
||||||
|
Result: taskNeed.Result.String(),
|
||||||
|
Outputs: taskNeed.Outputs,
|
||||||
|
}
|
||||||
|
jobResults[jobID] = jobResult
|
||||||
|
}
|
||||||
|
jobResults[job.JobID] = &jobparser.JobResult{
|
||||||
|
Needs: job.Needs,
|
||||||
|
}
|
||||||
|
return jobResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvaluateJobConcurrencyFillModel evaluates the expressions in a job-level concurrency,
|
||||||
|
// and fills the job's model fields with `concurrency.group` and `concurrency.cancel-in-progress`.
|
||||||
|
// Job-level concurrency may depend on other job's outputs (via `needs`): `concurrency.group: my-group-${{ needs.job1.outputs.out1 }}`
|
||||||
|
// If the needed jobs haven't been executed yet, this evaluation will also fail.
|
||||||
|
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency
|
||||||
|
func EvaluateJobConcurrencyFillModel(ctx context.Context, run *actions_model.ActionRun, actionRunJob *actions_model.ActionRunJob, vars map[string]string) error {
|
||||||
|
if err := actionRunJob.LoadAttributes(ctx); err != nil {
|
||||||
|
return fmt.Errorf("job LoadAttributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawConcurrency act_model.RawConcurrency
|
||||||
|
if err := yaml.Unmarshal([]byte(actionRunJob.RawConcurrency), &rawConcurrency); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal raw concurrency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsJobCtx := GenerateGiteaContext(run, actionRunJob)
|
||||||
|
|
||||||
|
jobResults, err := findJobNeedsAndFillJobResults(ctx, actionRunJob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find job needs and fill job results: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs, err := getInputsFromRun(run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get inputs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleWorkflows is created from an ActionJob, which always contains exactly a single job's YAML definition.
|
||||||
|
// Ideally it shouldn't be called "Workflow", it is just a job with global workflow fields + trigger
|
||||||
|
singleWorkflows, err := jobparser.Parse(actionRunJob.WorkflowPayload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse single workflow: %w", err)
|
||||||
|
} else if len(singleWorkflows) != 1 {
|
||||||
|
return errors.New("not single workflow")
|
||||||
|
}
|
||||||
|
_, singleWorkflowJob := singleWorkflows[0].Job()
|
||||||
|
|
||||||
|
actionRunJob.ConcurrencyGroup, actionRunJob.ConcurrencyCancel, err = jobparser.EvaluateConcurrency(&rawConcurrency, actionRunJob.JobID, singleWorkflowJob, actionsJobCtx, jobResults, vars, inputs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate concurrency: %w", err)
|
||||||
|
}
|
||||||
|
actionRunJob.IsConcurrencyEvaluated = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInputsFromRun(run *actions_model.ActionRun) (map[string]any, error) {
|
||||||
|
if run.Event != "workflow_dispatch" {
|
||||||
|
return map[string]any{}, nil
|
||||||
|
}
|
||||||
|
var payload api.WorkflowDispatchPayload
|
||||||
|
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return payload.Inputs, nil
|
||||||
|
}
|
@ -10,9 +10,12 @@ import (
|
|||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
@ -25,7 +28,7 @@ type jobUpdate struct {
|
|||||||
RunID int64
|
RunID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func EmitJobsIfReady(runID int64) error {
|
func EmitJobsIfReadyByRun(runID int64) error {
|
||||||
err := jobEmitterQueue.Push(&jobUpdate{
|
err := jobEmitterQueue.Push(&jobUpdate{
|
||||||
RunID: runID,
|
RunID: runID,
|
||||||
})
|
})
|
||||||
@ -35,53 +38,77 @@ func EmitJobsIfReady(runID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EmitJobsIfReadyByJobs(jobs []*actions_model.ActionRunJob) {
|
||||||
|
checkedRuns := make(container.Set[int64])
|
||||||
|
for _, job := range jobs {
|
||||||
|
if !job.Status.IsDone() || checkedRuns.Contains(job.RunID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := EmitJobsIfReadyByRun(job.RunID); err != nil {
|
||||||
|
log.Error("Check jobs of run %d: %v", job.RunID, err)
|
||||||
|
}
|
||||||
|
checkedRuns.Add(job.RunID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func jobEmitterQueueHandler(items ...*jobUpdate) []*jobUpdate {
|
func jobEmitterQueueHandler(items ...*jobUpdate) []*jobUpdate {
|
||||||
ctx := graceful.GetManager().ShutdownContext()
|
ctx := graceful.GetManager().ShutdownContext()
|
||||||
var ret []*jobUpdate
|
var ret []*jobUpdate
|
||||||
for _, update := range items {
|
for _, update := range items {
|
||||||
if err := checkJobsOfRun(ctx, update.RunID); err != nil {
|
if err := checkJobsByRunID(ctx, update.RunID); err != nil {
|
||||||
|
log.Error("check run %d: %v", update.RunID, err)
|
||||||
ret = append(ret, update)
|
ret = append(ret, update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkJobsOfRun(ctx context.Context, runID int64) error {
|
func checkJobsByRunID(ctx context.Context, runID int64) error {
|
||||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID})
|
run, exist, err := db.GetByID[actions_model.ActionRun](ctx, runID)
|
||||||
|
if !exist {
|
||||||
|
return fmt.Errorf("run %d does not exist", runID)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("get action run: %w", err)
|
||||||
}
|
}
|
||||||
var updatedjobs []*actions_model.ActionRunJob
|
var jobs, updatedJobs []*actions_model.ActionRunJob
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
|
// check jobs of the current run
|
||||||
for _, job := range jobs {
|
if js, ujs, err := checkJobsOfRun(ctx, run); err != nil {
|
||||||
idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := newJobStatusResolver(jobs).Resolve()
|
|
||||||
for _, job := range jobs {
|
|
||||||
if status, ok := updates[job.ID]; ok {
|
|
||||||
job.Status = status
|
|
||||||
if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else if n != 1 {
|
} else {
|
||||||
return fmt.Errorf("no affected for updating blocked job %v", job.ID)
|
jobs = append(jobs, js...)
|
||||||
}
|
updatedJobs = append(updatedJobs, ujs...)
|
||||||
updatedjobs = append(updatedjobs, job)
|
|
||||||
}
|
}
|
||||||
|
if js, ujs, err := checkRunConcurrency(ctx, run); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
jobs = append(jobs, js...)
|
||||||
|
updatedJobs = append(updatedJobs, ujs...)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
CreateCommitStatus(ctx, jobs...)
|
CreateCommitStatus(ctx, jobs...)
|
||||||
for _, job := range updatedjobs {
|
for _, job := range updatedJobs {
|
||||||
_ = job.LoadAttributes(ctx)
|
_ = job.LoadAttributes(ctx)
|
||||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||||
}
|
}
|
||||||
if len(jobs) > 0 {
|
runJobs := make(map[int64][]*actions_model.ActionRunJob)
|
||||||
runUpdated := true
|
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
|
runJobs[job.RunID] = append(runJobs[job.RunID], job)
|
||||||
|
}
|
||||||
|
runUpdatedJobs := make(map[int64][]*actions_model.ActionRunJob)
|
||||||
|
for _, uj := range updatedJobs {
|
||||||
|
runUpdatedJobs[uj.RunID] = append(runUpdatedJobs[uj.RunID], uj)
|
||||||
|
}
|
||||||
|
for runID, js := range runJobs {
|
||||||
|
if len(runUpdatedJobs[runID]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runUpdated := true
|
||||||
|
for _, job := range js {
|
||||||
if !job.Status.IsDone() {
|
if !job.Status.IsDone() {
|
||||||
runUpdated = false
|
runUpdated = false
|
||||||
break
|
break
|
||||||
@ -94,6 +121,118 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findBlockedRunByConcurrency finds the blocked concurrent run in a repo and returns `nil, nil` when there is no blocked run.
|
||||||
|
func findBlockedRunByConcurrency(ctx context.Context, repoID int64, concurrencyGroup string) (*actions_model.ActionRun, error) {
|
||||||
|
if concurrencyGroup == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
cRuns, cJobs, err := actions_model.GetConcurrentRunsAndJobs(ctx, repoID, concurrencyGroup, []actions_model.Status{actions_model.StatusBlocked})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There can be at most one blocked run or job
|
||||||
|
var concurrentRun *actions_model.ActionRun
|
||||||
|
if len(cRuns) > 0 {
|
||||||
|
concurrentRun = cRuns[0]
|
||||||
|
} else if len(cJobs) > 0 {
|
||||||
|
jobRun, exist, err := db.GetByID[actions_model.ActionRun](ctx, cJobs[0].RunID)
|
||||||
|
if !exist {
|
||||||
|
return nil, fmt.Errorf("run %d does not exist", cJobs[0].RunID)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get run by job %d: %w", cJobs[0].ID, err)
|
||||||
|
}
|
||||||
|
concurrentRun = jobRun
|
||||||
|
}
|
||||||
|
|
||||||
|
return concurrentRun, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) {
|
||||||
|
checkedConcurrencyGroup := make(container.Set[string])
|
||||||
|
|
||||||
|
// check run (workflow-level) concurrency
|
||||||
|
if run.ConcurrencyGroup != "" {
|
||||||
|
concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, run.ConcurrencyGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err)
|
||||||
|
}
|
||||||
|
if concurrentRun != nil && !concurrentRun.NeedApproval {
|
||||||
|
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
jobs = append(jobs, js...)
|
||||||
|
updatedJobs = append(updatedJobs, ujs...)
|
||||||
|
}
|
||||||
|
checkedConcurrencyGroup.Add(run.ConcurrencyGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check job concurrency
|
||||||
|
runJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("find run %d jobs: %w", run.ID, err)
|
||||||
|
}
|
||||||
|
for _, job := range runJobs {
|
||||||
|
if !job.Status.IsDone() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if job.ConcurrencyGroup == "" && checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
concurrentRun, err := findBlockedRunByConcurrency(ctx, job.RepoID, job.ConcurrencyGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err)
|
||||||
|
}
|
||||||
|
if concurrentRun != nil && !concurrentRun.NeedApproval {
|
||||||
|
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
jobs = append(jobs, js...)
|
||||||
|
updatedJobs = append(updatedJobs, ujs...)
|
||||||
|
}
|
||||||
|
checkedConcurrencyGroup.Add(job.ConcurrencyGroup)
|
||||||
|
}
|
||||||
|
return jobs, updatedJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkJobsOfRun(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) {
|
||||||
|
jobs, err = db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
for _, job := range jobs {
|
||||||
|
job.Run = run
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := newJobStatusResolver(jobs, vars).Resolve(ctx)
|
||||||
|
for _, job := range jobs {
|
||||||
|
if status, ok := updates[job.ID]; ok {
|
||||||
|
job.Status = status
|
||||||
|
if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if n != 1 {
|
||||||
|
return fmt.Errorf("no affected for updating blocked job %v", job.ID)
|
||||||
|
}
|
||||||
|
updatedJobs = append(updatedJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs, updatedJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) {
|
func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) {
|
||||||
job.Run = nil
|
job.Run = nil
|
||||||
if err := job.LoadAttributes(ctx); err != nil {
|
if err := job.LoadAttributes(ctx); err != nil {
|
||||||
@ -107,9 +246,10 @@ type jobStatusResolver struct {
|
|||||||
statuses map[int64]actions_model.Status
|
statuses map[int64]actions_model.Status
|
||||||
needs map[int64][]int64
|
needs map[int64][]int64
|
||||||
jobMap map[int64]*actions_model.ActionRunJob
|
jobMap map[int64]*actions_model.ActionRunJob
|
||||||
|
vars map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
|
func newJobStatusResolver(jobs actions_model.ActionJobList, vars map[string]string) *jobStatusResolver {
|
||||||
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
|
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
|
||||||
jobMap := make(map[int64]*actions_model.ActionRunJob)
|
jobMap := make(map[int64]*actions_model.ActionRunJob)
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
@ -131,13 +271,14 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
|
|||||||
statuses: statuses,
|
statuses: statuses,
|
||||||
needs: needs,
|
needs: needs,
|
||||||
jobMap: jobMap,
|
jobMap: jobMap,
|
||||||
|
vars: vars,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status {
|
func (r *jobStatusResolver) Resolve(ctx context.Context) map[int64]actions_model.Status {
|
||||||
ret := map[int64]actions_model.Status{}
|
ret := map[int64]actions_model.Status{}
|
||||||
for i := 0; i < len(r.statuses); i++ {
|
for i := 0; i < len(r.statuses); i++ {
|
||||||
updated := r.resolve()
|
updated := r.resolve(ctx)
|
||||||
if len(updated) == 0 {
|
if len(updated) == 0 {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@ -149,13 +290,8 @@ func (r *jobStatusResolver) Resolve() map[int64]actions_model.Status {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
|
func (r *jobStatusResolver) resolveCheckNeeds(id int64) (allDone, allSucceed bool) {
|
||||||
ret := map[int64]actions_model.Status{}
|
allDone, allSucceed = true, true
|
||||||
for id, status := range r.statuses {
|
|
||||||
if status != actions_model.StatusBlocked {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allDone, allSucceed := true, true
|
|
||||||
for _, need := range r.needs[id] {
|
for _, need := range r.needs[id] {
|
||||||
needStatus := r.statuses[need]
|
needStatus := r.statuses[need]
|
||||||
if !needStatus.IsDone() {
|
if !needStatus.IsDone() {
|
||||||
@ -165,27 +301,75 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
|
|||||||
allSucceed = false
|
allSucceed = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allDone {
|
return allDone, allSucceed
|
||||||
if allSucceed {
|
}
|
||||||
ret[id] = actions_model.StatusWaiting
|
|
||||||
} else {
|
func (r *jobStatusResolver) resolveJobHasIfCondition(actionRunJob *actions_model.ActionRunJob) (hasIf bool) {
|
||||||
// Check if the job has an "if" condition
|
if wfJobs, _ := jobparser.Parse(actionRunJob.WorkflowPayload); len(wfJobs) == 1 {
|
||||||
hasIf := false
|
|
||||||
if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
|
|
||||||
_, wfJob := wfJobs[0].Job()
|
_, wfJob := wfJobs[0].Job()
|
||||||
hasIf = len(wfJob.If.Value) > 0
|
hasIf = len(wfJob.If.Value) > 0
|
||||||
}
|
}
|
||||||
|
return hasIf
|
||||||
|
}
|
||||||
|
|
||||||
if hasIf {
|
func (r *jobStatusResolver) resolve(ctx context.Context) map[int64]actions_model.Status {
|
||||||
// act_runner will check the "if" condition
|
ret := map[int64]actions_model.Status{}
|
||||||
ret[id] = actions_model.StatusWaiting
|
for id, status := range r.statuses {
|
||||||
} else {
|
actionRunJob := r.jobMap[id]
|
||||||
// If the "if" condition is empty and not all dependent jobs completed successfully,
|
if status != actions_model.StatusBlocked {
|
||||||
// the job should be skipped.
|
continue
|
||||||
ret[id] = actions_model.StatusSkipped
|
}
|
||||||
|
allDone, allSucceed := r.resolveCheckNeeds(id)
|
||||||
|
if !allDone {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// update concurrency and check whether the job can run now
|
||||||
|
err := updateConcurrencyEvaluationForJobWithNeeds(ctx, actionRunJob, r.vars)
|
||||||
|
if err != nil {
|
||||||
|
// The err can be caused by different cases: database error, or syntax error, or the needed jobs haven't completed
|
||||||
|
// At the moment there is no way to distinguish them.
|
||||||
|
// Actually, for most cases, the error is caused by "syntax error" / "the needed jobs haven't completed (skipped?)"
|
||||||
|
// TODO: if workflow or concurrency expression has syntax error, there should be a user error message, need to show it to end users
|
||||||
|
log.Debug("updateConcurrencyEvaluationForJobWithNeeds failed, this job will stay blocked: job: %d, err: %v", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldStartJob := true
|
||||||
|
if !allSucceed {
|
||||||
|
// Not all dependent jobs completed successfully:
|
||||||
|
// * if the job has "if" condition, it can be started, then the act_runner will evaluate the "if" condition.
|
||||||
|
// * otherwise, the job should be skipped.
|
||||||
|
shouldStartJob = r.resolveJobHasIfCondition(actionRunJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
newStatus := util.Iif(shouldStartJob, actions_model.StatusWaiting, actions_model.StatusSkipped)
|
||||||
|
if newStatus == actions_model.StatusWaiting {
|
||||||
|
newStatus, err = PrepareToStartJobWithConcurrency(ctx, actionRunJob)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ShouldBlockJobByConcurrency failed, this job will stay blocked: job: %d, err: %v", id, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newStatus != actions_model.StatusBlocked {
|
||||||
|
ret[id] = newStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateConcurrencyEvaluationForJobWithNeeds(ctx context.Context, actionRunJob *actions_model.ActionRunJob, vars map[string]string) error {
|
||||||
|
if setting.IsInTesting && actionRunJob.RepoID == 0 {
|
||||||
|
return nil // for testing purpose only, no repo, no evaluation
|
||||||
|
}
|
||||||
|
|
||||||
|
err := EvaluateJobConcurrencyFillModel(ctx, actionRunJob.Run, actionRunJob, vars)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := actions_model.UpdateRunJob(ctx, actionRunJob, nil, "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"); err != nil {
|
||||||
|
return fmt.Errorf("update run job: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -129,8 +129,8 @@ jobs:
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := newJobStatusResolver(tt.jobs)
|
r := newJobStatusResolver(tt.jobs, nil)
|
||||||
assert.Equal(t, tt.want, r.Resolve())
|
assert.Equal(t, tt.want, r.Resolve(t.Context()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,6 +357,19 @@ func handleWorkflows(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(dwf.Content)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ReadWorkflowRawConcurrency: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if wfRawConcurrency != nil {
|
||||||
|
err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("EvaluateRunConcurrencyFillModel: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
giteaCtx := GenerateGiteaContext(run, nil)
|
giteaCtx := GenerateGiteaContext(run, nil)
|
||||||
|
|
||||||
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars), jobparser.WithGitContext(giteaCtx.ToGitHubContext()))
|
||||||
@ -369,21 +382,7 @@ func handleWorkflows(
|
|||||||
run.Title = jobs[0].RunName
|
run.Title = jobs[0].RunName
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel running jobs if the event is push or pull_request_sync
|
if err := InsertRun(ctx, run, jobs); err != nil {
|
||||||
if run.Event == webhook_module.HookEventPush ||
|
|
||||||
run.Event == webhook_module.HookEventPullRequestSync {
|
|
||||||
if err := CancelPreviousJobs(
|
|
||||||
ctx,
|
|
||||||
run.RepoID,
|
|
||||||
run.Ref,
|
|
||||||
run.WorkflowID,
|
|
||||||
run.Event,
|
|
||||||
); err != nil {
|
|
||||||
log.Error("CancelPreviousJobs: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
|
|
||||||
log.Error("InsertRun: %v", err)
|
log.Error("InsertRun: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
127
services/actions/run.go
Normal file
127
services/actions/run.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InsertRun inserts a run
|
||||||
|
// The title will be cut off at 255 characters if it's longer than 255 characters.
|
||||||
|
func InsertRun(ctx context.Context, run *actions_model.ActionRun, jobs []*jobparser.SingleWorkflow) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
run.Index = index
|
||||||
|
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
||||||
|
|
||||||
|
// check run (workflow-level) concurrency
|
||||||
|
run.Status, err = PrepareToStartRunWithConcurrency(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Insert(ctx, run); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := run.LoadRepo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := actions_model.UpdateRepoRunsNumbers(ctx, run.Repo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// query vars for evaluating job concurrency groups
|
||||||
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get run %d variables: %w", run.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runJobs := make([]*actions_model.ActionRunJob, 0, len(jobs))
|
||||||
|
var hasWaitingJobs bool
|
||||||
|
for _, v := range jobs {
|
||||||
|
id, job := v.Job()
|
||||||
|
needs := job.Needs()
|
||||||
|
if err := v.SetJob(id, job.EraseNeeds()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload, _ := v.Marshal()
|
||||||
|
|
||||||
|
shouldBlockJob := len(needs) > 0 || run.NeedApproval || run.Status == actions_model.StatusBlocked
|
||||||
|
|
||||||
|
job.Name = util.EllipsisDisplayString(job.Name, 255)
|
||||||
|
runJob := &actions_model.ActionRunJob{
|
||||||
|
RunID: run.ID,
|
||||||
|
RepoID: run.RepoID,
|
||||||
|
OwnerID: run.OwnerID,
|
||||||
|
CommitSHA: run.CommitSHA,
|
||||||
|
IsForkPullRequest: run.IsForkPullRequest,
|
||||||
|
Name: job.Name,
|
||||||
|
WorkflowPayload: payload,
|
||||||
|
JobID: id,
|
||||||
|
Needs: needs,
|
||||||
|
RunsOn: job.RunsOn(),
|
||||||
|
Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting),
|
||||||
|
}
|
||||||
|
// check job concurrency
|
||||||
|
if job.RawConcurrency != nil {
|
||||||
|
rawConcurrency, err := yaml.Marshal(job.RawConcurrency)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal raw concurrency: %w", err)
|
||||||
|
}
|
||||||
|
runJob.RawConcurrency = string(rawConcurrency)
|
||||||
|
|
||||||
|
// do not evaluate job concurrency when it requires `needs`, the jobs with `needs` will be evaluated later by job emitter
|
||||||
|
if len(needs) == 0 {
|
||||||
|
err = EvaluateJobConcurrencyFillModel(ctx, run, runJob, vars)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate job concurrency: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a job needs other jobs ("needs" is not empty), its status is set to StatusBlocked at the entry of the loop
|
||||||
|
// No need to check job concurrency for a blocked job (it will be checked by job emitter later)
|
||||||
|
if runJob.Status == actions_model.StatusWaiting {
|
||||||
|
runJob.Status, err = PrepareToStartJobWithConcurrency(ctx, runJob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("prepare to start job with concurrency: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWaitingJobs = hasWaitingJobs || runJob.Status == actions_model.StatusWaiting
|
||||||
|
if err := db.Insert(ctx, runJob); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
runJobs = append(runJobs, runJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
run.Status = actions_model.AggregateJobStatus(runJobs)
|
||||||
|
if err := actions_model.UpdateRun(ctx, run, "status"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a job in the waiting status, increase tasks version.
|
||||||
|
if hasWaitingJobs {
|
||||||
|
if err := actions_model.IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
@ -53,20 +53,6 @@ func startTasks(ctx context.Context) error {
|
|||||||
|
|
||||||
// Loop through each spec and create a schedule task for it
|
// Loop through each spec and create a schedule task for it
|
||||||
for _, row := range specs {
|
for _, row := range specs {
|
||||||
// cancel running jobs if the event is push
|
|
||||||
if row.Schedule.Event == webhook_module.HookEventPush {
|
|
||||||
// cancel running jobs of the same workflow
|
|
||||||
if err := CancelPreviousJobs(
|
|
||||||
ctx,
|
|
||||||
row.RepoID,
|
|
||||||
row.Schedule.Ref,
|
|
||||||
row.Schedule.WorkflowID,
|
|
||||||
webhook_module.HookEventSchedule,
|
|
||||||
); err != nil {
|
|
||||||
log.Error("CancelPreviousJobs: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if row.Repo.IsArchived {
|
if row.Repo.IsArchived {
|
||||||
// Skip if the repo is archived
|
// Skip if the repo is archived
|
||||||
continue
|
continue
|
||||||
@ -144,9 +130,19 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
wfRawConcurrency, err := jobparser.ReadWorkflowRawConcurrency(cron.Content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if wfRawConcurrency != nil {
|
||||||
|
err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the action run and its associated jobs into the database
|
// Insert the action run and its associated jobs into the database
|
||||||
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
if err := InsertRun(ctx, run, workflows); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||||
|
@ -100,6 +100,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
// find workflow from commit
|
// find workflow from commit
|
||||||
var workflows []*jobparser.SingleWorkflow
|
var workflows []*jobparser.SingleWorkflow
|
||||||
var entry *git.TreeEntry
|
var entry *git.TreeEntry
|
||||||
|
var wfRawConcurrency *model.RawConcurrency
|
||||||
|
|
||||||
run := &actions_model.ActionRun{
|
run := &actions_model.ActionRun{
|
||||||
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
|
||||||
@ -170,6 +171,11 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wfRawConcurrency, err = jobparser.ReadWorkflowRawConcurrency(content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
|
// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
|
||||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||||
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
|
// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
|
||||||
@ -187,19 +193,20 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
|
|||||||
}
|
}
|
||||||
run.EventPayload = string(eventPayload)
|
run.EventPayload = string(eventPayload)
|
||||||
|
|
||||||
// cancel running jobs of the same workflow
|
// cancel running jobs of the same concurrency group
|
||||||
if err := CancelPreviousJobs(
|
if wfRawConcurrency != nil {
|
||||||
ctx,
|
vars, err := actions_model.GetVariablesOfRun(ctx, run)
|
||||||
run.RepoID,
|
if err != nil {
|
||||||
run.Ref,
|
return fmt.Errorf("GetVariablesOfRun: %w", err)
|
||||||
run.WorkflowID,
|
}
|
||||||
run.Event,
|
err = EvaluateRunConcurrencyFillModel(ctx, run, wfRawConcurrency, vars)
|
||||||
); err != nil {
|
if err != nil {
|
||||||
log.Error("CancelRunningJobs: %v", err)
|
return fmt.Errorf("EvaluateRunConcurrencyFillModel: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the action run and its associated jobs into the database
|
// Insert the action run and its associated jobs into the database
|
||||||
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
if err := InsertRun(ctx, run, workflows); err != nil {
|
||||||
return fmt.Errorf("InsertRun: %w", err)
|
return fmt.Errorf("InsertRun: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1709
tests/integration/actions_concurrency_test.go
Normal file
1709
tests/integration/actions_concurrency_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user