Files
forge/tests/integration/actions_log_test.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

309 lines
9.6 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
actions_model "github.com/hanzoai/git/models/actions"
auth_model "github.com/hanzoai/git/models/auth"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unittest"
user_model "github.com/hanzoai/git/models/user"
runner_module "github.com/hanzoai/forge/modules/actions/runner"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/storage"
"github.com/hanzoai/git/modules/test"
"github.com/stretchr/testify/assert"
)
func TestDownloadTaskLogs(t *testing.T) {
now := time.Now()
testCases := []struct {
treePath string
fileContent string
outcome []*mockTaskOutcome
zstdEnabled bool
}{
{
treePath: ".hanzo/workflows/download-task-logs-zstd.yml",
fileContent: `name: download-task-logs-zstd
on:
push:
paths:
- '.hanzo/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: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(1 * time.Second).UnixNano(),
Content: " \U0001F433 docker create image",
},
{
Time: now.Add(2 * time.Second).UnixNano(),
Content: "job1 zstd enabled",
},
{
Time: now.Add(3 * time.Second).UnixNano(),
Content: "\U0001F3C1 Job succeeded",
},
},
},
{
result: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(1 * time.Second).UnixNano(),
Content: " \U0001F433 docker create image",
},
{
Time: now.Add(2 * time.Second).UnixNano(),
Content: "job2 zstd enabled",
},
{
Time: now.Add(3 * time.Second).UnixNano(),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
zstdEnabled: true,
},
{
treePath: ".hanzo/workflows/download-task-logs-no-zstd.yml",
fileContent: `name: download-task-logs-no-zstd
on:
push:
paths:
- '.hanzo/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: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(4 * time.Second).UnixNano(),
Content: " \U0001F433 docker create image",
},
{
Time: now.Add(5 * time.Second).UnixNano(),
Content: "job1 zstd disabled",
},
{
Time: now.Add(6 * time.Second).UnixNano(),
Content: "\U0001F3C1 Job succeeded",
},
},
},
{
result: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(4 * time.Second).UnixNano(),
Content: " \U0001F433 docker create image",
},
{
Time: now.Add(5 * time.Second).UnixNano(),
Content: "job2 zstd disabled",
},
{
Time: now.Add(6 * time.Second).UnixNano(),
Content: "\U0001F3C1 Job succeeded",
},
},
},
},
zstdEnabled: false,
},
}
onGitRun(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.logLines))
for idx, lr := range outcome.logLines {
assert.Equal(
t,
fmt.Sprintf("%s %s", time.Unix(0, lr.Time).UTC().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("/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.logLines))
for idx, lr := range outcome.logLines {
assert.Equal(
t,
fmt.Sprintf("%s %s", time.Unix(0, lr.Time).UTC().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
logTextLines[idx],
)
}
}
resetFunc()
})
}
t.Run("DownloadRerunTaskLogs", func(t *testing.T) {
treePath := ".hanzo/workflows/download-rerun-logs.yml"
fileContent := `name: download-rerun-logs
on:
push:
paths:
- '.hanzo/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: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(1 * time.Second).UnixNano(),
Content: "job1 first run",
},
},
})
job2Task1 := runner.fetchTask(t)
_, job2, run := getTaskAndJobAndRunByTaskID(t, job2Task1.ID)
runner.execTask(t, job2Task1, &mockTaskOutcome{
result: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(1 * time.Second).UnixNano(),
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: runner_module.Success,
logLines: []runner_module.Line{
{
Time: now.Add(1 * time.Second).UnixNano(),
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")
})
})
}