diff --git a/.github/workflows/mirror-upstream.yml b/.github/workflows/mirror-upstream.yml new file mode 100644 index 0000000..d336208 --- /dev/null +++ b/.github/workflows/mirror-upstream.yml @@ -0,0 +1,88 @@ +name: Mirror an upstream file + +# Brings a file its upstream publishes outside GitHub into a release here, for a single-package +# catalog that names it: the catalog's "upstream" link is fetched, checked against the sha256 and +# size the catalog promises (tools/mirror/expectations.py), uploaded under the catalog's release, +# and its sha256 confirmed against what GitHub stored. A SHA256SUMS and a MIRROR.md naming this run +# go with it. Nothing is uploaded by hand. + +on: + workflow_dispatch: + inputs: + catalog: + description: 'Single-package catalog naming the release, the file and its upstream link' + required: true + default: 'fex-rootfs.json' + +permissions: + contents: write + +jobs: + mirror: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Mirror + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + CATALOG: ${{ inputs.catalog }} + run: | + set -euo pipefail + case "$CATALOG" in ""|*[!a-z0-9.-]*|.*) echo "::error::catalog must be a file name in this repository"; exit 1 ;; esac + [ -f "$CATALOG" ] || { echo "::error::$CATALOG not found"; exit 1; } + tag=$(jq -r '.release // empty' "$CATALOG") + url=$(jq -r '.url // empty' "$CATALOG") + upstream=$(jq -r '.upstream // empty' "$CATALOG") + title=$(jq -r '.name // empty' "$CATALOG") + [ -n "$tag" ] && [ -n "$upstream" ] || { echo "::error::$CATALOG names no release or no upstream link"; exit 1; } + case "$url" in + "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$tag/"*) ;; + *) echo "::error::$CATALOG's url is not on this repository's $tag release"; exit 1 ;; + esac + name=${url##*/} + work=$(mktemp -d) + python3 tools/mirror/expectations.py "$tag" "$CATALOG" > "$work/expect.json" + sha=$(jq -r --arg n "$name" '.[$n][0].sha256 // empty' "$work/expect.json") + size=$(jq -r --arg n "$name" '.[$n][0].size // empty' "$work/expect.json") + [ ${#sha} -eq 64 ] && [ -n "$size" ] || { echo "::error::$CATALOG gives $name no sha256 and size"; exit 1; } + + stored() { + gh api "repos/$GITHUB_REPOSITORY/releases/tags/$tag" --jq '.assets[] | "\(.name) \(.digest // "")"' 2>/dev/null | + awk -v n="$name" '$1==n{print $2}' + } + if [ "$(stored)" = "sha256:$sha" ]; then + echo "$name is already on $tag with the catalog's sha256; nothing to do" + exit 0 + fi + + curl -fL --retry 3 --retry-delay 15 --max-time 3000 -o "$work/$name" "$upstream" + got_sha=$(sha256sum "$work/$name" | cut -d' ' -f1) + got_size=$(stat -c %s "$work/$name") + if [ "$got_sha" != "$sha" ] || [ "$got_size" != "$size" ]; then + echo "::error::$upstream is $got_size bytes, sha256 $got_sha; $CATALOG promises $size bytes, sha256 $sha" + exit 1 + fi + + (cd "$work" && sha256sum "$name" > SHA256SUMS) + { + echo "# Mirror of $upstream" + echo + echo "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + echo "Verified against: $CATALOG" + echo + echo "| asset | size | catalog check | origin |" + echo "|---|---|---|---|" + echo "| $name | $got_size | sha256 and size match | fetched from the upstream: $(echo "$upstream" | cut -d/ -f3) |" + } > "$work/MIRROR.md" + if ! gh release view "$tag" >/dev/null 2>&1; then + gh release create "$tag" --title "${title:-$tag}" --latest=false \ + --notes "Mirrored by CI from $upstream: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID. Checked against $CATALOG (sha256 and size); see MIRROR.md." + fi + gh release upload "$tag" "$work/$name" "$work/SHA256SUMS" "$work/MIRROR.md" --clobber + [ "$(stored)" = "sha256:$sha" ] || { echo "::error::GitHub stored $name as $(stored), not sha256:$sha"; exit 1; } + echo "$name mirrored to $tag ($got_size bytes, sha256 $sha)" diff --git a/README.md b/README.md index f082b49..6136476 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,12 @@ LGPL-2.1-or-later. DroidDeck reads its package catalogs from this repository's `main`: `components.json` (Windows components), `desktop.json` (desktop and emulator packages), `linuxfs.json` (the Linux runtime) and `steam-seed.json` (the Steam seed). `fex.json` records the FEX build the optional x86-64 Steam client -downloads; `droiddeck-steam-x64` in the app pins the same sha256. Every file they point at lives on a +downloads and `fex-rootfs.json` the x86-64 system it runs in (FEX-Emu's Ubuntu 24.04 rootfs); +`droiddeck-steam-x64` in the app pins the same sha256s. Every file they point at lives on a release here, produced by a workflow in `.github/workflows/`: `mirror-release.yml` brings an existing release in with every asset checked against the catalog (and leaves `SHA256SUMS` + `MIRROR.md` naming the run), +`mirror-upstream.yml` does the same for a file its upstream publishes outside GitHub, `component-snapshots.yml` records what a Windows component's installer leaves behind, `build-desktop.yml`, `build-emulator-source.yml`, `mirror-appimages.yml` and `build-linuxfs.yml` build the rest. Nothing on a release is uploaded by hand. diff --git a/fex-rootfs.json b/fex-rootfs.json new file mode 100644 index 0000000..102469b --- /dev/null +++ b/fex-rootfs.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "category": "fex-rootfs", + "release": "fex-rootfs-ubuntu-24.04-2026-08-11", + "name": "x86-64 system for the x86-64 Steam client (Ubuntu 24.04)", + "version": "ubuntu-24.04-2026-08-11", + "url": "https://github.com/Droid-Deck/DroidDeck-Components/releases/download/fex-rootfs-ubuntu-24.04-2026-08-11/Ubuntu_24_04.sqsh", + "upstream": "https://rootfs.fex-emu.gg/Ubuntu_24_04/2026-08-11/Ubuntu_24_04.sqsh", + "sha256": "2854b06d3ff1b8f6e526135bfb6dd5b7b30ab3ab73e79ae933a3d9fed959a178", + "size": 525615104, + "file_size": 525615104, + "note": "FEX-Emu's Ubuntu 24.04 x86-64 rootfs (SquashFS, 2026-08-11), unmodified. droiddeck-steam-x64 downloads it once for the optional x86-64 Steam client, unpacks it as that client's own x86-64 system and pins this sha256. Fetched from rootfs.fex-emu.gg by mirror-upstream.yml." +}