mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-12-07 14:09:47 +00:00
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>
239 lines
7.3 KiB
Go
239 lines
7.3 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"testing"
|
|
|
|
"forgejo.org/models/db"
|
|
"forgejo.org/models/unittest"
|
|
"forgejo.org/modules/translation"
|
|
|
|
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestActionRunJob_ItRunsOn(t *testing.T) {
|
|
actionJob := ActionRunJob{RunsOn: []string{"ubuntu"}}
|
|
agentLabels := []string{"ubuntu", "node-20"}
|
|
|
|
assert.True(t, actionJob.ItRunsOn(agentLabels))
|
|
assert.False(t, actionJob.ItRunsOn([]string{}))
|
|
|
|
actionJob.RunsOn = append(actionJob.RunsOn, "node-20")
|
|
|
|
assert.True(t, actionJob.ItRunsOn(agentLabels))
|
|
|
|
agentLabels = []string{"ubuntu"}
|
|
|
|
assert.False(t, actionJob.ItRunsOn(agentLabels))
|
|
|
|
actionJob.RunsOn = []string{}
|
|
|
|
assert.False(t, actionJob.ItRunsOn(agentLabels))
|
|
}
|
|
|
|
func TestActionRunJob_HTMLURL(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
tests := []struct {
|
|
id int64
|
|
expected string
|
|
}{
|
|
{
|
|
id: 192,
|
|
expected: "https://try.gitea.io/user5/repo4/actions/runs/187/jobs/0/attempt/1",
|
|
},
|
|
{
|
|
id: 393,
|
|
expected: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/1/attempt/1",
|
|
},
|
|
{
|
|
id: 394,
|
|
expected: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/2/attempt/2",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("id=%d", tt.id), func(t *testing.T) {
|
|
var job ActionRunJob
|
|
has, err := db.GetEngine(t.Context()).Where("id=?", tt.id).Get(&job)
|
|
require.NoError(t, err)
|
|
require.True(t, has, "load ActionRunJob from fixture")
|
|
|
|
err = job.LoadAttributes(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
url, err := job.HTMLURL(t.Context())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, url)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActionRunJob_StatusDiagnostics(t *testing.T) {
|
|
translation.InitLocales(t.Context())
|
|
english := translation.NewLocale("en-US")
|
|
|
|
tests := []struct {
|
|
name string
|
|
job ActionRunJob
|
|
expected []template.HTML
|
|
}{
|
|
{
|
|
name: "Unknown status",
|
|
job: ActionRunJob{RunsOn: []string{"windows"}, Status: StatusUnknown, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Unknown"},
|
|
},
|
|
{
|
|
name: "Waiting without labels",
|
|
job: ActionRunJob{RunsOn: []string{}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Waiting for a runner with the following labels: "},
|
|
},
|
|
{
|
|
name: "Waiting with one label",
|
|
job: ActionRunJob{RunsOn: []string{"freebsd"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Waiting for a runner with the following label: freebsd"},
|
|
},
|
|
{
|
|
name: "Waiting with labels, no approval",
|
|
job: ActionRunJob{RunsOn: []string{"docker", "ubuntu"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Waiting for a runner with the following labels: docker, ubuntu"},
|
|
},
|
|
{
|
|
name: "Waiting with labels, approval",
|
|
job: ActionRunJob{RunsOn: []string{"docker", "ubuntu"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: true}},
|
|
expected: []template.HTML{
|
|
"Waiting for a runner with the following labels: docker, ubuntu",
|
|
"Need approval to run workflows for fork pull request.",
|
|
},
|
|
},
|
|
{
|
|
name: "Running",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusRunning, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Running"},
|
|
},
|
|
{
|
|
name: "Success",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusSuccess, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Success"},
|
|
},
|
|
{
|
|
name: "Failure",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusFailure, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Failure"},
|
|
},
|
|
{
|
|
name: "Cancelled",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusCancelled, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Canceled"},
|
|
},
|
|
{
|
|
name: "Skipped",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusSkipped, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Skipped"},
|
|
},
|
|
{
|
|
name: "Blocked",
|
|
job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusBlocked, Run: &ActionRun{NeedApproval: false}},
|
|
expected: []template.HTML{"Blocked"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, tt.job.StatusDiagnostics(english))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActionRunJob_IsIncompleteMatrix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
job ActionRunJob
|
|
isIncomplete bool
|
|
needs *jobparser.IncompleteNeeds
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "normal workflow",
|
|
job: ActionRunJob{WorkflowPayload: []byte("name: workflow")},
|
|
isIncomplete: false,
|
|
},
|
|
{
|
|
name: "incomplete_matrix workflow",
|
|
job: ActionRunJob{WorkflowPayload: []byte("name: workflow\nincomplete_matrix: true\nincomplete_matrix_needs: { job: abc }")},
|
|
needs: &jobparser.IncompleteNeeds{Job: "abc"},
|
|
isIncomplete: true,
|
|
},
|
|
{
|
|
name: "unparseable workflow",
|
|
job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_matrix: true")},
|
|
errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
isIncomplete, needs, err := tt.job.IsIncompleteMatrix()
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|