added Beta and Nightly Download Links to website using nightly.link

This commit is contained in:
2026-08-03 22:31:14 +02:00
parent c7d1dfbe92
commit c04117ad0a
4 changed files with 708 additions and 29 deletions
+135
View File
@@ -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
};
};
+191
View File
@@ -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
};
};
+180 -2
View File
@@ -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;
}
+202 -27
View File
@@ -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.
---
<section class="hero">
<p class="badge">Stable release</p>
<p class="badge">Stable, Beta &amp; Nightly channels</p>
<h1>Download KST4Contest</h1>
<p class="lead">
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.
</p>
<div class="actions">
<a class="button" href="https://github.com/praktimarc/kst4contest/releases/latest">Latest release on GitHub</a>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases">All releases</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Packages by platform</p>
<h2>Choose the format that fits your system</h2>
</div>
<div class="channel-picker">
<fieldset class="channel-switch">
<legend class="sr-only">Choose a build channel</legend>
<label class="channel-option">
<input type="radio" name="channel" id="channel-stable" checked>
<span>Stable release</span>
</label>
<label class="channel-option">
<input type="radio" name="channel" id="channel-beta">
<span>Beta{% if beta.available %} · {{ beta.tag }}{% endif %}</span>
</label>
<label class="channel-option">
<input type="radio" name="channel" id="channel-nightly">
<span>Nightly builds</span>
</label>
</fieldset>
<div class="grid">
{% for item in downloads %}
<article class="card download-card">
<div class="feature-icon">{{ item.icon }}</div>
<div class="channel-panel" data-channel="stable">
<div class="section-heading">
<p class="eyebrow">Packages by platform</p>
<h2>Choose the format that fits your system</h2>
</div>
{% if item.recommended %}
<p class="badge">Default choice</p>
{% endif %}
<div class="actions">
<a class="button" href="https://github.com/praktimarc/kst4contest/releases/latest">Latest release on GitHub</a>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases">All releases</a>
</div>
<h3>{{ item.os }}</h3>
<p><strong>{{ item.format }}</strong></p>
<p>{{ item.note }}</p>
<div class="grid">
{% for item in downloads %}
<article class="card download-card">
<div class="feature-icon">{{ item.icon }}</div>
<a class="button secondary" href="{{ item.url }}">Download →</a>
</article>
{% endfor %}
{% if item.recommended %}
<p class="badge">Default choice</p>
{% endif %}
<h3>{{ item.os }}</h3>
<p><strong>{{ item.format }}</strong></p>
<p>{{ item.note }}</p>
<a class="button secondary" href="{{ item.url }}">Download →</a>
</article>
{% endfor %}
</div>
</div>
<div class="channel-panel" data-channel="beta">
{% if beta.available %}
<div class="notice-banner">
<div class="disclaimer-icon" aria-hidden="true">🧪</div>
<div>
<h3>Pre-release — do a quick check before a contest</h3>
<p>
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.
</p>
</div>
</div>
<div class="section-heading">
<p class="eyebrow">Current beta</p>
<h2>{{ beta.name or beta.tag }} · {{ beta.publishedAt | dateToIso }}</h2>
</div>
<p class="lead">
<a href="{{ beta.releaseUrl }}">View this release on GitHub</a>
</p>
<div class="grid">
{% for item in beta.items %}
<article class="card download-card">
<div class="feature-icon">{{ item.icon }}</div>
<h3>{{ item.os }}</h3>
<p><strong>{{ item.format }}</strong></p>
<p>{{ item.note }}</p>
<a class="button secondary" href="{{ item.url }}">Download →</a>
</article>
{% endfor %}
</div>
<div class="card content-card">
<h3>Flatpak beta channel</h3>
<p>
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:
</p>
<pre><code>flatpak install kst4contest de.x08.KST4Contest//beta</code></pre>
<p>
See the <a href="/manual/en/installation/">installation guide</a> for the full Flatpak setup.
</p>
</div>
{% else %}
<div class="card content-card empty-state">
<div class="disclaimer-icon" aria-hidden="true">🧪</div>
<h3>No Beta build right now</h3>
<p>
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.
</p>
<div class="actions">
<label for="channel-stable" class="button secondary">Switch to Stable</label>
<label for="channel-nightly" class="button ghost">Switch to Nightly</label>
<a class="button ghost" href="{{ beta.releasesUrl }}">All releases on GitHub</a>
</div>
</div>
{% endif %}
</div>
<div class="channel-panel" data-channel="nightly">
<div class="disclaimer-banner">
<div class="disclaimer-icon" aria-hidden="true">⚠️</div>
<div>
<h3>Testing only — not for contest operation</h3>
<p>
Nightly builds are automated, unreviewed snapshots of the <code>main</code> 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.
</p>
<p>
Use the <label for="channel-stable" class="inline-link">Stable release</label> for actual
operation. Nightly builds exist to test a specific fix or feature ahead of the next release.
</p>
</div>
</div>
<div class="section-heading">
<p class="eyebrow">
{% if nightly.meta %}
Latest successful build
{% else %}
Current nightly build
{% endif %}
</p>
<h2>
{% if nightly.meta %}
Build {{ nightly.meta.shortSha }} · {{ nightly.meta.createdAt | dateToIso }}
{% else %}
Always points to the newest successful workflow run
{% endif %}
</h2>
</div>
{% if nightly.meta %}
<p class="lead">
From commit <a href="{{ nightly.meta.commitUrl }}"><code>{{ nightly.meta.shortSha }}</code></a> ·
<a href="{{ nightly.meta.runUrl }}">view this workflow run on GitHub</a>
</p>
{% else %}
<p class="lead">
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.
</p>
{% endif %}
<div class="grid">
{% for item in nightly.items %}
<article class="card download-card">
<div class="feature-icon">{{ item.icon }}</div>
<h3>{{ item.os }}</h3>
<p><strong>{{ item.format }}</strong></p>
<p>{{ item.note }}</p>
{% if item.size %}<p><small>{{ item.size }}</small></p>{% endif %}
<a class="button secondary" href="{{ item.url }}">Download →</a>
</article>
{% endfor %}
</div>
<div class="card content-card">
<h3>How these downloads work</h3>
<p>
GitHub Actions build artifacts normally require a signed-in GitHub account to download. The
links above go through <a href="https://nightly.link/">nightly.link</a>, a public, open-source
proxy that mirrors the most recent successful build with no login required. Each link downloads
a <code>.zip</code> file; the actual installer, package or AppImage is inside and needs to be
extracted once.
</p>
<p>
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:
</p>
<pre><code>flatpak install kst4contest de.x08.KST4Contest//nightly</code></pre>
<p>
See the <a href="/manual/en/installation/">installation guide</a> 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
<a href="{{ nightly.runsUrl }}">nightly workflow run list on GitHub</a>.
</p>
</div>
</div>
</div>
</section>
@@ -77,4 +246,10 @@ description: Download the latest stable KST4Contest packages for Windows, Linux
<a class="button" href="/support/">Support the project</a>
</div>
</div>
</section>
</section>
<script>
var channelTabByHash = { "#beta": "channel-beta", "#nightly": "channel-nightly" };
var channelTab = document.getElementById(channelTabByHash[location.hash]);
if (channelTab) { channelTab.checked = true; }
</script>