From c04117ad0ad72c7f034644248ae328a822dcba0a Mon Sep 17 00:00:00 2001 From: Philipp Wagner Date: Mon, 3 Aug 2026 22:31:14 +0200 Subject: [PATCH] added Beta and Nightly Download Links to website using nightly.link --- website/src/_data/beta.js | 135 +++++++++++++++++++ website/src/_data/nightly.js | 191 ++++++++++++++++++++++++++ website/src/assets/css/main.css | 182 ++++++++++++++++++++++++- website/src/download/index.njk | 229 ++++++++++++++++++++++++++++---- 4 files changed, 708 insertions(+), 29 deletions(-) create mode 100644 website/src/_data/beta.js create mode 100644 website/src/_data/nightly.js diff --git a/website/src/_data/beta.js b/website/src/_data/beta.js new file mode 100644 index 0000000..b7d1c3d --- /dev/null +++ b/website/src/_data/beta.js @@ -0,0 +1,135 @@ +const REPO = "praktimarc/kst4contest"; +const API = `https://api.github.com/repos/${REPO}`; +const RELEASES_URL = `https://github.com/${REPO}/releases`; + +/** + * Beta builds are published by .github/workflows/tagged-release.yml whenever + * a tag starting with "beta-" is pushed: it creates a GitHub prerelease with + * the same asset naming as a Stable release (based on the tag), except for + * the flatpakref, which is named ...beta.flatpakref instead of .flatpakref. + * + * There is no dedicated "beta" marker in the GitHub API beyond the + * prerelease flag, but that flag is exactly what this workflow sets, and + * nothing else in this repository publishes prereleases, so it is a safe + * signal to use here. + */ +async function fetchLatestBetaRelease() { + const headers = { Accept: "application/vnd.github+json" }; + + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + try { + const res = await fetch(`${API}/releases?per_page=10`, { headers }); + + if (!res.ok) { + throw new Error(`GitHub API /releases failed: ${res.status}`); + } + + const releases = await res.json(); + + return releases.find((release) => release.prerelease && !release.draft) || null; + } catch (err) { + console.warn( + `[beta] Could not load the latest beta release. ` + + `The Beta tab will show its empty state instead: ${err.message}` + ); + + return null; + } +} + +function unavailable() { + return { + available: false, + releasesUrl: RELEASES_URL, + items: [] + }; +} + +/** + * Builds the Beta download list from the most recent GitHub prerelease. + * If there is currently no Beta release, the Beta tab is expected to show + * nothing but an explanatory empty state rather than guessed-at links. + */ +module.exports = async function () { + const release = await fetchLatestBetaRelease(); + + if (!release) { + return unavailable(); + } + + const tag = release.tag_name; + const assetUrl = (filenameTemplate) => + `https://github.com/${REPO}/releases/download/${tag}/${filenameTemplate.replace(/\{tag\}/g, tag)}`; + + const items = [ + { + os: "Windows", + format: "ZIP x64", + icon: "๐ŸชŸ", + note: "Extract the archive, then start praktiKST.exe. No separate Java installation is required.", + url: assetUrl("praktiKST-{tag}-windows-x64.zip") + }, + { + os: "Linux", + format: "Flatpak (.flatpakref)", + icon: "๐Ÿง", + note: "Installs the beta branch of the Flatpak repo through Flatpak or the desktop software centre.", + url: assetUrl("de.x08.KST4Contest.beta.flatpakref") + }, + { + os: "Linux", + format: "AppImage x86_64", + icon: "๐Ÿง", + note: "Portable build without package installation. Make the downloaded file executable before the first launch.", + url: assetUrl("KST4Contest-{tag}-linux-x86_64.AppImage") + }, + { + os: "Debian / Ubuntu", + format: "DEB amd64", + icon: "๐Ÿ“ฆ", + note: "Native package for Debian, Ubuntu and distributions based on them.", + url: assetUrl("KST4Contest-{tag}-debian-amd64.deb") + }, + { + os: "Fedora", + format: "RPM x86_64", + icon: "๐Ÿ“ฆ", + note: "Native package built for Fedora and compatible RPM-based systems.", + url: assetUrl("KST4Contest-{tag}-fedora-x86_64.rpm") + }, + { + os: "Arch Linux", + format: "pkg.tar.zst", + icon: "๐Ÿ“ฆ", + note: "Release package for direct installation with pacman.", + url: assetUrl("KST4Contest-{tag}-archlinux-x86_64.pkg.tar.zst") + }, + { + os: "macOS Apple Silicon", + format: "DMG arm64", + icon: "๐ŸŽ", + note: "Best-effort build for Apple Silicon Macs. Not currently notarized by Apple.", + url: assetUrl("KST4Contest-{tag}-macos-arm64.dmg") + }, + { + os: "macOS Intel", + format: "DMG x86_64", + icon: "๐ŸŽ", + note: "Best-effort build for Intel Macs. Not currently notarized by Apple.", + url: assetUrl("KST4Contest-{tag}-macos-x86_64.dmg") + } + ]; + + return { + available: true, + releasesUrl: RELEASES_URL, + tag, + name: release.name, + publishedAt: release.published_at, + releaseUrl: release.html_url, + items + }; +}; diff --git a/website/src/_data/nightly.js b/website/src/_data/nightly.js new file mode 100644 index 0000000..a8d10b3 --- /dev/null +++ b/website/src/_data/nightly.js @@ -0,0 +1,191 @@ +const REPO = "praktimarc/kst4contest"; +const API = `https://api.github.com/repos/${REPO}`; +const WORKFLOW_FILE = "nightly-artifacts.yml"; +const WORKFLOW_BASENAME = "nightly-artifacts"; +const BRANCH = "main"; +const WORKFLOW_RUNS_URL = `https://github.com/${REPO}/actions/workflows/${WORKFLOW_FILE}`; + +/** + * nightly.link (https://nightly.link) mirrors the most recent successful + * GitHub Actions artifact for a given workflow/branch/artifact-name as a + * plain public download, with no GitHub account required. It always follows + * the latest successful run on its own, so these URLs stay correct without + * this site having to resolve a run ID at build time. + */ +const NIGHTLY_LINK_BASE = `https://nightly.link/${REPO}/workflows/${WORKFLOW_BASENAME}/${BRANCH}`; + +/** + * Static metadata for the artifacts produced by + * .github/workflows/nightly-artifacts.yml, keyed by the upload-artifact name + * used in that workflow. Anything not listed here (e.g. the raw + * flatpak-ostree-repo folder) is skipped on the nightly page. + */ +const ARTIFACT_INFO = { + "windows-zip": { + os: "Windows", + format: "ZIP x64", + icon: "๐ŸชŸ", + note: "Extract the archive, then start praktiKST.exe. No separate Java installation is required.", + order: 1 + }, + "linux-appimage": { + os: "Linux", + format: "AppImage x86_64", + icon: "๐Ÿง", + note: "Portable build. Make the downloaded file executable before the first launch.", + order: 2 + }, + flatpakref: { + os: "Linux", + format: "Flatpak (.flatpakref, nightly branch)", + icon: "๐Ÿง", + note: "Adds the nightly branch of the KST4Contest Flatpak repo. See the installation guide for the flatpak CLI alternative.", + order: 3 + }, + "linux-debian": { + os: "Debian / Ubuntu", + format: "DEB amd64", + icon: "๐Ÿ“ฆ", + note: "Native package for Debian, Ubuntu and distributions based on them.", + order: 4 + }, + "linux-fedora": { + os: "Fedora", + format: "RPM x86_64", + icon: "๐Ÿ“ฆ", + note: "Native package built for Fedora and compatible RPM-based systems.", + order: 5 + }, + "linux-arch": { + os: "Arch Linux", + format: "pkg.tar.zst", + icon: "๐Ÿ“ฆ", + note: "Package for direct installation with pacman.", + order: 6 + }, + "macos-dmg-macos-latest": { + os: "macOS Apple Silicon", + format: "DMG arm64", + icon: "๐ŸŽ", + note: "Best-effort build for Apple Silicon Macs. Not notarized by Apple.", + order: 7 + }, + "macos-dmg-macos-15-intel": { + os: "macOS Intel", + format: "DMG x86_64", + icon: "๐ŸŽ", + note: "Best-effort build for Intel Macs. Not notarized by Apple.", + order: 8 + } +}; + +function buildItems() { + return Object.entries(ARTIFACT_INFO) + .map(([name, info]) => ({ + ...info, + name, + url: `${NIGHTLY_LINK_BASE}/${name}.zip` + })) + .sort((a, b) => a.order - b.order); +} + +function authHeaders() { + const headers = { Accept: "application/vnd.github+json" }; + + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + return headers; +} + +function formatSize(bytes) { + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(0)} KB`; + } + + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +/** + * Best-effort lookup of the run behind the current nightly.link downloads, + * purely to show a commit/date on the page. The download links themselves + * (buildItems) do not depend on this succeeding. + */ +async function fetchLatestRunMeta(headers) { + const runsRes = await fetch( + `${API}/actions/workflows/${WORKFLOW_FILE}/runs?branch=${BRANCH}&status=success&per_page=1`, + { headers } + ); + + if (!runsRes.ok) { + throw new Error(`workflow runs request failed: ${runsRes.status}`); + } + + const runsData = await runsRes.json(); + const run = runsData.workflow_runs && runsData.workflow_runs[0]; + + if (!run) { + throw new Error("no successful nightly run found"); + } + + const meta = { + runUrl: run.html_url, + commitUrl: `https://github.com/${REPO}/commit/${run.head_sha}`, + shortSha: run.head_sha.slice(0, 7), + createdAt: run.created_at + }; + + try { + const artifactsRes = await fetch( + `${API}/actions/runs/${run.id}/artifacts?per_page=100`, + { headers } + ); + + if (artifactsRes.ok) { + const artifactsData = await artifactsRes.json(); + const sizeByName = {}; + + for (const artifact of artifactsData.artifacts || []) { + if (!artifact.expired) { + sizeByName[artifact.name] = formatSize(artifact.size_in_bytes); + } + } + + meta.sizeByName = sizeByName; + } + } catch { + // Sizes are a nice-to-have; the run metadata above is still useful without them. + } + + return meta; +} + +module.exports = async function () { + const items = buildItems(); + + let meta = null; + + try { + meta = await fetchLatestRunMeta(authHeaders()); + } catch (err) { + console.warn( + `[nightly] Could not load latest nightly run metadata (commit/date will be omitted). ` + + `Download links are unaffected since they are served by nightly.link: ${err.message}` + ); + } + + if (meta && meta.sizeByName) { + for (const item of items) { + if (meta.sizeByName[item.name]) { + item.size = meta.sizeByName[item.name]; + } + } + } + + return { + runsUrl: WORKFLOW_RUNS_URL, + meta, + items + }; +}; diff --git a/website/src/assets/css/main.css b/website/src/assets/css/main.css index 4e5d0cb..119a985 100644 --- a/website/src/assets/css/main.css +++ b/website/src/assets/css/main.css @@ -10,6 +10,8 @@ --accent: #38bdf8; --accent2: #a855f7; --accent3: #22c55e; + --warn: #f59e0b; + --danger: #f43f5e; --shadow: 0 24px 80px rgba(0, 0, 0, 0.45); } @@ -259,6 +261,7 @@ a:hover { color: #020617; background: linear-gradient(135deg, var(--accent), var(--accent2)); font-weight: 800; + cursor: pointer; box-shadow: 0 16px 44px rgba(56, 189, 248, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.35); @@ -463,6 +466,181 @@ a:hover { min-height: 220px; } +.channel-picker { + margin: 34px 0 0; +} + +.channel-switch { + display: inline-flex; + gap: 4px; + padding: 5px; + margin: 0 0 40px; + border: 1px solid var(--border); + border-radius: 999px; + background: rgba(2, 6, 23, 0.55); +} + +.channel-option { + position: relative; + display: inline-flex; +} + +.channel-option input { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.channel-option span { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 11px 22px; + border-radius: 999px; + font-weight: 800; + color: var(--muted); + cursor: pointer; + transition: color 0.15s ease; +} + +.channel-option input:checked + span { + color: #020617; + background: linear-gradient(135deg, var(--accent), var(--accent2)); + box-shadow: 0 10px 30px rgba(56, 189, 248, 0.25); +} + +.channel-option input:focus-visible + span { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.channel-panel { + display: none; +} + +.channel-picker:has(#channel-stable:checked) .channel-panel[data-channel="stable"], +.channel-picker:has(#channel-beta:checked) .channel-panel[data-channel="beta"], +.channel-picker:has(#channel-nightly:checked) .channel-panel[data-channel="nightly"] { + display: block; +} + +.notice-banner { + display: flex; + gap: 18px; + align-items: flex-start; + padding: clamp(18px, 3vw, 26px); + margin-bottom: 30px; + border: 1px solid var(--warn); + border-radius: 20px; + background: linear-gradient(180deg, rgba(245, 158, 11, 0.14), rgba(245, 158, 11, 0.04)); +} + +.notice-banner .disclaimer-icon { + font-size: 2rem; +} + +.notice-banner h3 { + margin: 0 0 6px; + font-size: 1.1rem; + color: #fde68a; +} + +.notice-banner p { + margin: 0; + color: var(--muted); +} + +.empty-state { + text-align: center; + padding: clamp(30px, 5vw, 54px); +} + +.empty-state .disclaimer-icon { + font-size: 2.4rem; + margin-bottom: 12px; +} + +.empty-state h3 { + margin: 0 0 10px; +} + +.empty-state p { + color: var(--muted); + max-width: 560px; + margin: 0 auto; +} + +.empty-state .actions { + justify-content: center; +} + +.disclaimer-banner { + display: flex; + gap: 20px; + align-items: flex-start; + padding: clamp(22px, 4vw, 34px); + margin-bottom: 34px; + border: 2px solid var(--danger); + border-radius: 22px; + background: linear-gradient(180deg, rgba(244, 63, 94, 0.16), rgba(244, 63, 94, 0.05)); + box-shadow: 0 0 40px rgba(244, 63, 94, 0.16); +} + +.disclaimer-icon { + flex: none; + font-size: 2.6rem; + line-height: 1; +} + +.disclaimer-banner h3 { + margin: 0 0 8px; + font-size: 1.35rem; + color: #fecdd3; +} + +.disclaimer-banner p { + margin: 0; + color: var(--text); +} + +.disclaimer-banner p + p { + margin-top: 10px; + color: var(--muted); +} + +.disclaimer-banner .inline-link { + color: #fecdd3; + text-decoration: underline; + cursor: pointer; + font-weight: 700; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.channel-panel .section-heading + .actions { + margin: -10px 0 30px; +} + +.channel-panel .content-card { + margin-top: 30px; +} + .screenshot-placeholder { display: grid; place-items: center; @@ -491,14 +669,14 @@ a:hover { line-height: 1.2; } -.manual-content pre { +pre { overflow-x: auto; padding: 16px; border-radius: 14px; background: #020617; } -.manual-content code { +code { color: #bae6fd; } diff --git a/website/src/download/index.njk b/website/src/download/index.njk index 4e79e5a..d7287be 100644 --- a/website/src/download/index.njk +++ b/website/src/download/index.njk @@ -1,45 +1,214 @@ --- layout: base.njk title: Download KST4Contest -description: Download the latest stable KST4Contest packages for Windows, Linux and macOS. The required Java runtime is included. +description: Download the latest stable KST4Contest packages for Windows, Linux and macOS, or switch to a nightly build for testing. The required Java runtime is included. ---
-

Stable release

+

Stable, Beta & Nightly channels

Download KST4Contest

- Choose the package for your operating system. Official builds are published through GitHub Releases, - and the required Java runtime is already included. + Choose the package for your operating system. The required Java runtime is already included. Use + the switch below to pick between the Stable release, Beta and current Nightly builds.

- -
-
-

Packages by platform

-

Choose the format that fits your system

-
+
+
+ Choose a build channel + + + +
-
- {% for item in downloads %} -
-
{{ item.icon }}
+
+
+

Packages by platform

+

Choose the format that fits your system

+
- {% if item.recommended %} -

Default choice

- {% endif %} + -

{{ item.os }}

-

{{ item.format }}

-

{{ item.note }}

+
+ {% for item in downloads %} + - {% endfor %} + {% if item.recommended %} +

Default choice

+ {% endif %} + +

{{ item.os }}

+

{{ item.format }}

+

{{ item.note }}

+ + Download โ†’ +
+ {% endfor %} +
+
+ +
+ {% if beta.available %} +
+ +
+

Pre-release โ€” do a quick check before a contest

+

+ Beta builds have a largely defined feature set and are used for focused testing ahead of + a Stable release. They can still contain new defects. If you plan to use one on contest + day, try it beforehand. +

+
+
+ +
+

Current beta

+

{{ beta.name or beta.tag }} ยท {{ beta.publishedAt | dateToIso }}

+
+

+ View this release on GitHub +

+ +
+ {% for item in beta.items %} +
+
{{ item.icon }}
+ +

{{ item.os }}

+

{{ item.format }}

+

{{ item.note }}

+ + Download โ†’ +
+ {% endfor %} +
+ +
+

Flatpak beta channel

+

+ Instead of the .flatpakref download above, Flatpak users can also track the beta channel + directly through the CLI once the Stable repo is already added: +

+
flatpak install kst4contest de.x08.KST4Contest//beta
+

+ See the installation guide for the full Flatpak setup. +

+
+ {% else %} +
+ +

No Beta build right now

+

+ There is currently no Beta release. Beta builds are published occasionally for focused + testing ahead of a Stable release, tagged separately from Nightly builds. Check back later, + or use the Stable release or a Nightly build in the meantime. +

+
+ + + All releases on GitHub +
+
+ {% endif %} +
+ +
+
+ +
+

Testing only โ€” not for contest operation

+

+ Nightly builds are automated, unreviewed snapshots of the main branch. They can + be unstable, contain unfinished features, break your settings or corrupt log data. Do not + rely on a nightly build during a contest. +

+

+ Use the for actual + operation. Nightly builds exist to test a specific fix or feature ahead of the next release. +

+
+
+ +
+

+ {% if nightly.meta %} + Latest successful build + {% else %} + Current nightly build + {% endif %} +

+

+ {% if nightly.meta %} + Build {{ nightly.meta.shortSha }} ยท {{ nightly.meta.createdAt | dateToIso }} + {% else %} + Always points to the newest successful workflow run + {% endif %} +

+
+ + {% if nightly.meta %} +

+ From commit {{ nightly.meta.shortSha }} ยท + view this workflow run on GitHub +

+ {% else %} +

+ Commit and date details could not be loaded when this page was built. The download links below + are unaffected โ€” nightly.link always resolves to the latest successful run on its own. +

+ {% endif %} + +
+ {% for item in nightly.items %} +
+
{{ item.icon }}
+ +

{{ item.os }}

+

{{ item.format }}

+

{{ item.note }}

+ {% if item.size %}

{{ item.size }}

{% endif %} + + Download โ†’ +
+ {% endfor %} +
+ +
+

How these downloads work

+

+ GitHub Actions build artifacts normally require a signed-in GitHub account to download. The + links above go through nightly.link, a public, open-source + proxy that mirrors the most recent successful build with no login required. Each link downloads + a .zip file; the actual installer, package or AppImage is inside and needs to be + extracted once. +

+

+ Instead of the .flatpakref download, Flatpak users can also track the nightly channel directly + through the CLI once the Stable or Beta repo is already added: +

+
flatpak install kst4contest de.x08.KST4Contest//nightly
+

+ See the installation guide for how the Stable, Beta and + Nightly channels relate to each other, and for the full Flatpak setup. Older or in-progress + builds can be browsed on the + nightly workflow run list on GitHub. +

+
+
@@ -77,4 +246,10 @@ description: Download the latest stable KST4Contest packages for Windows, Linux Support the project - \ No newline at end of file + + +