github.com/nektos/act@v0.2.83/pkg/runner/action_cache.go (about) 1 package runner 2 3 import ( 4 "archive/tar" 5 "context" 6 "crypto/rand" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io" 11 "io/fs" 12 "path" 13 "strings" 14 "time" 15 16 git "github.com/go-git/go-git/v5" 17 config "github.com/go-git/go-git/v5/config" 18 "github.com/go-git/go-git/v5/plumbing" 19 "github.com/go-git/go-git/v5/plumbing/object" 20 "github.com/go-git/go-git/v5/plumbing/transport" 21 "github.com/go-git/go-git/v5/plumbing/transport/http" 22 "github.com/nektos/act/pkg/common" 23 ) 24 25 type ActionCache interface { 26 Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) 27 GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) 28 } 29 30 type GoGitActionCache struct { 31 Path string 32 } 33 34 func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { 35 logger := common.Logger(ctx) 36 37 gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") 38 39 logger.Infof("GoGitActionCache fetch %s with ref %s at %s", url, ref, gitPath) 40 41 gogitrepo, err := git.PlainInit(gitPath, true) 42 if errors.Is(err, git.ErrRepositoryAlreadyExists) { 43 logger.Debugf("GoGitActionCache cache hit %s with ref %s at %s", url, ref, gitPath) 44 gogitrepo, err = git.PlainOpen(gitPath) 45 } 46 if err != nil { 47 return "", fmt.Errorf("GoGitActionCache failed to open bare git %s with ref %s at %s: %w", url, ref, gitPath, err) 48 } 49 tmpBranch := make([]byte, 12) 50 if _, err := rand.Read(tmpBranch); err != nil { 51 return "", fmt.Errorf("GoGitActionCache failed to generate random tmp branch %s with ref %s at %s: %w", url, ref, gitPath, err) 52 } 53 branchName := hex.EncodeToString(tmpBranch) 54 55 var auth transport.AuthMethod 56 if token != "" { 57 auth = &http.BasicAuth{ 58 Username: "token", 59 Password: token, 60 } 61 } 62 remote, err := gogitrepo.CreateRemoteAnonymous(&config.RemoteConfig{ 63 Name: "anonymous", 64 URLs: []string{ 65 url, 66 }, 67 }) 68 if err != nil { 69 return "", fmt.Errorf("GoGitActionCache failed to create remote %s with ref %s at %s: %w", url, ref, gitPath, err) 70 } 71 defer func() { 72 _ = gogitrepo.DeleteBranch(branchName) 73 }() 74 if err := remote.FetchContext(ctx, &git.FetchOptions{ 75 RefSpecs: []config.RefSpec{ 76 config.RefSpec(ref + ":" + branchName), 77 }, 78 Auth: auth, 79 Force: true, 80 Depth: 1, 81 }); err != nil { 82 return "", fmt.Errorf("GoGitActionCache failed to fetch %s with ref %s at %s: %w", url, ref, gitPath, err) 83 } 84 hash, err := gogitrepo.ResolveRevision(plumbing.Revision(branchName)) 85 if err != nil { 86 return "", fmt.Errorf("GoGitActionCache failed to resolve sha %s with ref %s at %s: %w", url, ref, gitPath, err) 87 } 88 logger.Infof("GoGitActionCache fetch %s with ref %s at %s resolved to %s", url, ref, gitPath, hash.String()) 89 return hash.String(), nil 90 } 91 92 type GitFileInfo struct { 93 name string 94 size int64 95 modTime time.Time 96 isDir bool 97 mode fs.FileMode 98 } 99 100 // IsDir implements fs.FileInfo. 101 func (g *GitFileInfo) IsDir() bool { 102 return g.isDir 103 } 104 105 // ModTime implements fs.FileInfo. 106 func (g *GitFileInfo) ModTime() time.Time { 107 return g.modTime 108 } 109 110 // Mode implements fs.FileInfo. 111 func (g *GitFileInfo) Mode() fs.FileMode { 112 return g.mode 113 } 114 115 // Name implements fs.FileInfo. 116 func (g *GitFileInfo) Name() string { 117 return g.name 118 } 119 120 // Size implements fs.FileInfo. 121 func (g *GitFileInfo) Size() int64 { 122 return g.size 123 } 124 125 // Sys implements fs.FileInfo. 126 func (g *GitFileInfo) Sys() any { 127 return nil 128 } 129 130 func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { 131 logger := common.Logger(ctx) 132 133 gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") 134 135 logger.Infof("GoGitActionCache get content %s with sha %s subpath '%s' at %s", cacheDir, sha, includePrefix, gitPath) 136 137 gogitrepo, err := git.PlainOpen(gitPath) 138 if err != nil { 139 return nil, fmt.Errorf("GoGitActionCache failed to open bare git %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err) 140 } 141 commit, err := gogitrepo.CommitObject(plumbing.NewHash(sha)) 142 if err != nil { 143 return nil, fmt.Errorf("GoGitActionCache failed to get commit %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err) 144 } 145 t, err := commit.Tree() 146 if err != nil { 147 return nil, fmt.Errorf("GoGitActionCache failed to open git tree %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err) 148 } 149 files, err := commit.Files() 150 if err != nil { 151 return nil, fmt.Errorf("GoGitActionCache failed to list files %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err) 152 } 153 rpipe, wpipe := io.Pipe() 154 // Interrupt io.Copy using ctx 155 ch := make(chan int, 1) 156 go func() { 157 select { 158 case <-ctx.Done(): 159 wpipe.CloseWithError(ctx.Err()) 160 case <-ch: 161 } 162 }() 163 go func() { 164 defer wpipe.Close() 165 defer close(ch) 166 tw := tar.NewWriter(wpipe) 167 cleanIncludePrefix := path.Clean(includePrefix) 168 wpipe.CloseWithError(files.ForEach(func(f *object.File) error { 169 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, f.Name, f) 170 })) 171 }() 172 return rpipe, err 173 } 174 175 func actionCacheCopyFileOrDir(ctx context.Context, cleanIncludePrefix string, t *object.Tree, tw *tar.Writer, origin string, f *object.File) error { 176 if err := ctx.Err(); err != nil { 177 return err 178 } 179 name := origin 180 if strings.HasPrefix(name, cleanIncludePrefix+"/") { 181 name = name[len(cleanIncludePrefix)+1:] 182 } else if cleanIncludePrefix != "." && name != cleanIncludePrefix { 183 return nil 184 } 185 fmode, err := f.Mode.ToOSFileMode() 186 if err != nil { 187 return err 188 } 189 if fmode&fs.ModeSymlink == fs.ModeSymlink { 190 content, err := f.Contents() 191 if err != nil { 192 return err 193 } 194 195 destPath := path.Join(path.Dir(f.Name), content) 196 197 subtree, err := t.Tree(destPath) 198 if err == nil { 199 return subtree.Files().ForEach(func(ft *object.File) error { 200 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin+strings.TrimPrefix(ft.Name, f.Name), f) 201 }) 202 } 203 204 f, err := t.File(destPath) 205 if err != nil { 206 return fmt.Errorf("%s (%s): %w", destPath, origin, err) 207 } 208 return actionCacheCopyFileOrDir(ctx, cleanIncludePrefix, t, tw, origin, f) 209 } 210 header, err := tar.FileInfoHeader(&GitFileInfo{ 211 name: name, 212 mode: fmode, 213 size: f.Size, 214 }, "") 215 if err != nil { 216 return err 217 } 218 err = tw.WriteHeader(header) 219 if err != nil { 220 return err 221 } 222 reader, err := f.Reader() 223 if err != nil { 224 return err 225 } 226 _, err = io.Copy(tw, reader) 227 return err 228 }