mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-19 11:36:43 +00:00
The current shared tools cache is not concurrency-safe, e.g. multiple jobs can write and corrupt it, for example `setup-go` with explicit go version under concurrency reliably corrupts the tool cache and fails all jobs. This adds a new `runner.tool_cache_mode` (and `--tool-cache-mode` exec option) option which defaults to unshared tools cache: - `none` mounts nothing, so a job uses what its image ships there and discards what it installs - `shared` keeps the single volume every job reuses, and warns when `runner.capacity` is above 1 Under `none` effective tool cache can only come from the image or host, which is the same as it is on GitHub Actions which ships many preinstalled tools in its fat VM images. Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1171 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
235 lines
6.0 KiB
Go
235 lines
6.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.com/gitea/runner/internal/pkg/config"
|
|
|
|
"gitea.dev/actionslib/pkg/model"
|
|
"github.com/stretchr/testify/require"
|
|
"go.yaml.in/yaml/v4"
|
|
)
|
|
|
|
func TestExecuteArgsResolve(t *testing.T) {
|
|
workdir := t.TempDir()
|
|
args := &executeArgs{workdir: workdir}
|
|
|
|
require.Empty(t, args.resolve(""))
|
|
require.Equal(t, filepath.Join(workdir, "sub", "file"), args.resolve("sub/file"))
|
|
|
|
abs := filepath.Join(workdir, "abs")
|
|
require.Equal(t, abs, args.resolve(abs))
|
|
}
|
|
|
|
func TestSharedToolCache(t *testing.T) {
|
|
shared, err := sharedToolCache(config.ToolCacheModeShared)
|
|
require.NoError(t, err)
|
|
require.True(t, shared)
|
|
|
|
shared, err = sharedToolCache(config.ToolCacheModeNone)
|
|
require.NoError(t, err)
|
|
require.False(t, shared)
|
|
|
|
_, err = sharedToolCache("everyone")
|
|
require.ErrorContains(t, err, "tool-cache-mode")
|
|
}
|
|
|
|
func TestExecuteArgsPaths(t *testing.T) {
|
|
workdir := t.TempDir()
|
|
args := &executeArgs{
|
|
workdir: workdir,
|
|
workflowsPath: ".gitea/workflows",
|
|
envfile: ".env",
|
|
}
|
|
|
|
require.Equal(t, filepath.Join(workdir, ".gitea/workflows"), args.WorkflowsPath())
|
|
require.Equal(t, filepath.Join(workdir, ".env"), args.Envfile())
|
|
require.Equal(t, workdir, args.Workdir())
|
|
}
|
|
|
|
func TestExecuteArgsLoadVars(t *testing.T) {
|
|
require.Empty(t, (&executeArgs{}).LoadVars())
|
|
|
|
args := &executeArgs{vars: []string{"FOO=bar", "EMPTY", "WITH=eq=sign"}}
|
|
require.Equal(t, map[string]string{
|
|
"FOO": "bar",
|
|
"EMPTY": "",
|
|
"WITH": "eq=sign",
|
|
}, args.LoadVars())
|
|
}
|
|
|
|
func TestExecuteArgsLoadSecrets(t *testing.T) {
|
|
t.Setenv("FROMENV", "from-env-value")
|
|
|
|
args := &executeArgs{secrets: []string{"token=abc", "fromenv"}}
|
|
require.Equal(t, map[string]string{
|
|
"TOKEN": "abc",
|
|
"FROMENV": "from-env-value",
|
|
}, args.LoadSecrets())
|
|
}
|
|
|
|
func TestReadEnvs(t *testing.T) {
|
|
dir := t.TempDir()
|
|
envFile := filepath.Join(dir, ".env")
|
|
require.NoError(t, os.WriteFile(envFile, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
|
|
|
|
envs := map[string]string{"EXISTING": "keep"}
|
|
require.True(t, readEnvs(envFile, envs))
|
|
require.Equal(t, map[string]string{
|
|
"EXISTING": "keep",
|
|
"FOO": "bar",
|
|
"BAZ": "qux",
|
|
}, envs)
|
|
|
|
missing := map[string]string{}
|
|
require.False(t, readEnvs(filepath.Join(dir, "does-not-exist"), missing))
|
|
require.Empty(t, missing)
|
|
}
|
|
|
|
func TestRunExecListUsesJobEventAndAllPlans(t *testing.T) {
|
|
planner := &fakeWorkflowPlanner{
|
|
events: []string{"push", "pull_request"},
|
|
plans: map[string]*model.Plan{
|
|
"job:build": listPlan("build", "Build", "push"),
|
|
"event:push": listPlan("test", "Test", "push"),
|
|
"all": listPlan("lint", "Lint", "push"),
|
|
},
|
|
}
|
|
|
|
out := captureStdout(t, func() {
|
|
require.NoError(t, runExecList(planner, &executeArgs{job: "build"}))
|
|
require.NoError(t, runExecList(planner, &executeArgs{event: "push"}))
|
|
require.NoError(t, runExecList(planner, &executeArgs{autodetectEvent: true}))
|
|
require.NoError(t, runExecList(planner, &executeArgs{}))
|
|
})
|
|
|
|
require.Contains(t, out, "Build")
|
|
require.Contains(t, out, "Test")
|
|
require.Contains(t, out, "Lint")
|
|
require.Equal(t, []string{"job:build", "event:push", "event:push", "all"}, planner.calls)
|
|
}
|
|
|
|
func TestPrintListReportsDuplicateJobIDs(t *testing.T) {
|
|
workflowA := workflowForList("A", "a.yml", "push", "build", "Build A")
|
|
workflowB := workflowForList("B", "b.yml", "pull_request", "build", "Build B")
|
|
plan := &model.Plan{Stages: []*model.Stage{{
|
|
Runs: []*model.Run{
|
|
{Workflow: workflowA, JobID: "build"},
|
|
{Workflow: workflowB, JobID: "build"},
|
|
},
|
|
}}}
|
|
|
|
out := captureStdout(t, func() {
|
|
printList(plan)
|
|
})
|
|
|
|
require.Contains(t, out, "Workflow file")
|
|
require.Contains(t, out, "Build A")
|
|
require.Contains(t, out, "Build B")
|
|
require.Contains(t, out, "Detected multiple jobs with the same job name")
|
|
}
|
|
|
|
func TestLoadExecCmdDefinesExpectedFlags(t *testing.T) {
|
|
cmd := loadExecCmd(context.Background())
|
|
|
|
for _, name := range []string{
|
|
"list",
|
|
"job",
|
|
"event",
|
|
"workflows",
|
|
"directory",
|
|
"env",
|
|
"secret",
|
|
"var",
|
|
"dryrun",
|
|
"image",
|
|
"gitea-instance",
|
|
} {
|
|
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
|
t.Fatalf("expected flag %q to be registered", name)
|
|
}
|
|
}
|
|
|
|
require.Equal(t, "exec", cmd.Use)
|
|
require.NoError(t, cmd.Args(cmd, strings.Split("a b c", " ")))
|
|
require.Error(t, cmd.Args(cmd, strings.Fields(strings.Repeat("arg ", 21))))
|
|
}
|
|
|
|
type fakeWorkflowPlanner struct {
|
|
events []string
|
|
plans map[string]*model.Plan
|
|
calls []string
|
|
}
|
|
|
|
func (p *fakeWorkflowPlanner) PlanEvent(eventName string) (*model.Plan, error) {
|
|
p.calls = append(p.calls, "event:"+eventName)
|
|
return p.plans["event:"+eventName], nil
|
|
}
|
|
|
|
func (p *fakeWorkflowPlanner) PlanJob(jobName string) (*model.Plan, error) {
|
|
p.calls = append(p.calls, "job:"+jobName)
|
|
return p.plans["job:"+jobName], nil
|
|
}
|
|
|
|
func (p *fakeWorkflowPlanner) PlanAll() (*model.Plan, error) {
|
|
p.calls = append(p.calls, "all")
|
|
return p.plans["all"], nil
|
|
}
|
|
|
|
func (p *fakeWorkflowPlanner) GetEvents() []string {
|
|
return p.events
|
|
}
|
|
|
|
func listPlan(jobID, jobName, event string) *model.Plan {
|
|
workflow := workflowForList("Workflow "+jobID, jobID+".yml", event, jobID, jobName)
|
|
return &model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: workflow, JobID: jobID}}}}}
|
|
}
|
|
|
|
func workflowForList(name, file, event, jobID, jobName string) *model.Workflow {
|
|
return &model.Workflow{
|
|
Name: name,
|
|
File: file,
|
|
RawOn: rawOnNode(event),
|
|
Jobs: map[string]*model.Job{
|
|
jobID: {Name: jobName},
|
|
},
|
|
}
|
|
}
|
|
|
|
func rawOnNode(event string) yaml.Node {
|
|
var node yaml.Node
|
|
if err := yaml.Unmarshal([]byte(event), &node); err != nil {
|
|
panic(err)
|
|
}
|
|
return *node.Content[0]
|
|
}
|
|
|
|
func captureStdout(t *testing.T, fn func()) string {
|
|
t.Helper()
|
|
|
|
old := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
require.NoError(t, err)
|
|
os.Stdout = w
|
|
|
|
fn()
|
|
|
|
require.NoError(t, w.Close())
|
|
os.Stdout = old
|
|
|
|
var buf bytes.Buffer
|
|
_, err = io.Copy(&buf, r)
|
|
require.NoError(t, err)
|
|
require.NoError(t, r.Close())
|
|
return buf.String()
|
|
}
|