From 7fabe1846e1db1a60821dbf73dd8adb52440609d Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Thu, 17 Sep 2026 17:31:16 +0200 Subject: [PATCH] Pin installer and updater downloads to the builds tip commit. raw.githubusercontent.com caches branch-named files, so the installer was serving a stale zip after CI rewrote builds. Co-authored-by: Cursor --- README.md | 35 ++++--- installers/maturita-installer-linux.sh | 27 ++++-- installers/maturita-installer-macos.command | 30 +++--- installers/maturita-installer-windows.cmd | 23 ++--- src/update.c | 100 ++++++++++++++++---- 5 files changed, 151 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index dbeec4a..668d53e 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,18 @@ zatím zamčené placeholdery. Aplikace se po instalaci aktualizuje sama. Žádné releasy ani čísla verzí nejsou. CI po každém pushi na `main` uloží balíčky do větve [`builds`](https://github.com/pepaskopec-blip/maturita.c/tree/builds). -Instalátor samotnou aplikaci neobsahuje — při spuštění si stáhne aktuální -build, takže soubor nikdy nezestárne. +Instalátor samotnou aplikaci neobsahuje — při spuštění si dohledá aktuální +commit větve `builds` a stáhne ten soubor, ne kešovanou kopii podle názvu +větve. -**Nejjednodušší je instalátor pro vaši platformu:** +**Nejjednodušší je instalátor pro vaši platformu** (soubory z `main`, ať +nedostanete starý skript z keše větve `builds`): | Platforma | Soubor | Jak spustit | | --------- | ------ | ----------- | -| macOS (Apple Silicon) | [maturita-installer-macos.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-macos.zip) | Rozbalit, pravý klik → **Otevřít** | -| Windows (x64) | [maturita-installer-windows.cmd](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-windows.cmd) | Dvojklik; u SmartScreenu **Další informace → Přesto spustit** | -| Linux (x86_64) | [maturita-installer-linux.sh](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` | +| macOS (Apple Silicon) | [maturita-installer-macos.command](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-macos.command) | Stáhnout, v Terminálu `chmod +x` a pak pravý klik → **Otevřít** | +| Windows (x64) | [maturita-installer-windows.cmd](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-windows.cmd) | Dvojklik; u SmartScreenu **Další informace → Přesto spustit** | +| Linux (x86_64) | [maturita-installer-linux.sh](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` | Kam se to nainstaluje: `/Applications` nebo `~/Applications` (macOS), `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). @@ -41,7 +43,9 @@ v **Nastavení → Aktualizace**. #### Bez instalátoru -Na stejné větvi jsou i hotové balíčky: +Na větvi [`builds`](https://github.com/pepaskopec-blip/maturita.c/tree/builds) +jsou i hotové balíčky. Odkazy podle názvu větve umí CDN kešovat, proto je +spolehlivější instalátor výše, nebo stažení přímo z té stránky větve: - [maturita-macos-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-arm64.zip) - [maturita-windows-x64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-windows-x64.zip) @@ -152,15 +156,17 @@ placeholders. After install the app updates itself. There are no releases and no version numbers. CI writes packages to the [`builds`](https://github.com/pepaskopec-blip/maturita.c/tree/builds) branch whenever `main` changes. The installer carries no app of its own — it fetches -the current build when you run it, so the file never goes stale. +the current `builds` commit when you run it, not a CDN-cached copy of the +branch-named file. -**Start with the installer for your platform:** +**Start with the installer for your platform** (files from `main`, so you +do not get a stale script cached from the `builds` branch): | Platform | File | How to run | | -------- | ---- | ---------- | -| macOS (Apple Silicon) | [maturita-installer-macos.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-macos.zip) | Unzip, then right-click → **Open** | -| Windows (x64) | [maturita-installer-windows.cmd](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-windows.cmd) | Double-click; if SmartScreen appears, **More info → Run anyway** | -| Linux (x86_64) | [maturita-installer-linux.sh](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` | +| macOS (Apple Silicon) | [maturita-installer-macos.command](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-macos.command) | Download, `chmod +x` in Terminal, then right-click → **Open** | +| Windows (x64) | [maturita-installer-windows.cmd](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-windows.cmd) | Double-click; if SmartScreen appears, **More info → Run anyway** | +| Linux (x86_64) | [maturita-installer-linux.sh](https://github.com/pepaskopec-blip/maturita.c/raw/main/installers/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` | Install locations: `/Applications` or `~/Applications` (macOS), `%LOCALAPPDATA%\Programs\Maturita` (Windows), `~/Applications` (Linux). @@ -172,6 +178,11 @@ banner. Manual check: **Settings → Updates**. #### Packages without the installer +The same files live on +[`builds`](https://github.com/pepaskopec-blip/maturita.c/tree/builds). +Branch-named raw URLs can be cached by the CDN, so prefer the installer +above, or download from that branch page: + - [maturita-macos-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-arm64.zip) - [maturita-windows-x64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-windows-x64.zip) - [maturita-linux-x86_64.AppImage](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-linux-x86_64.AppImage) diff --git a/installers/maturita-installer-linux.sh b/installers/maturita-installer-linux.sh index 5b8ed63..57254a5 100755 --- a/installers/maturita-installer-linux.sh +++ b/installers/maturita-installer-linux.sh @@ -1,9 +1,9 @@ #!/bin/sh # maturita.c installer for Linux. # -# Carries no application of its own: it downloads the current build straight -# from the repository's `builds` branch, which CI rewrites whenever main -# changes. This file therefore never goes stale. +# Carries no application of its own. Fastly caches branch-named URLs on +# raw.githubusercontent.com, so this script looks up the tip of `builds` +# and downloads that commit — not a stale AppImage from the last five minutes. # # Run it with: sh maturita-installer-linux.sh # The app updates itself from then on. @@ -13,10 +13,7 @@ set -eu REPO="pepaskopec-blip/maturita.c" BRANCH="builds" ASSET="maturita-linux-x86_64.AppImage" -URL="https://raw.githubusercontent.com/$REPO/$BRANCH/$ASSET" -# ~/Applications is where desktop environments look for AppImages, and the app -# keeps its progress/ directory next to its own file. DEST_DIR="$HOME/Applications" DEST="$DEST_DIR/maturita.AppImage" DESKTOP_DIR="$HOME/.local/share/applications" @@ -25,6 +22,13 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/512x512/apps" say() { printf '%s\n' "$*"; } die() { printf '\nError: %s\n' "$*" >&2; exit 1; } +builds_sha() { + curl -fsSL --max-time 20 \ + "https://github.com/$REPO/commits/$BRANCH.atom" | + sed -n 's/.*Commit\/\([0-9a-f]\{40\}\).*/\1/p' | + head -n 1 +} + say "Installing maturita.C" say "---------------------" @@ -41,16 +45,19 @@ mkdir -p "$DEST_DIR" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT INT TERM -say "Downloading the latest build..." -curl -fL --progress-bar -o "$tmp/$ASSET" "$URL" || +say "Looking up the latest build..." +sha=$(builds_sha) +[ ${#sha} -eq 40 ] || die "could not find the latest build on GitHub." +URL="https://raw.githubusercontent.com/$REPO/$sha/$ASSET" + +say "Downloading build $(printf '%.7s' "$sha")..." +curl -fL --progress-bar --max-time 1800 -o "$tmp/$ASSET" "$URL" || die "the download failed. Check your internet connection." say "Installing to $DEST" chmod +x "$tmp/$ASSET" mv -f "$tmp/$ASSET" "$DEST" || die "could not write to $DEST_DIR." -# A menu entry needs the icon as a separate file, which the AppImage can -# unpack without FUSE. Losing the icon is not worth failing the install over. if (cd "$tmp" && "$DEST" --appimage-extract \ 'usr/share/icons/hicolor/512x512/apps/maturita.png' >/dev/null 2>&1); then icon="$tmp/squashfs-root/usr/share/icons/hicolor/512x512/apps/maturita.png" diff --git a/installers/maturita-installer-macos.command b/installers/maturita-installer-macos.command index 1f64c98..1489b97 100755 --- a/installers/maturita-installer-macos.command +++ b/installers/maturita-installer-macos.command @@ -1,9 +1,9 @@ #!/bin/sh # maturita.c installer for macOS. # -# Carries no application of its own: it downloads the current build straight -# from the repository's `builds` branch, which CI rewrites whenever main -# changes. This file therefore never goes stale. +# Carries no application of its own. Fastly caches branch-named URLs on +# raw.githubusercontent.com, so this script looks up the tip of `builds` +# and downloads that commit — not a stale zip from the last five minutes. # # Double-click the file to run it. The app updates itself from then on. @@ -12,15 +12,21 @@ set -eu REPO="pepaskopec-blip/maturita.c" BRANCH="builds" ASSET="maturita-macos-arm64.zip" -URL="https://raw.githubusercontent.com/$REPO/$BRANCH/$ASSET" say() { printf '%s\n' "$*"; } die() { printf '\nError: %s\n' "$*" >&2; printf 'Press Return to close. '; read -r _; exit 1; } +# Atom feed of the builds branch: curl only, no GitHub API token. +builds_sha() { + curl -fsSL --max-time 20 \ + "https://github.com/$REPO/commits/$BRANCH.atom" | + sed -n 's/.*Commit\/\([0-9a-f]\{40\}\).*/\1/p' | + head -n 1 +} + say "Installing maturita.C" say "---------------------" -# Only Apple Silicon builds are published; Rosetta cannot run an arm64 app. arch=$(uname -m) if [ "$arch" != "arm64" ]; then die "this build is for Apple Silicon Macs, but this one reports '$arch'. @@ -29,7 +35,6 @@ fi command -v curl >/dev/null 2>&1 || die "curl was not found." -# Prefer the shared folder, fall back to the user's own when it is locked down. if [ -w /Applications ]; then dest_dir="/Applications" else @@ -40,13 +45,17 @@ fi tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT INT TERM -say "Downloading the latest build..." -curl -fL --progress-bar -o "$tmp/$ASSET" "$URL" || +say "Looking up the latest build..." +sha=$(builds_sha) +[ ${#sha} -eq 40 ] || die "could not find the latest build on GitHub." +URL="https://raw.githubusercontent.com/$REPO/$sha/$ASSET" + +say "Downloading build $(printf '%.7s' "$sha")..." +curl -fL --progress-bar --max-time 1800 -o "$tmp/$ASSET" "$URL" || die "the download failed. Check your internet connection." say "Unpacking..." mkdir -p "$tmp/new" -# ditto keeps bundle symlinks and permissions intact, unlike plain unzip. ditto -x -k "$tmp/$ASSET" "$tmp/new" || die "the archive could not be unpacked." app=$(find "$tmp/new" -maxdepth 1 -name '*.app' -print -quit) @@ -61,13 +70,10 @@ fi if mv "$app" "$dest_dir/$name"; then rm -rf "$dest_dir/$name.old" else - # Put the previous copy back rather than leaving nothing installed. [ -e "$dest_dir/$name.old" ] && mv "$dest_dir/$name.old" "$dest_dir/$name" die "could not write to $dest_dir." fi -# The download carries a quarantine flag, and the app is not signed, so -# without this macOS refuses to open it. xattr -cr "$dest_dir/$name" 2>/dev/null || true say "" diff --git a/installers/maturita-installer-windows.cmd b/installers/maturita-installer-windows.cmd index b1125e5..0dbf2cb 100644 --- a/installers/maturita-installer-windows.cmd +++ b/installers/maturita-installer-windows.cmd @@ -1,23 +1,21 @@ @echo off rem maturita.c installer for Windows. rem -rem Carries no application of its own: it downloads the current build straight -rem from the repository's `builds` branch, which CI rewrites whenever main -rem changes. This file therefore never goes stale. +rem Carries no application of its own. Fastly caches branch-named URLs on +rem raw.githubusercontent.com, so this script looks up the tip of `builds` +rem and downloads that commit — not a stale zip from the last five minutes. rem rem Double-click the file to run it. The app updates itself from then on. -setlocal +setlocal EnableDelayedExpansion set "REPO=pepaskopec-blip/maturita.c" set "BRANCH=builds" set "ASSET=maturita-windows-x64.zip" -set "URL=https://raw.githubusercontent.com/%REPO%/%BRANCH%/%ASSET%" set "DEST=%LOCALAPPDATA%\Programs\Maturita" echo Installing maturita.C echo --------------------- -rem curl and tar both ship with Windows 10 1803 and later. where curl >nul 2>&1 || goto :no_tools where tar >nul 2>&1 || goto :no_tools @@ -25,17 +23,20 @@ set "TMP_DIR=%TEMP%\maturita-installer" rmdir /s /q "%TMP_DIR%" 2>nul mkdir "%TMP_DIR%" || goto :fail_tmp -echo Downloading the latest build... -curl -fL --progress-bar -o "%TMP_DIR%\%ASSET%" "%URL%" || goto :fail_download +echo Looking up the latest build... +set "SHA=" +for /f "usebackq delims=" %%S in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$r = & curl.exe -fsSL --max-time 20 'https://github.com/%REPO%/commits/%BRANCH%.atom'; if ($r -match 'Commit/([0-9a-f]{40})') { $Matches[1] }"`) do set "SHA=%%S" +if not defined SHA goto :fail_download +set "URL=https://raw.githubusercontent.com/%REPO%/%SHA%/%ASSET%" + +echo Downloading build %SHA:~0,7%... +curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%URL%" || goto :fail_download echo Installing to %DEST% if not exist "%DEST%" mkdir "%DEST%" || goto :fail_dest -rem Extracted over any existing install rather than replacing the folder, so -rem the progress files kept inside it survive an upgrade. tar -xf "%TMP_DIR%\%ASSET%" -C "%DEST%" || goto :fail_extract -rem Start menu entry, so the app can be found without digging for the folder. set "LNK=%APPDATA%\Microsoft\Windows\Start Menu\Programs\maturita.C.lnk" powershell -NoProfile -ExecutionPolicy Bypass -Command ^ "$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LNK%');" ^ diff --git a/src/update.c b/src/update.c index c91c61a..109abc9 100644 --- a/src/update.c +++ b/src/update.c @@ -1,5 +1,7 @@ #include "maturita.h" +#include + #ifdef _WIN32 #include #define update_getpid() ((long)_getpid()) @@ -23,9 +25,14 @@ * the repository, which CI rewrites on every change to main, so an update is * simply whatever that branch currently holds. The branch also carries a * VERSION file naming the commit its packages were built from; comparing that - * against the commit baked into this binary is the whole check. Serving it - * from raw.githubusercontent.com avoids the REST API and its unauthenticated - * rate limit, and needs no JSON parser. + * against the commit baked into this binary is the whole check. + * + * raw.githubusercontent.com is cached by Fastly by branch name, so a URL that + * says `/builds/VERSION` can keep serving yesterday's file after CI rewrote + * the branch. The check therefore reads the branch tip from the Atom feed + * (plain curl, no API token) and then fetches VERSION and the package from + * that commit SHA. Serving files from raw.githubusercontent.com still avoids + * the REST API and its unauthenticated rate limit, and needs no JSON parser. * * An application cannot reliably replace its own files while it is running, so * all three platforms follow the same shape: unpack the new version into a @@ -34,9 +41,8 @@ * script detached and quit. Staging beside the install keeps the final move on * one filesystem and proves up front that the location is writable. */ -#define UPDATE_BASE_URL "https://raw.githubusercontent.com/" UPDATE_REPO \ - "/" UPDATE_BRANCH "/" -#define UPDATE_VERSION_URL UPDATE_BASE_URL "VERSION" +#define UPDATE_ATOM_URL "https://github.com/" UPDATE_REPO \ + "/commits/" UPDATE_BRANCH ".atom" #define UPDATE_STAGING_DIR ".maturita-update" #ifdef _WIN32 @@ -68,6 +74,7 @@ typedef enum { static struct { UpdateState state; gboolean interactive; /* user asked, so also report "up to date" */ + char *tip; /* current commit of the builds branch itself */ char *latest; /* commit the published packages were built from */ char *staging; /* scratch directory beside the install */ char *archive; /* asset downloaded into the staging directory */ @@ -114,6 +121,35 @@ static char *parse_commit(const char *text) { return NULL; } +/* First Grit::Commit/<40-hex> in a GitHub Atom feed is the branch tip. */ +static char *parse_atom_commit(const char *text) { + const char *p; + + if (!text) + return NULL; + p = strstr(text, "Commit/"); + while (p) { + const char *s = p + 7; + size_t n = 0; + + while (g_ascii_isxdigit(s[n])) + n++; + if (n == 40) + return g_strndup(s, 40); + p = strstr(s, "Commit/"); + } + return NULL; +} + +/* Pin raw.githubusercontent.com to a commit so Fastly cannot serve a stale + * branch-named file. Falls back to the branch name if the tip is unknown. */ +static char *builds_raw_url(const char *name) { + const char *ref = (up.tip && up.tip[0]) ? up.tip : UPDATE_BRANCH; + + return g_strdup_printf("https://raw.githubusercontent.com/%s/%s/%s", + UPDATE_REPO, ref, name); +} + static gboolean same_commit(const char *a, const char *b) { char *ca = parse_commit(a); char *cb = parse_commit(b); @@ -614,7 +650,7 @@ static void size_probe_done(GObject *src, GAsyncResult *res, gpointer data) { if (up.state != UPDATE_DOWNLOADING) return; - url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL); + url = builds_raw_url(asset_name()); argv[0] = "curl"; argv[1] = "-fsSL"; argv[2] = "--max-time"; @@ -663,7 +699,7 @@ static void update_start_download(void) { update_set_state(UPDATE_DOWNLOADING); /* HEAD the asset with its headers on stdout, to size the progress bar. */ - url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL); + url = builds_raw_url(asset_name()); argv[0] = "curl"; argv[1] = "-fsSLI"; argv[2] = "--max-time"; @@ -729,9 +765,41 @@ static void check_done(GObject *src, GAsyncResult *res, gpointer data) { update_set_state(UPDATE_IDLE); } -void update_check_async(gboolean interactive) { - const char *argv[8]; +static void tip_done(GObject *src, GAsyncResult *res, gpointer data) { + GSubprocess *proc = G_SUBPROCESS(src); + char *out = NULL; + gboolean ok; char *url; + const char *argv[6]; + + (void)data; + g_subprocess_communicate_utf8_finish(proc, res, &out, NULL, NULL); + ok = g_subprocess_get_successful(proc); + g_object_unref(proc); + + g_free(up.tip); + up.tip = (ok && out) ? parse_atom_commit(out) : NULL; + g_free(out); + if (!up.tip) { + check_giveup(); + return; + } + + url = builds_raw_url("VERSION"); + argv[0] = "curl"; + argv[1] = "-fsSL"; + argv[2] = "--max-time"; + argv[3] = "20"; + argv[4] = url; + argv[5] = NULL; + + if (!curl_async(argv, check_done)) + check_giveup(); + g_free(url); +} + +void update_check_async(gboolean interactive) { + const char *argv[6]; if (up.state == UPDATE_CHECKING || up.state == UPDATE_DOWNLOADING || up.state == UPDATE_STAGED) @@ -748,25 +816,19 @@ void update_check_async(gboolean interactive) { update_set_state(UPDATE_CHECKING); - /* raw.githubusercontent.com caches VERSION; a unique query bypasses it. */ - url = g_strdup_printf("%s?t=%ld", UPDATE_VERSION_URL, - (long)g_get_real_time()); argv[0] = "curl"; argv[1] = "-fsSL"; argv[2] = "--max-time"; argv[3] = "20"; - argv[4] = "-H"; - argv[5] = "Cache-Control: no-cache"; - argv[6] = url; - argv[7] = NULL; + argv[4] = UPDATE_ATOM_URL; + argv[5] = NULL; - if (!curl_async(argv, check_done)) { + if (!curl_async(argv, tip_done)) { if (interactive) update_fail(tr("update_err_curl")); else update_set_state(UPDATE_IDLE); } - g_free(url); } /* ------------------------------------------------------------------ */