Compare commits

..
Author SHA1 Message Date
silverwind 5f255a3e92 test: expect the slash form of the CA path on Windows
HostEnvironment.GetActPath converts backslashes to slashes on Windows, so
NODE_EXTRA_CA_CERTS points at the slash form of the temp dir and the
assertion against the raw path failed there.

Assisted-by: Claude Code:claude-opus-5-5
2026-11-05 07:20:46 +01:00
28 changed files with 290 additions and 260 deletions

No files matched your search

+7 -10
View File
@@ -851,9 +851,6 @@ func TestHandler_touchCacheSkipsFreshCompletedEntries(t *testing.T) {
func newTestHandler(t *testing.T, policy Policy, entries ...*Cache) *Handler {
t.Helper()
handler := startTestHandler(t, Options{Policy: policy})
if len(entries) == 0 {
return handler
}
db, err := handler.openDB()
require.NoError(t, err)
@@ -943,10 +940,6 @@ func TestHandler_RejectsBearer(t *testing.T) {
func TestHandler_UnregisterRevokes(t *testing.T) {
t.Parallel()
handler, err := StartHandler(Options{Dir: filepath.Join(t.TempDir(), "artifactcache")})
require.NoError(t, err)
defer handler.Close()
for _, tc := range []struct {
name string
token string
@@ -957,6 +950,9 @@ func TestHandler_UnregisterRevokes(t *testing.T) {
{"reference counted", "shared", "v", 2},
} {
t.Run(tc.name, func(t *testing.T) {
handler, err := StartHandler(Options{Dir: filepath.Join(t.TempDir(), "artifactcache")})
require.NoError(t, err)
defer handler.Close()
revokers := make([]func(), tc.registrations)
for i := range revokers {
revokers[i] = handler.RegisterJob(tc.token, JobCredential{Repo: testRepo})
@@ -1053,8 +1049,9 @@ func TestHandler_ArtifactSignature(t *testing.T) {
base := handler.ExternalURL() + apiPath
good := signArtifactURL(handler, 1)
secret := bytes.Clone(handler.secret)
secret[0] ^= 1
other, err := StartHandler(Options{Dir: filepath.Join(t.TempDir(), "artifactcache2")})
require.NoError(t, err)
defer other.Close()
for _, tc := range []struct {
name string
@@ -1063,7 +1060,7 @@ func TestHandler_ArtifactSignature(t *testing.T) {
{"missing signature", fmt.Sprintf("%s/artifacts/%d", base, 1)},
{"tampered signature", good[:len(good)-4] + "dead"},
{"expired signature", handler.signedArtifactURL(JobCredential{}, 1, time.Now().Add(-time.Second))},
{"signature from a different server", base + strings.SplitN(signArtifactURL(&Handler{secret: secret}, 1), apiPath, 2)[1]},
{"signature from a different server", base + strings.SplitN(signArtifactURL(other, 1), apiPath, 2)[1]},
} {
t.Run(tc.name, func(t *testing.T) {
resp, err := testClient.Get(tc.url)
+3 -14
View File
@@ -36,7 +36,7 @@ func TestFetchAction(t *testing.T) {
root := t.TempDir()
remote := filepath.Join(root, "org", "action")
require.NoError(t, os.CopyFS(remote, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
require.NoError(t, gitCmd("init", "--initial-branch=main", remote))
require.NoError(t, os.WriteFile(filepath.Join(remote, "action.yml"), []byte("name: action\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(remote, "run.sh"), []byte("#!/bin/sh\n"), 0o755))
require.NoError(t, os.Symlink("action.yml", filepath.Join(remote, "link")))
@@ -53,7 +53,6 @@ func TestFetchAction(t *testing.T) {
var archiveRequests []http.Header
var mirror *httptest.Server
archiveCache := map[string][]byte{}
revisions := map[string]string{}
requests, archives, failures := 0, "", 0
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
mu.Lock()
@@ -80,14 +79,7 @@ func TestFetchAction(t *testing.T) {
http.Redirect(writer, request, mirror.URL+request.URL.Path, http.StatusFound)
return
}
sha, ok := revisions[ref]
if !ok {
var err error
sha, err = gitOutput(request.Context(), remote, "rev-parse", strings.TrimSuffix(ref, ".tar.gz"))
if err == nil {
revisions[ref] = sha
}
}
sha, _ := gitOutput(request.Context(), remote, "rev-parse", strings.TrimSuffix(ref, ".tar.gz"))
etag := fmt.Sprintf(`W/"%s-tar.gz-%t"`, sha, archives != "unprefixed")
if request.Header.Get("If-None-Match") == etag {
writer.WriteHeader(http.StatusNotModified)
@@ -216,9 +208,6 @@ func TestFetchAction(t *testing.T) {
require.NoError(t, gitCmd("-C", remote, "commit", "--allow-empty", "-m", "moved"))
require.NoError(t, gitCmd("-C", remote, "tag", "--force", "v1"))
expectedSha = gitRevParse(t, remote, "v1")
mu.Lock()
clear(revisions)
mu.Unlock()
moved, movedSha, err := FetchAction(t.Context(), input)
require.NoError(t, err)
assert.Equal(t, expectedSha, movedSha)
@@ -264,7 +253,7 @@ func TestFetchAction(t *testing.T) {
})
t.Run("downloads again over a tree whose files changed size or that has no record, not under a job copying it", func(t *testing.T) {
sha := expectedSha
sha := gitRevParse(t, remote, "main")
for _, mode := range []string{"prefixed", "none"} {
serve(mode, 0)
input := newInput(sha)
+31 -25
View File
@@ -112,7 +112,8 @@ func TestFindGitMetadataOfSHA256RepositoryUsesOrigin(t *testing.T) {
t.Parallel()
basedir := t.TempDir()
const remoteURL = "https://github.com/owner/repo.git"
require.NoError(t, gitCmd("init", "--template=", "--object-format=sha256", "--initial-branch=main", basedir))
require.NoError(t, gitCmd("init", "--object-format=sha256", "--initial-branch=main", basedir))
require.NoError(t, os.RemoveAll(filepath.Join(basedir, ".git", "hooks")))
require.NoError(t, gitCmd("-C", basedir, "commit", "--allow-empty", "-m", "init"))
require.NoError(t, gitCmd("-C", basedir, "remote", "add", "origin", remoteURL))
require.NoError(t, gitCmd("-C", basedir, "config", "--add", "remote.origin.url", "https://github.com/other/repo.git"))
@@ -204,13 +205,16 @@ func TestGitCloneExecutor(t *testing.T) {
require.NoError(t, gitCmd("-C", workDir, "add", "action.sh", ".gitattributes"))
require.NoError(t, gitCmd("-C", workDir, "commit", "-m", "initial"))
require.NoError(t, gitCmd("-C", workDir, "tag", "v2"))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "v2"))
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "act-fails"))
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "branch-commit"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "act-fails"))
fullSha := gitRevParse(t, workDir, "main")
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main", "v2", "act-fails", "main:refs/heads/"+fullSha[:4]))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main:refs/heads/"+fullSha[:4]))
hostConfig := filepath.Join(t.TempDir(), "gitconfig")
require.NoError(t, os.WriteFile(hostConfig, []byte("[core]\n\tautocrlf = true\n\teol = crlf\n[url "+strconv.Quote(remoteDir)+"]\n\tinsteadOf = https://example.invalid/action\n"), 0o644))
@@ -278,10 +282,10 @@ func TestGitCloneExecutor(t *testing.T) {
func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
t.Parallel()
createRemote := func(message string) string {
dir := t.TempDir()
require.NoError(t, os.CopyFS(dir, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", message))
return dir
remoteDir, workDir := newGitRemote(t, message)
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
return remoteDir
}
oldRemoteDir := createRemote("old-action")
@@ -401,30 +405,31 @@ func TestGitCloneExecutorShallow(t *testing.T) {
remoteDir, workDir := newGitRemote(t, "c1", "c2", "c3")
require.NoError(t, gitCmd("-C", workDir, "tag", "v1"))
unadvertisedSHA := gitRevParse(t, workDir, "HEAD~1")
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main", "v1"))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "v1"))
shallowMarker := func(dir string) string { return filepath.Join(dir, ".git", "shallow") }
for name, ref := range map[string]string{"branch is cloned shallowly": "main", "tag is cloned shallowly": "v1"} {
t.Run(name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir, Ref: ref, Dir: dir, Depth: 1,
})(t.Context()))
assert.FileExists(t, shallowMarker(dir), "clone should be shallow")
assert.Equal(t, []string{"c3"}, gitSubjects(t, dir), "only the tip commit should be present")
assert.Equal(t, 1, gitRevCount(t, dir), "only the tip commit should be present")
assert.Equal(t, "c3", gitHeadSubject(t, dir))
})
}
unadvertisedRemote := func(t *testing.T, allowUnadvertised bool) string {
remote := filepath.Join(t.TempDir(), "remote.git")
require.NoError(t, gitCmd("clone", "--bare", "--template=", "--config", "uploadpack.allowAnySHA1InWant="+strconv.FormatBool(allowUnadvertised), remoteDir, remote))
require.NoError(t, gitCmd("clone", "--bare", remoteDir, remote))
require.NoError(t, gitCmd("-C", remote, "config", "uploadpack.allowAnySHA1InWant", strconv.FormatBool(allowUnadvertised)))
return remote
}
t.Run("commit hash resolves when the remote accepts a direct shallow fetch", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: unadvertisedRemote(t, false), Ref: unadvertisedSHA, Dir: dir, Depth: 1,
@@ -433,7 +438,6 @@ func TestGitCloneExecutorShallow(t *testing.T) {
})
t.Run("commit hash is fetched shallowly, moved to another hash, then reused without reaching the remote", func(t *testing.T) {
t.Parallel()
remote := unadvertisedRemote(t, true)
dir := t.TempDir()
cloneAt := func(ref string) error {
@@ -444,7 +448,7 @@ func TestGitCloneExecutorShallow(t *testing.T) {
require.NoError(t, cloneAt(unadvertisedSHA))
assert.FileExists(t, shallowMarker(dir))
assert.Len(t, gitSubjects(t, dir), 1)
assert.Equal(t, 1, gitRevCount(t, dir))
assert.Equal(t, unadvertisedSHA, gitRevParse(t, dir, "HEAD"))
olderSHA := gitRevParse(t, workDir, "HEAD~2")
@@ -457,8 +461,6 @@ func TestGitCloneExecutorShallow(t *testing.T) {
})
t.Run("moving hexadecimal-named branch updates while staying shallow", func(t *testing.T) {
t.Parallel()
remoteDir, workDir := newGitRemote(t, "c2", "c3")
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main:deadbeef"))
dir := t.TempDir()
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
@@ -471,17 +473,19 @@ func TestGitCloneExecutorShallow(t *testing.T) {
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main:deadbeef"))
require.NoError(t, clone(t.Context()))
assert.Equal(t, []string{"c4"}, gitSubjects(t, dir), "reused shallow clone should update to the new tip")
assert.Equal(t, "c4", gitHeadSubject(t, dir), "reused shallow clone should update to the new tip")
assert.FileExists(t, shallowMarker(dir), "repo should remain shallow after update")
assert.Equal(t, 1, gitRevCount(t, dir))
})
t.Run("sha256 full hash is cloned shallowly, reused without the remote, and its 40-character prefix is short", func(t *testing.T) {
t.Parallel()
remote := filepath.Join(t.TempDir(), "remote.git")
require.NoError(t, gitCmd("init", "--bare", "--object-format=sha256", "--initial-branch=main", remote))
work := t.TempDir()
require.NoError(t, gitCmd("init", "--template=", "--object-format=sha256", "--initial-branch=main", work))
require.NoError(t, gitCmd("init", "--object-format=sha256", "--initial-branch=main", work))
require.NoError(t, gitCmd("-C", work, "remote", "add", "origin", remote))
require.NoError(t, gitCmd("-C", work, "commit", "--allow-empty", "-m", "pinned"))
require.NoError(t, gitCmd("clone", "--bare", "--template=", work, remote))
require.NoError(t, gitCmd("-C", work, "push", "origin", "main"))
sha := gitRevParse(t, work, "HEAD")
dir := t.TempDir()
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{URL: remote, Ref: sha, Dir: dir, Depth: 1})
@@ -500,11 +504,11 @@ func TestGitCloneExecutorPinnedHashIgnoresShadowingRef(t *testing.T) {
pinned := gitRevParse(t, workDir, "HEAD")
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "decoy"))
require.NoError(t, gitCmd("-C", workDir, "tag", pinned))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main", pinned))
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
require.NoError(t, gitCmd("-C", workDir, "push", "origin", pinned))
for name, depth := range map[string]int{"shallow": 1, "full clone": 0} {
t.Run(name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
URL: remoteDir, Ref: pinned, Dir: dir, Depth: depth,
@@ -536,11 +540,13 @@ func gitRevParse(t *testing.T, dir, rev string) string {
return strings.TrimSpace(string(out))
}
func gitSubjects(t *testing.T, dir string) []string {
func gitRevCount(t *testing.T, dir string) int {
t.Helper()
out, err := exec.Command("git", "-C", dir, "log", "--format=%s").Output()
out, err := exec.Command("git", "-C", dir, "rev-list", "--count", "HEAD").Output()
require.NoError(t, err)
return strings.Split(strings.TrimSpace(string(out)), "\n")
n, err := strconv.Atoi(strings.TrimSpace(string(out)))
require.NoError(t, err)
return n
}
func gitHeadSubject(t *testing.T, dir string) string {
@@ -674,7 +680,7 @@ func TestNewGitCloneExecutorFetchSendsTokenAndHonoursContext(t *testing.T) {
})
dir := filepath.Join(t.TempDir(), "cached-action")
require.NoError(t, os.CopyFS(dir, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
require.NoError(t, gitCmd("init", dir))
require.NoError(t, gitCmd("-C", dir, "remote", "add", "origin", server.URL))
ctx, cancel := context.WithCancel(t.Context())
+1 -1
View File
@@ -42,11 +42,11 @@ func TestDockerBuildLabelsImageWithItsTagAndReportsDaemonError(t *testing.T) {
func TestCreateBuildContextFollowsDockerignoreAndSymlinkedRootButKeepsDockerfile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(dir, "build"), 0o755))
for name, content := range map[string]string{
".dockerignore": "*.log\nbuild\n!build/keep.txt\nDockerfile", "Dockerfile": "FROM scratch", "a.txt": "a",
"a.log": "log", "build/drop.txt": "drop", "build/keep.txt": "keep",
} {
require.NoError(t, os.MkdirAll(filepath.Join(dir, filepath.Dir(name)), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644))
}
require.NoError(t, os.Mkdir(filepath.Join(dir, "empty"), 0o755))
+5 -7
View File
@@ -540,9 +540,6 @@ func TestRejectsMissingContainer(t *testing.T) {
}
func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
source := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(source, "x"), nil, 0o644))
for _, testcase := range []struct {
name, destination, extractionRoot, entry string
directory bool
@@ -570,6 +567,8 @@ func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
cr := &containerReference{id: "stale", cli: client, input: &NewContainerInput{Name: "job-1"}}
if testcase.directory {
source := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(source, "x"), nil, 0o644))
require.NoError(t, cr.CopyDir(testcase.destination, source+string(filepath.Separator), false, true)(ctx))
} else {
require.NoError(t, cr.Copy(testcase.destination, &FileEntry{Name: "x", Mode: 0o644})(ctx))
@@ -1109,10 +1108,6 @@ func TestConnectTakesDaemonOSFromPing(t *testing.T) {
}
func TestContainerCopyModes(t *testing.T) {
source := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(source, "run.sh"), []byte("#!/bin/sh\n"), 0o666))
require.NoError(t, os.Chmod(filepath.Join(source, "run.sh"), 0o666))
for _, testcase := range []struct {
hostOS, serverOS string
expected int64
@@ -1122,6 +1117,9 @@ func TestContainerCopyModes(t *testing.T) {
{"linux", "linux", 0o666},
} {
t.Run(testcase.hostOS+"-"+testcase.serverOS, func(t *testing.T) {
source := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(source, "run.sh"), []byte("#!/bin/sh\n"), 0o666))
require.NoError(t, os.Chmod(filepath.Join(source, "run.sh"), 0o666))
file := &FileEntry{Name: "run.sh", Mode: 0o600, Body: "#!/bin/sh\n"}
original := hostOS
hostOS = testcase.hostOS
+5 -5
View File
@@ -32,11 +32,6 @@ func TestActionPlatformNames(t *testing.T) {
}
func TestCopyDir(t *testing.T) {
src := filepath.Join(t.TempDir(), "action")
require.NoError(t, os.MkdirAll(filepath.Join(src, ".git", "objects"), 0o700))
require.NoError(t, os.WriteFile(filepath.Join(src, ".git", "objects", "pack"), []byte("x"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(src, "action.yml"), []byte("name: x\n"), 0o600))
for _, test := range []struct {
name string
useGitIgnore, skipGitDir bool
@@ -46,6 +41,11 @@ func TestCopyDir(t *testing.T) {
{name: "unfiltered outside a git repository", useGitIgnore: true},
} {
t.Run(test.name, func(t *testing.T) {
src := filepath.Join(t.TempDir(), "action")
require.NoError(t, os.MkdirAll(filepath.Join(src, ".git", "objects"), 0o700))
require.NoError(t, os.WriteFile(filepath.Join(src, ".git", "objects", "pack"), []byte("x"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(src, "action.yml"), []byte("name: x\n"), 0o600))
dest := filepath.Join(t.TempDir(), "dest")
require.NoError(t, (&HostEnvironment{}).CopyDir(dest, src+string(filepath.Separator), test.useGitIgnore, test.skipGitDir)(t.Context()))
+15 -17
View File
@@ -5,7 +5,6 @@ package container
import (
"bufio"
"bytes"
"os"
"path/filepath"
"strings"
@@ -16,9 +15,14 @@ import (
"golang.org/x/text/encoding/unicode"
)
func newTestHostEnv(t *testing.T) (*HostEnvironment, string) {
t.Helper()
e := &HostEnvironment{Path: t.TempDir()}
return e, filepath.Join(e.Path, "envfile")
}
func TestParseEnvFile(t *testing.T) {
t.Parallel()
hostEnv := &HostEnvironment{Path: t.TempDir()}
largeValue := strings.Repeat("x", 2*1024*1024)
encodeUTF16 := func(endianness unicode.Endianness) string {
content, err := unicode.UTF16(endianness, unicode.UseBOM).NewEncoder().Bytes([]byte("FOO=bar\r\nMULTI<<EOF\r\nline1\r\nEOF\r\n"))
@@ -47,15 +51,15 @@ func TestParseEnvFile(t *testing.T) {
{"UTF16 big endian", encodeUTF16(unicode.BigEndian), map[string]string{"FOO": "bar\r", "MULTI": "line1\r"}},
} {
t.Run(test.name, func(t *testing.T) {
envPath := filepath.Join(hostEnv.Path, test.name)
e, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte(test.content), 0o600))
env := map[string]string{}
require.NoError(t, parseEnvFile(hostEnv, envPath, &env, false)(t.Context()))
require.NoError(t, parseEnvFile(e, envPath, &env, false)(t.Context()))
assert.Equal(t, test.want, env)
})
}
t.Run("ordered entries preserve repeated names", func(t *testing.T) {
envPath := filepath.Join(hostEnv.Path, "ordered")
hostEnv, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("FOO=first\r\nfoo<<EOF\r\nmiddle\r\nEOF\r\nFOO=last\r\n"), 0o600))
var entries [][2]string
require.NoError(t, ParseEnvFile(hostEnv, envPath, func(name, value string) {
@@ -66,7 +70,7 @@ func TestParseEnvFile(t *testing.T) {
}, entries)
})
t.Run("Windows line endings", func(t *testing.T) {
envPath := filepath.Join(hostEnv.Path, "windows")
hostEnv, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\r\nMULTI<<EOF\r\nline1\r\nline2\nEOF\r\n"), 0o600))
env := map[string]string{}
require.NoError(t, parseEnvFile(hostEnv, envPath, &env, true)(t.Context()))
@@ -76,22 +80,16 @@ func TestParseEnvFile(t *testing.T) {
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
t.Parallel()
hostEnv := &HostEnvironment{Path: t.TempDir()}
envPath := filepath.Join(hostEnv.Path, "envfile")
const header, footer = "FOO<<EOF\n", "\nEOF\n"
content := bytes.Repeat([]byte("x"), len(header)+17*1024*1024+len(footer))
copy(content, header)
copy(content[len(content)-len(footer):], footer)
require.NoError(t, os.WriteFile(envPath, content, 0o600))
e, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\n"+strings.Repeat("x", 17*1024*1024)+"\nEOF\n"), 0o600))
env := map[string]string{}
err := parseEnvFile(hostEnv, envPath, &env, false)(t.Context())
err := parseEnvFile(e, envPath, &env, false)(t.Context())
require.ErrorIs(t, err, bufio.ErrTooLong)
assert.Contains(t, err.Error(), "reading env file")
}
func TestParseEnvFileInvalidHeredoc(t *testing.T) {
hostEnv := &HostEnvironment{Path: t.TempDir()}
for name, content := range map[string]string{
"missing delimiter": "FOO<<EOF\nline1\nline2\n",
"empty name": "<<EOF\nline1\nEOF\n",
@@ -99,11 +97,11 @@ func TestParseEnvFileInvalidHeredoc(t *testing.T) {
"delimiter with carriage return": "FOO<<EOF\nline1\nEOF\r\n",
} {
t.Run(name, func(t *testing.T) {
envPath := filepath.Join(hostEnv.Path, name)
e, envPath := newTestHostEnv(t)
require.NoError(t, os.WriteFile(envPath, []byte("APPLIED=before-error\n"+content), 0o600))
env := map[string]string{}
err := parseEnvFile(hostEnv, envPath, &env, false)(t.Context())
err := parseEnvFile(e, envPath, &env, false)(t.Context())
require.Error(t, err)
assert.Contains(t, err.Error(), "delimiter")
assert.Equal(t, map[string]string{"APPLIED": "before-error"}, env)
+7 -9
View File
@@ -28,12 +28,10 @@ func gitCmd(t *testing.T, args ...string) {
}
func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
repoDir := t.TempDir()
gitCmd(t, "init", "-q", "--template=", repoDir)
for _, dir := range []string{"sub", "nested"} {
require.NoError(t, os.CopyFS(filepath.Join(repoDir, dir, ".git"), os.DirFS(filepath.Join(repoDir, ".git"))))
repoDir := filepath.Join(t.TempDir(), "mygitrepo")
for _, dir := range []string{repoDir, filepath.Join(repoDir, "sub"), filepath.Join(repoDir, "nested")} {
gitCmd(t, "init", "-q", dir)
}
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "ign"), 0o755))
for name, body := range map[string]string{
".gitignore": ".*\nign/\n",
".env": "",
@@ -45,15 +43,15 @@ func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
"sub/kept": "",
"nested/kept": "",
} {
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(repoDir, name)), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(repoDir, name), []byte(body), 0o644))
}
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "uninitialized"), 0o755))
gitCmd(t, "-C", repoDir, "add", "-f", ".gitignore", "Renamed", "ign/tracked")
require.NoError(t, os.Rename(filepath.Join(repoDir, "Renamed"), filepath.Join(repoDir, "renamed")))
gitCmd(t, "-C", repoDir, "update-index", "--add",
"--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,sub",
"--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,uninitialized",
)
for _, gitlink := range []string{"sub", "uninitialized"} {
gitCmd(t, "-C", repoDir, "update-index", "--add", "--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,"+gitlink)
}
expected := map[string]string{".gitignore": "", "ign/tracked": "", "nested/kept": "", "renamed": "", "sub/.gitignore": "", "sub/kept": ""}
if runtime.GOOS != "windows" {
require.NoError(t, os.Chmod(filepath.Join(repoDir, "renamed"), 0o777|os.ModeSticky))
+1 -1
View File
@@ -92,7 +92,7 @@ func TestCompositeActionParity(t *testing.T) {
return "", "", assert.AnError
})
rc := &RunContext{
Config: &Config{GitHubInstance: "github.com", PresetGitHubContext: &model.GithubContext{}},
Config: &Config{GitHubInstance: "github.com", ActionCacheDir: t.TempDir(), PresetGitHubContext: &model.GithubContext{}},
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
JobContainer: &jobContainerMock{},
}
+13 -17
View File
@@ -249,7 +249,6 @@ func TestActionRunner(t *testing.T) {
}
func TestGoActionUsesLinuxExecutablePaths(t *testing.T) {
actionRoot := t.TempDir()
for _, stage := range []string{"main", "pre", "post"} {
t.Run(stage, func(t *testing.T) {
cm := &containerMock{}
@@ -265,7 +264,7 @@ func TestGoActionUsesLinuxExecutablePaths(t *testing.T) {
JobContainer: cm,
},
action: &model.Action{Runs: model.ActionRuns{Using: model.ActionRunsUsingGo, Main: "main.go", Pre: "pre.go", Post: "post.go"}},
actionDir: filepath.Join(actionRoot, stage),
actionDir: t.TempDir(),
env: map[string]string{},
remoteAction: mustNewRemoteAction(t, "org/repo@ref"),
}
@@ -505,21 +504,11 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
}
func TestExecAsDockerImageChecks(t *testing.T) {
actionRoot := t.TempDir()
originalExists, originalBuild, originalContainer := ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer
t.Cleanup(func() {
ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer = originalExists, originalBuild, originalContainer
})
ContainerNewContainer = func(*container.NewContainerInput) container.ExecutionsEnvironment { return fakeContainer{} }
var removed bool
fakeDockerDaemon(t, func(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodDelete {
removed = true
_, _ = io.WriteString(writer, `[]`)
} else {
_, _ = io.WriteString(writer, `{"Architecture":"aarch64"}`)
}
})
for _, testcase := range []struct {
name, platform string
@@ -540,11 +529,18 @@ func TestExecAsDockerImageChecks(t *testing.T) {
checks = append(checks, platform)
return testcase.exists && (platform == "any" || testcase.matches), nil
}
var built bool
removed = false
var built, removed bool
ContainerNewDockerBuildExecutor = func(container.NewDockerBuildExecutorInput) common.Executor {
return func(context.Context) error { built = true; return nil }
}
fakeDockerDaemon(t, func(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodDelete {
removed = true
_, _ = io.WriteString(writer, `[]`)
} else {
_, _ = io.WriteString(writer, `{"Architecture":"aarch64"}`)
}
})
step := &stepActionRemote{
Step: &model.Step{ID: "action"},
RunContext: &RunContext{
@@ -559,7 +555,7 @@ func TestExecAsDockerImageChecks(t *testing.T) {
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
env: map[string]string{},
}
dir := filepath.Join(actionRoot, testcase.name)
dir := t.TempDir()
require.NoError(t, execAsDocker(t.Context(), step, "action", dir, dir, false, stepStageMain))
assert.Equal(t, testcase.wantChecks, checks)
assert.Equal(t, testcase.wantBuild, built)
@@ -731,9 +727,9 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
cm.On("Close").Return(func(context.Context) error { return nil })
if tc.stage == stepStageMain {
require.NoError(t, runActionImpl(step, "", step.remoteAction)(ctx))
require.NoError(t, runActionImpl(step, t.TempDir(), step.remoteAction)(ctx))
} else {
require.NoError(t, execAsDocker(ctx, step, "action", "", "", false, tc.stage))
require.NoError(t, execAsDocker(ctx, step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
}
if tc.warning == "" {
assert.Empty(t, hook.AllRecords())
+3 -2
View File
@@ -412,10 +412,11 @@ func TestGetEvaluatorInputsBoolean(t *testing.T) {
}
for _, eventName := range []string{"workflow_call", "workflow_dispatch"} {
wf, err := model.ReadWorkflow(strings.NewReader(workflow(eventName)))
require.NoError(t, err)
for _, table := range tables {
t.Run(eventName+"/"+table.name, func(t *testing.T) {
wf, err := model.ReadWorkflow(strings.NewReader(workflow(eventName)))
require.NoError(t, err)
rc := &RunContext{
Config: &Config{Workdir: "."},
Run: &model.Run{JobID: "job1", Workflow: wf},
+3 -4
View File
@@ -993,7 +993,6 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
buf := &bytes.Buffer{}
tw := tar.NewWriter(buf)
for _, entry := range entries {
buf.Grow(len(entry.body) + 3*512)
typeflag := entry.typeflag
if typeflag == 0 {
typeflag = tar.TypeReg
@@ -1010,7 +1009,7 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
}
require.NoError(t, tw.WriteHeader(header))
if typeflag == tar.TypeReg {
_, err := io.CopyN(tw, strings.NewReader(entry.body), int64(len(entry.body)))
_, err := tw.Write([]byte(entry.body))
require.NoError(t, err)
}
}
@@ -1150,12 +1149,12 @@ func TestTryUploadJobSummaryMasksSecrets(t *testing.T) {
}))
defer server.Close()
content, err := testcase.encoding.NewEncoder().String(testcase.content)
content, err := testcase.encoding.NewEncoder().Bytes([]byte(testcase.content))
require.NoError(t, err)
cm := &containerMock{}
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{
name: "step-summary-0.md", body: content,
name: "step-summary-0.md", body: string(content),
}))),
nil,
).Once()
+6 -7
View File
@@ -4,7 +4,6 @@
package runner
import (
"bytes"
"context"
"encoding/base64"
"io"
@@ -210,14 +209,14 @@ func TestPatchedBundleAcrossActions(t *testing.T) {
assert.Equal(t, data, out, "an untouched bundle must come back byte for byte")
return
}
assert.False(t, bytes.Contains(out, []byte(".LOCALHOST")), "a copy of the gate was missed")
assert.NotContains(t, string(out), ".LOCALHOST", "a copy of the gate was missed")
if !bytes.Contains(data, []byte(CacheServiceV2Env)) {
if !strings.Contains(string(data), CacheServiceV2Env) {
return
}
assert.NotZero(t, bytes.Count(out, []byte("(process.env."+cacheURLEnv+"||process.env")),
assert.NotZero(t, strings.Count(string(out), "(process.env."+cacheURLEnv+"||process.env"),
"the cache service URL was not redirected")
assert.Equal(t, bytes.Count(data, []byte(resultsURLEnv)), bytes.Count(out, []byte(resultsURLEnv)),
assert.Equal(t, strings.Count(string(data), resultsURLEnv), strings.Count(string(out), resultsURLEnv),
"a read of the results URL was lost, it must stay as the fallback")
})
}
@@ -312,18 +311,18 @@ func TestCodeSplitActionFindsTheCacheService(t *testing.T) {
requireHostTools(t, "node", "rustc")
dir := tempDirPath(t)
require.NoError(t, os.Mkdir(filepath.Join(dir, "dist"), 0o755))
for _, name := range []string{
"action.yml", "dist/restore.js", "dist/save.js", "dist/cleanup-BWEbZ6YT.js",
"dist/cache-CTfQoCTL.js", "dist/cache-DC26rUaF.js", "dist/state-cjs-CkvFUaNy.js",
} {
body, err := os.ReadFile(bundleFromGitHub(t, "Swatinem/rust-cache", "v2.9.2", name))
require.NoError(t, err)
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, name), body, 0o600))
}
body, err := os.ReadFile(filepath.Join(dir, "action.yml"))
require.NoError(t, err)
action, err := model.ReadAction(bytes.NewReader(body))
action, err := model.ReadAction(strings.NewReader(string(body)))
require.NoError(t, err)
inputs := map[string]string{"lookup-only": "true", "add-rust-environment-hash-key": "false", "cache-bin": "false"}
for name, input := range action.Inputs {
+12 -14
View File
@@ -4,7 +4,6 @@
package runner
import (
"bytes"
"context"
"os"
"os/exec"
@@ -183,9 +182,9 @@ func TestPatchedBundleSurvivesInsideAStringLiteral(t *testing.T) {
out, patched := patchedBundle([]byte(embedded))
require.True(t, patched)
cmd := exec.CommandContext(t.Context(), "node", "--check", "-")
cmd.Stdin = bytes.NewReader(out)
checked, err := cmd.CombinedOutput()
file := filepath.Join(t.TempDir(), "bundle.js")
require.NoError(t, os.WriteFile(file, out, 0o600))
checked, err := exec.CommandContext(t.Context(), "node", "--check", file).CombinedOutput()
require.NoError(t, err, "%s", checked)
}
@@ -257,14 +256,6 @@ func TestActionScriptPaths(t *testing.T) {
}
func TestPatchActionsAtTheContainerCopy(t *testing.T) {
actionDir := t.TempDir()
script := filepath.Join(actionDir, "dist", "cache.js")
require.NoError(t, os.MkdirAll(filepath.Dir(script), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(script), "index.js"), []byte("require('./cache.js')"), 0o600))
require.NoError(t, os.WriteFile(script, []byte(gateTSC), 0o600))
info, err := os.Stat(script)
require.NoError(t, err)
copiedBundles := func(t *testing.T, noPatch bool) ([]string, []*container.FileEntry, int64) {
t.Helper()
@@ -273,12 +264,19 @@ func TestPatchActionsAtTheContainerCopy(t *testing.T) {
Step: &model.Step{Uses: "owner/repo/sub@v1"},
remoteAction: &remoteAction{Org: "owner", Repo: "repo", Path: "sub", Ref: "v1"},
action: &model.Action{Runs: model.ActionRuns{Using: "node20", Main: "../dist/index.js"}},
actionDir: actionDir,
actionDir: t.TempDir(),
RunContext: &RunContext{
Config: &Config{NoActionPatch: noPatch},
Config: &Config{ActionCacheDir: t.TempDir(), NoActionPatch: noPatch},
JobContainer: cm,
},
}
script := filepath.Join(sar.actionDir, "dist", "cache.js")
require.NoError(t, os.MkdirAll(filepath.Dir(script), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(script), "index.js"), []byte("require('./cache.js')"), 0o600))
require.NoError(t, os.WriteFile(script, []byte(gateTSC), 0o600))
info, err := os.Stat(script)
require.NoError(t, err)
var calls []string
var copied []*container.FileEntry
cm.On("CopyDir", "/var/run/act/actions/repo/", sar.actionDir+"/", false, true).Return(func(context.Context) error {
+16 -8
View File
@@ -33,17 +33,24 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
}
remoteDir := t.TempDir()
gitMust(t, "", "init", "--template=", "--initial-branch=master", remoteDir)
gitMust(t, "", "init", "--bare", "--initial-branch=master", remoteDir)
workDir := t.TempDir()
gitMust(t, "", "clone", remoteDir, workDir)
gitMust(t, workDir, "config", "user.email", "test@test")
gitMust(t, workDir, "config", "user.name", "test")
gitMust(t, workDir, "checkout", "-b", "master")
const workflowPath = ".gitea/workflows/reusable.yml"
tmpl := func(tag string) string {
return "name: reusable\non:\n workflow_call:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo " + tag + "\n"
}
require.NoError(t, os.MkdirAll(filepath.Join(remoteDir, ".gitea/workflows"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(remoteDir, workflowPath), []byte(tmpl("v1")), 0o644))
gitMust(t, remoteDir, "add", workflowPath)
gitMust(t, remoteDir, "commit", "-m", "v1")
require.NoError(t, os.MkdirAll(filepath.Join(workDir, ".gitea/workflows"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v1")), 0o644))
gitMust(t, workDir, "add", workflowPath)
gitMust(t, workDir, "commit", "-m", "v1")
gitMust(t, workDir, "push", "-u", "origin", "master")
rc := &RunContext{
Config: &Config{Env: testCheckoutEnv()},
@@ -63,8 +70,9 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
require.Equal(t, tmpl("v1"), string(got))
// Branch tip moves; cache key (cacheDir) does not.
require.NoError(t, os.WriteFile(filepath.Join(remoteDir, workflowPath), []byte(tmpl("v2")), 0o644))
gitMust(t, remoteDir, "commit", "-am", "v2")
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v2")), 0o644))
gitMust(t, workDir, "commit", "-am", "v2")
gitMust(t, workDir, "push", "origin", "master")
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
got, err = os.ReadFile(filepath.Join(cacheDir, workflowPath))
@@ -206,7 +214,7 @@ func TestInstanceAuthAndTransportOnlyApplyToTheInstance(t *testing.T) {
func gitMust(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{"-c", "user.name=test", "-c", "user.email=test@test"}, args...)...)
cmd := exec.Command("git", args...)
if dir != "" {
cmd.Dir = dir
}
+7 -7
View File
@@ -1490,7 +1490,7 @@ func TestCopyWorkflowFilesHandsNodeTheRunnerCAUnlessTheJobOrImageSetsIt(t *testi
return env
}
assert.Equal(t, actPath+"/ca.crt", stepEnv(caFile, nil, nil, nil)["NODE_EXTRA_CA_CERTS"])
assert.Equal(t, filepath.ToSlash(actPath)+"/ca.crt", stepEnv(caFile, nil, nil, nil)["NODE_EXTRA_CA_CERTS"])
ca, err := os.ReadFile(filepath.Join(actPath, "ca.crt"))
require.NoError(t, err)
assert.Equal(t, "PEM\n", string(ca))
@@ -1666,21 +1666,21 @@ func TestRunContextUpdateExtraPath(t *testing.T) {
longPath := strings.Repeat("x", 64*1024+1)
for _, testcase := range []struct {
name string
content string
content []byte
want string
wantErr bool
}{
{name: "UTF-8 BOM", content: "\xef\xbb\xbf/utf8/tools\n", want: "/utf8/tools"},
{name: "UTF-16 BOM", content: "\xff\xfeC\x00:\x00\\\x00t\x00o\x00o\x00l\x00s\x00\n\x00", want: `C:\tools`},
{name: "over 64 KiB", content: longPath + "\n", want: longPath},
{name: "over 16 MiB", content: strings.Repeat("x", 16*1024*1024+1), wantErr: true},
{name: "UTF-8 BOM", content: []byte("\xef\xbb\xbf/utf8/tools\n"), want: "/utf8/tools"},
{name: "UTF-16 BOM", content: []byte{0xff, 0xfe, 'C', 0, ':', 0, '\\', 0, 't', 0, 'o', 0, 'o', 0, 'l', 0, 's', 0, '\n', 0}, want: `C:\tools`},
{name: "over 64 KiB", content: []byte(longPath + "\n"), want: longPath},
{name: "over 16 MiB", content: []byte(strings.Repeat("x", 16*1024*1024+1)), wantErr: true},
} {
t.Run(testcase.name, func(t *testing.T) {
logger := logging.New(logging.DiscardHandler)
ctx := common.WithLogger(t.Context(), logger)
jobContainer := &containerMock{}
jobContainer.On("GetContainerArchive", mock.Anything, "/github/path").
Return(io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{name: "path", body: testcase.content}))), nil).Once()
Return(io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{name: "path", body: string(testcase.content)}))), nil).Once()
defer jobContainer.AssertExpectations(t)
rc := &RunContext{JobContainer: jobContainer}
+9 -3
View File
@@ -35,8 +35,14 @@ func setFetch(t *testing.T, fetch func(context.Context, git.NewGitCloneExecutorI
func TestShortSHAActionRejected(t *testing.T) {
actionRoot := t.TempDir()
repo := filepath.Join(actionRoot, "actions", "hello-world-docker-action")
gitMust(t, "", "init", "--template=", "--initial-branch=main", repo)
gitMust(t, repo, "commit", "--allow-empty", "-m", "initial")
require.NoError(t, os.MkdirAll(repo, 0o755))
gitMust(t, "", "init", "--initial-branch=main", repo)
gitMust(t, repo, "config", "user.email", "test@test")
gitMust(t, repo, "config", "user.name", "test")
require.NoError(t, os.WriteFile(filepath.Join(repo, "action.yml"),
[]byte("name: hello\nruns:\n using: node24\n main: index.js\n"), 0o644))
gitMust(t, repo, "add", ".")
gitMust(t, repo, "commit", "-m", "initial")
output, err := exec.Command("git", "-C", repo, "rev-parse", "--short=7", "HEAD").Output()
require.NoError(t, err)
@@ -387,7 +393,7 @@ func TestStepActionRemotePreResolvesDownloadedCommit(t *testing.T) {
[]byte("name: setup-go\nruns:\n using: node20\n main: index.js\n"), 0o600))
for _, args := range [][]string{
{"init", "--template=", "--initial-branch=main", actionDir},
{"init", "--initial-branch=main", actionDir},
{"-C", actionDir, "add", "action.yml"},
{"-C", actionDir, "-c", "user.name=runner", "-c", "[email protected]", "-c", "commit.gpgsign=false", "commit", "-m", "action"},
} {
+4 -5
View File
@@ -128,11 +128,6 @@ func (*daemonTestService) UpdateLog(_ context.Context, req *runnerv1.UpdateLogRe
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
defer func(delay time.Duration) { declareRetryDelay = delay }(declareRetryDelay)
declareRetryDelay = 0
dir := t.TempDir()
regFile := filepath.Join(dir, "registration.json")
configFile := filepath.Join(dir, "config.yaml")
require.NoError(t, os.WriteFile(configFile, []byte("runner:\n file: "+regFile+"\n idle_cleanup_interval: 0s\ncache:\n enabled: false\n"), 0o600))
for _, tc := range []struct {
name string
ephemeral, beforeTask bool
@@ -159,6 +154,10 @@ func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(
connecthttp.Mount(mux, rpcServer, connecthttp.WithReadMaxBytes(0))
server := httptest.NewServer(http.StripPrefix("/api/actions", mux))
defer server.Close()
dir := t.TempDir()
regFile := filepath.Join(dir, "registration.json")
configFile := filepath.Join(dir, "config.yaml")
require.NoError(t, os.WriteFile(configFile, []byte("runner:\n file: "+regFile+"\n idle_cleanup_interval: 0s\ncache:\n enabled: false\n"), 0o600))
require.NoError(t, config.SaveRegistration(regFile, &config.Registration{Address: server.URL, UUID: "test", Name: "test", Token: "test", Labels: []string{"host:host"}, Ephemeral: tc.ephemeral}))
err := runDaemon(ctx, &daemonArgs{Once: true}, &configFile)(nil, nil)
if tc.declareErr == nil && tc.fetchErr == nil || tc.beforeTask {
+35 -27
View File
@@ -75,35 +75,40 @@ func TestExecuteArgsLoadInputs(t *testing.T) {
}
func TestExecuteArgsEventJSON(t *testing.T) {
dir := t.TempDir()
payload := func(content string) string {
path := filepath.Join(t.TempDir(), "event.json")
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
return path
}
tests := []struct {
name string
payload string
inputs []string
expected string
name string
eventpath string
inputs []string
expected string
}{
{
name: "keeps a payload without inputs to merge",
payload: `{"inputs": {"kept": "from_event"}}`,
expected: `{"inputs": {"kept": "from_event"}}`,
name: "keeps a payload without inputs to merge",
eventpath: payload(`{"inputs": {"kept": "from_event"}}`),
expected: `{"inputs": {"kept": "from_event"}}`,
},
{
name: "overrides the event inputs it names, keeping the rest",
payload: `{"inputs": {"kept": "from_event", "named": "from_event"}}`,
inputs: []string{"named=from_cli"},
expected: `{"inputs": {"kept": "from_event", "named": "from_cli"}}`,
name: "overrides the event inputs it names, keeping the rest",
eventpath: payload(`{"inputs": {"kept": "from_event", "named": "from_event"}}`),
inputs: []string{"named=from_cli"},
expected: `{"inputs": {"kept": "from_event", "named": "from_cli"}}`,
},
{
name: "adds inputs to a payload carrying none",
payload: `{"repository": {"name": "test-repo"}}`,
inputs: []string{"flag=val"},
expected: `{"repository": {"name": "test-repo"}, "inputs": {"flag": "val"}}`,
name: "adds inputs to a payload carrying none",
eventpath: payload(`{"repository": {"name": "test-repo"}}`),
inputs: []string{"flag=val"},
expected: `{"repository": {"name": "test-repo"}, "inputs": {"flag": "val"}}`,
},
{
name: "handles a null payload",
payload: "null",
inputs: []string{"flag=val"},
expected: `{"inputs": {"flag": "val"}}`,
name: "handles a null payload",
eventpath: payload("null"),
inputs: []string{"flag=val"},
expected: `{"inputs": {"flag": "val"}}`,
},
{
name: "builds a payload without an event file",
@@ -114,12 +119,7 @@ func TestExecuteArgsEventJSON(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var eventpath string
if tt.payload != "" {
eventpath = filepath.Join(dir, tt.name+".json")
require.NoError(t, os.WriteFile(eventpath, []byte(tt.payload), 0o600))
}
eventJSON, err := (&executeArgs{eventpath: eventpath, inputs: tt.inputs}).eventJSON()
eventJSON, err := (&executeArgs{eventpath: tt.eventpath, inputs: tt.inputs}).eventJSON()
require.NoError(t, err)
require.JSONEq(t, tt.expected, eventJSON)
})
@@ -275,13 +275,21 @@ func workflowForList(name, file, event, jobID, jobName string) *model.Workflow {
return &model.Workflow{
Name: name,
File: file,
RawOn: yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: event},
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 TestExecuteArgsLoadEnvsResultsOrigin(t *testing.T) {
t.Setenv("ACTIONS_RESULTS_URL", "") // the default is only supplied when nothing else does
handler, err := artifactcache.StartHandler(artifactcache.Options{Dir: t.TempDir(), OutboundIP: "127.0.0.1"})
+14 -3
View File
@@ -27,9 +27,16 @@ func TestRunPostTaskScriptSkippedWhenEmpty(t *testing.T) {
r := &Runner{
cfg: &config.Config{},
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
task := &runnerv1.Task{Id: 1, Context: taskCtx}
reporter := report.NewReporter(ctx, cancel, nil, task, r.cfg)
require.NotPanics(t, func() {
r.runPostTaskScript(t.Context(), nil, nil, "/workspace/owner/repo")
r.runPostTaskScript(ctx, reporter, task, "/workspace/owner/repo")
})
}
@@ -52,7 +59,9 @@ func TestRunPostTaskScriptNonZeroExitDoesNotPanic(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
task := &runnerv1.Task{Id: 1, Context: &structpb.Struct{}}
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
task := &runnerv1.Task{Id: 1, Context: taskCtx}
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
require.NotPanics(t, func() {
@@ -137,7 +146,9 @@ func TestRunPostTaskScriptIntegration(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
task := &runnerv1.Task{Id: 11, Context: &structpb.Struct{}}
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
task := &runnerv1.Task{Id: 11, Context: taskCtx}
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
setReporterJobResult(t, reporter, runnerv1.Result_RESULT_SUCCESS)
+6 -3
View File
@@ -21,7 +21,8 @@ import (
func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
workdirRoot := t.TempDir()
workdirRoot := filepath.Join(t.TempDir(), "workspace")
require.NoError(t, os.MkdirAll(workdirRoot, 0o700))
oldTask := filepath.Join(workdirRoot, "1001")
freshTask := filepath.Join(workdirRoot, "1002")
@@ -58,7 +59,8 @@ func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
// be reclaimed, while the shared tool_cache and operator data are preserved.
func TestRunnerOnIdleCleansStaleHostScratchDirs(t *testing.T) {
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
hostRoot := t.TempDir()
hostRoot := filepath.Join(t.TempDir(), "act")
require.NoError(t, os.MkdirAll(hostRoot, 0o700))
staleScratch := filepath.Join(hostRoot, "0123456789abcdef") // 16 hex
freshScratch := filepath.Join(hostRoot, "fedcba9876543210")
@@ -116,7 +118,8 @@ func TestRunnerCleanupStaleTaskDirsMissingRoot(t *testing.T) {
func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
workdirRoot := t.TempDir()
workdirRoot := filepath.Join(t.TempDir(), "workspace")
require.NoError(t, os.MkdirAll(workdirRoot, 0o700))
for i := 1001; i <= 1003; i++ {
dir := filepath.Join(workdirRoot, strconv.Itoa(i))
+1 -2
View File
@@ -96,7 +96,6 @@ func TestSetupLinesInputs(t *testing.T) {
}
func TestPrettyOSName(t *testing.T) {
dir := t.TempDir()
for _, tt := range []struct {
name string
osRelease string
@@ -110,7 +109,7 @@ func TestPrettyOSName(t *testing.T) {
{"missing file", "", "", true},
} {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(dir, tt.name)
path := filepath.Join(t.TempDir(), "os-release")
if !tt.missing {
require.NoError(t, os.WriteFile(path, []byte(tt.osRelease), 0o600))
}
+11 -32
View File
@@ -59,7 +59,7 @@ func TestCheckout(t *testing.T) {
t.Run("history", func(t *testing.T) {
t.Parallel()
work := t.TempDir()
runGit(t, "init", "--template=", "--initial-branch=main", work)
runGit(t, "init", "--initial-branch=main", work)
runGit(t, "-C", work, "commit", "--allow-empty", "-m", "c1")
sha = runGit(t, "-C", work, "rev-parse", "HEAD")
runGit(t, "-C", work, "tag", "v1")
@@ -77,7 +77,7 @@ func TestCheckout(t *testing.T) {
t.Run("sha256", func(t *testing.T) {
t.Parallel()
sha256Repo := filepath.Join(server, "org", "sha256")
runGit(t, "init", "--template=", "--object-format=sha256", "--initial-branch=main", sha256Repo)
runGit(t, "init", "--object-format=sha256", "--initial-branch=main", sha256Repo)
runGit(t, "-C", sha256Repo, "commit", "--allow-empty", "-m", "s1")
runGit(t, "-C", sha256Repo, "checkout", "--detach")
})
@@ -267,41 +267,21 @@ exec "$LFS" "$@"
assert.Contains(t, string(outputs), "ref="+cmp.Or(tc.eventRef, "refs/heads/main")+"\n")
return
}
headArgs := []string{"-C", dst, "rev-parse", "HEAD", "--symbolic-full-name", "HEAD"}
if tc.head != "" {
headArgs = append(headArgs, "--abbrev-ref", "@{u}")
}
head := strings.Fields(runGit(t, headArgs...))
if tc.head == "" {
require.Len(t, head, 2)
assert.Equal(t, "HEAD", head[1])
assert.Empty(t, gitOutput(dst, "symbolic-ref", "-q", "HEAD"))
} else {
require.Len(t, head, 3)
assert.Equal(t, "refs/heads/"+tc.head, head[1])
assert.Equal(t, "origin/"+tc.head, head[2])
assert.Equal(t, "refs/heads/"+tc.head, gitOutput(dst, "symbolic-ref", "-q", "HEAD"))
assert.Equal(t, "origin/"+tc.head, gitOutput(dst, "rev-parse", "--abbrev-ref", "@{u}"))
}
assert.Contains(t, string(outputs), "commit="+head[0]+"\n")
assert.Contains(t, string(outputs), "commit="+gitOutput(dst, "rev-parse", "HEAD")+"\n")
if inputRef := tc.inputs["ref"]; inputRef != "" && inputRef != sha {
assert.Contains(t, string(outputs), "ref="+inputRef+"\n")
}
assert.Equal(t, tc.inputs["sparse-checkout"] != "", gitOutput(dst, "config", "--local", "extensions.worktreeConfig") == "true")
persisted := tc.inputs["persist-credentials"] != "false"
for _, repo := range repos {
var credentialIncludes []string
var worktreeConfig string
for entry := range strings.SplitSeq(runGit(t, "-C", repo, "config", "--local", "--no-includes", "--null", "--list"), "\x00") {
key, value, _ := strings.Cut(entry, "\n")
switch key {
case "extensions.worktreeconfig":
worktreeConfig = value
case strings.ToLower(credentialsInclude):
credentialIncludes = append(credentialIncludes, value)
}
}
if repo == dst {
assert.Equal(t, tc.inputs["sparse-checkout"] != "", strings.TrimSpace(worktreeConfig) == "true")
}
assert.Equal(t, tc.inputs["clean"] == "false", fileExists(filepath.Join(repo, "untracked")), repo)
assert.Len(t, strings.Fields(strings.Join(credentialIncludes, "\n")), 1, repo)
assert.Len(t, strings.Fields(gitOutput(repo, "config", "--local", "--get-all", credentialsInclude)), 1, repo)
headers, _ := exec.Command("git", "-C", repo, "config", "--get-all", "http."+serverURL+"/.extraheader").Output()
assert.Equal(t, persisted, string(headers) == "\nAuthorization: Basic dG9rZW46dG9r\n", repo)
assert.Equal(t, persisted, gitOutput(repo, "config", "http."+serverURL+"/.sslcainfo") != "", repo)
@@ -365,16 +345,15 @@ func TestCheckoutTrustsTheRunnerCAAndOffersClientCertOnlyDuringTheStep(t *testin
func publish(t *testing.T, server, name string, files, submodules map[string]string) {
work := t.TempDir()
runGit(t, "init", "--template=", "--initial-branch=main", work)
runGit(t, "init", "--initial-branch=main", work)
for file, content := range files {
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(work, file)), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(work, file), []byte(content), 0o644))
}
runGit(t, "-C", work, "add", ".")
var moduleConfig []byte
for path, url := range submodules {
moduleConfig = fmt.Appendf(moduleConfig, "[submodule %q]\n\tpath = %q\n\turl = %q\n", path, path, url)
require.NoError(t, os.WriteFile(filepath.Join(work, ".gitmodules"), moduleConfig, 0o644))
runGit(t, "-C", work, "config", "-f", ".gitmodules", "submodule."+path+".path", path)
runGit(t, "-C", work, "config", "-f", ".gitmodules", "submodule."+path+".url", url)
commit := runGit(t, "--git-dir", filepath.Join(server, name, url), "rev-parse", "main")
runGit(t, "-C", work, "update-index", "--add", "--cacheinfo", "160000,"+commit+","+path)
runGit(t, "-C", work, "add", ".gitmodules")
+10 -5
View File
@@ -6,6 +6,7 @@ package process
import (
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/require"
)
@@ -22,13 +23,16 @@ func TestGroupClosePropagatesToOrphan(t *testing.T) {
childPID := ready()
require.NoError(t, cmd.Wait())
require.True(t, cmd.ProcessState.Exited(), "the step process should have exited on its own")
require.Eventually(t, func() bool {
return !processAlive(cmd.Process.Pid)
}, 20*time.Second, 200*time.Millisecond, "the step process should have exited on its own")
require.True(t, processAlive(childPID), "orphan should outlive its parent")
waitForExit := newProcessExitWaiter(t, childPID)
require.NoError(t, group.Close())
waitForExit()
require.Eventually(t, func() bool {
return !processAlive(childPID)
}, 20*time.Second, 200*time.Millisecond, "closing the job must terminate the orphan")
}
// TestGroupNestsWithKiller: assigned to the group first and the step's Killer
@@ -49,9 +53,10 @@ func TestGroupNestsWithKiller(t *testing.T) {
require.NoError(t, err, "the step's job object must nest inside the group's")
defer killer.Close()
waitForExit := newProcessExitWaiter(t, cmd.Process.Pid)
require.NoError(t, killer.Kill())
waitForExit()
require.Eventually(t, func() bool {
return !processAlive(cmd.Process.Pid)
}, 20*time.Second, 200*time.Millisecond, "cancelling the step should still kill its tree")
}
// TestGroupNilSafe covers the fallback path: when the job object cannot be
+35 -3
View File
@@ -9,6 +9,8 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
@@ -44,8 +46,23 @@ func processAlive(pid int) bool {
return true
}
// TestKillerKillsTree verifies that a process group captured by the killer is
// terminated together with a child the process spawns afterwards. This mirrors
// a step or post-task script that launches a child which spawns further
// processes, where cancelling must take down the whole tree, not just the
// direct child.
func TestKillerKillsTree(t *testing.T) {
cmd, ready := newTestProcess(t, "parent", false)
dir := t.TempDir()
pidFile := filepath.Join(dir, "child.pid")
// Parent shell backgrounds a long-lived child (writing its PID to a file)
// and then sleeps. With job control off (non-interactive sh) the backgrounded
// child stays in the parent's process group, so the group kill must reach it.
script := fmt.Sprintf(`sleep 600 & echo $! > %q; sleep 600`, pidFile)
cmd := exec.Command("/bin/sh", "-c", script)
// Launch as its own process-group leader, exactly like a real process does
// (see SysProcAttr), so the killer's PGID == the process PID.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
require.NoError(t, cmd.Start())
t.Cleanup(func() {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
@@ -56,10 +73,25 @@ func TestKillerKillsTree(t *testing.T) {
require.NoError(t, err)
defer killer.Close()
childPID := ready()
require.True(t, processAlive(childPID), "child process should start")
// Wait for the backgrounded child PID to be reported.
var childPID int
require.Eventually(t, func() bool {
b, e := os.ReadFile(pidFile)
if e != nil {
return false
}
s := strings.TrimSpace(string(b))
if s == "" {
return false
}
childPID, _ = strconv.Atoi(s)
return childPID > 0 && processAlive(childPID)
}, 20*time.Second, 100*time.Millisecond, "child process should start")
// Killing the group must terminate both the parent and the backgrounded child.
require.NoError(t, killer.Kill())
// Reap the parent so it does not linger as a zombie (which would still report
// as alive); SIGKILL makes Wait return promptly.
_ = cmd.Wait()
require.Eventually(t, func() bool {
+5 -19
View File
@@ -5,6 +5,7 @@ package process
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
@@ -24,23 +25,6 @@ func processAlive(pid int) bool {
return code == stillActive
}
func newProcessExitWaiter(t *testing.T, pids ...int) func() {
t.Helper()
handles := make([]windows.Handle, 0, len(pids))
for _, pid := range pids {
handle, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, windows.CloseHandle(handle)) })
handles = append(handles, handle)
}
return func() {
t.Helper()
result, err := windows.WaitForMultipleObjects(handles, true, 20000)
require.NoError(t, err)
require.EqualValues(t, windows.WAIT_OBJECT_0, result, "processes should exit: %v", pids)
}
}
func TestKillerKillsTree(t *testing.T) {
cmd, ready := newTestProcess(t, "parent", false)
require.NoError(t, cmd.Start())
@@ -55,7 +39,9 @@ func TestKillerKillsTree(t *testing.T) {
childPID := ready()
require.True(t, processAlive(childPID), "child process should start")
waitForExit := newProcessExitWaiter(t, cmd.Process.Pid, childPID)
require.NoError(t, killer.Kill())
waitForExit()
require.Eventually(t, func() bool {
return !processAlive(cmd.Process.Pid) && !processAlive(childPID)
}, 20*time.Second, 200*time.Millisecond, "parent and child should both be terminated")
}
+24 -8
View File
@@ -173,9 +173,11 @@ func TestReporter_RunDaemonClose_Race(t *testing.T) {
)
ctx, cancel := context.WithCancel(t.Context())
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
Context: &structpb.Struct{},
Context: taskCtx,
}, cfg)
reporter.ResetSteps(newTestSteps(1))
@@ -472,8 +474,10 @@ func TestReporter_ServerCancelStillFlushesFinal(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: &structpb.Struct{}}, cfg)
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
reporter.ResetSteps(newTestSteps(1))
reporter.stateMu.Lock()
@@ -523,8 +527,10 @@ func TestReporter_CloseReportsCancelledOnCanceledCtx(t *testing.T) {
)
ctx, cancel := context.WithCancel(t.Context())
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: &structpb.Struct{}}, cfg)
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
reporter.ResetSteps(newTestSteps(1))
cancel()
@@ -665,15 +671,19 @@ func TestReporter_SetOutputs(t *testing.T) {
require.True(t, ok)
assert.Equal(t, "caf\xff", got.value)
r.SetOutputs(map[string]string{"atlimit": longValue[:maxOutputValueLen]})
maxValue := strings.Repeat("v", maxOutputValueLen)
r.SetOutputs(map[string]string{"atlimit": maxValue})
got, ok = r.outputs["atlimit"]
require.True(t, ok)
assert.Len(t, got.value, maxOutputValueLen)
}
func TestReporter_DebugSettingsAreNotMasked(t *testing.T) {
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
reporter := NewReporter(t.Context(), nil, nil, &runnerv1.Task{
Context: &structpb.Struct{},
Context: taskCtx,
Secrets: map[string]string{"ACTIONS_STEP_DEBUG": "true", "ACTIONS_RUNNER_DEBUG": "true", "TOKEN": "s3cr3t"},
}, &config.Config{})
defer deregisterGlobalMasks(reporter)
@@ -851,8 +861,10 @@ func TestReporter_ServerFailureResultIsReportedAsFailure(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: &structpb.Struct{}}, cfg)
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
close(reporter.daemon)
require.NoError(t, reporter.ReportState(false))
@@ -1019,7 +1031,9 @@ func newTestReporter(t *testing.T, client *mocks.Client, cfg *config.Config) *Re
t.Helper()
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
return NewReporter(ctx, cancel, client, &runnerv1.Task{Context: &structpb.Struct{}}, cfg)
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
return NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
}
func newTestSteps(n int) []*model.Step {
@@ -1037,9 +1051,11 @@ func TestReporter_ReportedSteps(t *testing.T) {
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
})
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(&runnerv1.UpdateTaskResponse{}, nil)
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
ctx, cancel := context.WithCancel(t.Context())
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: &structpb.Struct{}, Secrets: map[string]string{"TOKEN": "s3cr3t"}}, cfg)
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx, Secrets: map[string]string{"TOKEN": "s3cr3t"}}, cfg)
reporter.RunDaemon()
reporter.Logf("runner version")
reporter.ResetSteps(steps)
+1 -2
View File
@@ -117,8 +117,7 @@ func TestExportRetriesAndMatchesOTLP(t *testing.T) {
certificate := server.TLS.Certificates[0]
key, err := x509.MarshalPKCS8PrivateKey(certificate.PrivateKey)
require.NoError(t, err)
dir := t.TempDir()
certificatePath, keyPath := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
certificatePath, keyPath := filepath.Join(t.TempDir(), "cert.pem"), filepath.Join(t.TempDir(), "key.pem")
require.NoError(t, os.WriteFile(certificatePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Certificate[0]}), 0o600))
require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}), 0o600))
setEnv(t, map[string]string{