mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-10 05:24:18 +09:00
Pairs with https://gitea.com/gitea/runner/pulls/1143. Gitea depends on `gitea.com/gitea/runner` for exactly two packages: `act/model` and `act/exprparser`, the workflow model and the expression evaluator it needs to parse workflows and to build the task payload the runner consumes. Pulling the whole runner module in for that is heavy and puts shared code in the repository of one of the two consumers. Both packages now live in `gitea.dev/actionslib` (`pkg/model`, `pkg/exprparser`), the module Gitea and the runner already share for the runner API, so the dependency on the runner repository is dropped here. ### Changes - `gitea.com/gitea/runner/act/model` -> `gitea.dev/actionslib/pkg/model`, `.../act/exprparser` -> `gitea.dev/actionslib/pkg/exprparser` (22 files, import paths only). - `routers/api/actions/runner/interceptor.go` takes the `x-runner-uuid` / `x-runner-token` names from `gitea.dev/actionslib/pkg/protocol` instead of repeating the literals the runner also has. - `go.mod`: `gitea.com/gitea/runner` removed. --------- Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Zettat123 <zettat123@gmail.com>
393 lines
14 KiB
Go
393 lines
14 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"strconv"
|
|
"testing"
|
|
|
|
act_model "gitea.dev/actionslib/pkg/model"
|
|
actions_model "gitea.dev/models/actions"
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
actions_module "gitea.dev/modules/actions"
|
|
"gitea.dev/modules/json"
|
|
api "gitea.dev/modules/structs"
|
|
webhook_module "gitea.dev/modules/webhook"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEvaluateRunConcurrency_RunIDFallback(t *testing.T) {
|
|
// Unit-level check that EvaluateRunConcurrencyFillModel resolves github.run_id from run.ID.
|
|
// The full-flow regression (run.ID non-zero by evaluation time) is TestPrepareRunAndInsert_ExpressionsSeeRunID.
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
ctx := t.Context()
|
|
|
|
runA := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791})
|
|
runB := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 792})
|
|
|
|
attemptA := &actions_model.ActionRunAttempt{RepoID: runA.RepoID, RunID: runA.ID, Attempt: 1}
|
|
attemptB := &actions_model.ActionRunAttempt{RepoID: runB.RepoID, RunID: runB.ID, Attempt: 1}
|
|
|
|
expr := &act_model.RawConcurrency{
|
|
Group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}",
|
|
CancelInProgress: "true",
|
|
}
|
|
|
|
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runA, attemptA, expr, nil, nil))
|
|
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runB, attemptB, expr, nil, nil))
|
|
|
|
assert.Contains(t, attemptA.ConcurrencyGroup, "791")
|
|
assert.Contains(t, attemptB.ConcurrencyGroup, "792")
|
|
assert.NotEqual(t, attemptA.ConcurrencyGroup, attemptB.ConcurrencyGroup)
|
|
}
|
|
|
|
func TestPrepareRunAndInsert_ExpressionsSeeRunID(t *testing.T) {
|
|
// Regression for the cross-branch concurrency leak: github.run_id must be available during both
|
|
// jobparser.Parse (run-name) and concurrency evaluation; inserting run after either leaves run.ID at 0.
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
ctx := t.Context()
|
|
|
|
content := []byte(`name: cross-branch
|
|
run-name: "Run ${{ github.run_id }}"
|
|
on: push
|
|
concurrency:
|
|
group: group-${{ github.run_id }}
|
|
cancel-in-progress: true
|
|
jobs:
|
|
hello:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo hi
|
|
`)
|
|
|
|
run := &actions_model.ActionRun{
|
|
Title: "before parse",
|
|
RepoID: 4,
|
|
OwnerID: 1,
|
|
WorkflowID: "expr-runid.yaml",
|
|
TriggerUserID: 1,
|
|
Ref: "refs/heads/master",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
Event: "push",
|
|
TriggerEvent: "push",
|
|
EventPayload: "{}",
|
|
WorkflowRepoID: 4,
|
|
WorkflowCommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
}
|
|
require.NoError(t, PrepareRunAndInsert(ctx, content, run, nil))
|
|
require.Positive(t, run.ID)
|
|
|
|
persisted := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
|
runIDStr := strconv.FormatInt(run.ID, 10)
|
|
assert.Equal(t, "Run "+runIDStr, persisted.Title)
|
|
// ConcurrencyGroup lives on the latest attempt after migration v331.
|
|
require.Positive(t, persisted.LatestAttemptID)
|
|
attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: persisted.LatestAttemptID})
|
|
assert.Equal(t, "group-"+runIDStr, attempt.ConcurrencyGroup)
|
|
// Rerun reads raw_concurrency from the DB to re-evaluate the group;
|
|
// see services/actions/rerun.go. Must survive the insert.
|
|
assert.NotEmpty(t, persisted.RawConcurrency)
|
|
}
|
|
|
|
func TestComputeReusableCallerOutputs(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
ctx := t.Context()
|
|
|
|
var nextRunIndex int64 = 9001
|
|
insertRun := func(t *testing.T, workflowID string) *actions_model.ActionRun {
|
|
t.Helper()
|
|
run := &actions_model.ActionRun{
|
|
Title: "reusable-out",
|
|
RepoID: 4,
|
|
Index: nextRunIndex,
|
|
OwnerID: 1,
|
|
WorkflowID: workflowID,
|
|
TriggerUserID: 1,
|
|
Ref: "refs/heads/master",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
Event: "push",
|
|
TriggerEvent: "push",
|
|
EventPayload: "{}",
|
|
Status: actions_model.StatusSuccess,
|
|
}
|
|
nextRunIndex++
|
|
require.NoError(t, db.Insert(ctx, run))
|
|
return run
|
|
}
|
|
|
|
insertCaller := func(t *testing.T, run *actions_model.ActionRun, jobID string, parentID int64, content, callPayload string) *actions_model.ActionRunJob {
|
|
t.Helper()
|
|
job := &actions_model.ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: jobID,
|
|
JobID: jobID,
|
|
Attempt: 1,
|
|
Status: actions_model.StatusSuccess,
|
|
ParentJobID: parentID,
|
|
IsReusableCaller: true,
|
|
IsExpanded: true,
|
|
ReusableWorkflowContent: []byte(content),
|
|
CallPayload: callPayload,
|
|
}
|
|
require.NoError(t, db.Insert(ctx, job))
|
|
return job
|
|
}
|
|
|
|
// Each call to insertChildJobAndTask with non-empty outputs allocates a fresh TaskID
|
|
// so its action_task_output rows stay isolated per subtest.
|
|
var nextTaskID int64 = 90001
|
|
insertChildJobAndTask := func(t *testing.T, run *actions_model.ActionRun, jobID string, parentID int64, outputs map[string]string) *actions_model.ActionRunJob {
|
|
t.Helper()
|
|
var taskID int64
|
|
if len(outputs) > 0 {
|
|
taskID = nextTaskID
|
|
nextTaskID++
|
|
}
|
|
job := &actions_model.ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: jobID,
|
|
JobID: jobID,
|
|
Attempt: 1,
|
|
Status: actions_model.StatusSuccess,
|
|
ParentJobID: parentID,
|
|
TaskID: taskID,
|
|
}
|
|
require.NoError(t, db.Insert(ctx, job))
|
|
for k, v := range outputs {
|
|
require.NoError(t, db.Insert(ctx, &actions_model.ActionTaskOutput{
|
|
TaskID: taskID,
|
|
OutputKey: k,
|
|
OutputValue: v,
|
|
}))
|
|
}
|
|
return job
|
|
}
|
|
|
|
// childrenByParentOfRun returns the run's jobs indexed by ParentJobID, the shape computeReusableCallerOutputs expects.
|
|
childrenByParentOfRun := func(t *testing.T, runID int64) map[int64][]*actions_model.ActionRunJob {
|
|
t.Helper()
|
|
all, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID})
|
|
require.NoError(t, err)
|
|
index := make(map[int64][]*actions_model.ActionRunJob)
|
|
for _, j := range all {
|
|
if j.ParentJobID != 0 {
|
|
index[j.ParentJobID] = append(index[j.ParentJobID], j)
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
t.Run("returns empty when callee declares no outputs", func(t *testing.T) {
|
|
run := insertRun(t, "no-outputs.yaml")
|
|
caller := insertCaller(t, run, "caller", 0, `on:
|
|
workflow_call:
|
|
outputs: {}
|
|
`, "")
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Empty(t, out)
|
|
})
|
|
|
|
t.Run("unexpanded (skipped) caller yields empty outputs without error", func(t *testing.T) {
|
|
run := insertRun(t, "skipped-caller.yaml")
|
|
// A reusable caller skipped before expansion: IsExpanded=false, empty ReusableWorkflowContent, no children.
|
|
caller := &actions_model.ActionRunJob{
|
|
RunID: run.ID,
|
|
RepoID: run.RepoID,
|
|
OwnerID: run.OwnerID,
|
|
CommitSHA: run.CommitSHA,
|
|
Name: "caller",
|
|
JobID: "caller",
|
|
Attempt: 1,
|
|
Status: actions_model.StatusSkipped,
|
|
IsReusableCaller: true,
|
|
IsExpanded: false,
|
|
}
|
|
require.NoError(t, db.Insert(ctx, caller))
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Empty(t, out)
|
|
})
|
|
|
|
t.Run("literal output value passes through", func(t *testing.T) {
|
|
run := insertRun(t, "literal-out.yaml")
|
|
caller := insertCaller(t, run, "caller", 0, `on:
|
|
workflow_call:
|
|
outputs:
|
|
hello:
|
|
value: world
|
|
`, "")
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"hello": "world"}, out)
|
|
})
|
|
|
|
t.Run("output expression reads child task outputs", func(t *testing.T) {
|
|
run := insertRun(t, "child-out.yaml")
|
|
caller := insertCaller(t, run, "caller", 0, `on:
|
|
workflow_call:
|
|
outputs:
|
|
result:
|
|
value: ${{ jobs.child.outputs.foo }}
|
|
`, "")
|
|
insertChildJobAndTask(t, run, "child", caller.ID, map[string]string{"foo": "bar"})
|
|
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"result": "bar"}, out)
|
|
})
|
|
|
|
t.Run("CallPayload inputs reachable in output expression", func(t *testing.T) {
|
|
run := insertRun(t, "payload-out.yaml")
|
|
payload, err := json.Marshal(api.WorkflowCallPayload{
|
|
Inputs: map[string]any{"env": "staging"},
|
|
})
|
|
require.NoError(t, err)
|
|
caller := insertCaller(t, run, "caller", 0, `on:
|
|
workflow_call:
|
|
inputs:
|
|
env:
|
|
type: string
|
|
outputs:
|
|
env:
|
|
value: ${{ inputs.env }}
|
|
`, string(payload))
|
|
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"env": "staging"}, out)
|
|
})
|
|
|
|
t.Run("nested caller outputs propagate to outer", func(t *testing.T) {
|
|
run := insertRun(t, "nested-out.yaml")
|
|
outer := insertCaller(t, run, "outer", 0, `on:
|
|
workflow_call:
|
|
outputs:
|
|
bubbled:
|
|
value: ${{ jobs.inner.outputs.up }}
|
|
`, "")
|
|
inner := insertCaller(t, run, "inner", outer.ID, `on:
|
|
workflow_call:
|
|
outputs:
|
|
up:
|
|
value: ${{ jobs.leaf.outputs.foo }}
|
|
`, "")
|
|
insertChildJobAndTask(t, run, "leaf", inner.ID, map[string]string{"foo": "bubble-value"})
|
|
|
|
out, err := computeReusableCallerOutputs(ctx, outer, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"bubbled": "bubble-value"}, out)
|
|
})
|
|
|
|
t.Run("matrix children with same JobID prefer non-empty values", func(t *testing.T) {
|
|
run := insertRun(t, "matrix-out.yaml")
|
|
caller := insertCaller(t, run, "caller", 0, `on:
|
|
workflow_call:
|
|
outputs:
|
|
foo:
|
|
value: ${{ jobs.matrix.outputs.foo }}
|
|
`, "")
|
|
insertChildJobAndTask(t, run, "matrix", caller.ID, map[string]string{"foo": ""})
|
|
insertChildJobAndTask(t, run, "matrix", caller.ID, map[string]string{"foo": "filled"})
|
|
|
|
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"foo": "filled"}, out)
|
|
})
|
|
}
|
|
|
|
func TestFindTaskNeeds(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
|
|
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
|
|
|
ret, err := FindTaskNeeds(t.Context(), job)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, ret, 1)
|
|
assert.Contains(t, ret, "job1")
|
|
assert.Len(t, ret["job1"].Outputs, 2)
|
|
assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
|
|
assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
|
|
}
|
|
|
|
func TestGenerateGiteaContextPullRequestTarget(t *testing.T) {
|
|
payload := api.PullRequestPayload{
|
|
PullRequest: &api.PullRequest{
|
|
Base: &api.PRBranchInfo{
|
|
Name: "owner:main",
|
|
Ref: "main",
|
|
Sha: "1234567890abcdef",
|
|
},
|
|
Head: &api.PRBranchInfo{
|
|
Name: "fork:feature",
|
|
Ref: "feature",
|
|
Sha: "fedcba0987654321",
|
|
},
|
|
},
|
|
}
|
|
payloadBytes, err := json.Marshal(payload)
|
|
assert.NoError(t, err)
|
|
|
|
run := &actions_model.ActionRun{
|
|
Event: webhook_module.HookEventPullRequest,
|
|
TriggerEvent: string(actions_module.GithubEventPullRequestTarget),
|
|
EventPayload: string(payloadBytes),
|
|
TriggerUser: &user_model.User{Name: "test-user"},
|
|
Repo: &repo_model.Repository{Name: "test-repo", OwnerName: "test-owner"},
|
|
}
|
|
|
|
giteaCtx := GenerateGiteaContext(t.Context(), run, nil, nil)
|
|
|
|
assert.Equal(t, "refs/heads/main", giteaCtx["ref"])
|
|
assert.Equal(t, "main", giteaCtx["ref_name"])
|
|
}
|
|
|
|
// TestGenerateGiteaContext_NilAttempt verifies that, with no explicit attempt,
|
|
// use GetLatestAttempt to load the latest attempt and resolve attempt-related context variables.
|
|
func TestGenerateGiteaContext_NilAttempt(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
require.NoError(t, repo.LoadOwner(t.Context()))
|
|
actor := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // initiated the run
|
|
triggerer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // initiated the latest attempt
|
|
|
|
run := &actions_model.ActionRun{
|
|
RepoID: repo.ID, Repo: repo, OwnerID: repo.OwnerID,
|
|
TriggerUserID: actor.ID, TriggerUser: actor,
|
|
WorkflowID: "test.yml", Index: 99600, Ref: "refs/heads/main",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", TriggerEvent: "push",
|
|
Status: actions_model.StatusRunning,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), run))
|
|
attempt := &actions_model.ActionRunAttempt{
|
|
RepoID: repo.ID, RunID: run.ID, Attempt: 3, TriggerUserID: triggerer.ID, Status: actions_model.StatusRunning,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), attempt))
|
|
run.LatestAttemptID = attempt.ID
|
|
job := &actions_model.ActionRunJob{
|
|
RunID: run.ID, RunAttemptID: attempt.ID, AttemptJobID: 1, RepoID: repo.ID, OwnerID: repo.OwnerID,
|
|
Name: "j", JobID: "j", Attempt: attempt.Attempt, Status: actions_model.StatusRunning,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), job))
|
|
|
|
// attempt == nil forces the fallback lookup via run.GetLatestAttempt.
|
|
gitCtx := GenerateGiteaContext(t.Context(), run, nil, job)
|
|
assert.Equal(t, actor.Name, gitCtx["actor"])
|
|
assert.Equal(t, triggerer.Name, gitCtx["triggering_actor"])
|
|
assert.Equal(t, "3", gitCtx["run_attempt"])
|
|
}
|