Files
gitea/tests/integration/actions_log_test.go
T
2087d4a1a5 refactor: use the shared workflow model from actionslib (#38768)
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>
2026-08-07 20:56:40 -07:00

310 lines
9.6 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"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"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestDownloadTaskLogs(t *testing.T) {
now := time.Now()
testCases := []struct {
treePath string
fileContent string
outcome []*mockTaskOutcome
zstdEnabled bool
}{
{
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
fileContent: `name: download-task-logs-zstd
on:
push:
paths:
- '.gitea/workflows/download-task-logs-zstd.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo job1 with zstd enabled
job2:
runs-on: ubuntu-latest
steps:
- run: echo job2 with zstd enabled
`,
outcome: []*mockTaskOutcome{
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(2 * time.Second)),
Content: "job1 zstd enabled",
},
{
Time: timestamppb.New(now.Add(3 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(2 * time.Second)),
Content: "job2 zstd enabled",
},
{
Time: timestamppb.New(now.Add(3 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
zstdEnabled: true,
},
{
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
fileContent: `name: download-task-logs-no-zstd
on:
push:
paths:
- '.gitea/workflows/download-task-logs-no-zstd.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo job1 with zstd disabled
job2:
runs-on: ubuntu-latest
steps:
- run: echo job2 with zstd disabled
`,
outcome: []*mockTaskOutcome{
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(4 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(5 * time.Second)),
Content: "job1 zstd disabled",
},
{
Time: timestamppb.New(now.Add(6 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(4 * time.Second)),
Content: " \U0001F433 docker create image",
},
{
Time: timestamppb.New(now.Add(5 * time.Second)),
Content: "job2 zstd disabled",
},
{
Time: timestamppb.New(now.Add(6 * time.Second)),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
zstdEnabled: false,
},
}
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-download-task-logs", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
runner := newMockRunner()
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
for _, tc := range testCases {
t.Run("test "+tc.treePath, func(t *testing.T) {
var resetFunc func()
if tc.zstdEnabled {
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd")
assert.True(t, setting.Actions.LogCompression.IsZstd())
} else {
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none")
assert.False(t, setting.Actions.LogCompression.IsZstd())
}
// create the workflow file
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
// fetch and execute tasks
for _, outcome := range tc.outcome {
task := runner.fetchTask(t)
runner.execTask(t, task, outcome)
// check whether the log file exists
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
if setting.Actions.LogCompression.IsZstd() {
logFileName += ".zst"
}
_, err := storage.Actions.Stat(logFileName)
assert.NoError(t, err)
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
// download task logs and check content
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(outcome.logRows))
for idx, lr := range outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
// download task logs from API and check content
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
assert.Len(t, logTextLines, len(outcome.logRows))
for idx, lr := range outcome.logRows {
assert.Equal(
t,
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
}
resetFunc()
})
}
t.Run("DownloadRerunTaskLogs", func(t *testing.T) {
treePath := ".gitea/workflows/download-rerun-logs.yml"
fileContent := `name: download-rerun-logs
on:
push:
paths:
- '.gitea/workflows/download-rerun-logs.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo job1
job2:
runs-on: ubuntu-latest
needs: [job1]
steps:
- run: echo job2
`
// create the workflow file
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+treePath, fileContent)
createWorkflowFile(t, token, user2.Name, repo.Name, treePath, opts)
// first run
job1Task1 := runner.fetchTask(t)
_, job1, _ := getTaskAndJobAndRunByTaskID(t, job1Task1.Id)
runner.execTask(t, job1Task1, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: "job1 first run",
},
},
})
job2Task1 := runner.fetchTask(t)
_, job2, run := getTaskAndJobAndRunByTaskID(t, job2Task1.Id)
runner.execTask(t, job2Task1, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: "job2 first run",
},
},
})
// check job1 log
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1.ID)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "job1 first run")
// check job2 log
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "job2 first run")
// only rerun job2
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
session.MakeRequest(t, req, http.StatusOK)
job2TaskRerun := runner.fetchTask(t)
runner.execTask(t, job2TaskRerun, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
logRows: []*runnerv1.LogRow{
{
Time: timestamppb.New(now.Add(1 * time.Second)),
Content: "job2 rerun",
},
},
})
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
job1Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
assert.Equal(t, run.LatestAttemptID, job1Rerun.RunAttemptID)
job2Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
assert.Equal(t, run.LatestAttemptID, job2Rerun.RunAttemptID)
// check job1 rerun log
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1Rerun.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "job1 first run") // should return the log of first run because job1 didn't rerun
// check job2 rerun log
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2Rerun.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "job2 rerun")
})
})
}