mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-12 22:11:07 +00:00
feat(actions): implement jobs.<job_id>.continue-on-error (#38100)
Support `continue-on-error` for workflow jobs when aggregating an Actions workflow run status. Previously, `continue-on-error` was parsed from workflow YAML but was not persisted or used when calculating the overall run result. As a result, a failed job could incorrectly fail the entire workflow even when the workflow explicitly allowed that job to fail. This PR stores the parsed `continue-on-error` value on each action run job and treats failed jobs with `continue-on-error: true` as successful when computing the workflow run status, matching GitHub Actions behavior. ## Changes - Add `ContinueOnError` to `jobparser.Job`. - Add `continue_on_error` to `ActionRunJob` with a `NOT NULL DEFAULT FALSE` migration. - Populate `ActionRunJob.ContinueOnError` when creating workflow run jobs. - Update workflow status aggregation so failed `continue-on-error` jobs do not fail the overall run. - Leave `resolveCheckNeeds` unchanged so dependent jobs still see the job result as `failure` and are skipped by default. ## Compatibility This is backward compatible. If only the runner or only the server is updated, `continue-on-error` continues to degrade to the previous behavior and is effectively ignored until both sides support it. Related runner PR: https://gitea.com/gitea/runner/pulls/1032 --------- Signed-off-by: bircni <bircni@icloud.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
@@ -108,6 +108,10 @@ type ActionRunJob struct {
|
||||
// ParentJobID scopes `Needs` resolution: name lookups happen only among rows sharing the same ParentJobID. 0 for top-level rows.
|
||||
ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"`
|
||||
|
||||
// ContinueOnError mirrors the job-level continue-on-error field from the workflow YAML.
|
||||
// When true, a failure of this job does not fail the overall workflow run.
|
||||
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
|
||||
Started timeutil.TimeStamp
|
||||
Stopped timeutil.TimeStamp
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
@@ -500,9 +504,12 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
allSkipped := len(jobs) != 0
|
||||
var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool
|
||||
for _, job := range jobs {
|
||||
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
|
||||
// A failed job with continue-on-error:true does not fail the workflow run.
|
||||
// It counts as a "continued failure" and is treated like success for aggregation.
|
||||
isContinuedFailure := job.ContinueOnError && job.Status == StatusFailure
|
||||
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped || isContinuedFailure)
|
||||
allSkipped = allSkipped && job.Status == StatusSkipped
|
||||
hasFailure = hasFailure || job.Status == StatusFailure
|
||||
hasFailure = hasFailure || (job.Status == StatusFailure && !job.ContinueOnError)
|
||||
hasCancelled = hasCancelled || job.Status == StatusCancelled
|
||||
hasCancelling = hasCancelling || job.Status == StatusCancelling
|
||||
hasWaiting = hasWaiting || job.Status == StatusWaiting
|
||||
|
||||
@@ -48,3 +48,57 @@ func TestStatusFromResult(t *testing.T) {
|
||||
assert.Equal(t, tt.want, StatusFromResult(tt.result), "result=%s", tt.result)
|
||||
}
|
||||
}
|
||||
|
||||
func newJob(status Status, continueOnError bool) *ActionRunJob {
|
||||
return &ActionRunJob{Status: status, ContinueOnError: continueOnError}
|
||||
}
|
||||
|
||||
func TestAggregateJobStatusContinueOnError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
jobs []*ActionRunJob
|
||||
want Status
|
||||
}{
|
||||
{
|
||||
name: "all success",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusSuccess, false)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "one failure without continue-on-error",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, false)},
|
||||
want: StatusFailure,
|
||||
},
|
||||
{
|
||||
name: "one failure with continue-on-error",
|
||||
jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, true)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "only continued-failure",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
{
|
||||
name: "continued-failure plus real failure",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusFailure, false)},
|
||||
want: StatusFailure,
|
||||
},
|
||||
{
|
||||
name: "all skipped",
|
||||
jobs: []*ActionRunJob{newJob(StatusSkipped, false), newJob(StatusSkipped, false)},
|
||||
want: StatusSkipped,
|
||||
},
|
||||
{
|
||||
name: "continued-failure plus skipped counts as success",
|
||||
jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusSkipped, false)},
|
||||
want: StatusSuccess,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, AggregateJobStatus(tt.jobs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam),
|
||||
newMigration(338, "Expand legacy MSSQL issue/comment long-text columns", v1_27.ExpandIssueAndCommentLongTextFieldsForMSSQL),
|
||||
newMigration(339, "Extend action c_u index to include created_unix for faster dashboard feed queries", v1_27.AddCreatedUnixToActionUserIsDeletedIndex),
|
||||
newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob,
|
||||
// storing the job-level continue-on-error value from the workflow YAML.
|
||||
func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error {
|
||||
type ActionRunJob struct {
|
||||
ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
}
|
||||
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ActionRunJob))
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user