Files
kst4contest/website/src/_data/beta.js
T
Rsclub2_2andClaude Opus 5 af30c17a0d Macos signing (#80)
* Sign and notarize macOS builds

jpackage cannot produce a distributable macOS bundle on its own. It ad-hoc
signs the embedded runtime and then re-runs codesign on the same files without
--force, which codesign rejects; and "--type dmg --app-image" re-signs the app
it is handed, replacing a Developer ID signature with an ad-hoc one. So the
build now creates an unsigned app-image, signs it from the inside out, and
wraps it with hdiutil.

Apple's notary service also unpacks JARs and checks the native libraries
inside them, which sqlite-jdbc ships for both architectures. Those are signed
before the bundle is sealed, since rewriting a JAR afterwards would invalidate
the seal. A preflight check verifies Apple's two criteria locally, so a missed
binary costs seconds rather than a round trip to the notary service.

Two long-standing defects surfaced while testing and are fixed here: the
bundle identifier defaulted to the main class's package name (kst4contest.view
instead of de.x08.KST4Contest), and every release reported version 1.0 in
Finder because --app-version was never passed. Neither affects existing users:
the app keeps its settings in ~/.praktiKST, independent of the bundle ID.

Both workflows call the same script the local Mac uses, so the two cannot
drift apart. Signing needs a keychain that can answer a UI prompt, which a
runner cannot, so ci-import-cert.sh creates a throwaway keychain whose
password is generated per job and discarded with it. Notarization goes through
an App Store Connect API key and needs no keychain at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Sign bundle contents serially

Signing the app image's Mach-O files with "xargs -P 8" passed locally but
failed on a runner: codesign reported "replacing existing signature" and then
"No such file or directory" for that same path. The two libjli.dylib copies are
separate inodes, so this is not hard links being signed twice -- concurrent
codesign runs over one bundle are simply not reliable. Serially costs about a
minute, since each call waits on Apple's timestamp server.

Also stop the matrix from cancelling the other architecture on a failure; that
throws away half the diagnostic information from a failed run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Document macOS signing from 1.42

The installation guides described the right-click workaround as the normal
first launch. That stays, but as the path for 1.41.1 and older; from 1.42 a
double-click works. Both guides also show how to verify a download with spctl,
so the claim is checkable rather than something to take on faith.

The per-channel download notes distinguish where the channels actually stand:
Nightly is built from main and is signed as of now, while Stable still points
at 1.41.1, so those notes name the version instead of claiming it outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:48:31 +02:00

154 lines
5.3 KiB
JavaScript

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.
*
* A prerelease flag alone isn't enough though: GitHub never clears
* `prerelease` once the corresponding Stable version has actually shipped,
* so the latest prerelease can be stale (e.g. beta-1.41-rc04 published
* 2026-06-30, followed by Stable v1.41.0 on 2026-07-01). If the newest
* Stable release is more recent than the newest prerelease, there is no
* current beta and the empty state should be shown instead.
*/
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();
const latestBeta = releases.find((release) => release.prerelease && !release.draft) || null;
const latestStable = releases.find((release) => !release.prerelease && !release.draft) || null;
if (!latestBeta) {
return null;
}
if (latestStable && latestStable.published_at > latestBeta.published_at) {
return null;
}
return latestBeta;
} 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. Signed and notarized by Apple from version 1.42 onwards.",
url: assetUrl("KST4Contest-{tag}-macos-arm64.dmg")
},
{
os: "macOS Intel",
format: "DMG x86_64",
icon: "🍎",
note: "Best-effort build for Intel Macs. Signed and notarized by Apple from version 1.42 onwards.",
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
};
};