mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 06:45:44 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0732cacaac |
No files matched your search
+7
-4
@@ -23,11 +23,14 @@ depending on the prefix:
|
|||||||
covers both forms: it escapes `%` first, so a literal `%3B` becomes `%253B` that the extra
|
covers both forms: it escapes `%` first, so a literal `%3B` becomes `%253B` that the extra
|
||||||
`##[…]` rules cannot match, and a raw `;` or `]` is never decoded. It is also what makes
|
`##[…]` rules cannot match, and a raw `;` or `]` is never decoded. It is also what makes
|
||||||
multi-line work, `\n` becomes `%0A` and the UI turns it back into a line break.
|
multi-line work, `\n` becomes `%0A` and the UI turns it back into a line break.
|
||||||
- **Forwarding a command from step output?** Leave the payload alone, it arrived escaped and is
|
- **Forwarding a command from step output?** Preserve its escaping unless masking changes it.
|
||||||
decoded once. Decoding here double-decodes and destroys multi-line.
|
Decoding for display happens in the UI.
|
||||||
- **No prefix?** Do not escape, and split multi-line values into one row each.
|
- **No prefix?** Do not escape, and split multi-line values into one row each.
|
||||||
- **Interpolating a secret?** Masking runs after escaping, so `AppendSecretMasker` registers the
|
- **Interpolating a secret?** Masking runs after escaping. Recognised command payloads are
|
||||||
encoded forms too.
|
normalised before masking to cover partially escaped values. Their original escaping is
|
||||||
|
preserved when no replacement occurs. `AppendSecretMasker` also registers encoded forms for
|
||||||
|
other output. Complete nonblank one-character values are masked, while one-byte fragments
|
||||||
|
of multiline values are skipped.
|
||||||
- Command *properties* also escape `%3A` and `%2C`, which the UI never decodes, so the command
|
- Command *properties* also escape `%3A` and `%2C`, which the UI never decodes, so the command
|
||||||
handler folds an annotation's location into its decoded message and escapes the result again.
|
handler folds an annotation's location into its decoded message and escapes the result again.
|
||||||
|
|
||||||
|
|||||||
@@ -851,9 +851,6 @@ func TestHandler_touchCacheSkipsFreshCompletedEntries(t *testing.T) {
|
|||||||
func newTestHandler(t *testing.T, policy Policy, entries ...*Cache) *Handler {
|
func newTestHandler(t *testing.T, policy Policy, entries ...*Cache) *Handler {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
handler := startTestHandler(t, Options{Policy: policy})
|
handler := startTestHandler(t, Options{Policy: policy})
|
||||||
if len(entries) == 0 {
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := handler.openDB()
|
db, err := handler.openDB()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -943,10 +940,6 @@ func TestHandler_RejectsBearer(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandler_UnregisterRevokes(t *testing.T) {
|
func TestHandler_UnregisterRevokes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, err := StartHandler(Options{Dir: filepath.Join(t.TempDir(), "artifactcache")})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer handler.Close()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
token string
|
token string
|
||||||
@@ -957,6 +950,9 @@ func TestHandler_UnregisterRevokes(t *testing.T) {
|
|||||||
{"reference counted", "shared", "v", 2},
|
{"reference counted", "shared", "v", 2},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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)
|
revokers := make([]func(), tc.registrations)
|
||||||
for i := range revokers {
|
for i := range revokers {
|
||||||
revokers[i] = handler.RegisterJob(tc.token, JobCredential{Repo: testRepo})
|
revokers[i] = handler.RegisterJob(tc.token, JobCredential{Repo: testRepo})
|
||||||
@@ -1053,8 +1049,9 @@ func TestHandler_ArtifactSignature(t *testing.T) {
|
|||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
good := signArtifactURL(handler, 1)
|
good := signArtifactURL(handler, 1)
|
||||||
secret := bytes.Clone(handler.secret)
|
other, err := StartHandler(Options{Dir: filepath.Join(t.TempDir(), "artifactcache2")})
|
||||||
secret[0] ^= 1
|
require.NoError(t, err)
|
||||||
|
defer other.Close()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1063,7 +1060,7 @@ func TestHandler_ArtifactSignature(t *testing.T) {
|
|||||||
{"missing signature", fmt.Sprintf("%s/artifacts/%d", base, 1)},
|
{"missing signature", fmt.Sprintf("%s/artifacts/%d", base, 1)},
|
||||||
{"tampered signature", good[:len(good)-4] + "dead"},
|
{"tampered signature", good[:len(good)-4] + "dead"},
|
||||||
{"expired signature", handler.signedArtifactURL(JobCredential{}, 1, time.Now().Add(-time.Second))},
|
{"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) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
resp, err := testClient.Get(tc.url)
|
resp, err := testClient.Get(tc.url)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
remote := filepath.Join(root, "org", "action")
|
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, "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.WriteFile(filepath.Join(remote, "run.sh"), []byte("#!/bin/sh\n"), 0o755))
|
||||||
require.NoError(t, os.Symlink("action.yml", filepath.Join(remote, "link")))
|
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 archiveRequests []http.Header
|
||||||
var mirror *httptest.Server
|
var mirror *httptest.Server
|
||||||
archiveCache := map[string][]byte{}
|
archiveCache := map[string][]byte{}
|
||||||
revisions := map[string]string{}
|
|
||||||
requests, archives, failures := 0, "", 0
|
requests, archives, failures := 0, "", 0
|
||||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -80,14 +79,7 @@ func TestFetchAction(t *testing.T) {
|
|||||||
http.Redirect(writer, request, mirror.URL+request.URL.Path, http.StatusFound)
|
http.Redirect(writer, request, mirror.URL+request.URL.Path, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sha, ok := revisions[ref]
|
sha, _ := gitOutput(request.Context(), remote, "rev-parse", strings.TrimSuffix(ref, ".tar.gz"))
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
sha, err = gitOutput(request.Context(), remote, "rev-parse", strings.TrimSuffix(ref, ".tar.gz"))
|
|
||||||
if err == nil {
|
|
||||||
revisions[ref] = sha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
etag := fmt.Sprintf(`W/"%s-tar.gz-%t"`, sha, archives != "unprefixed")
|
etag := fmt.Sprintf(`W/"%s-tar.gz-%t"`, sha, archives != "unprefixed")
|
||||||
if request.Header.Get("If-None-Match") == etag {
|
if request.Header.Get("If-None-Match") == etag {
|
||||||
writer.WriteHeader(http.StatusNotModified)
|
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, "commit", "--allow-empty", "-m", "moved"))
|
||||||
require.NoError(t, gitCmd("-C", remote, "tag", "--force", "v1"))
|
require.NoError(t, gitCmd("-C", remote, "tag", "--force", "v1"))
|
||||||
expectedSha = gitRevParse(t, remote, "v1")
|
expectedSha = gitRevParse(t, remote, "v1")
|
||||||
mu.Lock()
|
|
||||||
clear(revisions)
|
|
||||||
mu.Unlock()
|
|
||||||
moved, movedSha, err := FetchAction(t.Context(), input)
|
moved, movedSha, err := FetchAction(t.Context(), input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, expectedSha, movedSha)
|
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) {
|
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"} {
|
for _, mode := range []string{"prefixed", "none"} {
|
||||||
serve(mode, 0)
|
serve(mode, 0)
|
||||||
input := newInput(sha)
|
input := newInput(sha)
|
||||||
|
|||||||
+31
-25
@@ -112,7 +112,8 @@ func TestFindGitMetadataOfSHA256RepositoryUsesOrigin(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
basedir := t.TempDir()
|
basedir := t.TempDir()
|
||||||
const remoteURL = "https://github.com/owner/repo.git"
|
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, "commit", "--allow-empty", "-m", "init"))
|
||||||
require.NoError(t, gitCmd("-C", basedir, "remote", "add", "origin", remoteURL))
|
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"))
|
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, "add", "action.sh", ".gitattributes"))
|
||||||
require.NoError(t, gitCmd("-C", workDir, "commit", "-m", "initial"))
|
require.NoError(t, gitCmd("-C", workDir, "commit", "-m", "initial"))
|
||||||
require.NoError(t, gitCmd("-C", workDir, "tag", "v2"))
|
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, "checkout", "-b", "act-fails"))
|
||||||
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "branch-commit"))
|
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")
|
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")
|
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))
|
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) {
|
func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
createRemote := func(message string) string {
|
createRemote := func(message string) string {
|
||||||
dir := t.TempDir()
|
remoteDir, workDir := newGitRemote(t, message)
|
||||||
require.NoError(t, os.CopyFS(dir, os.DirFS(filepath.Join(gitFixtureDir, "work"))))
|
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
|
||||||
require.NoError(t, gitCmd("-C", dir, "commit", "--allow-empty", "-m", message))
|
|
||||||
return dir
|
return remoteDir
|
||||||
}
|
}
|
||||||
|
|
||||||
oldRemoteDir := createRemote("old-action")
|
oldRemoteDir := createRemote("old-action")
|
||||||
@@ -401,30 +405,31 @@ func TestGitCloneExecutorShallow(t *testing.T) {
|
|||||||
remoteDir, workDir := newGitRemote(t, "c1", "c2", "c3")
|
remoteDir, workDir := newGitRemote(t, "c1", "c2", "c3")
|
||||||
require.NoError(t, gitCmd("-C", workDir, "tag", "v1"))
|
require.NoError(t, gitCmd("-C", workDir, "tag", "v1"))
|
||||||
unadvertisedSHA := gitRevParse(t, workDir, "HEAD~1")
|
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") }
|
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"} {
|
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.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
URL: remoteDir, Ref: ref, Dir: dir, Depth: 1,
|
URL: remoteDir, Ref: ref, Dir: dir, Depth: 1,
|
||||||
})(t.Context()))
|
})(t.Context()))
|
||||||
assert.FileExists(t, shallowMarker(dir), "clone should be shallow")
|
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 {
|
unadvertisedRemote := func(t *testing.T, allowUnadvertised bool) string {
|
||||||
remote := filepath.Join(t.TempDir(), "remote.git")
|
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
|
return remote
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("commit hash resolves when the remote accepts a direct shallow fetch", func(t *testing.T) {
|
t.Run("commit hash resolves when the remote accepts a direct shallow fetch", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
URL: unadvertisedRemote(t, false), Ref: unadvertisedSHA, Dir: dir, Depth: 1,
|
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.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)
|
remote := unadvertisedRemote(t, true)
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
cloneAt := func(ref string) error {
|
cloneAt := func(ref string) error {
|
||||||
@@ -444,7 +448,7 @@ func TestGitCloneExecutorShallow(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, cloneAt(unadvertisedSHA))
|
require.NoError(t, cloneAt(unadvertisedSHA))
|
||||||
assert.FileExists(t, shallowMarker(dir))
|
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"))
|
assert.Equal(t, unadvertisedSHA, gitRevParse(t, dir, "HEAD"))
|
||||||
|
|
||||||
olderSHA := gitRevParse(t, workDir, "HEAD~2")
|
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.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"))
|
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "main:deadbeef"))
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
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, gitCmd("-C", workDir, "push", "origin", "main:deadbeef"))
|
||||||
|
|
||||||
require.NoError(t, clone(t.Context()))
|
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.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.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")
|
remote := filepath.Join(t.TempDir(), "remote.git")
|
||||||
|
require.NoError(t, gitCmd("init", "--bare", "--object-format=sha256", "--initial-branch=main", remote))
|
||||||
work := t.TempDir()
|
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("-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")
|
sha := gitRevParse(t, work, "HEAD")
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{URL: remote, Ref: sha, Dir: dir, Depth: 1})
|
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")
|
pinned := gitRevParse(t, workDir, "HEAD")
|
||||||
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "decoy"))
|
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, "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} {
|
for name, depth := range map[string]int{"shallow": 1, "full clone": 0} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
URL: remoteDir, Ref: pinned, Dir: dir, Depth: depth,
|
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))
|
return strings.TrimSpace(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitSubjects(t *testing.T, dir string) []string {
|
func gitRevCount(t *testing.T, dir string) int {
|
||||||
t.Helper()
|
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)
|
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 {
|
func gitHeadSubject(t *testing.T, dir string) string {
|
||||||
@@ -674,7 +680,7 @@ func TestNewGitCloneExecutorFetchSendsTokenAndHonoursContext(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
dir := filepath.Join(t.TempDir(), "cached-action")
|
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))
|
require.NoError(t, gitCmd("-C", dir, "remote", "add", "origin", server.URL))
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ func TestDockerBuildLabelsImageWithItsTagAndReportsDaemonError(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateBuildContextFollowsDockerignoreAndSymlinkedRootButKeepsDockerfile(t *testing.T) {
|
func TestCreateBuildContextFollowsDockerignoreAndSymlinkedRootButKeepsDockerfile(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(dir, "build"), 0o755))
|
|
||||||
for name, content := range map[string]string{
|
for name, content := range map[string]string{
|
||||||
".dockerignore": "*.log\nbuild\n!build/keep.txt\nDockerfile", "Dockerfile": "FROM scratch", "a.txt": "a",
|
".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",
|
"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.WriteFile(filepath.Join(dir, name), []byte(content), 0o644))
|
||||||
}
|
}
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(dir, "empty"), 0o755))
|
require.NoError(t, os.Mkdir(filepath.Join(dir, "empty"), 0o755))
|
||||||
|
|||||||
@@ -540,9 +540,6 @@ func TestRejectsMissingContainer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicCopyPipelineHandlesStaleID(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 {
|
for _, testcase := range []struct {
|
||||||
name, destination, extractionRoot, entry string
|
name, destination, extractionRoot, entry string
|
||||||
directory bool
|
directory bool
|
||||||
@@ -570,6 +567,8 @@ func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
|
|||||||
|
|
||||||
cr := &containerReference{id: "stale", cli: client, input: &NewContainerInput{Name: "job-1"}}
|
cr := &containerReference{id: "stale", cli: client, input: &NewContainerInput{Name: "job-1"}}
|
||||||
if testcase.directory {
|
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))
|
require.NoError(t, cr.CopyDir(testcase.destination, source+string(filepath.Separator), false, true)(ctx))
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, cr.Copy(testcase.destination, &FileEntry{Name: "x", Mode: 0o644})(ctx))
|
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) {
|
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 {
|
for _, testcase := range []struct {
|
||||||
hostOS, serverOS string
|
hostOS, serverOS string
|
||||||
expected int64
|
expected int64
|
||||||
@@ -1122,6 +1117,9 @@ func TestContainerCopyModes(t *testing.T) {
|
|||||||
{"linux", "linux", 0o666},
|
{"linux", "linux", 0o666},
|
||||||
} {
|
} {
|
||||||
t.Run(testcase.hostOS+"-"+testcase.serverOS, func(t *testing.T) {
|
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"}
|
file := &FileEntry{Name: "run.sh", Mode: 0o600, Body: "#!/bin/sh\n"}
|
||||||
original := hostOS
|
original := hostOS
|
||||||
hostOS = testcase.hostOS
|
hostOS = testcase.hostOS
|
||||||
|
|||||||
@@ -32,11 +32,6 @@ func TestActionPlatformNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCopyDir(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 {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
useGitIgnore, skipGitDir bool
|
useGitIgnore, skipGitDir bool
|
||||||
@@ -46,6 +41,11 @@ func TestCopyDir(t *testing.T) {
|
|||||||
{name: "unfiltered outside a git repository", useGitIgnore: true},
|
{name: "unfiltered outside a git repository", useGitIgnore: true},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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")
|
dest := filepath.Join(t.TempDir(), "dest")
|
||||||
require.NoError(t, (&HostEnvironment{}).CopyDir(dest, src+string(filepath.Separator), test.useGitIgnore, test.skipGitDir)(t.Context()))
|
require.NoError(t, (&HostEnvironment{}).CopyDir(dest, src+string(filepath.Separator), test.useGitIgnore, test.skipGitDir)(t.Context()))
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,9 +15,14 @@ import (
|
|||||||
"golang.org/x/text/encoding/unicode"
|
"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) {
|
func TestParseEnvFile(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
hostEnv := &HostEnvironment{Path: t.TempDir()}
|
|
||||||
largeValue := strings.Repeat("x", 2*1024*1024)
|
largeValue := strings.Repeat("x", 2*1024*1024)
|
||||||
encodeUTF16 := func(endianness unicode.Endianness) string {
|
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"))
|
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"}},
|
{"UTF16 big endian", encodeUTF16(unicode.BigEndian), map[string]string{"FOO": "bar\r", "MULTI": "line1\r"}},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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))
|
require.NoError(t, os.WriteFile(envPath, []byte(test.content), 0o600))
|
||||||
env := map[string]string{}
|
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)
|
assert.Equal(t, test.want, env)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
t.Run("ordered entries preserve repeated names", func(t *testing.T) {
|
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))
|
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
|
var entries [][2]string
|
||||||
require.NoError(t, ParseEnvFile(hostEnv, envPath, func(name, value string) {
|
require.NoError(t, ParseEnvFile(hostEnv, envPath, func(name, value string) {
|
||||||
@@ -66,7 +70,7 @@ func TestParseEnvFile(t *testing.T) {
|
|||||||
}, entries)
|
}, entries)
|
||||||
})
|
})
|
||||||
t.Run("Windows line endings", func(t *testing.T) {
|
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))
|
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\r\nMULTI<<EOF\r\nline1\r\nline2\nEOF\r\n"), 0o600))
|
||||||
env := map[string]string{}
|
env := map[string]string{}
|
||||||
require.NoError(t, parseEnvFile(hostEnv, envPath, &env, true)(t.Context()))
|
require.NoError(t, parseEnvFile(hostEnv, envPath, &env, true)(t.Context()))
|
||||||
@@ -76,22 +80,16 @@ func TestParseEnvFile(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
hostEnv := &HostEnvironment{Path: t.TempDir()}
|
e, envPath := newTestHostEnv(t)
|
||||||
envPath := filepath.Join(hostEnv.Path, "envfile")
|
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\n"+strings.Repeat("x", 17*1024*1024)+"\nEOF\n"), 0o600))
|
||||||
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))
|
|
||||||
|
|
||||||
env := map[string]string{}
|
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)
|
require.ErrorIs(t, err, bufio.ErrTooLong)
|
||||||
assert.Contains(t, err.Error(), "reading env file")
|
assert.Contains(t, err.Error(), "reading env file")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEnvFileInvalidHeredoc(t *testing.T) {
|
func TestParseEnvFileInvalidHeredoc(t *testing.T) {
|
||||||
hostEnv := &HostEnvironment{Path: t.TempDir()}
|
|
||||||
for name, content := range map[string]string{
|
for name, content := range map[string]string{
|
||||||
"missing delimiter": "FOO<<EOF\nline1\nline2\n",
|
"missing delimiter": "FOO<<EOF\nline1\nline2\n",
|
||||||
"empty name": "<<EOF\nline1\nEOF\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",
|
"delimiter with carriage return": "FOO<<EOF\nline1\nEOF\r\n",
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
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))
|
require.NoError(t, os.WriteFile(envPath, []byte("APPLIED=before-error\n"+content), 0o600))
|
||||||
|
|
||||||
env := map[string]string{}
|
env := map[string]string{}
|
||||||
err := parseEnvFile(hostEnv, envPath, &env, false)(t.Context())
|
err := parseEnvFile(e, envPath, &env, false)(t.Context())
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "delimiter")
|
assert.Contains(t, err.Error(), "delimiter")
|
||||||
assert.Equal(t, map[string]string{"APPLIED": "before-error"}, env)
|
assert.Equal(t, map[string]string{"APPLIED": "before-error"}, env)
|
||||||
|
|||||||
@@ -28,12 +28,10 @@ func gitCmd(t *testing.T, args ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
|
func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
|
||||||
repoDir := t.TempDir()
|
repoDir := filepath.Join(t.TempDir(), "mygitrepo")
|
||||||
gitCmd(t, "init", "-q", "--template=", repoDir)
|
for _, dir := range []string{repoDir, filepath.Join(repoDir, "sub"), filepath.Join(repoDir, "nested")} {
|
||||||
for _, dir := range []string{"sub", "nested"} {
|
gitCmd(t, "init", "-q", dir)
|
||||||
require.NoError(t, os.CopyFS(filepath.Join(repoDir, dir, ".git"), os.DirFS(filepath.Join(repoDir, ".git"))))
|
|
||||||
}
|
}
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "ign"), 0o755))
|
|
||||||
for name, body := range map[string]string{
|
for name, body := range map[string]string{
|
||||||
".gitignore": ".*\nign/\n",
|
".gitignore": ".*\nign/\n",
|
||||||
".env": "",
|
".env": "",
|
||||||
@@ -45,15 +43,15 @@ func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
|
|||||||
"sub/kept": "",
|
"sub/kept": "",
|
||||||
"nested/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.WriteFile(filepath.Join(repoDir, name), []byte(body), 0o644))
|
||||||
}
|
}
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "uninitialized"), 0o755))
|
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "uninitialized"), 0o755))
|
||||||
gitCmd(t, "-C", repoDir, "add", "-f", ".gitignore", "Renamed", "ign/tracked")
|
gitCmd(t, "-C", repoDir, "add", "-f", ".gitignore", "Renamed", "ign/tracked")
|
||||||
require.NoError(t, os.Rename(filepath.Join(repoDir, "Renamed"), filepath.Join(repoDir, "renamed")))
|
require.NoError(t, os.Rename(filepath.Join(repoDir, "Renamed"), filepath.Join(repoDir, "renamed")))
|
||||||
gitCmd(t, "-C", repoDir, "update-index", "--add",
|
for _, gitlink := range []string{"sub", "uninitialized"} {
|
||||||
"--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,sub",
|
gitCmd(t, "-C", repoDir, "update-index", "--add", "--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,"+gitlink)
|
||||||
"--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,uninitialized",
|
}
|
||||||
)
|
|
||||||
expected := map[string]string{".gitignore": "", "ign/tracked": "", "nested/kept": "", "renamed": "", "sub/.gitignore": "", "sub/kept": ""}
|
expected := map[string]string{".gitignore": "", "ign/tracked": "", "nested/kept": "", "renamed": "", "sub/.gitignore": "", "sub/kept": ""}
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
require.NoError(t, os.Chmod(filepath.Join(repoDir, "renamed"), 0o777|os.ModeSticky))
|
require.NoError(t, os.Chmod(filepath.Join(repoDir, "renamed"), 0o777|os.ModeSticky))
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func TestCompositeActionParity(t *testing.T) {
|
|||||||
return "", "", assert.AnError
|
return "", "", assert.AnError
|
||||||
})
|
})
|
||||||
rc := &RunContext{
|
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": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
JobContainer: &jobContainerMock{},
|
JobContainer: &jobContainerMock{},
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-17
@@ -249,7 +249,6 @@ func TestActionRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGoActionUsesLinuxExecutablePaths(t *testing.T) {
|
func TestGoActionUsesLinuxExecutablePaths(t *testing.T) {
|
||||||
actionRoot := t.TempDir()
|
|
||||||
for _, stage := range []string{"main", "pre", "post"} {
|
for _, stage := range []string{"main", "pre", "post"} {
|
||||||
t.Run(stage, func(t *testing.T) {
|
t.Run(stage, func(t *testing.T) {
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
@@ -265,7 +264,7 @@ func TestGoActionUsesLinuxExecutablePaths(t *testing.T) {
|
|||||||
JobContainer: cm,
|
JobContainer: cm,
|
||||||
},
|
},
|
||||||
action: &model.Action{Runs: model.ActionRuns{Using: model.ActionRunsUsingGo, Main: "main.go", Pre: "pre.go", Post: "post.go"}},
|
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{},
|
env: map[string]string{},
|
||||||
remoteAction: mustNewRemoteAction(t, "org/repo@ref"),
|
remoteAction: mustNewRemoteAction(t, "org/repo@ref"),
|
||||||
}
|
}
|
||||||
@@ -505,21 +504,11 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExecAsDockerImageChecks(t *testing.T) {
|
func TestExecAsDockerImageChecks(t *testing.T) {
|
||||||
actionRoot := t.TempDir()
|
|
||||||
originalExists, originalBuild, originalContainer := ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer
|
originalExists, originalBuild, originalContainer := ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer = originalExists, originalBuild, originalContainer
|
ContainerImageExistsLocally, ContainerNewDockerBuildExecutor, ContainerNewContainer = originalExists, originalBuild, originalContainer
|
||||||
})
|
})
|
||||||
ContainerNewContainer = func(*container.NewContainerInput) container.ExecutionsEnvironment { return fakeContainer{} }
|
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 {
|
for _, testcase := range []struct {
|
||||||
name, platform string
|
name, platform string
|
||||||
@@ -540,11 +529,18 @@ func TestExecAsDockerImageChecks(t *testing.T) {
|
|||||||
checks = append(checks, platform)
|
checks = append(checks, platform)
|
||||||
return testcase.exists && (platform == "any" || testcase.matches), nil
|
return testcase.exists && (platform == "any" || testcase.matches), nil
|
||||||
}
|
}
|
||||||
var built bool
|
var built, removed bool
|
||||||
removed = false
|
|
||||||
ContainerNewDockerBuildExecutor = func(container.NewDockerBuildExecutorInput) common.Executor {
|
ContainerNewDockerBuildExecutor = func(container.NewDockerBuildExecutorInput) common.Executor {
|
||||||
return func(context.Context) error { built = true; return nil }
|
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 := &stepActionRemote{
|
||||||
Step: &model.Step{ID: "action"},
|
Step: &model.Step{ID: "action"},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
@@ -559,7 +555,7 @@ func TestExecAsDockerImageChecks(t *testing.T) {
|
|||||||
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
||||||
env: map[string]string{},
|
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))
|
require.NoError(t, execAsDocker(t.Context(), step, "action", dir, dir, false, stepStageMain))
|
||||||
assert.Equal(t, testcase.wantChecks, checks)
|
assert.Equal(t, testcase.wantChecks, checks)
|
||||||
assert.Equal(t, testcase.wantBuild, built)
|
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 })
|
cm.On("Close").Return(func(context.Context) error { return nil })
|
||||||
|
|
||||||
if tc.stage == stepStageMain {
|
if tc.stage == stepStageMain {
|
||||||
require.NoError(t, runActionImpl(step, "", step.remoteAction)(ctx))
|
require.NoError(t, runActionImpl(step, t.TempDir(), step.remoteAction)(ctx))
|
||||||
} else {
|
} 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 == "" {
|
if tc.warning == "" {
|
||||||
assert.Empty(t, hook.AllRecords())
|
assert.Empty(t, hook.AllRecords())
|
||||||
|
|||||||
@@ -193,6 +193,11 @@ func TestCommandLogWriter(t *testing.T) {
|
|||||||
log []string
|
log []string
|
||||||
masks []string
|
masks []string
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "mask commands decode modern and legacy escapes once",
|
||||||
|
steps: [][]string{{"::add-mask::%25", "##[add-mask]alpha beta%3Bgamma%5Ddelta%250A"}},
|
||||||
|
masks: []string{"%", "alpha beta;gamma]delta%0A"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "applied commands are hidden unless echoed",
|
name: "applied commands are hidden unless echoed",
|
||||||
steps: [][]string{{
|
steps: [][]string{{
|
||||||
|
|||||||
@@ -412,10 +412,11 @@ func TestGetEvaluatorInputsBoolean(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, eventName := range []string{"workflow_call", "workflow_dispatch"} {
|
for _, eventName := range []string{"workflow_call", "workflow_dispatch"} {
|
||||||
wf, err := model.ReadWorkflow(strings.NewReader(workflow(eventName)))
|
|
||||||
require.NoError(t, err)
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
t.Run(eventName+"/"+table.name, func(t *testing.T) {
|
t.Run(eventName+"/"+table.name, func(t *testing.T) {
|
||||||
|
wf, err := model.ReadWorkflow(strings.NewReader(workflow(eventName)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{Workdir: "."},
|
Config: &Config{Workdir: "."},
|
||||||
Run: &model.Run{JobID: "job1", Workflow: wf},
|
Run: &model.Run{JobID: "job1", Workflow: wf},
|
||||||
|
|||||||
@@ -993,7 +993,6 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
|
|||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
tw := tar.NewWriter(buf)
|
tw := tar.NewWriter(buf)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
buf.Grow(len(entry.body) + 3*512)
|
|
||||||
typeflag := entry.typeflag
|
typeflag := entry.typeflag
|
||||||
if typeflag == 0 {
|
if typeflag == 0 {
|
||||||
typeflag = tar.TypeReg
|
typeflag = tar.TypeReg
|
||||||
@@ -1010,7 +1009,7 @@ func tarArchive(t *testing.T, entries ...tarEntry) []byte {
|
|||||||
}
|
}
|
||||||
require.NoError(t, tw.WriteHeader(header))
|
require.NoError(t, tw.WriteHeader(header))
|
||||||
if typeflag == tar.TypeReg {
|
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)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1150,12 +1149,12 @@ func TestTryUploadJobSummaryMasksSecrets(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
content, err := testcase.encoding.NewEncoder().String(testcase.content)
|
content, err := testcase.encoding.NewEncoder().Bytes([]byte(testcase.content))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
|
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
|
||||||
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{
|
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{
|
||||||
name: "step-summary-0.md", body: content,
|
name: "step-summary-0.md", body: string(content),
|
||||||
}))),
|
}))),
|
||||||
nil,
|
nil,
|
||||||
).Once()
|
).Once()
|
||||||
|
|||||||
+37
-9
@@ -263,29 +263,27 @@ func AppendSecretMaskers(oldnew []string, secrets map[string]string) []string {
|
|||||||
return oldnew
|
return oldnew
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendSecretMasker registers v and each of its lines, as GitHub does.
|
// AppendSecretMasker skips one-byte line fragments to avoid masking punctuation from structured secrets.
|
||||||
func AppendSecretMasker(oldnew []string, v string) []string {
|
func AppendSecretMasker(oldnew []string, v string) []string {
|
||||||
ret := appendMaskedValue(oldnew, v)
|
ret := appendMaskedValue(oldnew, v)
|
||||||
for l := range strings.FieldsFuncSeq(v, func(r rune) bool { return r == '\r' || r == '\n' }) {
|
for l := range strings.FieldsFuncSeq(v, func(r rune) bool { return r == '\r' || r == '\n' }) {
|
||||||
ret = appendMaskedValue(ret, strings.TrimSpace(l))
|
if line := strings.TrimSpace(l); len(line) > 1 {
|
||||||
|
ret = appendMaskedValue(ret, line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendMaskedValue registers one value and every shape it takes on its way into a log.
|
|
||||||
func appendMaskedValue(ret []string, v string) []string {
|
func appendMaskedValue(ret []string, v string) []string {
|
||||||
// formatted JSON secrets could otherwise mask {,[,],} everywhere
|
if strings.TrimSpace(v) == "" || slices.Contains(ret, v) {
|
||||||
if len(strings.TrimSpace(v)) <= 1 || slices.Contains(ret, v) {
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
ret = append(ret, v, "***")
|
ret = append(ret, v, "***")
|
||||||
// command data reaches the log escaped, so "pass%word" also arrives as "pass%25word"
|
|
||||||
if strings.ContainsAny(v, "%\r\n") {
|
if strings.ContainsAny(v, "%\r\n") {
|
||||||
ret = append(ret, EscapeCommandData(v), "***")
|
ret = append(ret, EscapeCommandData(v), "***")
|
||||||
}
|
}
|
||||||
for _, encode := range secretValueEncoders {
|
for _, encode := range secretValueEncoders {
|
||||||
encoded := encode(v)
|
encoded := encode(v)
|
||||||
// An encoding that leaves the value unchanged is already masked above.
|
|
||||||
if encoded == v || len(encoded) <= 1 || slices.Contains(ret, encoded) {
|
if encoded == v || len(encoded) <= 1 || slices.Contains(ret, encoded) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -319,7 +317,7 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
|||||||
masks := Masks(ctx)
|
masks := Masks(ctx)
|
||||||
|
|
||||||
if len(*masks) == 0 {
|
if len(*masks) == 0 {
|
||||||
r.Message = defReplacer.Replace(r.Message)
|
r.Message = MaskLog(r.Message, defReplacer)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,12 +337,42 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
|||||||
cmasker := replacer
|
cmasker := replacer
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
r.Message = cmasker.Replace(r.Message)
|
r.Message = MaskLog(r.Message, cmasker)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaskLog decodes command data before masking to cover partially escaped values.
|
||||||
|
func MaskLog(message string, replacer *strings.Replacer) string {
|
||||||
|
var command, data string
|
||||||
|
var found bool
|
||||||
|
unescape := UnescapeCommandData
|
||||||
|
if strings.HasPrefix(message, "##[") {
|
||||||
|
command, data, found = strings.Cut(message[3:], "]")
|
||||||
|
unescape = UnescapeLegacyCommand
|
||||||
|
} else if strings.HasPrefix(message, "::") {
|
||||||
|
command, data, found = strings.Cut(message[2:], "::")
|
||||||
|
if name, _, properties := strings.Cut(command, " "); properties {
|
||||||
|
switch name {
|
||||||
|
case "error", "warning", "notice", "debug":
|
||||||
|
command = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch command {
|
||||||
|
case "error", "warning", "notice", "debug", "group", "endgroup", "add-matcher":
|
||||||
|
if found {
|
||||||
|
trimmedData := strings.TrimRight(data, "\r\n")
|
||||||
|
normalized := message[:len(message)-len(data)] + EscapeCommandData(unescape(trimmedData)) + data[len(trimmedData):]
|
||||||
|
if masked := replacer.Replace(normalized); masked != normalized {
|
||||||
|
return masked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return replacer.Replace(message)
|
||||||
|
}
|
||||||
|
|
||||||
type maskedHandler struct {
|
type maskedHandler struct {
|
||||||
logging.Handler
|
logging.Handler
|
||||||
masker entryProcessor
|
masker entryProcessor
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func TestValueMasker(t *testing.T) {
|
|||||||
secrets map[string]string
|
secrets map[string]string
|
||||||
masks []string
|
masks []string
|
||||||
disallowed []string
|
disallowed []string
|
||||||
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Multiline Private Key",
|
name: "Multiline Private Key",
|
||||||
@@ -49,6 +50,60 @@ func TestValueMasker(t *testing.T) {
|
|||||||
secrets: map[string]string{"TOKEN": "pass%word"},
|
secrets: map[string]string{"TOKEN": "pass%word"},
|
||||||
disallowed: []string{"pass%25word"},
|
disallowed: []string{"pass%25word"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "one-character mask",
|
||||||
|
lines: "%",
|
||||||
|
masks: []string{"%"},
|
||||||
|
want: "***",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fully escaped legacy secret",
|
||||||
|
lines: "##[group]alpha beta%3Bgamma%5Ddelta",
|
||||||
|
secrets: map[string]string{"TOKEN": "alpha beta;gamma]delta"},
|
||||||
|
want: "##[group]***",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partially escaped legacy mask",
|
||||||
|
lines: "##[group]alpha beta%3Bgamma]delta",
|
||||||
|
masks: []string{"alpha beta;gamma]delta"},
|
||||||
|
want: "##[group]***",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partially escaped modern mask preserves unrelated escapes",
|
||||||
|
lines: "::group::alpha%25beta%gamma tail%250A%0A",
|
||||||
|
masks: []string{"alpha%beta%gamma"},
|
||||||
|
want: "::group::*** tail%250A%0A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "command escapes are decoded once",
|
||||||
|
lines: "##[group]alpha%253Bbeta%5Dgamma",
|
||||||
|
masks: []string{"alpha;beta]gamma"},
|
||||||
|
want: "##[group]alpha%253Bbeta%5Dgamma",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain output preserves escapes",
|
||||||
|
lines: "alpha beta%3Bgamma%5Ddelta",
|
||||||
|
masks: []string{"alpha beta;gamma]delta"},
|
||||||
|
want: "alpha beta%3Bgamma%5Ddelta",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "command display preserves literal escapes around a mask",
|
||||||
|
lines: "##[command]alpha tail%3Bvalue%5D",
|
||||||
|
masks: []string{"alpha"},
|
||||||
|
want: "##[command]*** tail%3Bvalue%5D",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown modern commands preserve literal escapes",
|
||||||
|
lines: "::unknown::alpha tail%25value",
|
||||||
|
masks: []string{"alpha"},
|
||||||
|
want: "::unknown::*** tail%25value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "masks can span command headers and data",
|
||||||
|
lines: "::group::hidden other",
|
||||||
|
masks: []string{"group::hidden", "other"},
|
||||||
|
want: "::*** ***",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, entry := range table {
|
for _, entry := range table {
|
||||||
t.Run(entry.name, func(t *testing.T) {
|
t.Run(entry.name, func(t *testing.T) {
|
||||||
@@ -56,6 +111,9 @@ func TestValueMasker(t *testing.T) {
|
|||||||
masker := valueMasker(false, AppendSecretMaskers(nil, entry.secrets))
|
masker := valueMasker(false, AppendSecretMaskers(nil, entry.secrets))
|
||||||
for line := range strings.SplitSeq(entry.lines, "\n") {
|
for line := range strings.SplitSeq(entry.lines, "\n") {
|
||||||
lentry := masker(ctx, logging.Record{Message: line})
|
lentry := masker(ctx, logging.Record{Message: line})
|
||||||
|
if entry.want != "" {
|
||||||
|
assert.Equal(t, entry.want, lentry.Message)
|
||||||
|
}
|
||||||
for _, line := range entry.disallowed {
|
for _, line := range entry.disallowed {
|
||||||
assert.NotContains(t, lentry.Message, line)
|
assert.NotContains(t, lentry.Message, line)
|
||||||
}
|
}
|
||||||
@@ -139,6 +197,7 @@ func TestValueMaskerEncodedMasks(t *testing.T) {
|
|||||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte(" s3cr3t ")), "encoded: ***"},
|
{"encoded: " + base64.StdEncoding.EncodeToString([]byte(" s3cr3t ")), "encoded: ***"},
|
||||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("s3cr3t")), "encoded: ***"},
|
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("s3cr3t")), "encoded: ***"},
|
||||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("first")), "encoded: ***"},
|
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("first")), "encoded: ***"},
|
||||||
|
{"##[group]s3cr3t value\r\n", "##[group]***\r\n"},
|
||||||
} {
|
} {
|
||||||
entry := masker(WithMasks(t.Context(), &masks), logging.Record{Message: tc.line})
|
entry := masker(WithMasks(t.Context(), &masks), logging.Record{Message: tc.line})
|
||||||
assert.Equal(t, tc.want, entry.Message, tc.line)
|
assert.Equal(t, tc.want, entry.Message, tc.line)
|
||||||
@@ -194,17 +253,15 @@ func TestValueMaskerCachedReplacerSeesNewMasks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendSecretMaskerSkipsUselessEncodings(t *testing.T) {
|
func TestAppendSecretMaskerSkipsUselessEncodings(t *testing.T) {
|
||||||
// A token with no character an escape would touch only gains its base64 forms:
|
|
||||||
// JSON, query and path escaping all leave it unchanged.
|
|
||||||
pairs := AppendSecretMasker(nil, "plaintoken")
|
pairs := AppendSecretMasker(nil, "plaintoken")
|
||||||
assert.Equal(t, []string{
|
assert.Equal(t, []string{
|
||||||
"plaintoken", "***", "cGxhaW50b2tlbg==", "***", "bGFpbnRva2Vu", "***", "YWludG9rZW4=", "***",
|
"plaintoken", "***", "cGxhaW50b2tlbg==", "***", "bGFpbnRva2Vu", "***", "YWludG9rZW4=", "***",
|
||||||
"aW50b2tl", "***", "YWludG9r", "***", "bGFpbnRv", "***",
|
"aW50b2tl", "***", "YWludG9r", "***", "bGFpbnRv", "***",
|
||||||
}, pairs)
|
}, pairs)
|
||||||
|
|
||||||
// Too short to mask.
|
assert.Contains(t, AppendSecretMasker(nil, "x"), "x")
|
||||||
assert.Empty(t, AppendSecretMasker(nil, "x"))
|
|
||||||
assert.Empty(t, AppendSecretMasker(nil, " \t"))
|
assert.Empty(t, AppendSecretMasker(nil, " \t"))
|
||||||
|
assert.NotContains(t, AppendSecretMasker(nil, "{\nvalue\n}"), "{")
|
||||||
assert.NotContains(t, AppendSecretMasker(nil, `"123456"`), "123456")
|
assert.NotContains(t, AppendSecretMasker(nil, `"123456"`), "123456")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
@@ -210,14 +209,14 @@ func TestPatchedBundleAcrossActions(t *testing.T) {
|
|||||||
assert.Equal(t, data, out, "an untouched bundle must come back byte for byte")
|
assert.Equal(t, data, out, "an untouched bundle must come back byte for byte")
|
||||||
return
|
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
|
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")
|
"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")
|
"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")
|
requireHostTools(t, "node", "rustc")
|
||||||
|
|
||||||
dir := tempDirPath(t)
|
dir := tempDirPath(t)
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(dir, "dist"), 0o755))
|
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
"action.yml", "dist/restore.js", "dist/save.js", "dist/cleanup-BWEbZ6YT.js",
|
"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",
|
"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))
|
body, err := os.ReadFile(bundleFromGitHub(t, "Swatinem/rust-cache", "v2.9.2", name))
|
||||||
require.NoError(t, err)
|
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))
|
require.NoError(t, os.WriteFile(filepath.Join(dir, name), body, 0o600))
|
||||||
}
|
}
|
||||||
body, err := os.ReadFile(filepath.Join(dir, "action.yml"))
|
body, err := os.ReadFile(filepath.Join(dir, "action.yml"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
action, err := model.ReadAction(bytes.NewReader(body))
|
action, err := model.ReadAction(strings.NewReader(string(body)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
inputs := map[string]string{"lookup-only": "true", "add-rust-environment-hash-key": "false", "cache-bin": "false"}
|
inputs := map[string]string{"lookup-only": "true", "add-rust-environment-hash-key": "false", "cache-bin": "false"}
|
||||||
for name, input := range action.Inputs {
|
for name, input := range action.Inputs {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -183,9 +182,9 @@ func TestPatchedBundleSurvivesInsideAStringLiteral(t *testing.T) {
|
|||||||
out, patched := patchedBundle([]byte(embedded))
|
out, patched := patchedBundle([]byte(embedded))
|
||||||
require.True(t, patched)
|
require.True(t, patched)
|
||||||
|
|
||||||
cmd := exec.CommandContext(t.Context(), "node", "--check", "-")
|
file := filepath.Join(t.TempDir(), "bundle.js")
|
||||||
cmd.Stdin = bytes.NewReader(out)
|
require.NoError(t, os.WriteFile(file, out, 0o600))
|
||||||
checked, err := cmd.CombinedOutput()
|
checked, err := exec.CommandContext(t.Context(), "node", "--check", file).CombinedOutput()
|
||||||
require.NoError(t, err, "%s", checked)
|
require.NoError(t, err, "%s", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,14 +256,6 @@ func TestActionScriptPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPatchActionsAtTheContainerCopy(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) {
|
copiedBundles := func(t *testing.T, noPatch bool) ([]string, []*container.FileEntry, int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -273,12 +264,19 @@ func TestPatchActionsAtTheContainerCopy(t *testing.T) {
|
|||||||
Step: &model.Step{Uses: "owner/repo/sub@v1"},
|
Step: &model.Step{Uses: "owner/repo/sub@v1"},
|
||||||
remoteAction: &remoteAction{Org: "owner", Repo: "repo", Path: "sub", Ref: "v1"},
|
remoteAction: &remoteAction{Org: "owner", Repo: "repo", Path: "sub", Ref: "v1"},
|
||||||
action: &model.Action{Runs: model.ActionRuns{Using: "node20", Main: "../dist/index.js"}},
|
action: &model.Action{Runs: model.ActionRuns{Using: "node20", Main: "../dist/index.js"}},
|
||||||
actionDir: actionDir,
|
actionDir: t.TempDir(),
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Config: &Config{NoActionPatch: noPatch},
|
Config: &Config{ActionCacheDir: t.TempDir(), NoActionPatch: noPatch},
|
||||||
JobContainer: cm,
|
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 calls []string
|
||||||
var copied []*container.FileEntry
|
var copied []*container.FileEntry
|
||||||
cm.On("CopyDir", "/var/run/act/actions/repo/", sar.actionDir+"/", false, true).Return(func(context.Context) error {
|
cm.On("CopyDir", "/var/run/act/actions/repo/", sar.actionDir+"/", false, true).Return(func(context.Context) error {
|
||||||
|
|||||||
@@ -33,17 +33,24 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remoteDir := t.TempDir()
|
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"
|
const workflowPath = ".gitea/workflows/reusable.yml"
|
||||||
tmpl := func(tag string) string {
|
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"
|
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.MkdirAll(filepath.Join(workDir, ".gitea/workflows"), 0o755))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(remoteDir, workflowPath), []byte(tmpl("v1")), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v1")), 0o644))
|
||||||
gitMust(t, remoteDir, "add", workflowPath)
|
gitMust(t, workDir, "add", workflowPath)
|
||||||
gitMust(t, remoteDir, "commit", "-m", "v1")
|
gitMust(t, workDir, "commit", "-m", "v1")
|
||||||
|
gitMust(t, workDir, "push", "-u", "origin", "master")
|
||||||
|
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Config: &Config{Env: testCheckoutEnv()},
|
Config: &Config{Env: testCheckoutEnv()},
|
||||||
@@ -63,8 +70,9 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|||||||
require.Equal(t, tmpl("v1"), string(got))
|
require.Equal(t, tmpl("v1"), string(got))
|
||||||
|
|
||||||
// Branch tip moves; cache key (cacheDir) does not.
|
// Branch tip moves; cache key (cacheDir) does not.
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(remoteDir, workflowPath), []byte(tmpl("v2")), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(workDir, workflowPath), []byte(tmpl("v2")), 0o644))
|
||||||
gitMust(t, remoteDir, "commit", "-am", "v2")
|
gitMust(t, workDir, "commit", "-am", "v2")
|
||||||
|
gitMust(t, workDir, "push", "origin", "master")
|
||||||
|
|
||||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
||||||
got, err = os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
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) {
|
func gitMust(t *testing.T, dir string, args ...string) {
|
||||||
t.Helper()
|
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 != "" {
|
if dir != "" {
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1666,21 +1666,21 @@ func TestRunContextUpdateExtraPath(t *testing.T) {
|
|||||||
longPath := strings.Repeat("x", 64*1024+1)
|
longPath := strings.Repeat("x", 64*1024+1)
|
||||||
for _, testcase := range []struct {
|
for _, testcase := range []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content []byte
|
||||||
want string
|
want string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{name: "UTF-8 BOM", content: "\xef\xbb\xbf/utf8/tools\n", want: "/utf8/tools"},
|
{name: "UTF-8 BOM", content: []byte("\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: "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: longPath + "\n", want: longPath},
|
{name: "over 64 KiB", content: []byte(longPath + "\n"), want: longPath},
|
||||||
{name: "over 16 MiB", content: strings.Repeat("x", 16*1024*1024+1), wantErr: true},
|
{name: "over 16 MiB", content: []byte(strings.Repeat("x", 16*1024*1024+1)), wantErr: true},
|
||||||
} {
|
} {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
logger := logging.New(logging.DiscardHandler)
|
logger := logging.New(logging.DiscardHandler)
|
||||||
ctx := common.WithLogger(t.Context(), logger)
|
ctx := common.WithLogger(t.Context(), logger)
|
||||||
jobContainer := &containerMock{}
|
jobContainer := &containerMock{}
|
||||||
jobContainer.On("GetContainerArchive", mock.Anything, "/github/path").
|
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)
|
defer jobContainer.AssertExpectations(t)
|
||||||
rc := &RunContext{JobContainer: jobContainer}
|
rc := &RunContext{JobContainer: jobContainer}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,14 @@ func setFetch(t *testing.T, fetch func(context.Context, git.NewGitCloneExecutorI
|
|||||||
func TestShortSHAActionRejected(t *testing.T) {
|
func TestShortSHAActionRejected(t *testing.T) {
|
||||||
actionRoot := t.TempDir()
|
actionRoot := t.TempDir()
|
||||||
repo := filepath.Join(actionRoot, "actions", "hello-world-docker-action")
|
repo := filepath.Join(actionRoot, "actions", "hello-world-docker-action")
|
||||||
gitMust(t, "", "init", "--template=", "--initial-branch=main", repo)
|
require.NoError(t, os.MkdirAll(repo, 0o755))
|
||||||
gitMust(t, repo, "commit", "--allow-empty", "-m", "initial")
|
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()
|
output, err := exec.Command("git", "-C", repo, "rev-parse", "--short=7", "HEAD").Output()
|
||||||
require.NoError(t, err)
|
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))
|
[]byte("name: setup-go\nruns:\n using: node20\n main: index.js\n"), 0o600))
|
||||||
|
|
||||||
for _, args := range [][]string{
|
for _, args := range [][]string{
|
||||||
{"init", "--template=", "--initial-branch=main", actionDir},
|
{"init", "--initial-branch=main", actionDir},
|
||||||
{"-C", actionDir, "add", "action.yml"},
|
{"-C", actionDir, "add", "action.yml"},
|
||||||
{"-C", actionDir, "-c", "user.name=runner", "-c", "[email protected]", "-c", "commit.gpgsign=false", "commit", "-m", "action"},
|
{"-C", actionDir, "-c", "user.name=runner", "-c", "[email protected]", "-c", "commit.gpgsign=false", "commit", "-m", "action"},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -128,11 +128,6 @@ func (*daemonTestService) UpdateLog(_ context.Context, req *runnerv1.UpdateLogRe
|
|||||||
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||||
defer func(delay time.Duration) { declareRetryDelay = delay }(declareRetryDelay)
|
defer func(delay time.Duration) { declareRetryDelay = delay }(declareRetryDelay)
|
||||||
declareRetryDelay = 0
|
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 {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
ephemeral, beforeTask bool
|
ephemeral, beforeTask bool
|
||||||
@@ -159,6 +154,10 @@ func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(
|
|||||||
connecthttp.Mount(mux, rpcServer, connecthttp.WithReadMaxBytes(0))
|
connecthttp.Mount(mux, rpcServer, connecthttp.WithReadMaxBytes(0))
|
||||||
server := httptest.NewServer(http.StripPrefix("/api/actions", mux))
|
server := httptest.NewServer(http.StripPrefix("/api/actions", mux))
|
||||||
defer server.Close()
|
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}))
|
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)
|
err := runDaemon(ctx, &daemonArgs{Once: true}, &configFile)(nil, nil)
|
||||||
if tc.declareErr == nil && tc.fetchErr == nil || tc.beforeTask {
|
if tc.declareErr == nil && tc.fetchErr == nil || tc.beforeTask {
|
||||||
|
|||||||
@@ -75,35 +75,40 @@ func TestExecuteArgsLoadInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteArgsEventJSON(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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
payload string
|
eventpath string
|
||||||
inputs []string
|
inputs []string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "keeps a payload without inputs to merge",
|
name: "keeps a payload without inputs to merge",
|
||||||
payload: `{"inputs": {"kept": "from_event"}}`,
|
eventpath: payload(`{"inputs": {"kept": "from_event"}}`),
|
||||||
expected: `{"inputs": {"kept": "from_event"}}`,
|
expected: `{"inputs": {"kept": "from_event"}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "overrides the event inputs it names, keeping the rest",
|
name: "overrides the event inputs it names, keeping the rest",
|
||||||
payload: `{"inputs": {"kept": "from_event", "named": "from_event"}}`,
|
eventpath: payload(`{"inputs": {"kept": "from_event", "named": "from_event"}}`),
|
||||||
inputs: []string{"named=from_cli"},
|
inputs: []string{"named=from_cli"},
|
||||||
expected: `{"inputs": {"kept": "from_event", "named": "from_cli"}}`,
|
expected: `{"inputs": {"kept": "from_event", "named": "from_cli"}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "adds inputs to a payload carrying none",
|
name: "adds inputs to a payload carrying none",
|
||||||
payload: `{"repository": {"name": "test-repo"}}`,
|
eventpath: payload(`{"repository": {"name": "test-repo"}}`),
|
||||||
inputs: []string{"flag=val"},
|
inputs: []string{"flag=val"},
|
||||||
expected: `{"repository": {"name": "test-repo"}, "inputs": {"flag": "val"}}`,
|
expected: `{"repository": {"name": "test-repo"}, "inputs": {"flag": "val"}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "handles a null payload",
|
name: "handles a null payload",
|
||||||
payload: "null",
|
eventpath: payload("null"),
|
||||||
inputs: []string{"flag=val"},
|
inputs: []string{"flag=val"},
|
||||||
expected: `{"inputs": {"flag": "val"}}`,
|
expected: `{"inputs": {"flag": "val"}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "builds a payload without an event file",
|
name: "builds a payload without an event file",
|
||||||
@@ -114,12 +119,7 @@ func TestExecuteArgsEventJSON(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var eventpath string
|
eventJSON, err := (&executeArgs{eventpath: tt.eventpath, inputs: tt.inputs}).eventJSON()
|
||||||
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()
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.JSONEq(t, tt.expected, eventJSON)
|
require.JSONEq(t, tt.expected, eventJSON)
|
||||||
})
|
})
|
||||||
@@ -275,13 +275,21 @@ func workflowForList(name, file, event, jobID, jobName string) *model.Workflow {
|
|||||||
return &model.Workflow{
|
return &model.Workflow{
|
||||||
Name: name,
|
Name: name,
|
||||||
File: file,
|
File: file,
|
||||||
RawOn: yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: event},
|
RawOn: rawOnNode(event),
|
||||||
Jobs: map[string]*model.Job{
|
Jobs: map[string]*model.Job{
|
||||||
jobID: {Name: jobName},
|
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) {
|
func TestExecuteArgsLoadEnvsResultsOrigin(t *testing.T) {
|
||||||
t.Setenv("ACTIONS_RESULTS_URL", "") // the default is only supplied when nothing else does
|
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"})
|
handler, err := artifactcache.StartHandler(artifactcache.Options{Dir: t.TempDir(), OutboundIP: "127.0.0.1"})
|
||||||
|
|||||||
@@ -27,9 +27,16 @@ func TestRunPostTaskScriptSkippedWhenEmpty(t *testing.T) {
|
|||||||
r := &Runner{
|
r := &Runner{
|
||||||
cfg: &config.Config{},
|
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() {
|
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())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
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)
|
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
require.NotPanics(t, func() {
|
||||||
@@ -137,7 +146,9 @@ func TestRunPostTaskScriptIntegration(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
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)
|
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
|
||||||
setReporterJobResult(t, reporter, runnerv1.Result_RESULT_SUCCESS)
|
setReporterJobResult(t, reporter, runnerv1.Result_RESULT_SUCCESS)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import (
|
|||||||
|
|
||||||
func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
|
func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
|
||||||
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
|
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")
|
oldTask := filepath.Join(workdirRoot, "1001")
|
||||||
freshTask := filepath.Join(workdirRoot, "1002")
|
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.
|
// be reclaimed, while the shared tool_cache and operator data are preserved.
|
||||||
func TestRunnerOnIdleCleansStaleHostScratchDirs(t *testing.T) {
|
func TestRunnerOnIdleCleansStaleHostScratchDirs(t *testing.T) {
|
||||||
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
|
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
|
staleScratch := filepath.Join(hostRoot, "0123456789abcdef") // 16 hex
|
||||||
freshScratch := filepath.Join(hostRoot, "fedcba9876543210")
|
freshScratch := filepath.Join(hostRoot, "fedcba9876543210")
|
||||||
@@ -116,7 +118,8 @@ func TestRunnerCleanupStaleTaskDirsMissingRoot(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
|
func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
|
||||||
now := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
|
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++ {
|
for i := 1001; i <= 1003; i++ {
|
||||||
dir := filepath.Join(workdirRoot, strconv.Itoa(i))
|
dir := filepath.Join(workdirRoot, strconv.Itoa(i))
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ func TestSetupLinesInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrettyOSName(t *testing.T) {
|
func TestPrettyOSName(t *testing.T) {
|
||||||
dir := t.TempDir()
|
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
name string
|
name string
|
||||||
osRelease string
|
osRelease string
|
||||||
@@ -110,7 +109,7 @@ func TestPrettyOSName(t *testing.T) {
|
|||||||
{"missing file", "", "", true},
|
{"missing file", "", "", true},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
path := filepath.Join(dir, tt.name)
|
path := filepath.Join(t.TempDir(), "os-release")
|
||||||
if !tt.missing {
|
if !tt.missing {
|
||||||
require.NoError(t, os.WriteFile(path, []byte(tt.osRelease), 0o600))
|
require.NoError(t, os.WriteFile(path, []byte(tt.osRelease), 0o600))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func TestCheckout(t *testing.T) {
|
|||||||
t.Run("history", func(t *testing.T) {
|
t.Run("history", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
work := t.TempDir()
|
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")
|
runGit(t, "-C", work, "commit", "--allow-empty", "-m", "c1")
|
||||||
sha = runGit(t, "-C", work, "rev-parse", "HEAD")
|
sha = runGit(t, "-C", work, "rev-parse", "HEAD")
|
||||||
runGit(t, "-C", work, "tag", "v1")
|
runGit(t, "-C", work, "tag", "v1")
|
||||||
@@ -77,7 +77,7 @@ func TestCheckout(t *testing.T) {
|
|||||||
t.Run("sha256", func(t *testing.T) {
|
t.Run("sha256", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
sha256Repo := filepath.Join(server, "org", "sha256")
|
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, "commit", "--allow-empty", "-m", "s1")
|
||||||
runGit(t, "-C", sha256Repo, "checkout", "--detach")
|
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")
|
assert.Contains(t, string(outputs), "ref="+cmp.Or(tc.eventRef, "refs/heads/main")+"\n")
|
||||||
return
|
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 == "" {
|
if tc.head == "" {
|
||||||
require.Len(t, head, 2)
|
assert.Empty(t, gitOutput(dst, "symbolic-ref", "-q", "HEAD"))
|
||||||
assert.Equal(t, "HEAD", head[1])
|
|
||||||
} else {
|
} else {
|
||||||
require.Len(t, head, 3)
|
assert.Equal(t, "refs/heads/"+tc.head, gitOutput(dst, "symbolic-ref", "-q", "HEAD"))
|
||||||
assert.Equal(t, "refs/heads/"+tc.head, head[1])
|
assert.Equal(t, "origin/"+tc.head, gitOutput(dst, "rev-parse", "--abbrev-ref", "@{u}"))
|
||||||
assert.Equal(t, "origin/"+tc.head, head[2])
|
|
||||||
}
|
}
|
||||||
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 {
|
if inputRef := tc.inputs["ref"]; inputRef != "" && inputRef != sha {
|
||||||
assert.Contains(t, string(outputs), "ref="+inputRef+"\n")
|
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"
|
persisted := tc.inputs["persist-credentials"] != "false"
|
||||||
for _, repo := range repos {
|
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.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()
|
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, string(headers) == "\nAuthorization: Basic dG9rZW46dG9r\n", repo)
|
||||||
assert.Equal(t, persisted, gitOutput(repo, "config", "http."+serverURL+"/.sslcainfo") != "", 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) {
|
func publish(t *testing.T, server, name string, files, submodules map[string]string) {
|
||||||
work := t.TempDir()
|
work := t.TempDir()
|
||||||
runGit(t, "init", "--template=", "--initial-branch=main", work)
|
runGit(t, "init", "--initial-branch=main", work)
|
||||||
for file, content := range files {
|
for file, content := range files {
|
||||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(work, file)), 0o755))
|
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(work, file)), 0o755))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(work, file), []byte(content), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(work, file), []byte(content), 0o644))
|
||||||
}
|
}
|
||||||
runGit(t, "-C", work, "add", ".")
|
runGit(t, "-C", work, "add", ".")
|
||||||
var moduleConfig []byte
|
|
||||||
for path, url := range submodules {
|
for path, url := range submodules {
|
||||||
moduleConfig = fmt.Appendf(moduleConfig, "[submodule %q]\n\tpath = %q\n\turl = %q\n", path, path, url)
|
runGit(t, "-C", work, "config", "-f", ".gitmodules", "submodule."+path+".path", path)
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(work, ".gitmodules"), moduleConfig, 0o644))
|
runGit(t, "-C", work, "config", "-f", ".gitmodules", "submodule."+path+".url", url)
|
||||||
commit := runGit(t, "--git-dir", filepath.Join(server, name, url), "rev-parse", "main")
|
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, "update-index", "--add", "--cacheinfo", "160000,"+commit+","+path)
|
||||||
runGit(t, "-C", work, "add", ".gitmodules")
|
runGit(t, "-C", work, "add", ".gitmodules")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package process
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -22,13 +23,16 @@ func TestGroupClosePropagatesToOrphan(t *testing.T) {
|
|||||||
childPID := ready()
|
childPID := ready()
|
||||||
|
|
||||||
require.NoError(t, cmd.Wait())
|
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")
|
require.True(t, processAlive(childPID), "orphan should outlive its parent")
|
||||||
|
|
||||||
waitForExit := newProcessExitWaiter(t, childPID)
|
|
||||||
require.NoError(t, group.Close())
|
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
|
// 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")
|
require.NoError(t, err, "the step's job object must nest inside the group's")
|
||||||
defer killer.Close()
|
defer killer.Close()
|
||||||
|
|
||||||
waitForExit := newProcessExitWaiter(t, cmd.Process.Pid)
|
|
||||||
require.NoError(t, killer.Kill())
|
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
|
// TestGroupNilSafe covers the fallback path: when the job object cannot be
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -44,8 +46,23 @@ func processAlive(pid int) bool {
|
|||||||
return true
|
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) {
|
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())
|
require.NoError(t, cmd.Start())
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||||
@@ -56,10 +73,25 @@ func TestKillerKillsTree(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer killer.Close()
|
defer killer.Close()
|
||||||
|
|
||||||
childPID := ready()
|
// Wait for the backgrounded child PID to be reported.
|
||||||
require.True(t, processAlive(childPID), "child process should start")
|
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())
|
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()
|
_ = cmd.Wait()
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
@@ -24,23 +25,6 @@ func processAlive(pid int) bool {
|
|||||||
return code == stillActive
|
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) {
|
func TestKillerKillsTree(t *testing.T) {
|
||||||
cmd, ready := newTestProcess(t, "parent", false)
|
cmd, ready := newTestProcess(t, "parent", false)
|
||||||
require.NoError(t, cmd.Start())
|
require.NoError(t, cmd.Start())
|
||||||
@@ -55,7 +39,9 @@ func TestKillerKillsTree(t *testing.T) {
|
|||||||
childPID := ready()
|
childPID := ready()
|
||||||
require.True(t, processAlive(childPID), "child process should start")
|
require.True(t, processAlive(childPID), "child process should start")
|
||||||
|
|
||||||
waitForExit := newProcessExitWaiter(t, cmd.Process.Pid, childPID)
|
|
||||||
require.NoError(t, killer.Kill())
|
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")
|
||||||
}
|
}
|
||||||
@@ -960,7 +960,7 @@ func logRow(entry logging.Record) *runnerv1.LogRow {
|
|||||||
|
|
||||||
// mask repairs the content first, so a secret the repair itself spells out is still caught.
|
// mask repairs the content first, so a secret the repair itself spells out is still caught.
|
||||||
func (r *Reporter) mask(content string) string {
|
func (r *Reporter) mask(content string) string {
|
||||||
return r.logReplacer.Replace(strings.ToValidUTF8(content, "?"))
|
return runner.MaskLog(strings.ToValidUTF8(content, "?"), r.logReplacer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMask deliberately leaves the process-wide masker alone. Its entries come from every live
|
// AddMask deliberately leaves the process-wide masker alone. Its entries come from every live
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ func TestReporter_RunDaemonClose_Race(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
cfg, _ := config.LoadDefault("")
|
cfg, _ := config.LoadDefault("")
|
||||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{
|
||||||
Context: &structpb.Struct{},
|
Context: taskCtx,
|
||||||
}, cfg)
|
}, cfg)
|
||||||
reporter.ResetSteps(newTestSteps(1))
|
reporter.ResetSteps(newTestSteps(1))
|
||||||
|
|
||||||
@@ -472,8 +474,10 @@ func TestReporter_ServerCancelStillFlushesFinal(t *testing.T) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
cfg, _ := config.LoadDefault("")
|
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.ResetSteps(newTestSteps(1))
|
||||||
|
|
||||||
reporter.stateMu.Lock()
|
reporter.stateMu.Lock()
|
||||||
@@ -523,8 +527,10 @@ func TestReporter_CloseReportsCancelledOnCanceledCtx(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
cfg, _ := config.LoadDefault("")
|
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.ResetSteps(newTestSteps(1))
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
@@ -665,15 +671,19 @@ func TestReporter_SetOutputs(t *testing.T) {
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, "caf\xff", got.value)
|
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"]
|
got, ok = r.outputs["atlimit"]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Len(t, got.value, maxOutputValueLen)
|
assert.Len(t, got.value, maxOutputValueLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_DebugSettingsAreNotMasked(t *testing.T) {
|
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{
|
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"},
|
Secrets: map[string]string{"ACTIONS_STEP_DEBUG": "true", "ACTIONS_RUNNER_DEBUG": "true", "TOKEN": "s3cr3t"},
|
||||||
}, &config.Config{})
|
}, &config.Config{})
|
||||||
defer deregisterGlobalMasks(reporter)
|
defer deregisterGlobalMasks(reporter)
|
||||||
@@ -750,8 +760,7 @@ func TestReporter_ParseResult(t *testing.T) {
|
|||||||
func TestReporter_masksEncodedSecrets(t *testing.T) {
|
func TestReporter_masksEncodedSecrets(t *testing.T) {
|
||||||
secret := `p@ss w"rd/1`
|
secret := `p@ss w"rd/1`
|
||||||
r := &Reporter{logReplacer: strings.NewReplacer()}
|
r := &Reporter{logReplacer: strings.NewReplacer()}
|
||||||
r.oldnew = runner.AppendSecretMasker(r.oldnew, secret)
|
r.AddMask(secret)
|
||||||
r.logReplacer = strings.NewReplacer(r.oldnew...)
|
|
||||||
|
|
||||||
for _, line := range []string{
|
for _, line := range []string{
|
||||||
"token: " + secret,
|
"token: " + secret,
|
||||||
@@ -763,6 +772,17 @@ func TestReporter_masksEncodedSecrets(t *testing.T) {
|
|||||||
assert.NotContains(t, masked, secret)
|
assert.NotContains(t, masked, secret)
|
||||||
assert.NotContains(t, masked, base64.StdEncoding.EncodeToString([]byte(secret)))
|
assert.NotContains(t, masked, base64.StdEncoding.EncodeToString([]byte(secret)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.AddMask("alpha beta;gamma]delta")
|
||||||
|
r.AddMask("%")
|
||||||
|
for _, test := range []struct{ line, want string }{
|
||||||
|
{"##[group]alpha beta%3Bgamma%5Ddelta", "##[group]***"},
|
||||||
|
{"##[group]alpha beta%3Bgamma]delta", "##[group]***"},
|
||||||
|
{"%", "***"},
|
||||||
|
} {
|
||||||
|
r.appendLogRow(logRow(logging.Record{Message: test.line}))
|
||||||
|
assert.Equal(t, test.want, r.logRows[len(r.logRows)-1].Content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_AckIndexBeyondBuffer(t *testing.T) {
|
func TestReporter_AckIndexBeyondBuffer(t *testing.T) {
|
||||||
@@ -851,8 +871,10 @@ func TestReporter_ServerFailureResultIsReportedAsFailure(t *testing.T) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
cfg, _ := config.LoadDefault("")
|
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)
|
close(reporter.daemon)
|
||||||
|
|
||||||
require.NoError(t, reporter.ReportState(false))
|
require.NoError(t, reporter.ReportState(false))
|
||||||
@@ -1019,7 +1041,9 @@ func newTestReporter(t *testing.T, client *mocks.Client, cfg *config.Config) *Re
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
t.Cleanup(cancel)
|
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 {
|
func newTestSteps(n int) []*model.Step {
|
||||||
@@ -1037,9 +1061,11 @@ func TestReporter_ReportedSteps(t *testing.T) {
|
|||||||
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
|
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
|
||||||
})
|
})
|
||||||
client.On("UpdateTask", mock.Anything, mock.Anything).Maybe().Return(&runnerv1.UpdateTaskResponse{}, 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("")
|
cfg, _ := config.LoadDefault("")
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
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.RunDaemon()
|
||||||
reporter.Logf("runner version")
|
reporter.Logf("runner version")
|
||||||
reporter.ResetSteps(steps)
|
reporter.ResetSteps(steps)
|
||||||
|
|||||||
@@ -117,8 +117,7 @@ func TestExportRetriesAndMatchesOTLP(t *testing.T) {
|
|||||||
certificate := server.TLS.Certificates[0]
|
certificate := server.TLS.Certificates[0]
|
||||||
key, err := x509.MarshalPKCS8PrivateKey(certificate.PrivateKey)
|
key, err := x509.MarshalPKCS8PrivateKey(certificate.PrivateKey)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dir := t.TempDir()
|
certificatePath, keyPath := filepath.Join(t.TempDir(), "cert.pem"), filepath.Join(t.TempDir(), "key.pem")
|
||||||
certificatePath, keyPath := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
|
|
||||||
require.NoError(t, os.WriteFile(certificatePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Certificate[0]}), 0o600))
|
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))
|
require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}), 0o600))
|
||||||
setEnv(t, map[string]string{
|
setEnv(t, map[string]string{
|
||||||
|
|||||||
Reference in new issue
Block a user