enhance: report a GitHub-style "Set up job" section (#1089)

Reshapes the job log's "Set up job" section to mirror `actions/runner`:

- runner name/version, then `Runner Information` (labels, task, job, repository, event) and `Operating System` groups
- every required action downloaded up front under `Prepare all required actions`, each as `Download action repository '<action>@<ref>' (SHA:<sha>)`
- `Complete job name` closes the section

Downloading up front is the one behavioral change: the same set was already fetched during the pre stage regardless of a step's `if`, now just before the first pre step, so a download failure is reported against the job rather than a step.

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1089
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
This commit is contained in:
bircni
2026-07-22 14:58:23 +00:00
parent 46f22c78d2
commit 8af385d147
10 changed files with 612 additions and 4 deletions

View File

@@ -261,6 +261,10 @@ type NewGitCloneExecutorInput struct {
// 0 for full clone.
Depth int
// Quiet drops the informational clone line to debug level, for callers that log their own
// download summary (the setup section's action report).
Quiet bool
// For Gitea
InsecureSkipTLS bool
}
@@ -347,7 +351,11 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
return func(ctx context.Context) error {
logger := common.Logger(ctx)
if input.Quiet {
logger.Debugf("git clone '%s' # ref=%s", input.URL, input.Ref)
} else {
logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
}
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
defer AcquireCloneLock(input.Dir)()

View File

@@ -17,6 +17,10 @@ import (
"testing"
"time"
"gitea.com/gitea/runner/act/common"
log "github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -404,6 +408,44 @@ func TestGitCloneExecutorOfflineMode(t *testing.T) {
})
}
func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
remoteDir := t.TempDir()
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
workDir := t.TempDir()
require.NoError(t, gitCmd("clone", remoteDir, workDir))
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
// Quiet callers report the download themselves, so the clone line must not reach the job log.
for name, quiet := range map[string]bool{"quiet": true, "not quiet": false} {
t.Run(name, func(t *testing.T) {
logger, hook := logrustest.NewNullLogger()
logger.SetLevel(log.InfoLevel)
ctx := common.WithLogger(context.Background(), logger.WithField("job", "j1"))
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir,
Ref: "main",
Dir: t.TempDir(),
Quiet: quiet,
})(ctx))
var cloneLines int
for _, entry := range hook.AllEntries() {
if strings.HasPrefix(entry.Message, "git clone ") {
cloneLines++
}
}
if quiet {
assert.Zero(t, cloneLines)
} else {
assert.Equal(t, 1, cloneLines)
}
})
}
}
func TestGitCloneExecutorShallow(t *testing.T) {
// Build a local "remote" with several commits on main plus a tag, so a full clone would pull noticeably more history than a shallow one.
remoteDir := t.TempDir()

View File

@@ -71,9 +71,67 @@ func reportStepError(ctx context.Context, rc *RunContext, err error) {
rc.markFailed()
}
// actionPreparer is implemented by steps that download an action before they run, so the job
// executor can fetch all of them up front.
type actionPreparer interface {
prepareActionExecutor() common.Executor
actionDownloadInfo() (reference, sha string, ok bool)
}
// printPrepareActions downloads every action the job uses before its first step runs and reports
// them as actions/runner's "Prepare all required actions" section does. The steps still call
// prepareActionExecutor themselves; it is a no-op once the action is resolved here.
func printPrepareActions(rc *RunContext, preparers []actionPreparer) common.Executor {
return func(ctx context.Context) error {
if len(preparers) == 0 {
return nil
}
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
rawLogger.Infof("Prepare all required actions")
for _, preparer := range preparers {
if err := preparer.prepareActionExecutor()(ctx); err != nil {
// No step has run yet, so the failure belongs to the job.
reportStepError(ctx, rc, err)
return err
}
reference, sha, ok := preparer.actionDownloadInfo()
if !ok {
continue
}
if sha == "" {
rawLogger.Infof("Download action repository '%s'", reference)
} else {
rawLogger.Infof("Download action repository '%s' (SHA:%s)", reference, sha)
}
}
return nil
}
}
// printCompleteJobName closes the setup section the way actions/runner ends its "Set up job" step.
func printCompleteJobName(rc *RunContext) common.Executor {
return func(ctx context.Context) error {
// Name holds a matrix combination; JobName is the shared name GitHub reports.
name := rc.JobName
if name == "" {
name = rc.Name
}
if name == "" && rc.Run != nil {
name = rc.Run.JobID
}
common.Logger(ctx).WithField(rawOutputField, true).Infof("Complete job name: %s", name)
return nil
}
}
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
steps := make([]common.Executor, 0)
preSteps := make([]common.Executor, 0)
// Collected separately: every action is downloaded before the first pre step runs.
stepPreSteps := make([]common.Executor, 0)
preparers := make([]actionPreparer, 0)
var postExecutor common.Executor
steps = append(steps, func(ctx context.Context) error {
@@ -120,9 +178,13 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
return common.NewErrorExecutor(err)
}
if preparer, ok := step.(actionPreparer); ok {
preparers = append(preparers, preparer)
}
stepIdx := stepModel.Number
preExec := step.pre()
preSteps = append(preSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
stepPreSteps = append(stepPreSteps, useStepLogger(rc, stepModel, stepStagePre, func(ctx context.Context) error {
rc.CurrentStepIndex = stepIdx
preErr := preExec(ctx)
if preErr != nil {
@@ -164,6 +226,11 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
}
}
// The setup section of the job log: download the actions, run the pre steps, then name the job.
preSteps = append(preSteps, printPrepareActions(rc, preparers))
preSteps = append(preSteps, stepPreSteps...)
preSteps = append(preSteps, printCompleteJobName(rc))
postExecutor = postExecutor.Finally(func(ctx context.Context) error {
jobError := common.JobError(ctx)
var err error

View File

@@ -24,6 +24,7 @@ import (
"gitea.com/gitea/runner/act/container"
"gitea.com/gitea/runner/act/model"
log "github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -111,6 +112,182 @@ func (sfm *stepFactoryMock) newStep(model *model.Step, rc *RunContext) (step, er
return args.Get(0).(step), args.Error(1)
}
// actionPreparerMock stands in for a step whose action is downloaded before the job's first step.
type actionPreparerMock struct {
reference string
sha string
ok bool
err error
prepared int
}
func (apm *actionPreparerMock) prepareActionExecutor() common.Executor {
return func(context.Context) error {
apm.prepared++
return apm.err
}
}
func (apm *actionPreparerMock) actionDownloadInfo() (string, string, bool) {
return apm.reference, apm.sha, apm.ok
}
func TestPrintPrepareActionsGolden(t *testing.T) {
buf := &bytes.Buffer{}
logger := log.New()
logger.SetOutput(buf)
logger.SetLevel(log.InfoLevel)
logger.SetFormatter(&jobLogFormatter{color: cyan})
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
preparers := []actionPreparer{
&actionPreparerMock{reference: "actions/checkout@v7", sha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", ok: true},
// A resolved commit is best effort; the ref alone is reported when it is unknown.
&actionPreparerMock{reference: "actions/setup-go@v6", ok: true},
// A step that downloads nothing, such as the checkout of the workflow's own repository.
&actionPreparerMock{ok: false},
}
require.NoError(t, printPrepareActions(&RunContext{}, preparers)(ctx))
want := strings.Join([]string{
"[j1] | Prepare all required actions",
"[j1] | Download action repository 'actions/checkout@v7' (SHA:9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)",
"[j1] | Download action repository 'actions/setup-go@v6'",
"",
}, "\n")
assert.Equal(t, want, buf.String())
}
func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
buf := &bytes.Buffer{}
logger := log.New()
logger.SetOutput(buf)
logger.SetFormatter(&jobLogFormatter{color: cyan})
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
require.NoError(t, printPrepareActions(&RunContext{}, nil)(ctx))
assert.Empty(t, buf.String())
}
func TestPrintPrepareActionsFailsJobOnDownloadError(t *testing.T) {
logger, _ := logrustest.NewNullLogger()
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.WithField("job", "j1")))
downloadErr := errors.New("failed to fetch \"actions/checkout\"")
rc := &RunContext{}
remaining := &actionPreparerMock{reference: "actions/setup-go@v6", ok: true}
err := printPrepareActions(rc, []actionPreparer{
&actionPreparerMock{err: downloadErr},
remaining,
})(ctx)
require.ErrorIs(t, err, downloadErr)
// No step has run yet, so the failure has to be recorded against the job itself.
assert.Equal(t, downloadErr, common.JobError(ctx))
assert.True(t, rc.jobFailed)
assert.Zero(t, remaining.prepared)
}
func TestPrintCompleteJobName(t *testing.T) {
for name, tt := range map[string]struct {
rc *RunContext
want string
}{
"job name": {rc: &RunContext{JobName: "lint", Name: "lint-1"}, want: "lint"},
"falls back to name": {rc: &RunContext{Name: "lint-1"}, want: "lint-1"},
"falls back to jobID": {rc: &RunContext{Run: &model.Run{JobID: "lint"}}, want: "lint"},
} {
t.Run(name, func(t *testing.T) {
buf := &bytes.Buffer{}
logger := log.New()
logger.SetOutput(buf)
logger.SetFormatter(&jobLogFormatter{color: cyan})
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
require.NoError(t, printCompleteJobName(tt.rc)(ctx))
assert.Equal(t, "[j1] | Complete job name: "+tt.want+"\n", buf.String())
})
}
}
// actionStepMock is a step whose action has to be downloaded before it can run.
type actionStepMock struct {
*stepMock
*actionPreparerMock
}
// TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep pins the shape of the setup section:
// every action is downloaded before any step runs, and the job name closes the section. A pre
// step that downloaded its own action would leave the log interleaved with the downloads.
func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
ctx := common.WithJobErrorContainer(context.Background())
jim := &jobInfoMock{}
sfm := &stepFactoryMock{}
rc := &RunContext{
JobContainer: &jobContainerMock{},
Run: &model.Run{
JobID: "test",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{"test": {}},
},
},
Config: &Config{},
}
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
steps := []*model.Step{{ID: "1"}, {ID: "2"}}
executorOrder := make([]string, 0)
jim.On("steps").Return(steps)
jim.On("matrix").Return(map[string]any{})
jim.On("startContainer").Return(func(context.Context) error { return nil })
jim.On("stopContainer").Return(func(context.Context) error { return nil })
jim.On("closeContainer").Return(func(context.Context) error { return nil })
jim.On("interpolateOutputs").Return(func(context.Context) error { return nil })
jim.On("result", "success")
for _, stepModel := range steps {
sm := &stepMock{}
apm := &actionPreparerMock{reference: "actions/checkout@v" + stepModel.ID, ok: true}
sfm.On("newStep", stepModel, rc).Return(&actionStepMock{stepMock: sm, actionPreparerMock: apm}, nil)
sm.On("pre").Return(func(context.Context) error {
executorOrder = append(executorOrder, "pre"+stepModel.ID)
return nil
})
sm.On("main").Return(func(context.Context) error {
executorOrder = append(executorOrder, "step"+stepModel.ID)
return nil
})
sm.On("post").Return(func(context.Context) error { return nil })
defer sm.AssertExpectations(t)
}
logger, hook := logrustest.NewNullLogger()
err := newJobExecutor(jim, sfm, rc)(common.WithLogger(ctx, logger.WithField("job", "test")))
require.NoError(t, err)
assert.Equal(t, []string{"pre1", "pre2", "step1", "step2"}, executorOrder)
setup := make([]string, 0)
for _, entry := range hook.AllEntries() {
if strings.HasPrefix(entry.Message, "Prepare all required actions") || strings.HasPrefix(entry.Message, "Download action") ||
strings.HasPrefix(entry.Message, "Complete job name") {
setup = append(setup, entry.Message)
}
}
assert.Equal(t, []string{
"Prepare all required actions",
"Download action repository 'actions/checkout@v1'",
"Download action repository 'actions/checkout@v2'",
"Complete job name: test",
}, setup)
}
func TestNewJobExecutor(t *testing.T) {
table := []struct {
name string

View File

@@ -107,7 +107,13 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
if strings.Contains(stepString, "::add-mask::") {
stepString = "add-mask command"
}
if stage == stepStageMain {
// Main steps print their own raw "Run <title>" header, so this line is redundant and
// only leaks into the "Set up job" section for the first step; keep it as a debug trace.
logger.Debugf("Run %s %s", stage, stepString)
} else {
logger.Infof("Run %s %s", stage, stepString)
}
// Prepare and clean Runner File Commands
actPath := rc.JobContainer.GetActPath()

View File

@@ -131,6 +131,8 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
Token: token,
OfflineMode: sar.RunContext.Config.ActionOfflineMode,
Depth: sar.RunContext.Config.ActionCloneDepth,
// printPrepareActions reports the download with its resolved commit.
Quiet: true,
InsecureSkipTLS: sar.cloneSkipTLS(), // For Gitea
})
@@ -146,6 +148,13 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
}
}
// Best effort: the download report falls back to the ref alone when the commit is unknown.
if _, sha, err := git.FindGitRevision(ctx, actionDir); err != nil {
common.Logger(ctx).Debugf("unable to resolve the commit of %s: %v", sar.remoteAction.Reference(), err)
} else {
sar.resolvedSha = sha
}
remoteReader := func(ctx context.Context) actionYamlReader { //nolint:unparam // pre-existing issue from nektos/act
return func(filename string) (io.Reader, io.Closer, error) {
f, err := os.Open(filepath.Join(actionDir, sar.remoteAction.Path, filename))
@@ -165,6 +174,15 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
}
}
// actionDownloadInfo reports the action this step downloaded and the commit it resolved to. ok is
// false when nothing was fetched, as for the local checkout of the workflow's own repository.
func (sar *stepActionRemote) actionDownloadInfo() (reference, sha string, ok bool) {
if sar.remoteAction == nil || sar.action == nil {
return "", "", false
}
return sar.remoteAction.Reference(), sar.resolvedSha, true
}
func (sar *stepActionRemote) pre() common.Executor {
sar.env = map[string]string{}
@@ -313,6 +331,16 @@ func (ra *remoteAction) CloneURL(u string) string {
return fmt.Sprintf("%s/%s/%s", u, ra.Org, ra.Repo)
}
// Reference renders the action as {org}/{repo}[/path]@{ref}, omitting the download source, which
// can be interpolated from a secret.
func (ra *remoteAction) Reference() string {
repo := fmt.Sprintf("%s/%s", ra.Org, ra.Repo)
if ra.Path != "" {
repo = fmt.Sprintf("%s/%s", repo, ra.Path)
}
return fmt.Sprintf("%s@%s", repo, ra.Ref)
}
func (ra *remoteAction) IsCheckout() bool {
if ra.Org == "actions" && ra.Repo == "checkout" {
return true

View File

@@ -10,6 +10,9 @@ import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
@@ -818,6 +821,97 @@ func Test_newRemoteAction(t *testing.T) {
}
}
func Test_remoteActionReference(t *testing.T) {
tests := []struct {
uses string
want string
}{
{uses: "actions/checkout@v7", want: "actions/checkout@v7"},
{uses: "actions/aws/ec2@main", want: "actions/aws/ec2@main"},
// The download source can be interpolated from a secret and must stay out of the log.
{uses: "https://gitea.example.com/actions/checkout@v7", want: "actions/checkout@v7"},
}
for _, tt := range tests {
t.Run(tt.uses, func(t *testing.T) {
assert.Equal(t, tt.want, newRemoteAction(tt.uses).Reference())
})
}
}
// TestStepActionRemotePreResolvesDownloadedCommit runs the real download path against a local
// git repository standing in for the actions instance, so the reported commit is the one the
// clone actually checked out.
func TestStepActionRemotePreResolvesDownloadedCommit(t *testing.T) {
instance := t.TempDir()
actionDir := filepath.Join(instance, "actions", "setup-go")
require.NoError(t, os.MkdirAll(actionDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(actionDir, "action.yml"),
[]byte("name: setup-go\nruns:\n using: node20\n main: index.js\n"), 0o600))
// Supply an identity on the commit so the test does not depend on a
// git identity being configured in the environment; a CI runner without
// user.name/user.email would otherwise fail "commit" with exit code 128.
for _, args := range [][]string{
{"init", "--initial-branch=main", actionDir},
{"-C", actionDir, "add", "action.yml"},
{"-C", actionDir, "-c", "user.name=runner", "-c", "user.email=runner@example.com", "-c", "commit.gpgsign=false", "commit", "-m", "action"},
} {
cmd := exec.Command("git", args...)
require.NoError(t, cmd.Run(), "git %v", args)
}
out, err := exec.Command("git", "-C", actionDir, "rev-parse", "HEAD").Output()
require.NoError(t, err)
wantSha := strings.TrimSpace(string(out))
sar := &stepActionRemote{
Step: &model.Step{Uses: "actions/setup-go@main"},
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://gitea.example.com",
DefaultActionInstance: instance,
ActionCacheDir: t.TempDir(),
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{Jobs: map[string]*model.Job{"1": {}}},
},
},
readAction: readActionImpl,
}
require.NoError(t, sar.prepareActionExecutor()(context.Background()))
reference, sha, ok := sar.actionDownloadInfo()
assert.True(t, ok)
assert.Equal(t, "actions/setup-go@main", reference)
assert.Equal(t, wantSha, sha)
}
func TestStepActionRemoteActionDownloadInfo(t *testing.T) {
t.Run("reports the action and its resolved commit", func(t *testing.T) {
sar := &stepActionRemote{
remoteAction: newRemoteAction("actions/checkout@v7"),
action: &model.Action{},
resolvedSha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
}
reference, sha, ok := sar.actionDownloadInfo()
assert.True(t, ok)
assert.Equal(t, "actions/checkout@v7", reference)
assert.Equal(t, "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", sha)
})
t.Run("reports nothing when no action was downloaded", func(t *testing.T) {
// The local checkout of the workflow's own repository resolves no action.
sar := &stepActionRemote{remoteAction: newRemoteAction("actions/checkout@v7")}
_, _, ok := sar.actionDownloadInfo()
assert.False(t, ok)
})
}
func Test_safeFilename(t *testing.T) {
tests := []struct {
s string

View File

@@ -315,7 +315,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
}()
reporter.Logf("%s(version:%s) received task %v of job %v, be triggered by event: %s", r.name, ver.Version(), task.Id, task.Context.Fields["job"].GetStringValue(), task.Context.Fields["event_name"].GetStringValue())
r.reportSetup(reporter, task)
workflow, jobID, err := generateWorkflow(task)
if err != nil {

83
internal/app/run/setup.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"gitea.com/gitea/runner/internal/pkg/report"
"gitea.com/gitea/runner/internal/pkg/ver"
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
)
// osReleasePath describes the host distribution on Linux; absent elsewhere, where the platform
// falls back to the Go runtime alone. A var so tests can point it at a fixture.
var osReleasePath = "/etc/os-release"
// reportSetup opens the job log the way actions/runner opens its "Set up job" step. The action
// downloads and the closing job name are written later, as the job starts.
func (r *Runner) reportSetup(reporter *report.Reporter, task *runnerv1.Task) {
for _, line := range r.setupLines(task) {
reporter.Logf("%s", line)
}
}
// setupLines names the runner, then reports what it was asked to run and the host it runs on, each
// in its own group.
func (r *Runner) setupLines(task *runnerv1.Task) []string {
fields := task.Context.Fields
lines := []string{
fmt.Sprintf("%s(version:%s)", r.name, ver.Version()),
"::group::Runner Information",
}
if names := r.labels.Names(); len(names) > 0 {
lines = append(lines, "Runner labels: "+strings.Join(names, ", "))
}
lines = append(lines,
// The task id correlates the job log with the runner's log and the server's task list.
fmt.Sprintf("Task: %d", task.Id),
"Job: "+fields["job"].GetStringValue(),
"Repository: "+fields["repository"].GetStringValue(),
"Triggered by event: "+fields["event_name"].GetStringValue(),
"::endgroup::",
"::group::Operating System",
)
lines = append(lines, osInfo()...)
return append(lines, "::endgroup::")
}
// osInfo describes the host the runner executes on.
func osInfo() []string {
lines := make([]string, 0, 2)
if name := prettyOSName(); name != "" {
lines = append(lines, name)
}
return append(lines, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH))
}
// prettyOSName reads PRETTY_NAME (e.g. "Ubuntu 24.04.4 LTS") from os-release, or "" when absent.
func prettyOSName() string {
data, err := os.ReadFile(osReleasePath)
if err != nil {
return ""
}
for line := range strings.SplitSeq(string(data), "\n") {
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
if !ok || key != "PRETTY_NAME" {
continue
}
// Values are shell-quoted, but the quotes are optional.
if unquoted, err := strconv.Unquote(value); err == nil {
return unquoted
}
return value
}
return ""
}

View File

@@ -0,0 +1,103 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"gitea.com/gitea/runner/internal/pkg/labels"
"gitea.com/gitea/runner/internal/pkg/ver"
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
func TestSetupLines(t *testing.T) {
original := osReleasePath
path := filepath.Join(t.TempDir(), "os-release")
require.NoError(t, os.WriteFile(path, []byte("PRETTY_NAME=\"Ubuntu 24.04.4 LTS\"\n"), 0o600))
osReleasePath = path
defer func() { osReleasePath = original }()
r := &Runner{
name: "gitea-com-gitea-0003",
labels: labels.Labels{
{Name: "ubuntu-latest", Schema: labels.SchemeDocker, Arg: "//node:20"},
{Name: "ubuntu-22.04", Schema: labels.SchemeDocker, Arg: "//node:20"},
},
}
taskCtx, err := structpb.NewStruct(map[string]any{
"job": "lint",
"repository": "gitea/runner",
"event_name": "pull_request",
})
require.NoError(t, err)
assert.Equal(t, []string{
"gitea-com-gitea-0003(version:" + ver.Version() + ")",
"::group::Runner Information",
"Runner labels: ubuntu-latest, ubuntu-22.04",
"Task: 268506",
"Job: lint",
"Repository: gitea/runner",
"Triggered by event: pull_request",
"::endgroup::",
"::group::Operating System",
"Ubuntu 24.04.4 LTS",
fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
"::endgroup::",
}, r.setupLines(&runnerv1.Task{Id: 268506, Context: taskCtx}))
}
func TestPrettyOSName(t *testing.T) {
tests := map[string]struct {
osRelease string
want string
}{
"quoted value": {
osRelease: "NAME=\"Ubuntu\"\nVERSION_ID=\"24.04\"\nPRETTY_NAME=\"Ubuntu 24.04.4 LTS\"\n",
want: "Ubuntu 24.04.4 LTS",
},
"unquoted value": {
osRelease: "PRETTY_NAME=Alpine Linux v3.21\n",
want: "Alpine Linux v3.21",
},
"no pretty name": {
osRelease: "NAME=\"Ubuntu\"\nVERSION_ID=\"24.04\"\n",
want: "",
},
// A key that merely ends in PRETTY_NAME must not be mistaken for it.
"similar key": {
osRelease: "IMAGE_PRETTY_NAME=\"Ubuntu Core 24\"\n",
want: "",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "os-release")
require.NoError(t, os.WriteFile(path, []byte(tt.osRelease), 0o600))
original := osReleasePath
osReleasePath = path
defer func() { osReleasePath = original }()
assert.Equal(t, tt.want, prettyOSName())
})
}
t.Run("missing file", func(t *testing.T) {
original := osReleasePath
osReleasePath = filepath.Join(t.TempDir(), "absent")
defer func() { osReleasePath = original }()
assert.Empty(t, prettyOSName())
})
}