Container-Images
Zwei Abbilder je Release, linux/amd64 und linux/arm64. Das erste Release mit Abbildern ist 0.35.0; die Binaries auf der Release-Seite reichen weiter zurück.
ghcr.io/ohartwig/pinup: pinup,git,gpgsamt Agent,ssh-keygen, ein CA-Bundleghcr.io/ohartwig/pinup-toolchain: dasselbe, dazucomposer,php,node,npm,yarnundgo
docker run --rm -v "$PWD:/workspace" ghcr.io/ohartwig/pinup:0 \
pinup whatif --repo . --config .pinup.yaml --report plan.jsonWelches von beiden
Das schlanke. Einen Paketmanager braucht pinup nur, wenn ein Update eine Lock-Datei neu schreiben muss — eine composer.lock, eine package-lock.json, eine go.sum — oder wenn die Konfiguration einen postUpgradeTask ausführt. Alles andere ist das Binary und git.
Das Toolchain-Abbild ist rund fünfmal so groß, und jeder Interpreter darin kann eine CVE in die Pipeline tragen. Wo ein Update einen braucht, sagt pinup es statt zu raten: der Branch wird mit dem Grund pluginRequired gehalten und steht so im Plan.
Das schlanke Abbild hält sich selbst schlank. Der Bau scheitert, wenn node oder php im Pfad auftaucht. Das ist keine Zierde, sondern die Invariante, für die es dieses Abbild gibt.
Tags
0.35.0— bewegt sich nie. Für reproduzierbare Pipelines und alles, was geprüft wird.0.35— wandert mit jedem Patch.0— wandert mit jedem Minor. pinup steht vor 1.0, diese Linie trägt auch Verhaltensänderungen.latest— wandert mit jedem Release.
Nur ein Digest (ghcr.io/ohartwig/pinup@sha256:…) kann sich nicht unter dir ändern. In einer Pipeline also Tag und Digest pinnen und beides von einem Bot bewegen lassen — wofür es pinup schließlich gibt.
Woher das Binary kommt
Das Abbild kompiliert pinup nicht. Der Workflow lädt die Binaries des Releases, prüft sie gegen dessen SHA256SUMS und kopiert eines hinein. Gebaut und signiert werden sie einmal, davor, und dieselben Dateien liegen auf der Release-Seite.
Diese Trennung ist Absicht. Ein früherer Workflow hat die Binaries beim Eintreffen des Tags neu gebaut: ein zweites Artefakt unter dem Namen des Releases, mit niemandes Signatur darauf. Ein signiertes Binary zu verpacken ist etwas anderes, als eines zu bauen, und nur das Erste lässt sich gegen das Veröffentlichte prüfen.
Die Rezepte liegen im Repository unter packaging/. Die Basis ist Chainguards wolfi-base, per Digest gepinnt. Das Toolchain-Abbild pinnt jede Sprachlaufzeit auf eine exakte Wolfi-Paketversion, damit ein Bump ein geprüfter Diff ist und nicht das, was der Spiegel an dem Morgen ausgeliefert hat.
Prüfen, was ankommt
Beide Abbilder sind keyless signiert. Die Signatur entsteht gegen die OIDC-Identität des Workflows und lässt sich ohne einen Schlüssel von uns prüfen.
cosign verify ghcr.io/ohartwig/pinup:0.35.0 \
--certificate-identity-regexp '^https://github.com/ohartwig/pinup/' \
--certificate-oidc-issuer token.actions.githubusercontent.com
Dazu trägt jedes Abbild eine SBOM und die Herkunft seines Baus, als Attestation im Index; docker buildx imagetools inspect holt beides. Der Scan läuft vor der Signatur: ein behebbarer CRITICAL- oder HIGH-Fund lässt den Bau scheitern, und ein Abbild, das nicht besteht, wird nie signiert. Eine fehlende Signatur ist damit eine Aussage und kein Versehen.
Betrieb
Das Abbild läuft als uid 1000 in /workspace. Es hat keinen Entrypoint, pinup gehört also zum Kommando: docker run … pinup whatif …, nicht docker run … whatif …. Das ist Absicht. Ein CI-Runner startet das Job-Skript mit sh im Container, und ein Entrypoint pinup würde darauf mit „unknown command sh“ antworten.
docker run --rm \
-v "$PWD:/workspace" \
-e PINUP_GITLAB_URL -e PINUP_GITLAB_TOKEN \
ghcr.io/ohartwig/pinup:0 \
pinup run --project gruppe/projekt --config 'local>gruppe/runner' --report plan.json- Zugangsdaten kommen aus der Umgebung, eine je Plattform. Ein Lauf, der nur plant, braucht keine.
- git-Eigentum:
safe.directoryist im Abbild systemweit gesetzt. Ein gemounteter Checkout mit fremder uid hält git also nicht auf. - Signieren:
PINUP_SIGNING_FORMAT=openpgpbraucht den Schlüssel von außen. Das Abbild bringtgpgmit und keinen Schlüssel. - Der Cache (
PINUP_CACHE) gehört auf ein Volume, das den Lauf überlebt: ohne ihn istminimumReleaseAgefür Registries ohne Zeitstempel unzuverlässig.
Erreichen die Runner ghcr.io nicht, spiegle das Abbild in die eigene Registry und pinne den gespiegelten Digest. Ein kopiertes Abbild bleibt prüfbar, solange der Digest mitreist.