Skip to content

Commit a42f3cf

Browse files
feat: Add new Action Cache (#1913)
* feat: Add new Action Cache * fix some linter errors / warnings * fix lint * fix empty fpath parameter returns empty archive * rename fpath to includePrefix
1 parent 8314095 commit a42f3cf

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

pkg/runner/action_cache.go

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package runner
2+
3+
import (
4+
"archive/tar"
5+
"context"
6+
"crypto/rand"
7+
"encoding/hex"
8+
"errors"
9+
"io"
10+
"io/fs"
11+
"path"
12+
"strings"
13+
14+
git "github.com/go-git/go-git/v5"
15+
config "github.com/go-git/go-git/v5/config"
16+
"github.com/go-git/go-git/v5/plumbing"
17+
"github.com/go-git/go-git/v5/plumbing/object"
18+
"github.com/go-git/go-git/v5/plumbing/transport"
19+
"github.com/go-git/go-git/v5/plumbing/transport/http"
20+
)
21+
22+
type ActionCache interface {
23+
Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error)
24+
GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error)
25+
}
26+
27+
type GoGitActionCache struct {
28+
Path string
29+
}
30+
31+
func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
32+
gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
33+
gogitrepo, err := git.PlainInit(gitPath, true)
34+
if errors.Is(err, git.ErrRepositoryAlreadyExists) {
35+
gogitrepo, err = git.PlainOpen(gitPath)
36+
}
37+
if err != nil {
38+
return "", err
39+
}
40+
tmpBranch := make([]byte, 12)
41+
if _, err := rand.Read(tmpBranch); err != nil {
42+
return "", err
43+
}
44+
branchName := hex.EncodeToString(tmpBranch)
45+
var refSpec config.RefSpec
46+
spec := config.RefSpec(ref + ":" + branchName)
47+
tagOrSha := false
48+
if spec.IsExactSHA1() {
49+
refSpec = spec
50+
} else if strings.HasPrefix(ref, "refs/") {
51+
refSpec = config.RefSpec(ref + ":refs/heads/" + branchName)
52+
} else {
53+
tagOrSha = true
54+
refSpec = config.RefSpec("refs/*/" + ref + ":refs/heads/*/" + branchName)
55+
}
56+
var auth transport.AuthMethod
57+
if token != "" {
58+
auth = &http.BasicAuth{
59+
Username: "token",
60+
Password: token,
61+
}
62+
}
63+
remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{
64+
Name: "anonymous",
65+
URLs: []string{
66+
url,
67+
},
68+
})
69+
if err != nil {
70+
return "", err
71+
}
72+
defer func() {
73+
if refs, err := gogitrepo.References(); err == nil {
74+
_ = refs.ForEach(func(r *plumbing.Reference) error {
75+
if strings.Contains(r.Name().String(), branchName) {
76+
return gogitrepo.DeleteBranch(r.Name().String())
77+
}
78+
return nil
79+
})
80+
}
81+
}()
82+
if err := remote.FetchContext(ctx, &git.FetchOptions{
83+
RefSpecs: []config.RefSpec{
84+
refSpec,
85+
},
86+
Auth: auth,
87+
Force: true,
88+
}); err != nil {
89+
return "", err
90+
}
91+
if tagOrSha {
92+
for _, prefix := range []string{"refs/heads/tags/", "refs/heads/heads/"} {
93+
hash, err := gogitrepo.ResolveRevision(plumbing.Revision(prefix + branchName))
94+
if err == nil {
95+
return hash.String(), nil
96+
}
97+
}
98+
}
99+
hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName))
100+
if err != nil {
101+
return "", err
102+
}
103+
return hash.String(), nil
104+
}
105+
106+
func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
107+
gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git")
108+
gogitrepo, err := git.PlainOpen(gitPath)
109+
if err != nil {
110+
return nil, err
111+
}
112+
commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha))
113+
if err != nil {
114+
return nil, err
115+
}
116+
files, err := commit.Files()
117+
if err != nil {
118+
return nil, err
119+
}
120+
rpipe, wpipe := io.Pipe()
121+
// Interrupt io.Copy using ctx
122+
ch := make(chan int, 1)
123+
go func() {
124+
select {
125+
case <-ctx.Done():
126+
wpipe.CloseWithError(ctx.Err())
127+
case <-ch:
128+
}
129+
}()
130+
go func() {
131+
defer wpipe.Close()
132+
defer close(ch)
133+
tw := tar.NewWriter(wpipe)
134+
cleanIncludePrefix := path.Clean(includePrefix)
135+
wpipe.CloseWithError(files.ForEach(func(f *object.File) error {
136+
if err := ctx.Err(); err != nil {
137+
return err
138+
}
139+
name := f.Name
140+
if strings.HasPrefix(name, cleanIncludePrefix+"/") {
141+
name = name[len(cleanIncludePrefix)+1:]
142+
} else if cleanIncludePrefix != "." && name != cleanIncludePrefix {
143+
return nil
144+
}
145+
fmode, err := f.Mode.ToOSFileMode()
146+
if err != nil {
147+
return err
148+
}
149+
if fmode&fs.ModeSymlink == fs.ModeSymlink {
150+
content, err := f.Contents()
151+
if err != nil {
152+
return err
153+
}
154+
return tw.WriteHeader(&tar.Header{
155+
Name: name,
156+
Mode: int64(fmode),
157+
Linkname: content,
158+
})
159+
}
160+
err = tw.WriteHeader(&tar.Header{
161+
Name: name,
162+
Mode: int64(fmode),
163+
Size: f.Size,
164+
})
165+
if err != nil {
166+
return err
167+
}
168+
reader, err := f.Reader()
169+
if err != nil {
170+
return err
171+
}
172+
_, err = io.Copy(tw, reader)
173+
return err
174+
}))
175+
}()
176+
return rpipe, err
177+
}

pkg/runner/action_cache_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package runner
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"context"
7+
"io"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
//nolint:gosec
15+
func TestActionCache(t *testing.T) {
16+
a := assert.New(t)
17+
cache := &GoGitActionCache{
18+
Path: os.TempDir(),
19+
}
20+
ctx := context.Background()
21+
sha, err := cache.Fetch(ctx, "christopherhx/script", "https://github.com/christopherhx/script", "main", "")
22+
a.NoError(err)
23+
a.NotEmpty(sha)
24+
atar, err := cache.GetTarArchive(ctx, "christopherhx/script", sha, "node_modules")
25+
a.NoError(err)
26+
a.NotEmpty(atar)
27+
mytar := tar.NewReader(atar)
28+
th, err := mytar.Next()
29+
a.NoError(err)
30+
a.NotEqual(0, th.Size)
31+
buf := &bytes.Buffer{}
32+
// G110: Potential DoS vulnerability via decompression bomb (gosec)
33+
_, err = io.Copy(buf, mytar)
34+
a.NoError(err)
35+
str := buf.String()
36+
a.NotEmpty(str)
37+
}

0 commit comments

Comments
 (0)