diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 255d1dc1430..f9d8f5faec9 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -4569,7 +4569,7 @@ jobs: fi - name: Upload super-linter log if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: super-linter-log path: super-linter.log diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go index 151cedb3f29..d7f6114f3b4 100644 --- a/pkg/workflow/action_cache.go +++ b/pkg/workflow/action_cache.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "sort" "github.com/githubnext/gh-aw/pkg/logger" ) @@ -61,7 +62,7 @@ func (c *ActionCache) Load() error { return nil } -// Save saves the cache to disk +// Save saves the cache to disk with sorted entries func (c *ActionCache) Save() error { actionCacheLog.Printf("Saving action cache to: %s with %d entries", c.path, len(c.Entries)) @@ -72,7 +73,8 @@ func (c *ActionCache) Save() error { return err } - data, err := json.MarshalIndent(c, "", " ") + // Marshal with sorted entries + data, err := c.marshalSorted() if err != nil { actionCacheLog.Printf("Failed to marshal cache data: %v", err) return err @@ -90,6 +92,43 @@ func (c *ActionCache) Save() error { return nil } +// marshalSorted marshals the cache with entries sorted by key +func (c *ActionCache) marshalSorted() ([]byte, error) { + // Extract and sort the keys + keys := make([]string, 0, len(c.Entries)) + for key := range c.Entries { + keys = append(keys, key) + } + sort.Strings(keys) + + // Manually construct JSON with sorted keys + var result []byte + result = append(result, []byte("{\n \"entries\": {\n")...) + + for i, key := range keys { + entry := c.Entries[key] + + // Marshal the entry + entryJSON, err := json.MarshalIndent(entry, " ", " ") + if err != nil { + return nil, err + } + + // Add the key and entry + result = append(result, []byte(" \""+key+"\": ")...) + result = append(result, entryJSON...) + + // Add comma if not the last entry + if i < len(keys)-1 { + result = append(result, ',') + } + result = append(result, '\n') + } + + result = append(result, []byte(" }\n}")...) + return result, nil +} + // Get retrieves a cached entry if it exists func (c *ActionCache) Get(repo, version string) (string, bool) { key := repo + "@" + version diff --git a/pkg/workflow/action_cache_test.go b/pkg/workflow/action_cache_test.go index fcbeb19314a..88924de18bc 100644 --- a/pkg/workflow/action_cache_test.go +++ b/pkg/workflow/action_cache_test.go @@ -1,6 +1,7 @@ package workflow import ( + "encoding/json" "os" "path/filepath" "testing" @@ -124,3 +125,75 @@ func TestActionCacheTrailingNewline(t *testing.T) { t.Error("Cache file should end with a trailing newline for prettier compliance") } } + +func TestActionCacheSortedEntries(t *testing.T) { + // Create temporary directory for testing + tmpDir := t.TempDir() + + // Create cache and add entries in non-alphabetical order + cache := NewActionCache(tmpDir) + cache.Set("zzz/last-action", "v1", "sha111") + cache.Set("actions/checkout", "v5", "sha222") + cache.Set("mmm/middle-action", "v2", "sha333") + cache.Set("actions/setup-node", "v4", "sha444") + cache.Set("aaa/first-action", "v3", "sha555") + + // Save to disk + err := cache.Save() + if err != nil { + t.Fatalf("Failed to save cache: %v", err) + } + + // Read the file content + cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName) + data, err := os.ReadFile(cachePath) + if err != nil { + t.Fatalf("Failed to read cache file: %v", err) + } + + content := string(data) + + // Verify that entries appear in alphabetical order by checking their positions + entries := []string{ + "aaa/first-action@v3", + "actions/checkout@v5", + "actions/setup-node@v4", + "mmm/middle-action@v2", + "zzz/last-action@v1", + } + + lastPos := -1 + for _, entry := range entries { + pos := indexOf(content, entry) + if pos == -1 { + t.Errorf("Entry %s not found in cache file", entry) + continue + } + if pos < lastPos { + t.Errorf("Entry %s appears before previous entry (not sorted)", entry) + } + lastPos = pos + } + + // Also verify the file is valid JSON + var loadedCache ActionCache + err = json.Unmarshal(data, &loadedCache) + if err != nil { + t.Fatalf("Saved cache is not valid JSON: %v", err) + } + + // Verify all entries are present + if len(loadedCache.Entries) != 5 { + t.Errorf("Expected 5 entries, got %d", len(loadedCache.Entries)) + } +} + +// indexOf returns the index of substr in s, or -1 if not found +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} diff --git a/pkg/workflow/action_pins_test.go b/pkg/workflow/action_pins_test.go index 7af2d7a7377..4d14564dc15 100644 --- a/pkg/workflow/action_pins_test.go +++ b/pkg/workflow/action_pins_test.go @@ -345,9 +345,9 @@ func TestApplyActionPinToStep(t *testing.T) { func TestGetActionPinsSorting(t *testing.T) { pins := getActionPins() - // Verify we got all the pins (should be 18) - if len(pins) != 18 { - t.Errorf("getActionPins() returned %d pins, expected 18", len(pins)) + // Verify we got all the pins (should be 20) + if len(pins) != 20 { + t.Errorf("getActionPins() returned %d pins, expected 20", len(pins)) } // Verify they are sorted by version (descending) then by repository name (ascending)