Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion internal/system/cache_github_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"compress/gzip"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
Expand All @@ -16,9 +17,17 @@ import (
actionscache "github.com/tonistiigi/go-actions-cache"
)

// actionsCacheClient is the subset of the GitHub Actions cache client used by
// GitHubActionsCache. It exists so the save/load behaviour can be tested without
// talking to the real API.
type actionsCacheClient interface {
Load(ctx context.Context, keys ...string) (*actionscache.Entry, error)
Save(ctx context.Context, key string, b actionscache.Blob) error
}

// GitHubActionsCache implements Cache interface using GitHub Actions cache
type GitHubActionsCache struct {
client *actionscache.Cache
client actionsCacheClient
prefix string
tempFiles []string
mu sync.Mutex
Expand Down Expand Up @@ -78,6 +87,11 @@ func (c *GitHubActionsCache) Set(ctx context.Context, key string, data io.Reader

err = c.client.Save(ctx, cacheKey, blob)
if err != nil {
// Another job stored the same key first, the content is identical so this is not an error
Comment thread
tturkowski marked this conversation as resolved.
if errors.Is(err, os.ErrExist) {
return nil
}

return fmt.Errorf("failed to save to GitHub Actions cache: %w", err)
}

Expand Down Expand Up @@ -199,6 +213,11 @@ func (c *GitHubActionsCache) StoreFolderCache(ctx context.Context, key string, f
// Store the archive in cache
blob := actionscache.NewBlob(buf.Bytes())
if err := c.client.Save(ctx, cacheKey, blob); err != nil {
// Another job stored the same key first, the content is identical so this is not an error
if errors.Is(err, os.ErrExist) {
return nil
}

return fmt.Errorf("failed to save folder to GitHub Actions cache: %w", err)
}

Expand Down
95 changes: 95 additions & 0 deletions internal/system/cache_github_actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package system

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
actionscache "github.com/tonistiigi/go-actions-cache"
)

// fakeActionsCacheClient records Save calls and returns a canned error.
type fakeActionsCacheClient struct {
saveErr error
savedKeys []string
}

// Load is only present to satisfy actionsCacheClient; these tests exercise the save paths.
func (f *fakeActionsCacheClient) Load(ctx context.Context, keys ...string) (*actionscache.Entry, error) {
return nil, ErrCacheNotFound
}

func (f *fakeActionsCacheClient) Save(ctx context.Context, key string, b actionscache.Blob) error {
f.savedKeys = append(f.savedKeys, key)
return f.saveErr
}

// alreadyExistsError mimics what the GitHub Actions cache API returns when an
// entry for the key was already uploaded, wrapped the same way the library does.
func alreadyExistsError() error {
return actionscache.HTTPError{
StatusCode: 409,
Err: actionscache.GithubAPIError{
Message: "409 Conflict",
TypeKey: "ArtifactCacheItemAlreadyExistsException",
},
}
}

func TestGitHubActionsCacheSetIgnoresAlreadyExists(t *testing.T) {
t.Parallel()

client := &fakeActionsCacheClient{saveErr: alreadyExistsError()}
cache := &GitHubActionsCache{client: client, prefix: "sw-cli"}

err := cache.Set(t.Context(), "some-key", strings.NewReader("content"))

require.NoError(t, err, "already existing cache entry must not fail the build")
assert.Len(t, client.savedKeys, 1)
}

func TestGitHubActionsCacheSetReturnsOtherErrors(t *testing.T) {
t.Parallel()

client := &fakeActionsCacheClient{saveErr: assert.AnError}
cache := &GitHubActionsCache{client: client, prefix: "sw-cli"}

err := cache.Set(t.Context(), "some-key", strings.NewReader("content"))

require.Error(t, err)
assert.ErrorIs(t, err, assert.AnError)
}

func TestGitHubActionsCacheStoreFolderCacheIgnoresAlreadyExists(t *testing.T) {
t.Parallel()

folder := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(folder, "app.js"), []byte("console.log(1)"), 0o644))

client := &fakeActionsCacheClient{saveErr: alreadyExistsError()}
cache := &GitHubActionsCache{client: client, prefix: "sw-cli"}

err := cache.StoreFolderCache(t.Context(), "sw-cli-6.7.0.0-abc", folder)

require.NoError(t, err, "already existing folder cache entry must not fail the build")
assert.Len(t, client.savedKeys, 1)
}

func TestGitHubActionsCacheStoreFolderCacheReturnsOtherErrors(t *testing.T) {
t.Parallel()

folder := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(folder, "app.js"), []byte("console.log(1)"), 0o644))

client := &fakeActionsCacheClient{saveErr: assert.AnError}
cache := &GitHubActionsCache{client: client, prefix: "sw-cli"}

err := cache.StoreFolderCache(t.Context(), "sw-cli-6.7.0.0-abc", folder)

require.Error(t, err)
assert.ErrorIs(t, err, assert.AnError)
}
Loading