mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-25 22:23:42 +09:00
Updates actionslib to https://gitea.com/gitea/actionslib/releases/tag/v1.2.1, moves workflow parsing into it and aligns behaviour with GitHub. 1. `uses:` supports `self:` (Gitea-only feature) and `$/` paths. 1. `strategy`, `matrix`, `max-parallel` and `fail-fast` accept expressions, including over `needs`. A job whose `name`, `runs-on` or `continue-on-error` reads `needs` is resolved once they finish. 1. A job `if:` may only read `github`, `needs`, `vars` and `inputs` and is decided before the matrix, as on github.com. 1. Matrix `fail-fast` cancels the other combinations, and `always()` jobs keep running when a run is cancelled. 1. Invalid workflow files, including a malformed `on:` and unknown or cyclic `needs`, show up on push as failed runs with the error. 1. A job whose `if:` or `concurrency:` fails to evaluate is skipped or failed with the error, instead of staying blocked. 1. Reusable workflows: a missing and an unreadable repository fail alike, public callers cannot use private workflows, nested jobs cannot exceed the caller's token permissions. 1. Runner labels match case-insensitively, and `runs-on` accepts an array from an expression. Runner PR: https://gitea.com/gitea/runner/pulls/1247 Docs PR: https://gitea.com/gitea/docs/pulls/553 Fixes: https://github.com/go-gitea/gitea/issues/38990 Fixes: https://github.com/go-gitea/gitea/issues/39382 Fixes: https://github.com/go-gitea/gitea/issues/32364 Fixes: https://github.com/go-gitea/gitea/issues/36077 Fixes: https://github.com/go-gitea/gitea/issues/23277 Fixes: https://github.com/go-gitea/gitea/issues/29020 Co-authored-by: Claude (Opus 5) <noreply@anthropic.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
162 lines
5.9 KiB
Go
162 lines
5.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
actions_model "gitea.dev/models/actions"
|
|
auth_model "gitea.dev/models/auth"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDynamicMatrixEvaluation(t *testing.T) {
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
session := loginUser(t, user2.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
|
|
apiRepo := createActionsTestRepo(t, token, "actions-dynamic-matrix-eval", false)
|
|
runner := newMockRunner()
|
|
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
|
|
|
const workflow = `name: dynamic-matrix
|
|
on:
|
|
push:
|
|
paths: ['.gitea/workflows/dynamic-matrix.yml']
|
|
jobs:
|
|
generate:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.set.outputs.matrix }}
|
|
combos: ${{ steps.set.outputs.combos }}
|
|
steps:
|
|
- id: set
|
|
run: |
|
|
echo "matrix=[1,2]" >> "$GITHUB_OUTPUT"
|
|
echo 'combos=[{"name":"x"},{"name":"y"}]' >> "$GITHUB_OUTPUT"
|
|
build:
|
|
needs: [generate]
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
result: ${{ steps.out.outputs.result }}
|
|
strategy:
|
|
matrix:
|
|
value: ${{ fromJson(needs.generate.outputs.matrix) }}
|
|
steps:
|
|
- id: out
|
|
run: echo "result=built-${{ matrix.value }}" >> "$GITHUB_OUTPUT"
|
|
static:
|
|
needs: [generate]
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
os: [a, b]
|
|
steps:
|
|
- run: echo "${{ matrix.os }}"
|
|
included:
|
|
needs: [generate]
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
include: ${{ fromJson(needs.generate.outputs.combos) }}
|
|
steps:
|
|
- run: echo "${{ matrix.name }}"
|
|
gated:
|
|
needs: [generate]
|
|
if: false
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
value: ${{ fromJson(needs.generate.outputs.matrix) }}
|
|
steps:
|
|
- run: echo "${{ matrix.value }}"
|
|
report:
|
|
needs: [build]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo '${{ toJSON(needs.build.outputs) }}'
|
|
`
|
|
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create dynamic-matrix.yml", workflow)
|
|
createWorkflowFile(t, token, user2.Name, apiRepo.Name, ".gitea/workflows/dynamic-matrix.yml", opts)
|
|
|
|
generateTask := runner.fetchTask(t, 10*time.Second)
|
|
require.Equal(t, "generate", getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, generateTask.Id))
|
|
_, _, run := getTaskAndJobAndRunByTaskID(t, generateTask.Id)
|
|
runner.execTask(t, generateTask, &mockTaskOutcome{
|
|
result: runnerv1.Result_RESULT_SUCCESS,
|
|
outputs: map[string]string{"matrix": "[1,2]", "combos": `[{"name":"x"},{"name":"y"}]`},
|
|
})
|
|
|
|
// execAttempt runs the attempt's remaining tasks, recording each job's name and AttemptJobID(order is not fixed).
|
|
attemptJobIDs := map[string]int64{}
|
|
var reportTask *runnerv1.Task
|
|
execAttempt := func(t *testing.T, count int) []string {
|
|
names := make([]string, 0, count)
|
|
for range count {
|
|
task := runner.fetchTask(t, 10*time.Second)
|
|
_, taskJob, _ := getTaskAndJobAndRunByTaskID(t, task.Id)
|
|
names = append(names, taskJob.Name)
|
|
attemptJobIDs[taskJob.Name] = taskJob.AttemptJobID
|
|
outcome := &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS}
|
|
if value, ok := strings.CutPrefix(taskJob.Name, "build ("); ok {
|
|
outcome.outputs = map[string]string{"result": "built-" + strings.TrimSuffix(value, ")")}
|
|
}
|
|
if taskJob.JobID == "report" {
|
|
reportTask = task
|
|
}
|
|
runner.execTask(t, task, outcome)
|
|
}
|
|
runner.fetchNoTask(t, 300*time.Millisecond)
|
|
return names
|
|
}
|
|
|
|
seen := execAttempt(t, 7)
|
|
firstAttemptIDs := maps.Clone(attemptJobIDs)
|
|
assert.ElementsMatch(t, []string{
|
|
"build (1)", "build (2)", "included (x)", "included (y)", "static (a)", "static (b)", "report",
|
|
}, seen)
|
|
assert.Equal(t, actions_model.StatusSkipped, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, Name: "gated"}).Status)
|
|
|
|
buildNeed, ok := reportTask.Needs["build"]
|
|
require.True(t, ok, "report must see the expanded combinations as its 'build' need")
|
|
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, buildNeed.Result)
|
|
assert.Contains(t, []string{"built-1", "built-2"}, buildNeed.Outputs["result"])
|
|
|
|
// Re-running the whole run collapses the combinations back into a placeholder and re-derives
|
|
// the matrix from the new attempt's outputs instead of reusing the previous combinations.
|
|
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, apiRepo.Name, run.ID))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
generateTask2 := runner.fetchTask(t, 10*time.Second)
|
|
require.Equal(t, "generate", getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, generateTask2.Id))
|
|
assert.Equal(t, "2", generateTask2.Context.GetFields()["run_attempt"].GetStringValue())
|
|
runner.execTask(t, generateTask2, &mockTaskOutcome{
|
|
result: runnerv1.Result_RESULT_SUCCESS,
|
|
// `combos` is unchanged, so both `included` combinations recur and have to keep their AttemptJobIDs.
|
|
outputs: map[string]string{"matrix": "[1,2,3]", "combos": `[{"name":"x"},{"name":"y"}]`},
|
|
})
|
|
|
|
// The matrix now yields a third value, while `static` is cloned as-is rather than collapsed.
|
|
seenRerun := execAttempt(t, 8)
|
|
assert.ElementsMatch(t, []string{
|
|
"build (1)", "build (2)", "build (3)", "included (x)", "included (y)", "static (a)", "static (b)", "report",
|
|
}, seenRerun)
|
|
for name, firstID := range firstAttemptIDs {
|
|
assert.Equal(t, firstID, attemptJobIDs[name], "%s keeps its AttemptJobID across attempts", name)
|
|
}
|
|
})
|
|
}
|