mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-17 09:05:56 +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:
@@ -380,6 +380,11 @@ func (r *jobStatusResolver) resolveCheckNeeds(id int64) (allDone, allSucceed boo
|
||||
if !needStatus.IsDone() {
|
||||
allDone = false
|
||||
}
|
||||
// A failed need with continue-on-error:true is treated as success, matching AggregateJobStatus,
|
||||
// so a downstream job with an implicit `success()` is not skipped.
|
||||
if needJob := r.jobMap[need]; needJob != nil && needJob.ContinueOnError && needStatus == actions_model.StatusFailure {
|
||||
continue
|
||||
}
|
||||
if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) {
|
||||
allSucceed = false
|
||||
}
|
||||
|
||||
@@ -131,6 +131,24 @@ jobs:
|
||||
},
|
||||
want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
|
||||
},
|
||||
{
|
||||
name: "`if` is empty and a failed need has continue-on-error",
|
||||
jobs: actions_model.ActionJobList{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, ContinueOnError: true, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
`
|
||||
name: test
|
||||
on: push
|
||||
jobs:
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: job1
|
||||
steps:
|
||||
- run: echo "should run, job1 failure is masked by continue-on-error"
|
||||
`)},
|
||||
},
|
||||
want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -506,6 +506,7 @@ func cloneRunJobForAttempt(templateJob *actions_model.ActionRunJob, attempt *act
|
||||
AttemptJobID: templateJob.AttemptJobID,
|
||||
Needs: slices.Clone(templateJob.Needs),
|
||||
RunsOn: slices.Clone(templateJob.RunsOn),
|
||||
ContinueOnError: templateJob.ContinueOnError,
|
||||
Status: templateJob.Status,
|
||||
RawConcurrency: templateJob.RawConcurrency,
|
||||
IsConcurrencyEvaluated: templateJob.IsConcurrencyEvaluated,
|
||||
|
||||
@@ -80,6 +80,22 @@ func TestGetFailedJobsForRerun(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCloneRunJobForAttempt(t *testing.T) {
|
||||
attempt := &actions_model.ActionRunAttempt{ID: 42, Attempt: 2}
|
||||
|
||||
t.Run("preserves continue-on-error", func(t *testing.T) {
|
||||
template := &actions_model.ActionRunJob{ContinueOnError: true, Status: actions_model.StatusFailure}
|
||||
clone := cloneRunJobForAttempt(template, attempt)
|
||||
assert.True(t, clone.ContinueOnError)
|
||||
})
|
||||
|
||||
t.Run("defaults to false when template has it unset", func(t *testing.T) {
|
||||
template := &actions_model.ActionRunJob{ContinueOnError: false}
|
||||
clone := cloneRunJobForAttempt(template, attempt)
|
||||
assert.False(t, clone.ContinueOnError)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRerunValidation(t *testing.T) {
|
||||
runningRun := &actions_model.ActionRun{Status: actions_model.StatusRunning}
|
||||
|
||||
|
||||
@@ -325,6 +325,7 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att
|
||||
AttemptJobID: attemptJobID,
|
||||
Needs: needs,
|
||||
RunsOn: parsedChild.RunsOn(),
|
||||
ContinueOnError: parsedChild.GetContinueOnError(),
|
||||
Status: actions_model.StatusBlocked,
|
||||
ParentJobID: caller.ID,
|
||||
WorkflowSourceRepoID: sourceRepoID,
|
||||
|
||||
@@ -164,6 +164,7 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte
|
||||
Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting),
|
||||
WorkflowSourceRepoID: run.RepoID,
|
||||
WorkflowSourceCommitSHA: run.CommitSHA,
|
||||
ContinueOnError: job.GetContinueOnError(),
|
||||
}
|
||||
// Parse workflow/job permissions (no clamping here)
|
||||
if perms := ExtractJobPermissionsFromWorkflow(v, job); perms != nil {
|
||||
|
||||
Reference in New Issue
Block a user