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
};
};