gh-cache-download: GitHub-Actions-Caches herunterladen

Eine gh-Extension und eine Composite Action, die einen Cache-Eintrag von GitHub Actions per Key herunterladen, damit sich sein Inhalt lokal prüfen lässt.

Das Problem

GitHub speichert Cache-Einträge für actions/cache, setup-node, setup-python und ähnliche Actions, aber es gibt keinen Button, keinen API-Aufruf und keinen gh-Befehl, der das Archiv liefert. Der Cache-Dienst gibt Download-URLs nur an Workflow-Läufe heraus. Wer einen Cache debuggen will, der wiederhergestellt, aber nicht genutzt wird, oder der unerwartet groß ist, braucht den Inhalt.

Der Bedarf ist belegt:

  • cli/cli#9125, "Add a way to download github actions caches", hat 113 Reaktionen und ist seit Mai 2024 offen. Ein gh-Maintainer hat es im Juli 2025 an das Actions-Team weitergegeben. Die Umgehungen im Thread sind Workflow-Snippets, die den Cache mit actions/cache wiederherstellen und als Artefakt hochladen. Sie brauchen die ursprüngliche path-Liste, weil sie die Cache-Version bestimmt.
  • actions/cache#105, "Ability to download cache and inspect contents", hat 34 Reaktionen. Der Stale-Bot hat es 2023 geschlossen, nachdem ein Maintainer erklärt hatte, dass ein Download nicht geplant sei.
  • gh cache list, gh cache delete und actions/gh-actions-cache listen und löschen Einträge, können sie aber nicht herunterladen.

Wie es funktioniert

gh-cache-download besteht aus zwei Teilen. Der erste ist eine gh-Extension, eine einzelne Python-Datei ohne Abhängigkeiten. gh cache-download get KEY sucht den Key mit GET /repos/{owner}/{repo}/actions/caches (exakte Übereinstimmung), startet einen kleinen Helfer-Workflow auf dem Branch, dem der Cache gehört, wartet auf den Lauf und lädt das Ergebnis herunter. gh cache-download init schreibt diesen Helfer-Workflow, der committet und gepusht werden muss. Er läuft nur bei manuellem Start und hat nur Leserechte.

Der zweite Teil ist eine Composite Action, die der Helfer-Workflow nutzt. Innerhalb des Laufs ruft sie den Endpunkt GetCacheEntryDownloadURL des Cache-Dienstes mit Key und Version auf, lädt das Archiv und lädt es als Artefakt hoch. Weil sie über Key und Version abruft, muss sie nicht wissen, welche Pfade im Cache liegen.

actions/cache speichert Pfade relativ zum Workspace. Alles, was außerhalb davon liegt, etwa ~/.npm, erscheint im Archiv als ../../../.npm/.... gh cache-download extract (oder get -x) schreibt nie außerhalb des Zielverzeichnisses: Jedes .. wird zu einem Verzeichnis __parent__, absolute Pfade verlieren den führenden /, und Einträge, die hinausführen würden, etwa Symlinks auf absolute Pfade, werden übersprungen.

Das ist ein echter Lauf gegen einen Cache, den das CI des Repositorys aus e2e-data im Workspace und ~/.e2e-home außerhalb davon gespeichert hat:

$ gh cache list -R Jwrede/gh-cache-download
9507487879  e2e-40288214454-1  459 B  2026-10-26T07:50:08Z  2026-10-26T07:50:10Z

$ gh cache-download get e2e-40288214454-1 -R Jwrede/gh-cache-download -x
Cache 9507487879: refs/heads/main, 459 bytes, version b107c4986568
Run 40288300859: https://github.com/Jwrede/gh-cache-download/actions/runs/40288300859
Archive: cache-9507487879/cache.tzst (459 bytes)
Extracted 6 entries to cache-9507487879/files

$ find cache-9507487879/files -type f -o -type l
cache-9507487879/files/e2e-data/link
cache-9507487879/files/e2e-data/sub/a.txt
cache-9507487879/files/__parent__/__parent__/__parent__/.e2e-home/b.txt

Das gesamte get dauerte 14 Sekunden.

Tests

Die Python-Unit-Tests decken die Suche nach dem Key ab (nur exakte Übereinstimmung, Auflistung von Präfix-Treffern bei fehlendem Key, --ref nötig, wenn ein Key auf mehreren Branches existiert) und die Ablehnung von Pull-Request-Refs. Das Entpacken wird an tar-, gzip- und zstd-Archiven getestet, die einen Symlink, einen Hardlink, einen Pfad außerhalb des Workspaces und einen Symlink auf /etc/passwd enthalten, der übersprungen werden muss. Der Befehl get läuft gegen ein nachgebildetes gh, das die Aufrufe aufzeichnet, auch für einen fehlgeschlagenen Workflow-Start und einen fehlgeschlagenen Lauf. Node-Tests decken die Versionsauswahl der Action und die Erkennung des Archivtyps ab.

Das CI führt die Unit-Tests auf Python 3.10 und 3.14 aus. Ein End-to-End-Job legt Dateien innerhalb und außerhalb des Workspaces an, speichert sie mit actions/cache/save, lädt den Eintrag mit der Action herunter, entpackt ihn mit der CLI und vergleicht Inhalt und Archivgröße.

Grenzen

  • Ein Lauf kann nur Caches seiner eigenen Ref und des Default-Branches lesen. Der Helfer-Workflow muss deshalb auf dem Branch liegen, dem der Cache gehört.
  • Caches aus Pull-Request-Läufen (refs/pull/N/merge) lassen sich nicht über einen manuellen Start abrufen. Dafür muss die Action in den Workflow des Pull Requests aufgenommen werden.
  • GetCacheEntryDownloadURL ist die interne API, die actions/cache ab v4 nutzt. GitHub dokumentiert sie nicht als öffentliche API, sie kann sich also ändern. Getestet ist das Werkzeug nur auf github.com, mit Archiven von Linux-Runnern.

Code und Installationsanleitung liegen unter github.com/Jwrede/gh-cache-download.