mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-12-07 05:59:46 +00:00
feat(actions): support referencing ${{ needs... }} variables in runs-on (#10308)
Allows referencing the outputs of previously executed jobs in the `runs-on` field directly by a `${{ needs.some-job.outputs.some-output }}`, and also *indirectly* through the job's `strategy.matrix`. At its most complicated, supports a workflow with dynamic matrices like this:
```yaml
jobs:
define-matrix:
runs-on: docker
outputs:
array-value: ${{ steps.define.outputs.array }}
steps:
- id: define
run: |
echo 'array=["debian-bookworm", "debian-trixie"]' >> "$FORGEJO_OUTPUT"
runs-on-dynamic-matrix:
needs: define-matrix
strategy:
matrix:
my-runners: ${{ fromJSON(needs.define-matrix.outputs.array-value) }}
runs-on: ${{ matrix.my-runners }}
steps:
- run: uname -a
```
## Checklist
The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).
### Tests
- I added test coverage for Go changes...
- [x] in their respective `*_test.go` for unit tests.
- [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
- [ ] in `web_src/js/*.test.js` if it can be unit tested.
- [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).
### Documentation
- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.
- Documentation already (incorrectly) states that `jobs.<job-id>.runs-on` can access the `needs` context. 😛 https://forgejo.org/docs/latest/user/actions/reference/#availability
### Release notes
- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.
<!--start release-notes-assistant-->
## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
- [PR](https://codeberg.org/forgejo/forgejo/pulls/10308): <!--number 10308 --><!--line 0 --><!--description ZmVhdChhY3Rpb25zKTogc3VwcG9ydCByZWZlcmVuY2luZyBgJHt7IG5lZWRzLi4uIH19YCB2YXJpYWJsZXMgaW4gYHJ1bnMtb25g-->feat(actions): support referencing `${{ needs... }}` variables in `runs-on`<!--description-->
<!--end release-notes-assistant-->
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10308
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
parent
0ecc6ef632
commit
ffbd500600
21 changed files with 958 additions and 53 deletions
|
|
@ -18,9 +18,14 @@ type PreExecutionError int64
|
|||
const (
|
||||
ErrorCodeEventDetectionError PreExecutionError = iota + 1
|
||||
ErrorCodeJobParsingError
|
||||
ErrorCodePersistentIncompleteMatrix
|
||||
ErrorCodePersistentIncompleteMatrix // obsolete
|
||||
ErrorCodeIncompleteMatrixMissingJob
|
||||
ErrorCodeIncompleteMatrixMissingOutput
|
||||
ErrorCodeIncompleteMatrixUnknownCause
|
||||
ErrorCodeIncompleteRunsOnMissingJob
|
||||
ErrorCodeIncompleteRunsOnMissingOutput
|
||||
ErrorCodeIncompleteRunsOnMissingMatrixDimension
|
||||
ErrorCodeIncompleteRunsOnUnknownCause
|
||||
)
|
||||
|
||||
func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string {
|
||||
|
|
@ -42,6 +47,16 @@ func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string
|
|||
return lang.TrString("actions.workflow.incomplete_matrix_missing_job", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteMatrixMissingOutput:
|
||||
return lang.TrString("actions.workflow.incomplete_matrix_missing_output", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteMatrixUnknownCause:
|
||||
return lang.TrString("actions.workflow.incomplete_matrix_unknown_cause", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteRunsOnMissingJob:
|
||||
return lang.TrString("actions.workflow.incomplete_runson_missing_job", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteRunsOnMissingOutput:
|
||||
return lang.TrString("actions.workflow.incomplete_runson_missing_output", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteRunsOnMissingMatrixDimension:
|
||||
return lang.TrString("actions.workflow.incomplete_runson_missing_matrix_dimension", run.PreExecutionErrorDetails...)
|
||||
case ErrorCodeIncompleteRunsOnUnknownCause:
|
||||
return lang.TrString("actions.workflow.incomplete_runson_unknown_cause", run.PreExecutionErrorDetails...)
|
||||
}
|
||||
return fmt.Sprintf("<unsupported error: code=%v details=%#v", run.PreExecutionErrorCode, run.PreExecutionErrorDetails)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,46 @@ func TestTranslatePreExecutionError(t *testing.T) {
|
|||
},
|
||||
expected: "Unable to evaluate `strategy.matrix` of job blocked_job: job other_job is not in the `needs` list of job blocked_job (needs-1, needs-2).",
|
||||
},
|
||||
{
|
||||
name: "ErrorCodeIncompleteMatrixUnknownCause",
|
||||
run: &ActionRun{
|
||||
PreExecutionErrorCode: ErrorCodeIncompleteMatrixUnknownCause,
|
||||
PreExecutionErrorDetails: []any{"blocked_job"},
|
||||
},
|
||||
expected: "Unable to evaluate `strategy.matrix` of job blocked_job: unknown error.",
|
||||
},
|
||||
{
|
||||
name: "ErrorCodeIncompleteRunsOnMissingOutput",
|
||||
run: &ActionRun{
|
||||
PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingOutput,
|
||||
PreExecutionErrorDetails: []any{"blocked_job", "other_job", "some_output"},
|
||||
},
|
||||
expected: "Unable to evaluate `runs-on` of job blocked_job: job other_job does not have an output some_output.",
|
||||
},
|
||||
{
|
||||
name: "ErrorCodeIncompleteRunsOnMissingJob",
|
||||
run: &ActionRun{
|
||||
PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingJob,
|
||||
PreExecutionErrorDetails: []any{"blocked_job", "other_job", "needs-1, needs-2"},
|
||||
},
|
||||
expected: "Unable to evaluate `runs-on` of job blocked_job: job other_job is not in the `needs` list of job blocked_job (needs-1, needs-2).",
|
||||
},
|
||||
{
|
||||
name: "ErrorCodeIncompleteRunsOnMissingMatrixDimension",
|
||||
run: &ActionRun{
|
||||
PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingMatrixDimension,
|
||||
PreExecutionErrorDetails: []any{"blocked_job", "platfurm"},
|
||||
},
|
||||
expected: "Unable to evaluate `runs-on` of job blocked_job: matrix dimension platfurm does not exist.",
|
||||
},
|
||||
{
|
||||
name: "ErrorCodeIncompleteRunsOnUnknownCause",
|
||||
run: &ActionRun{
|
||||
PreExecutionErrorCode: ErrorCodeIncompleteRunsOnUnknownCause,
|
||||
PreExecutionErrorDetails: []any{"blocked_job"},
|
||||
},
|
||||
expected: "Unable to evaluate `runs-on` of job blocked_job: unknown error.",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ func InsertRunJobs(ctx context.Context, run *ActionRun, jobs []*jobparser.Single
|
|||
}
|
||||
payload, _ = v.Marshal()
|
||||
|
||||
if len(needs) > 0 || run.NeedApproval || v.IncompleteMatrix {
|
||||
if len(needs) > 0 || run.NeedApproval || v.IncompleteMatrix || v.IncompleteRunsOn {
|
||||
status = StatusBlocked
|
||||
} else {
|
||||
status = StatusWaiting
|
||||
|
|
|
|||
|
|
@ -289,3 +289,13 @@ func (job *ActionRunJob) IsIncompleteMatrix() (bool, *jobparser.IncompleteNeeds,
|
|||
}
|
||||
return jobWorkflow.IncompleteMatrix, jobWorkflow.IncompleteMatrixNeeds, nil
|
||||
}
|
||||
|
||||
// Checks whether the target job has a `runs-on` field with an expression that requires an input from another job. The
|
||||
// job will be blocked until the other job is complete, and then regenerated and deleted.
|
||||
func (job *ActionRunJob) IsIncompleteRunsOn() (bool, *jobparser.IncompleteNeeds, *jobparser.IncompleteMatrix, error) {
|
||||
jobWorkflow, err := job.decodeWorkflowPayload()
|
||||
if err != nil {
|
||||
return false, nil, nil, fmt.Errorf("failure decoding workflow payload: %w", err)
|
||||
}
|
||||
return jobWorkflow.IncompleteRunsOn, jobWorkflow.IncompleteRunsOnNeeds, jobWorkflow.IncompleteRunsOnMatrix, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,3 +189,51 @@ func TestActionRunJob_IsIncompleteMatrix(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionRunJob_IsIncompleteRunsOn(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
job ActionRunJob
|
||||
isIncomplete bool
|
||||
needs *jobparser.IncompleteNeeds
|
||||
matrix *jobparser.IncompleteMatrix
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "normal workflow",
|
||||
job: ActionRunJob{WorkflowPayload: []byte("name: workflow")},
|
||||
isIncomplete: false,
|
||||
},
|
||||
{
|
||||
name: "nincomplete_runs_on workflow",
|
||||
job: ActionRunJob{WorkflowPayload: []byte("name: workflow\nincomplete_runs_on: true\nincomplete_runs_on_needs: { job: abc }")},
|
||||
needs: &jobparser.IncompleteNeeds{Job: "abc"},
|
||||
isIncomplete: true,
|
||||
},
|
||||
{
|
||||
name: "nincomplete_runs_on workflow",
|
||||
job: ActionRunJob{WorkflowPayload: []byte("name: workflow\nincomplete_runs_on: true\nincomplete_runs_on_matrix: { dimension: abc }")},
|
||||
matrix: &jobparser.IncompleteMatrix{Dimension: "abc"},
|
||||
isIncomplete: true,
|
||||
},
|
||||
{
|
||||
name: "unparseable workflow",
|
||||
job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_runs_on: true")},
|
||||
errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
isIncomplete, needs, matrix, err := tt.job.IsIncompleteRunsOn()
|
||||
if tt.errContains != "" {
|
||||
assert.ErrorContains(t, err, tt.errContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.isIncomplete, isIncomplete)
|
||||
assert.Equal(t, tt.needs, needs)
|
||||
assert.Equal(t, tt.matrix, matrix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,3 +238,37 @@ jobs:
|
|||
// Expect job with an incomplete matrix to be StatusBlocked:
|
||||
assert.Equal(t, StatusBlocked, job.Status)
|
||||
}
|
||||
|
||||
func TestActionRun_IncompleteRunsOn(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
pullRequestPosterID := int64(4)
|
||||
repoID := int64(10)
|
||||
pullRequestID := int64(2)
|
||||
runDoesNotNeedApproval := &ActionRun{
|
||||
RepoID: repoID,
|
||||
PullRequestID: pullRequestID,
|
||||
PullRequestPosterID: pullRequestPosterID,
|
||||
}
|
||||
|
||||
workflowRaw := []byte(`
|
||||
jobs:
|
||||
job2:
|
||||
runs-on: ${{ needs.other-job.outputs.some-output }}
|
||||
steps:
|
||||
- run: true
|
||||
`)
|
||||
workflows, err := jobparser.Parse(workflowRaw, false, jobparser.WithJobOutputs(map[string]map[string]string{}), jobparser.SupportIncompleteRunsOn())
|
||||
require.NoError(t, err)
|
||||
require.True(t, workflows[0].IncompleteRunsOn) // must be set for this test scenario to be valid
|
||||
|
||||
require.NoError(t, InsertRun(t.Context(), runDoesNotNeedApproval, workflows))
|
||||
|
||||
jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: runDoesNotNeedApproval.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
job := jobs[0]
|
||||
|
||||
// Expect job with an incomplete runs-on to be StatusBlocked:
|
||||
assert.Equal(t, StatusBlocked, job.Status)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,6 +217,11 @@
|
|||
"actions.workflow.persistent_incomplete_matrix": "Unable to evaluate `strategy.matrix` of job %[1]s due to a `needs` expression that was invalid. It may reference a job that is not in it's 'needs' list (%[2]s), or an output that doesn't exist on one of those jobs.",
|
||||
"actions.workflow.incomplete_matrix_missing_job": "Unable to evaluate `strategy.matrix` of job %[1]s: job %[2]s is not in the `needs` list of job %[1]s (%[3]s).",
|
||||
"actions.workflow.incomplete_matrix_missing_output": "Unable to evaluate `strategy.matrix` of job %[1]s: job %[2]s does not have an output %[3]s.",
|
||||
"actions.workflow.incomplete_matrix_unknown_cause": "Unable to evaluate `strategy.matrix` of job %[1]s: unknown error.",
|
||||
"actions.workflow.incomplete_runson_missing_job": "Unable to evaluate `runs-on` of job %[1]s: job %[2]s is not in the `needs` list of job %[1]s (%[3]s).",
|
||||
"actions.workflow.incomplete_runson_missing_output": "Unable to evaluate `runs-on` of job %[1]s: job %[2]s does not have an output %[3]s.",
|
||||
"actions.workflow.incomplete_runson_missing_matrix_dimension": "Unable to evaluate `runs-on` of job %[1]s: matrix dimension %[2]s does not exist.",
|
||||
"actions.workflow.incomplete_runson_unknown_cause": "Unable to evaluate `runs-on` of job %[1]s: unknown error.",
|
||||
"actions.workflow.pre_execution_error": "Workflow was not executed due to an error that blocked the execution attempt.",
|
||||
"pulse.n_active_issues": {
|
||||
"one": "%s active issue",
|
||||
|
|
|
|||
|
|
@ -52,3 +52,39 @@
|
|||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 903
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 7
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 904
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 8
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
|
|
|||
|
|
@ -111,3 +111,64 @@
|
|||
task_id: 100
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 605
|
||||
run_id: 903
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 0
|
||||
job_id: job_1
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '[]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: ${{ matrix.platform-oops-wrong-dimension }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ nixos, ubuntu ]
|
||||
incomplete_runs_on: true
|
||||
incomplete_runs_on_matrix:
|
||||
dimension: platform-oops-wrong-dimension
|
||||
|
||||
-
|
||||
id: 606
|
||||
run_id: 904
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: job_1
|
||||
attempt: 0
|
||||
job_id: job_1
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '[]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
produce-artifacts:
|
||||
name: produce-artifacts
|
||||
runs-on: ${{ matrix.platform-oops-wrong-dimension }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "${{ needs.some-job.outputs.some-output }}", ubuntu ]
|
||||
incomplete_runs_on: true
|
||||
incomplete_runs_on_matrix:
|
||||
dimension: platform-oops-wrong-dimension
|
||||
incomplete_matrix: true
|
||||
|
|
|
|||
|
|
@ -169,3 +169,117 @@
|
|||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 909
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 13
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 910
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 14
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 911
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 15
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 912
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 16
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 913
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 17
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
-
|
||||
id: 914
|
||||
title: "running workflow_dispatch run"
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
workflow_id: "running.yaml"
|
||||
index: 18
|
||||
trigger_user_id: 2
|
||||
ref: "refs/heads/main"
|
||||
commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee"
|
||||
trigger_event: "workflow_dispatch"
|
||||
is_fork_pull_request: 0
|
||||
status: 6 # running
|
||||
started: 1683636528
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
concurrency_group: abc123
|
||||
|
|
|
|||
|
|
@ -359,3 +359,240 @@
|
|||
task_id: 100
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 617
|
||||
run_id: 909
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-runs-on"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on
|
||||
runs-on: ${{ needs.define-runs-on.outputs.run-on-this }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 618
|
||||
run_id: 909
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-runs-on
|
||||
attempt: 0
|
||||
job_id: define-runs-on
|
||||
task_id: 105
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 619
|
||||
run_id: 910
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-matrix"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on (incomplete matrix)
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.define-matrix.outputs.entire-matrix) }}
|
||||
runs-on: node-${{ matrix.node }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
incomplete_matrix: true
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 620
|
||||
run_id: 910
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-matrix
|
||||
attempt: 0
|
||||
job_id: define-matrix
|
||||
task_id: 106
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 621
|
||||
run_id: 911
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-runs-on"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on
|
||||
runs-on:
|
||||
- datacenter-alpha
|
||||
- ${{ needs.define-runs-on.outputs.run-on-this }}
|
||||
- node-27.x
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 622
|
||||
run_id: 911
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-runs-on
|
||||
attempt: 0
|
||||
job_id: define-runs-on
|
||||
task_id: 107
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 623
|
||||
run_id: 912
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-runs-on", "another-needs"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on
|
||||
runs-on: ${{ needs.oops-i-misspelt-the-job-id.outputs.run-on-this }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 624
|
||||
run_id: 912
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-runs-on
|
||||
attempt: 0
|
||||
job_id: define-runs-on
|
||||
task_id: 107
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 625
|
||||
run_id: 913
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-runs-on"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on
|
||||
runs-on: ${{ needs.define-runs-on.outputs.output-doesnt-exist }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 626
|
||||
run_id: 913
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-runs-on
|
||||
attempt: 0
|
||||
job_id: define-runs-on
|
||||
task_id: 107
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
||||
-
|
||||
id: 627
|
||||
run_id: 914
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: consume-runs-on
|
||||
attempt: 0
|
||||
job_id: consume-runs-on
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
runs_on: '["fedora"]'
|
||||
needs: '["define-runs-on"]'
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
consume-runs-on:
|
||||
name: consume-runs-on
|
||||
runs-on: ${{ matrix.dimension-oops-error }}
|
||||
steps:
|
||||
- run: echo "OK!"
|
||||
strategy:
|
||||
matrix:
|
||||
dimension1: [ abc, def ]
|
||||
incomplete_runs_on: true
|
||||
-
|
||||
id: 628
|
||||
run_id: 914
|
||||
repo_id: 63
|
||||
owner_id: 2
|
||||
commit_sha: 97f29ee599c373c729132a5c46a046978311e0ee
|
||||
is_fork_pull_request: 0
|
||||
name: define-runs-on
|
||||
attempt: 0
|
||||
job_id: define-runs-on
|
||||
task_id: 107
|
||||
status: 1 # success
|
||||
runs_on: '["fedora"]'
|
||||
|
|
|
|||
|
|
@ -33,3 +33,18 @@
|
|||
task_id: 104
|
||||
output_key: scalar-value
|
||||
output_value: just some value
|
||||
-
|
||||
id: 107
|
||||
task_id: 105
|
||||
output_key: run-on-this
|
||||
output_value: nixos-25.11
|
||||
-
|
||||
id: 108
|
||||
task_id: 106
|
||||
output_key: entire-matrix
|
||||
output_value: '{"datacenter": ["site-a", "site-b"], "node": ["12.x", "14.x"], "pg": [17, 18]}'
|
||||
-
|
||||
id: 109
|
||||
task_id: 107
|
||||
output_key: run-on-this
|
||||
output_value: nixos-25.11
|
||||
|
|
|
|||
|
|
@ -182,10 +182,18 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
|
|||
// Invoked once a job has all its `needs` parameters met and is ready to transition to waiting, this may expand the
|
||||
// job's `strategy.matrix` into multiple new jobs.
|
||||
func tryHandleIncompleteMatrix(ctx context.Context, blockedJob *actions_model.ActionRunJob, jobsInRun []*actions_model.ActionRunJob) (bool, error) {
|
||||
if incompleteMatrix, _, err := blockedJob.IsIncompleteMatrix(); err != nil {
|
||||
incompleteMatrix, _, err := blockedJob.IsIncompleteMatrix()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("job IsIncompleteMatrix: %w", err)
|
||||
} else if !incompleteMatrix {
|
||||
// Not relevant to attempt expansion if it wasn't marked IncompleteMatrix previously.
|
||||
}
|
||||
|
||||
incompleteRunsOn, _, _, err := blockedJob.IsIncompleteRunsOn()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("job IsIncompleteRunsOn: %w", err)
|
||||
}
|
||||
|
||||
if !incompleteMatrix && !incompleteRunsOn {
|
||||
// Not relevant to attempt re-parsing the job if it wasn't marked as Incomplete[...] previously.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -218,63 +226,34 @@ func tryHandleIncompleteMatrix(ctx context.Context, blockedJob *actions_model.Ac
|
|||
jobOutputs[job.JobID] = outputsMap
|
||||
}
|
||||
|
||||
// Re-parse the blocked job, providing all the other completed jobs' outputs, to turn this incomplete matrix into
|
||||
// Re-parse the blocked job, providing all the other completed jobs' outputs, to turn this incomplete job into
|
||||
// one-or-more new jobs:
|
||||
newJobWorkflows, err := jobparser.Parse(blockedJob.WorkflowPayload, false,
|
||||
jobparser.WithJobOutputs(jobOutputs),
|
||||
jobparser.WithWorkflowNeeds(blockedJob.Needs),
|
||||
jobparser.SupportIncompleteRunsOn(),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failure re-parsing SingleWorkflow: %w", err)
|
||||
}
|
||||
|
||||
// Sanity check that the expanded jobs are !IncompleteMatrix:
|
||||
// Even though every job in the `needs` list is done, perform a consistency check if the job was still unable to be
|
||||
// evaluated into a fully complete job with the correct matrix and runs-on values. Evaluation errors here need to be
|
||||
// reported back to the user for them to correct their workflow, so we slip this notification into
|
||||
// PreExecutionError.
|
||||
for _, swf := range newJobWorkflows {
|
||||
if swf.IncompleteMatrix {
|
||||
// Even though every job in the `needs` list is done, this job came back as `IncompleteMatrix`. This could
|
||||
// happen if the job referenced `needs.some-job` in the `strategy.matrix`, but the job didn't have `needs:
|
||||
// some-job`, or it could happen if it references an output that doesn't exist on that job. We don't have
|
||||
// enough information from the jobparser to determine what failed specifically.
|
||||
//
|
||||
// This is an error that needs to be reported back to the user for them to correct their workflow, so we
|
||||
// slip this notification into PreExecutionError.
|
||||
|
||||
run := blockedJob.Run
|
||||
|
||||
var errorCode actions_model.PreExecutionError
|
||||
var errorDetails []any
|
||||
|
||||
// `IncompleteMatrixNeeds` tells us which output was accessed that was missing
|
||||
if swf.IncompleteMatrixNeeds != nil {
|
||||
jobRef := swf.IncompleteMatrixNeeds.Job // always provided
|
||||
outputRef := swf.IncompleteMatrixNeeds.Output // missing if the entire job wasn't present
|
||||
if outputRef != "" {
|
||||
errorCode = actions_model.ErrorCodeIncompleteMatrixMissingOutput
|
||||
errorDetails = []any{
|
||||
blockedJob.JobID,
|
||||
jobRef,
|
||||
outputRef,
|
||||
}
|
||||
} else {
|
||||
errorCode = actions_model.ErrorCodeIncompleteMatrixMissingJob
|
||||
errorDetails = []any{
|
||||
blockedJob.JobID,
|
||||
jobRef,
|
||||
strings.Join(blockedJob.Needs, ", "),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorCode = actions_model.ErrorCodePersistentIncompleteMatrix
|
||||
errorDetails = []any{
|
||||
blockedJob.JobID,
|
||||
strings.Join(blockedJob.Needs, ", "),
|
||||
}
|
||||
}
|
||||
|
||||
if err := FailRunPreExecutionError(ctx, run, errorCode, errorDetails); err != nil {
|
||||
errorCode, errorDetails := persistentIncompleteMatrixError(blockedJob, swf.IncompleteMatrixNeeds)
|
||||
if err := FailRunPreExecutionError(ctx, blockedJob.Run, errorCode, errorDetails); err != nil {
|
||||
return false, fmt.Errorf("failure when marking run with error: %w", err)
|
||||
}
|
||||
// Return `true` to skip running this job in this invalid state
|
||||
return true, nil
|
||||
} else if swf.IncompleteRunsOn {
|
||||
errorCode, errorDetails := persistentIncompleteRunsOnError(blockedJob, swf.IncompleteRunsOnNeeds, swf.IncompleteRunsOnMatrix)
|
||||
if err := FailRunPreExecutionError(ctx, blockedJob.Run, errorCode, errorDetails); err != nil {
|
||||
return false, fmt.Errorf("failure when marking run with error: %w", err)
|
||||
}
|
||||
|
||||
// Return `true` to skip running this job in this invalid state
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -301,3 +280,78 @@ func tryHandleIncompleteMatrix(ctx context.Context, blockedJob *actions_model.Ac
|
|||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func persistentIncompleteMatrixError(job *actions_model.ActionRunJob, incompleteNeeds *jobparser.IncompleteNeeds) (actions_model.PreExecutionError, []any) {
|
||||
var errorCode actions_model.PreExecutionError
|
||||
var errorDetails []any
|
||||
|
||||
// `incompleteNeeds` tells us what part of a `${{ needs... }}` expression was missing
|
||||
if incompleteNeeds != nil {
|
||||
jobRef := incompleteNeeds.Job // always provided
|
||||
outputRef := incompleteNeeds.Output // missing if the entire job wasn't present
|
||||
if outputRef != "" {
|
||||
errorCode = actions_model.ErrorCodeIncompleteMatrixMissingOutput
|
||||
errorDetails = []any{
|
||||
job.JobID,
|
||||
jobRef,
|
||||
outputRef,
|
||||
}
|
||||
} else {
|
||||
errorCode = actions_model.ErrorCodeIncompleteMatrixMissingJob
|
||||
errorDetails = []any{
|
||||
job.JobID,
|
||||
jobRef,
|
||||
strings.Join(job.Needs, ", "),
|
||||
}
|
||||
}
|
||||
return errorCode, errorDetails
|
||||
}
|
||||
|
||||
// Not sure why we ended up in `IncompleteMatrix` when nothing was marked as incomplete
|
||||
errorCode = actions_model.ErrorCodeIncompleteMatrixUnknownCause
|
||||
errorDetails = []any{job.JobID}
|
||||
return errorCode, errorDetails
|
||||
}
|
||||
|
||||
func persistentIncompleteRunsOnError(job *actions_model.ActionRunJob, incompleteNeeds *jobparser.IncompleteNeeds, incompleteMatrix *jobparser.IncompleteMatrix) (actions_model.PreExecutionError, []any) {
|
||||
var errorCode actions_model.PreExecutionError
|
||||
var errorDetails []any
|
||||
|
||||
// `incompleteMatrix` tells us which dimension of a matrix was accessed that was missing
|
||||
if incompleteMatrix != nil {
|
||||
dimension := incompleteMatrix.Dimension
|
||||
errorCode = actions_model.ErrorCodeIncompleteRunsOnMissingMatrixDimension
|
||||
errorDetails = []any{
|
||||
job.JobID,
|
||||
dimension,
|
||||
}
|
||||
return errorCode, errorDetails
|
||||
}
|
||||
|
||||
// `incompleteNeeds` tells us what part of a `${{ needs... }}` expression was missing
|
||||
if incompleteNeeds != nil {
|
||||
jobRef := incompleteNeeds.Job // always provided
|
||||
outputRef := incompleteNeeds.Output // missing if the entire job wasn't present
|
||||
if outputRef != "" {
|
||||
errorCode = actions_model.ErrorCodeIncompleteRunsOnMissingOutput
|
||||
errorDetails = []any{
|
||||
job.JobID,
|
||||
jobRef,
|
||||
outputRef,
|
||||
}
|
||||
} else {
|
||||
errorCode = actions_model.ErrorCodeIncompleteRunsOnMissingJob
|
||||
errorDetails = []any{
|
||||
job.JobID,
|
||||
jobRef,
|
||||
strings.Join(job.Needs, ", "),
|
||||
}
|
||||
}
|
||||
return errorCode, errorDetails
|
||||
}
|
||||
|
||||
// Not sure why we ended up in `IncompleteRunsOn` when nothing was marked as incomplete
|
||||
errorCode = actions_model.ErrorCodeIncompleteRunsOnUnknownCause
|
||||
errorDetails = []any{job.JobID}
|
||||
return errorCode, errorDetails
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ import (
|
|||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/test"
|
||||
|
||||
"code.forgejo.org/forgejo/runner/v12/act/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||
|
|
@ -140,6 +143,11 @@ jobs:
|
|||
}
|
||||
|
||||
func Test_tryHandleIncompleteMatrix(t *testing.T) {
|
||||
// Shouldn't get any decoding errors during this test -- pop them up from a log warning to a test fatal error.
|
||||
defer test.MockVariableValue(&model.OnDecodeNodeError, func(node yaml.Node, out any, err error) {
|
||||
t.Fatalf("Failed to decode node %v into %T: %v", node, out, err)
|
||||
})()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runJobID int64
|
||||
|
|
@ -148,9 +156,10 @@ func Test_tryHandleIncompleteMatrix(t *testing.T) {
|
|||
runJobNames []string
|
||||
preExecutionError actions_model.PreExecutionError
|
||||
preExecutionErrorDetails []any
|
||||
runsOn map[string][]string
|
||||
}{
|
||||
{
|
||||
name: "not incomplete_matrix",
|
||||
name: "not incomplete",
|
||||
runJobID: 600,
|
||||
},
|
||||
{
|
||||
|
|
@ -225,11 +234,86 @@ func Test_tryHandleIncompleteMatrix(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "missing needs for strategy.matrix evaluation",
|
||||
name: "missing needs output for strategy.matrix evaluation",
|
||||
runJobID: 615,
|
||||
preExecutionError: actions_model.ErrorCodeIncompleteMatrixMissingOutput,
|
||||
preExecutionErrorDetails: []any{"job_1", "define-matrix-1", "colours-intentional-mistake"},
|
||||
},
|
||||
{
|
||||
name: "runs-on evaluation with needs",
|
||||
runJobID: 617,
|
||||
consumed: true,
|
||||
runJobNames: []string{
|
||||
"consume-runs-on",
|
||||
"define-runs-on",
|
||||
},
|
||||
runsOn: map[string][]string{
|
||||
"define-runs-on": {"fedora"},
|
||||
"consume-runs-on": {"nixos-25.11"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "runs-on evaluation with needs dynamic matrix",
|
||||
runJobID: 619,
|
||||
consumed: true,
|
||||
runJobNames: []string{
|
||||
"consume-runs-on (site-a, 12.x, 17)",
|
||||
"consume-runs-on (site-a, 12.x, 18)",
|
||||
"consume-runs-on (site-a, 14.x, 17)",
|
||||
"consume-runs-on (site-a, 14.x, 18)",
|
||||
"consume-runs-on (site-b, 12.x, 17)",
|
||||
"consume-runs-on (site-b, 12.x, 18)",
|
||||
"consume-runs-on (site-b, 14.x, 17)",
|
||||
"consume-runs-on (site-b, 14.x, 18)",
|
||||
"define-matrix",
|
||||
},
|
||||
runsOn: map[string][]string{
|
||||
"consume-runs-on (site-a, 12.x, 17)": {"node-12.x"},
|
||||
"consume-runs-on (site-a, 12.x, 18)": {"node-12.x"},
|
||||
"consume-runs-on (site-a, 14.x, 17)": {"node-14.x"},
|
||||
"consume-runs-on (site-a, 14.x, 18)": {"node-14.x"},
|
||||
"consume-runs-on (site-b, 12.x, 17)": {"node-12.x"},
|
||||
"consume-runs-on (site-b, 12.x, 18)": {"node-12.x"},
|
||||
"consume-runs-on (site-b, 14.x, 17)": {"node-14.x"},
|
||||
"consume-runs-on (site-b, 14.x, 18)": {"node-14.x"},
|
||||
"define-matrix": {"fedora"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "runs-on evaluation to part of array",
|
||||
runJobID: 621,
|
||||
consumed: true,
|
||||
runJobNames: []string{
|
||||
"consume-runs-on",
|
||||
"define-runs-on",
|
||||
},
|
||||
runsOn: map[string][]string{
|
||||
"define-runs-on": {"fedora"},
|
||||
"consume-runs-on": {
|
||||
"datacenter-alpha",
|
||||
"nixos-25.11",
|
||||
"node-27.x",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing needs job for runs-on evaluation",
|
||||
runJobID: 623,
|
||||
preExecutionError: actions_model.ErrorCodeIncompleteRunsOnMissingJob,
|
||||
preExecutionErrorDetails: []any{"consume-runs-on", "oops-i-misspelt-the-job-id", "define-runs-on, another-needs"},
|
||||
},
|
||||
{
|
||||
name: "missing needs output for runs-on evaluation",
|
||||
runJobID: 625,
|
||||
preExecutionError: actions_model.ErrorCodeIncompleteRunsOnMissingOutput,
|
||||
preExecutionErrorDetails: []any{"consume-runs-on", "define-runs-on", "output-doesnt-exist"},
|
||||
},
|
||||
{
|
||||
name: "missing matrix dimension for runs-on evaluation",
|
||||
runJobID: 627,
|
||||
preExecutionError: actions_model.ErrorCodeIncompleteRunsOnMissingMatrixDimension,
|
||||
preExecutionErrorDetails: []any{"consume-runs-on", "dimension-oops-error"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -255,7 +339,7 @@ func Test_tryHandleIncompleteMatrix(t *testing.T) {
|
|||
|
||||
// expectations are that the ActionRun has an empty PreExecutionError
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: blockedJob.RunID})
|
||||
assert.Empty(t, actionRun.PreExecutionError)
|
||||
assert.EqualValues(t, 0, actionRun.PreExecutionErrorCode)
|
||||
|
||||
// compare jobs that exist with `runJobNames` to ensure new jobs are inserted:
|
||||
allJobsInRun, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: blockedJob.RunID})
|
||||
|
|
@ -266,10 +350,21 @@ func Test_tryHandleIncompleteMatrix(t *testing.T) {
|
|||
}
|
||||
slices.Sort(allJobNames)
|
||||
assert.Equal(t, tt.runJobNames, allJobNames)
|
||||
|
||||
// Check the runs-on of all jobs
|
||||
if tt.runsOn != nil {
|
||||
for _, j := range allJobsInRun {
|
||||
expected, ok := tt.runsOn[j.Name]
|
||||
if assert.Truef(t, ok, "unable to find runsOn[%q] in test case", j.Name) {
|
||||
slices.Sort(j.RunsOn)
|
||||
slices.Sort(expected)
|
||||
assert.Equalf(t, expected, j.RunsOn, "comparing runsOn expectations for job %q", j.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if tt.preExecutionError != 0 {
|
||||
// expectations are that the ActionRun has a populated PreExecutionError, is marked as failed
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: blockedJob.RunID})
|
||||
assert.Empty(t, actionRun.PreExecutionError)
|
||||
assert.Equal(t, tt.preExecutionError, actionRun.PreExecutionErrorCode)
|
||||
assert.Equal(t, tt.preExecutionErrorDetails, actionRun.PreExecutionErrorDetails)
|
||||
assert.Equal(t, actions_model.StatusFailure, actionRun.Status)
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ func handleWorkflows(
|
|||
// We don't have any job outputs yet, but `WithJobOutputs(...)` triggers JobParser to supporting its
|
||||
// `IncompleteMatrix` tagging for any jobs that require the inputs of other jobs.
|
||||
jobparser.WithJobOutputs(map[string]map[string]string{}),
|
||||
jobparser.SupportIncompleteRunsOn(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
|
||||
|
|
|
|||
|
|
@ -298,3 +298,31 @@ func TestActionsNotifier_DynamicMatrix(t *testing.T) {
|
|||
// first inserted it is tagged w/ incomplete_matrix
|
||||
assert.Contains(t, string(job.WorkflowPayload), "incomplete_matrix: true")
|
||||
}
|
||||
|
||||
func TestActionsNotifier_RunsOnNeeds(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
|
||||
dw := &actions_module.DetectedWorkflow{
|
||||
Content: []byte("{ on: pull_request, jobs: { j1: { runs-on: \"${{ needs.other-job.outputs.some-output }}\" } } }"),
|
||||
}
|
||||
testActionsNotifierPullRequest(t, repo, pr, dw, webhook_module.HookEventPullRequestSync)
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||
RepoID: repo.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, runs, 1)
|
||||
run := runs[0]
|
||||
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
job := jobs[0]
|
||||
|
||||
// With a runs-on that contains ${{ needs ... }} references, the only requirement to work is that when the job is
|
||||
// first inserted it is tagged w/ incomplete_runs_on.
|
||||
assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,11 @@ func consistencyCheckRun(ctx context.Context, run *actions_model.ActionRun) erro
|
|||
} else if stop {
|
||||
break
|
||||
}
|
||||
if stop, err := checkJobRunsOnStaticMatrixError(ctx, job); err != nil {
|
||||
return err
|
||||
} else if stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -156,3 +161,39 @@ func checkJobWillRevisit(ctx context.Context, job *actions_model.ActionRunJob) (
|
|||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func checkJobRunsOnStaticMatrixError(ctx context.Context, job *actions_model.ActionRunJob) (bool, error) {
|
||||
// If a job has a `runs-on` field that references a matrix dimension like `runs-on: ${{ matrix.platorm }}`, and
|
||||
// `platform` is not part of the job's matrix at all, then it will be tagged as `IsIncompleteRunsOn` and will be
|
||||
// blocked forever. This only applies if the matrix is static -- that is, the job isn't also tagged
|
||||
// `IsIncompleteMatrix` and the matrix is yet to be fully defined.
|
||||
|
||||
isIncompleteRunsOn, _, matrixReference, err := job.IsIncompleteRunsOn()
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !isIncompleteRunsOn || matrixReference == nil {
|
||||
// Not incomplete, or, it's incomplete but not because of a matrix reference error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
isIncompleteMatrix, _, err := job.IsIncompleteMatrix()
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if isIncompleteMatrix {
|
||||
// Not a static matrix, so this might be resolved later when the job is expanded.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Job doesn't seem like it can proceed; mark the run with an error.
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := FailRunPreExecutionError(ctx, job.Run, actions_model.ErrorCodeIncompleteRunsOnMissingMatrixDimension, []any{
|
||||
job.JobID,
|
||||
matrixReference.Dimension,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@ func TestActions_consistencyCheckRun(t *testing.T) {
|
|||
preExecutionError: actions_model.ErrorCodeIncompleteMatrixMissingJob,
|
||||
preExecutionErrorDetails: []any{"job_1", "oops-something-wrong-here", "define-matrix"},
|
||||
},
|
||||
{
|
||||
name: "inconsistent: static matrix missing dimension",
|
||||
runID: 903,
|
||||
preExecutionError: actions_model.ErrorCodeIncompleteRunsOnMissingMatrixDimension,
|
||||
preExecutionErrorDetails: []any{"job_1", "platform-oops-wrong-dimension"},
|
||||
},
|
||||
{
|
||||
name: "consistent: matrix missing dimension but matrix is dynamic",
|
||||
runID: 904,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
|
|||
// We don't have any job outputs yet, but `WithJobOutputs(...)` triggers JobParser to supporting its
|
||||
// `IncompleteMatrix` tagging for any jobs that require the inputs of other jobs.
|
||||
jobparser.WithJobOutputs(map[string]map[string]string{}),
|
||||
jobparser.SupportIncompleteRunsOn(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -326,3 +326,62 @@ jobs:
|
|||
// first inserted it is tagged w/ incomplete_matrix
|
||||
assert.Contains(t, string(job.WorkflowPayload), "incomplete_matrix: true")
|
||||
}
|
||||
|
||||
func TestServiceActions_RunsOnNeeds(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestServiceActions_startTask")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Load fixtures that are corrupted and create one valid scheduled workflow
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
|
||||
workflowID := "some.yml"
|
||||
schedules := []*actions_model.ActionSchedule{
|
||||
{
|
||||
Title: "scheduletitle1",
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: workflowID,
|
||||
TriggerUserID: repo.OwnerID,
|
||||
Ref: "branch",
|
||||
CommitSHA: "fakeSHA",
|
||||
Event: webhook_module.HookEventSchedule,
|
||||
EventPayload: "fakepayload",
|
||||
Specs: []string{"* * * * *"},
|
||||
Content: []byte(
|
||||
`
|
||||
jobs:
|
||||
job2:
|
||||
runs-on: "${{ fromJSON(needs.other-job.outputs.some-output) }}"
|
||||
steps:
|
||||
- run: true
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, 2, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
|
||||
require.NoError(t, actions_model.CreateScheduleTask(t.Context(), schedules))
|
||||
require.Equal(t, 3, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
|
||||
_, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `action_schedule_spec` SET next = 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// After running startTasks an ActionRun row is created for the valid scheduled workflow
|
||||
require.Empty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
|
||||
require.NoError(t, startTasks(t.Context()))
|
||||
require.NotEmpty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
|
||||
|
||||
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
|
||||
WorkflowID: workflowID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, runs, 1)
|
||||
run := runs[0]
|
||||
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 1)
|
||||
job := jobs[0]
|
||||
|
||||
// With a runs-on that contains ${{ needs ... }} references, the only requirement to work is that when the job is
|
||||
// first inserted it is tagged w/ incomplete_runs_on
|
||||
assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
|
|||
// We don't have any job outputs yet, but `WithJobOutputs(...)` triggers JobParser to supporting its
|
||||
// `IncompleteMatrix` tagging for any jobs that require the inputs of other jobs.
|
||||
jobparser.WithJobOutputs(map[string]map[string]string{}),
|
||||
jobparser.SupportIncompleteRunsOn(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue