From 734de1c58d36ef7a89ace0b120324d96420c6a84 Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 14:51:34 +0200 Subject: [PATCH 1/7] feat: download the wiki stylesheet once per snapshot run The snapshot template linked liquipedia.net's load.php bundle directly, so every one of the 17 GoldenTest cases spawned its own browser and downloaded the same pinned bundle again. That is 17 downloads per run and 17 chances for a network hiccup to fail the job. Fetch it once up front instead, into lua/output/css/lakeside.css next to the compiled main.css, and point the template at that local copy. Every run still fetches a fresh copy, so there is no cache to invalidate. Every non-data URL in the bundle is root-relative (the FontAwesome webfonts, the Liquipedia logos, the magnify-clip icons). Served from disk those would resolve against the local filesystem, so the script rewrites them back to liquipedia.net. Without that the fonts fail, which snapshot.mjs catches, and the SVG logos silently do not render, which it does not. The output is named after the theme, since lakesideview is only the current production skin, while the script itself stays generic. GoldenTest is a no-op unless UPDATE_SNAPSHOTS is set, so the fetch hangs off update-snapshots rather than lua-test, keeping a plain busted run offline. That means the workflow has to call update-snapshots instead of setting UPDATE_SNAPSHOTS around lua-test, or CI would stop fetching and every snapshot would fail on a missing stylesheet. Co-Authored-By: Claude Opus 5 --- .github/workflows/update visual snapshots.yml | 4 +-- fetch-remote-css.mjs | 34 +++++++++++++++++++ lua/spec/helpers/template.html | 4 +-- package.json | 3 +- 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 fetch-remote-css.mjs diff --git a/.github/workflows/update visual snapshots.yml b/.github/workflows/update visual snapshots.yml index a16d5934bf2..b7ac9e205a9 100644 --- a/.github/workflows/update visual snapshots.yml +++ b/.github/workflows/update visual snapshots.yml @@ -48,9 +48,7 @@ jobs: luarocks install --lua-version=5.1 busted - name: Update snapshots - run: npm run lua-test - env: - UPDATE_SNAPSHOTS: true + run: npm run update-snapshots - name: Commit and push if snapshots changed uses: stefanzweifel/git-auto-commit-action@v7 diff --git a/fetch-remote-css.mjs b/fetch-remote-css.mjs new file mode 100644 index 00000000000..616b57949ae --- /dev/null +++ b/fetch-remote-css.mjs @@ -0,0 +1,34 @@ +import { mkdirSync, writeFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// The styles liquipedia.net loads for the lakesideview skin. The snapshot +// template renders against these, so we fetch them once per run instead of +// once per snapshot. +// TODO: We might in the future make the cacheversion dynamic +const REMOTE_CSS_URL = 'https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&modules=fontawesome-pro|ext.TeamLiquidIntegration.liquipedia-custom-icon|skins.lakesideview.mw|skins.lakesideview.theme.commons&only=styles&skin=lakesideview'; +const REMOTE_CSS_ORIGIN = 'https://liquipedia.net'; +const OUTPUT_PATH = resolve(__dirname, 'lua', 'output', 'css', 'lakeside.css'); + +let response; +try { + response = await fetch(REMOTE_CSS_URL); +} catch (error) { + console.error(`Error: could not reach ${REMOTE_CSS_ORIGIN} (${error.message})`); + process.exit(1); +} + +if (!response.ok) { + console.error(`Error: could not fetch the remote stylesheet (HTTP ${response.status})`); + process.exit(1); +} + +// Fonts, logos and icons are referenced from the site root. Served from disk +// those would resolve against the local filesystem, so point them back at the +// wiki. +const css = (await response.text()).replace(/url\((["']?)\/(?!\/)/g, `url($1${REMOTE_CSS_ORIGIN}/`); + +mkdirSync(dirname(OUTPUT_PATH), { recursive: true }); +writeFileSync(OUTPUT_PATH, css); diff --git a/lua/spec/helpers/template.html b/lua/spec/helpers/template.html index f6cf89ebb6e..d39b00f2df0 100644 --- a/lua/spec/helpers/template.html +++ b/lua/spec/helpers/template.html @@ -4,8 +4,8 @@ Liquipedia - - + + diff --git a/package.json b/package.json index 46262a788dd..da5c8596af7 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,11 @@ "lint:scss:ci": "stylelint \"stylesheets/**/*.scss\" --custom-formatter @csstools/stylelint-formatter-github", "test:js": "jest", "compile:scss": "sass stylesheets/Main.scss lua/output/css/main.css", + "fetch:remote-css": "node fetch-remote-css.mjs", "build:css": "npm run compile:scss", "build:js": "node build-js.js", "build": "npm run build:css && npm run build:js", "lua-test": "npm run build:css && busted -C lua", - "update-snapshots": "UPDATE_SNAPSHOTS=true npm run lua-test" + "update-snapshots": "npm run fetch:remote-css && UPDATE_SNAPSHOTS=true npm run lua-test" } } From 76e06fb0ad7769549d3cda275af29783cc060c7c Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 15:01:23 +0200 Subject: [PATCH 2/7] fix: time out the stylesheet download instead of stalling CI undici gives up after five minutes of its own accord, which is a long time for a CI job to sit on a stalled connection. Abort after thirty seconds and say so. AbortSignal.timeout starts counting at creation, so it can fire while the 519KB body is still streaming, after fetch() has already resolved. Reading the body therefore has to sit under the same guard as the request, or the rejection escapes uncaught. Both now live in one function with one message. Format the thrown value defensively while we are here. fetch() rejects with a TypeError or DOMException, both of which carry a message, so this is belt-and-braces rather than a fix. Co-Authored-By: Claude Opus 5 --- fetch-remote-css.mjs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/fetch-remote-css.mjs b/fetch-remote-css.mjs index 616b57949ae..0755be8a5b5 100644 --- a/fetch-remote-css.mjs +++ b/fetch-remote-css.mjs @@ -11,24 +11,38 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const REMOTE_CSS_URL = 'https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&modules=fontawesome-pro|ext.TeamLiquidIntegration.liquipedia-custom-icon|skins.lakesideview.mw|skins.lakesideview.theme.commons&only=styles&skin=lakesideview'; const REMOTE_CSS_ORIGIN = 'https://liquipedia.net'; const OUTPUT_PATH = resolve(__dirname, 'lua', 'output', 'css', 'lakeside.css'); +// Rather than let a stalled connection sit on a CI runner until undici's own +// five minute timeout gives up +const TIMEOUT_MS = 30_000; -let response; +const download = async () => { + // The timeout covers reading the body too, so both live under one guard + const response = await fetch(REMOTE_CSS_URL, { signal: AbortSignal.timeout(TIMEOUT_MS) }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.text(); +}; + +const describe = (error) => { + if (error?.name === 'TimeoutError') { + return `no response within ${TIMEOUT_MS / 1000}s`; + } + return error?.message ?? String(error); +}; + +let body; try { - response = await fetch(REMOTE_CSS_URL); + body = await download(); } catch (error) { - console.error(`Error: could not reach ${REMOTE_CSS_ORIGIN} (${error.message})`); - process.exit(1); -} - -if (!response.ok) { - console.error(`Error: could not fetch the remote stylesheet (HTTP ${response.status})`); + console.error(`Error: could not download the remote stylesheet (${describe(error)})`); process.exit(1); } // Fonts, logos and icons are referenced from the site root. Served from disk // those would resolve against the local filesystem, so point them back at the // wiki. -const css = (await response.text()).replace(/url\((["']?)\/(?!\/)/g, `url($1${REMOTE_CSS_ORIGIN}/`); +const css = body.replace(/url\((["']?)\/(?!\/)/g, `url($1${REMOTE_CSS_ORIGIN}/`); mkdirSync(dirname(OUTPUT_PATH), { recursive: true }); writeFileSync(OUTPUT_PATH, css); From 31acfb737e4a2823d63b88371f312877f0c1e35e Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 15:07:50 +0200 Subject: [PATCH 3/7] refactor: fetch the stylesheet with curl instead of node A download does not need a node script, and curl's --max-time covers the whole transfer rather than starting a clock at request time, so the abort that could fire mid-body while the 519KB response was still streaming is no longer something to work around. Downloads to a temporary file rather than piping into sed. sh has no pipefail, so a pipe would exit 0 with a truncated stylesheet on a failed transfer, and an empty stylesheet still loads, meaning snapshot.mjs would screenshot an unstyled page rather than refuse it. Errors now leave the previous copy in place and stop the chain before busted runs. The sed handles the quoted form too. The bundle uses both url(/x) and url("/x"), and rewriting only the bare form leaves four broken references. Kept as a file rather than a package.json one-liner. Inline, the same command is 512 characters of nested quote escaping, and the reason the download is not a pipe stops being sayable. Co-Authored-By: Claude Opus 5 --- fetch-remote-css.mjs | 48 -------------------------------------------- fetch-remote-css.sh | 24 ++++++++++++++++++++++ package.json | 2 +- 3 files changed, 25 insertions(+), 49 deletions(-) delete mode 100644 fetch-remote-css.mjs create mode 100755 fetch-remote-css.sh diff --git a/fetch-remote-css.mjs b/fetch-remote-css.mjs deleted file mode 100644 index 0755be8a5b5..00000000000 --- a/fetch-remote-css.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import { mkdirSync, writeFileSync } from 'fs'; -import { resolve, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -// The styles liquipedia.net loads for the lakesideview skin. The snapshot -// template renders against these, so we fetch them once per run instead of -// once per snapshot. -// TODO: We might in the future make the cacheversion dynamic -const REMOTE_CSS_URL = 'https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&modules=fontawesome-pro|ext.TeamLiquidIntegration.liquipedia-custom-icon|skins.lakesideview.mw|skins.lakesideview.theme.commons&only=styles&skin=lakesideview'; -const REMOTE_CSS_ORIGIN = 'https://liquipedia.net'; -const OUTPUT_PATH = resolve(__dirname, 'lua', 'output', 'css', 'lakeside.css'); -// Rather than let a stalled connection sit on a CI runner until undici's own -// five minute timeout gives up -const TIMEOUT_MS = 30_000; - -const download = async () => { - // The timeout covers reading the body too, so both live under one guard - const response = await fetch(REMOTE_CSS_URL, { signal: AbortSignal.timeout(TIMEOUT_MS) }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return response.text(); -}; - -const describe = (error) => { - if (error?.name === 'TimeoutError') { - return `no response within ${TIMEOUT_MS / 1000}s`; - } - return error?.message ?? String(error); -}; - -let body; -try { - body = await download(); -} catch (error) { - console.error(`Error: could not download the remote stylesheet (${describe(error)})`); - process.exit(1); -} - -// Fonts, logos and icons are referenced from the site root. Served from disk -// those would resolve against the local filesystem, so point them back at the -// wiki. -const css = body.replace(/url\((["']?)\/(?!\/)/g, `url($1${REMOTE_CSS_ORIGIN}/`); - -mkdirSync(dirname(OUTPUT_PATH), { recursive: true }); -writeFileSync(OUTPUT_PATH, css); diff --git a/fetch-remote-css.sh b/fetch-remote-css.sh new file mode 100755 index 00000000000..0697fc25216 --- /dev/null +++ b/fetch-remote-css.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# The styles liquipedia.net loads for the lakesideview skin. The snapshot +# template renders against these, so we fetch them once per run instead of +# once per snapshot. +# TODO: We might in the future make the cacheversion dynamic +URL='https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&modules=fontawesome-pro|ext.TeamLiquidIntegration.liquipedia-custom-icon|skins.lakesideview.mw|skins.lakesideview.theme.commons&only=styles&skin=lakesideview' +ORIGIN='https://liquipedia.net' +OUTPUT='lua/output/css/lakeside.css' + +cd "$(dirname "$0")" +mkdir -p "$(dirname "$OUTPUT")" + +# Download to a temporary file rather than piping into sed. A failed transfer +# would otherwise leave a truncated stylesheet behind, and an empty one still +# loads, so we would screenshot an unstyled page rather than fail. +curl -fsS --max-time 30 -o "$OUTPUT.tmp" "$URL" + +# Fonts, logos and icons are referenced from the site root. Served from disk +# those would resolve against the local filesystem, so point them back at the +# wiki. Mind the quotes, the bundle uses both url(/x) and url("/x"). +sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' "$OUTPUT.tmp" > "$OUTPUT" +rm "$OUTPUT.tmp" diff --git a/package.json b/package.json index da5c8596af7..aa4c6ef5e56 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "lint:scss:ci": "stylelint \"stylesheets/**/*.scss\" --custom-formatter @csstools/stylelint-formatter-github", "test:js": "jest", "compile:scss": "sass stylesheets/Main.scss lua/output/css/main.css", - "fetch:remote-css": "node fetch-remote-css.mjs", + "fetch:remote-css": "bash fetch-remote-css.sh", "build:css": "npm run compile:scss", "build:js": "node build-js.js", "build": "npm run build:css && npm run build:js", From 8e744e3ad4e474c9beab95cd15866f333c12729d Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 15:10:37 +0200 Subject: [PATCH 4/7] docs: trim the fetch script comments Three sentences where one does. Also fixes the template still pointing at fetch-remote-css.mjs, and the temp file comment, which claimed a pipe would pass unnoticed. That was true of the same command inlined in package.json, run by sh, but this script sets pipefail. What the temp file still buys is the previous copy surviving a failed transfer. Co-Authored-By: Claude Opus 5 --- fetch-remote-css.sh | 15 ++++++--------- lua/spec/helpers/template.html | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/fetch-remote-css.sh b/fetch-remote-css.sh index 0697fc25216..9389d57e6f2 100755 --- a/fetch-remote-css.sh +++ b/fetch-remote-css.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# The styles liquipedia.net loads for the lakesideview skin. The snapshot -# template renders against these, so we fetch them once per run instead of -# once per snapshot. +# The styles liquipedia.net serves for the lakesideview skin, fetched once per +# run rather than once per snapshot. # TODO: We might in the future make the cacheversion dynamic URL='https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&modules=fontawesome-pro|ext.TeamLiquidIntegration.liquipedia-custom-icon|skins.lakesideview.mw|skins.lakesideview.theme.commons&only=styles&skin=lakesideview' ORIGIN='https://liquipedia.net' @@ -12,13 +11,11 @@ OUTPUT='lua/output/css/lakeside.css' cd "$(dirname "$0")" mkdir -p "$(dirname "$OUTPUT")" -# Download to a temporary file rather than piping into sed. A failed transfer -# would otherwise leave a truncated stylesheet behind, and an empty one still -# loads, so we would screenshot an unstyled page rather than fail. +# Via a temporary file, so a failed transfer cannot clobber the previous copy. +# An empty stylesheet still loads, and would be screenshotted as if it were fine. curl -fsS --max-time 30 -o "$OUTPUT.tmp" "$URL" -# Fonts, logos and icons are referenced from the site root. Served from disk -# those would resolve against the local filesystem, so point them back at the -# wiki. Mind the quotes, the bundle uses both url(/x) and url("/x"). +# Fonts, logos and icons are referenced from the site root, which off the local +# filesystem resolves nowhere. The bundle uses both url(/x) and url("/x"). sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' "$OUTPUT.tmp" > "$OUTPUT" rm "$OUTPUT.tmp" diff --git a/lua/spec/helpers/template.html b/lua/spec/helpers/template.html index d39b00f2df0..e336e73d5b2 100644 --- a/lua/spec/helpers/template.html +++ b/lua/spec/helpers/template.html @@ -4,7 +4,7 @@ Liquipedia - + From 6ba33d81dfff4fdbd97c2c55042b04c5d8d4bb36 Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 15:17:57 +0200 Subject: [PATCH 5/7] refactor: move the fetch script to scripts/ Where the rest of the tooling lives, generate_changelog_local.sh included. Dropped the exec bit to match its neighbours, since package.json invokes it through bash either way. The output path is relative to the repository root, so the cd that makes the script work from any directory now has to climb out of scripts/. Co-Authored-By: Claude Opus 5 --- lua/spec/helpers/template.html | 2 +- package.json | 2 +- fetch-remote-css.sh => scripts/fetch-remote-css.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename fetch-remote-css.sh => scripts/fetch-remote-css.sh (97%) mode change 100755 => 100644 diff --git a/lua/spec/helpers/template.html b/lua/spec/helpers/template.html index e336e73d5b2..8f155e709dc 100644 --- a/lua/spec/helpers/template.html +++ b/lua/spec/helpers/template.html @@ -4,7 +4,7 @@ Liquipedia - + diff --git a/package.json b/package.json index aa4c6ef5e56..fc675dc336c 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "lint:scss:ci": "stylelint \"stylesheets/**/*.scss\" --custom-formatter @csstools/stylelint-formatter-github", "test:js": "jest", "compile:scss": "sass stylesheets/Main.scss lua/output/css/main.css", - "fetch:remote-css": "bash fetch-remote-css.sh", + "fetch:remote-css": "bash scripts/fetch-remote-css.sh", "build:css": "npm run compile:scss", "build:js": "node build-js.js", "build": "npm run build:css && npm run build:js", diff --git a/fetch-remote-css.sh b/scripts/fetch-remote-css.sh old mode 100755 new mode 100644 similarity index 97% rename from fetch-remote-css.sh rename to scripts/fetch-remote-css.sh index 9389d57e6f2..c475a638b4f --- a/fetch-remote-css.sh +++ b/scripts/fetch-remote-css.sh @@ -8,7 +8,7 @@ URL='https://liquipedia.net/commons/load.php?cacheversion=cipipeline&lang=en&mod ORIGIN='https://liquipedia.net' OUTPUT='lua/output/css/lakeside.css' -cd "$(dirname "$0")" +cd "$(dirname "$0")/.." mkdir -p "$(dirname "$OUTPUT")" # Via a temporary file, so a failed transfer cannot clobber the previous copy. From 7ce4db93d59fe9480a50111cc078d4a895d7142f Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 15:36:43 +0200 Subject: [PATCH 6/7] fix: retry the stylesheet download, and only replace it atomically The snapshot job failed on a 429. load.php rate limits CI's shared address now and then, having answered four earlier runs on this branch fine, so back off and retry rather than fall over. Also ask for the gzipped copy, which is 81KB against 519KB. Pipe into sed and mv the result into place, rather than redirecting over the output. The shell truncates a redirect target before sed runs, so the previous copy was still destroyed if sed failed, which is what the temporary file was there to prevent. pipefail covers the curl end of the pipeline. Co-Authored-By: Claude Opus 5 --- scripts/fetch-remote-css.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/fetch-remote-css.sh b/scripts/fetch-remote-css.sh index c475a638b4f..6d134b9b8da 100644 --- a/scripts/fetch-remote-css.sh +++ b/scripts/fetch-remote-css.sh @@ -10,12 +10,17 @@ OUTPUT='lua/output/css/lakeside.css' cd "$(dirname "$0")/.." mkdir -p "$(dirname "$OUTPUT")" +trap 'rm -f "$OUTPUT.tmp"' EXIT -# Via a temporary file, so a failed transfer cannot clobber the previous copy. -# An empty stylesheet still loads, and would be screenshotted as if it were fine. -curl -fsS --max-time 30 -o "$OUTPUT.tmp" "$URL" +# load.php occasionally answers 429 to CI's shared address, so back off and +# retry rather than failing the run. --compressed asks for the gzipped copy, +# which is a sixth of the traffic. +# +# The sed puts back the origin on fonts, logos and icons, which the bundle +# references from the site root, as both url(/x) and url("/x"). Off the local +# filesystem those resolve nowhere. +curl -fsS --compressed --retry 3 --retry-max-time 60 --max-time 30 "$URL" \ + | sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' > "$OUTPUT.tmp" -# Fonts, logos and icons are referenced from the site root, which off the local -# filesystem resolves nowhere. The bundle uses both url(/x) and url("/x"). -sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' "$OUTPUT.tmp" > "$OUTPUT" -rm "$OUTPUT.tmp" +# Replace a previous copy only once the whole pipeline has succeeded +mv "$OUTPUT.tmp" "$OUTPUT" From 805cd3dc7417cd26c6513f4952e713b100511ec8 Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Mon, 7 Sep 2026 16:17:31 +0200 Subject: [PATCH 7/7] refactor: let a blocked request fail the run A backoff does not help against an address that was already blocked on the first call, and the retry was not what turned the snapshot job green, since the 429 passed on its own. Rerunning the job is the answer. Dropping the temporary file with it. set -e stops the chain before busted, and the next attempt downloads again, so no one ever reads a half written stylesheet and there is nothing for the atomic replace to protect. Co-Authored-By: Claude Opus 5 --- scripts/fetch-remote-css.sh | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/scripts/fetch-remote-css.sh b/scripts/fetch-remote-css.sh index 6d134b9b8da..208289e9d7a 100644 --- a/scripts/fetch-remote-css.sh +++ b/scripts/fetch-remote-css.sh @@ -10,17 +10,9 @@ OUTPUT='lua/output/css/lakeside.css' cd "$(dirname "$0")/.." mkdir -p "$(dirname "$OUTPUT")" -trap 'rm -f "$OUTPUT.tmp"' EXIT -# load.php occasionally answers 429 to CI's shared address, so back off and -# retry rather than failing the run. --compressed asks for the gzipped copy, -# which is a sixth of the traffic. -# # The sed puts back the origin on fonts, logos and icons, which the bundle # references from the site root, as both url(/x) and url("/x"). Off the local # filesystem those resolve nowhere. -curl -fsS --compressed --retry 3 --retry-max-time 60 --max-time 30 "$URL" \ - | sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' > "$OUTPUT.tmp" - -# Replace a previous copy only once the whole pipeline has succeeded -mv "$OUTPUT.tmp" "$OUTPUT" +curl -fsS --compressed --max-time 30 "$URL" \ + | sed -E 's|url\((["'\'']?)/|url(\1'"$ORIGIN"'/|g' > "$OUTPUT"