Files
kst4contest/.github/workflows/nightly-artifacts.yml
T
Rsclub2_2andClaude Fable 5.1 5491cbcf8b Test suite cleanup, JaCoCo coverage report and tests before packaging (#62) (#112)
* docs: add the test-coverage design spec (#62)

Records the state found for issue #62 (JUnit classes under src/main that
never run, test libraries in the shipped app image, dead 2023/2024 scratch
tests, no coverage figure), the decisions taken (delete, move to src/test,
JaCoCo report-only without a threshold, tests before packaging), what is
deliberately left to separate issues, and the definition of done.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: move the JUnit classes out of src/main and drop the dead 2023 tests (#62)

Gradle's test task scans src/test only, so the six JUnit classes under
src/main compiled on every build and were never executed. They move to
src/test of the module that owns the class under test, keeping their
package names: On4KstProtocolTest, On4KstSocketThreadTest and
ChatMemberDirectionOpportunityTest within core; Utils4KSTFrequencyTest,
FrequencyTextParserTest and ChatPreferencesStationMapVisibilityTest from
app-desktop into core, next to the sibling tests that already live there,
because they test core classes only. All 26 of them run and pass.

The fully commented-out MessageBusManagementThreadFrequencyContextTest is
deleted. Eight files under core/src/test/java/kst4contest/test from
2023/2024 go as well: they carry no assertion, five of them annotate a
static main() with @Test, which Jupiter never runs, and some write the real
preferences file, send real UDP broadcasts or open the real user database
when started by hand. The five main() scratch programs under src/main
(ReadUDPByWintestThreadTest, PatternMatcherTest, mapTest, RegexTester,
locatorUtils/TestLocatorUtils) and MockKstServer, which only an unused
import in ChatController still referenced, are removed too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build: make JUnit and Mockito test-only dependencies (#62)

With no test class left under src/main, the implementation declarations of
junit-jupiter-api and mockito-core in core and app-desktop lose their only
justification. They put junit-jupiter-api, junit-platform-commons,
opentest4j, mockito-core, byte-buddy, byte-buddy-agent and objenesis on the
runtime classpath and into lib/app of every app image, Flatpak and
installer. The duplicate catalog entries junit-jupiter-api,
mockito-core-compile and the mockitoCompile version go with them.

Verified: `:app-desktop:dependencies --configuration runtimeClasspath` no
longer lists any of the seven artifacts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: state that test failures fail the build and tests live under src/test (#62)

AGENTS.md and docs/PROJECT_CONTEXT.md still said the build configuration
had allowed test failures to pass. That was true of the Maven build; under
Gradle a test failure fails the build, and only PMD and SpotBugs run with
ignoreFailures. Both documents now say so, note that test classes live under
src/test only and that JUnit and Mockito are test-only dependencies, and
keep the rule to read the test-result XML rather than the exit code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* build: add a JaCoCo coverage report (#62)

The jacoco plugin is applied to every module from the root build with the
tool version held in the catalog. jacocoTestReport now depends on test and
writes XML and HTML per module; a root task jacocoAggregatedReport merges
core and app-desktop into build/reports/jacoco/aggregated/ (XML plus HTML).
i18n-generator is excluded: it is a build tool and its coverage says nothing
about the shipped application. Kotlin classes are counted, because the
Kotlin plugin adds its output to the main source set the report reads.

Report only, on purpose. There is no jacocoTestCoverageVerification and no
threshold: a minimum set at today's number would trip on the next unrelated
change, and one set at a target would fail every build until tests exist for
the number's sake. The figure is published so it can be read when deciding
what to test next; the reasoning is in
docs/superpowers/specs/2026-10-11-test-coverage-design.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: run the tests before packaging and publish coverage (#62)

The nightly and tagged-release workflows built seven package formats from
commits nobody had tested on the way: pr-compile-check.yml runs the suite
for pull requests only, and main can be pushed to directly. Both workflows
now carry a copy of that test job, including the libgl1 step and the
XML-based summary, and every packaging job declares `needs: test`. Copied
rather than factored into a reusable workflow on purpose: the comments in
the job explain decisions the packaging workflows should carry with them,
and three copies read better than one indirection. build-docs-pdf runs
pandoc only and is left alone; publish-flatpak-repo and release-tag are
gated through the jobs they already need.

The PR test job additionally runs jacocoTestReport and
jacocoAggregatedReport once the tests have run (not `if: always()`: a
report from an incomplete run would mislead), prints a line/branch coverage
table per module and aggregated into the job summary from the JaCoCo XML,
and uploads the aggregated HTML report as the coverage-report artifact for
14 days whenever it exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: describe the coverage report in the project context (#62)

A short paragraph on JaCoCo in docs/PROJECT_CONTEXT.md: report only and
why, where the per-module and aggregated reports land, which Gradle tasks
write them, that i18n-generator is excluded, how the PR job summary and the
coverage-report artifact show the figure, and that the nightly and
tagged-release workflows run the tests before any packaging job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: round-trip every ChatPreferences field through the XML file (#62)

Drive all public single-argument setters by reflection to non-default
values, write preferences.xml, read it back and compare every getter.
Fields that are not persisted on purpose are listed with their evidence;
two real reader/writer gaps (second-category locator, sniffer words) are
documented with a BUG marker instead of being fixed here. Two further
tests load the bundled template and a legacy file without configVersion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: cover the DBController schema migration from older user_version values (#62)

Open the seeded template database and hand-built release 1.1 and 1.3
schema fixtures with a DBController and verify that PRAGMA user_version
reaches the current level, every column of the current CREATE TABLE
statements exists, legacy callsign keys are merged to base callsigns and
existing rows and flags survive. A further test proves that reopening an
up-to-date database changes neither schema nor rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: drop the committed JVM crash report and ignore future ones (#62)

hs_err_pid26344.log in the repository root was a HotSpot crash dump that
had been committed by accident. It carries no information about the
project and must not travel with the sources.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: drop the dead QSO sniffer word list that corrupted the callsign list on load (#103)

ChatPreferences carried a second sniffer list, lstNotify_QSOSniffer_sniffedWordsList,
that no UI, no manual and no consumer outside ChatPreferences ever touched. It was
still written to preferences.xml as <snifferWords><w>, and the matching reader block
was a copy of the <snifferCallSigns> block: it cleared the word list and then added
every word, normalised as a callsign, to lstNotify_QSOSniffer_sniffedCallSignList.
Any entry that ever reached the word list therefore leaked into QSO monitoring on
every load.

Remove the field, its accessors, the write block and the read block. A <snifferWords>
element in an older preferences file is now ignored like any unknown element; the
callsign list and its persistence are unchanged.

ChatPreferencesSnifferListTest loads a file that carries both lists and asserts that
only the <snifferCallSigns> entry is monitored. The round-trip test loses its
exclusion entry for the removed setter.

Fixes #103

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: remove the second-category locator that could never be set (#104)

ChatPreferences declared stn_loginLocatorSecondCat with the compiled-in default
"jn49fk". Nothing wrote it to or read it from preferences.xml, no settings tab
exposed it and no caller ever invoked the setter, so the field always held its
default. On4KstConnectionManager.configureMainChat nevertheless compared that
default with the station's locator and, with the second chat enabled, raised the
warning "ON4KST uses one locator per TCP session. The second-chat locator ... is
ignored" whenever the station's locator was not jn49fk, which is practically every
installation.

ON4KST uses one locator per TCP session by design, so a second-category locator can
never become meaningful. Remove the field, its accessors, the comparison and the
warning; the rest of configureMainChat is unchanged. The round-trip test loses its
exclusion entry for the removed setter.

No automated test covers configureMainChat: it is private, works on the manager's
private Session and needs a mocked ChatController plus a live socket handshake, so a
harness would far exceed the scope of this fix. The compile of core and app-desktop
and the shorter exclusion list are the evidence that nothing references the member.

Fixes #104

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 12:13:31 +02:00

710 lines
27 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Nightly Runtime Artifacts
on:
push:
branches:
- main
paths:
- "packaging/icons/**"
- "packaging/macos/**"
- "core/**"
- "app-desktop/**"
- "*.gradle.kts"
- "gradle.properties"
- "gradle/**"
- "gradlew"
- "gradlew.bat"
- ".github/workflows/nightly-artifacts.yml"
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: write
packages: write
jobs:
# Same job as in pr-compile-check.yml, copied rather than shared: a reusable workflow
# would be one more indirection to read, and the comments belong next to the steps they
# explain. Every packaging job below needs this one, so a commit whose tests fail builds
# no artifact (#62). Edit all three copies together.
test:
name: Test (Java 21, headless)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
# Skiko's native library links against libGL, so it has to be loadable even though
# nothing here renders through it: the drawing tests use Skia's software backend and
# runComposeUiTest never opens a window. Without this the whole Compose half of the
# suite dies with "libskiko-linux-x64.so: libGL.so.1: cannot open shared object
# file" -- which reads like a missing display and is not one. Found by running this
# job under act, where the container has no libGL; GitHub's own runner image happens
# to ship it, so the step is what makes the job reproducible off GitHub.
- name: Install the OpenGL client library skiko links against
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libgl1
# No Xvfb, deliberately. Everything that pins behaviour runs without a display:
# runComposeUiTest reaches the semantics tree headless (measured before this job was
# written), and the ImageComposeScene drawing tests render in software. The handful
# of tests that open a real window guard themselves with assumeFalse(isHeadless) and
# skip here; the summary below prints the skip count, so that is visible rather than
# assumed.
#
# Software GL under Xvfb was considered and rejected: a missing Skia GL context is
# the hardest failure in this codebase to read, and importing that class of flake
# into CI would cost more than the few window tests are worth here.
# All three modules. The translation generator is a Gradle module and not buildSrc
# precisely so that its 42 tests run here: buildSrc tests do not run on `./gradlew
# build` -- measured, not assumed. The summary below globs */build/test-results, so
# the module was counted the moment it existed.
- name: Test
run: ./gradlew -S test
- name: Summarise the test results
if: always()
run: |
set -euo pipefail
shopt -s nullglob
files=(*/build/test-results/test/TEST-*.xml)
if [ ${#files[@]} -eq 0 ]; then
echo "No test result XML was produced -- the test task did not run." >&2
exit 1
fi
# Counted from the XML and not from Gradle's console output: this project's own
# rule is never to infer "all tests passed" from an exit code.
summary=$(grep -h "<testsuite " "${files[@]}" \
| sed 's/.*tests="\([0-9]*\)".*skipped="\([0-9]*\)".*failures="\([0-9]*\)".*errors="\([0-9]*\)".*/\1 \2 \3 \4/' \
| awk '{t+=$1; s+=$2; f+=$3; e+=$4} END {print t" "s" "f" "e}')
read -r total skipped failures errors <<< "$summary"
echo "tests=$total skipped=$skipped failures=$failures errors=$errors"
{
echo "### Tests"
echo ""
echo "| total | skipped | failures | errors |"
echo "|---|---|---|---|"
echo "| $total | $skipped | $failures | $errors |"
echo ""
echo "Skipped are the tests that need a display, plus the deliberately"
echo "disabled reference-data generator. This job runs headless on purpose."
} >> "$GITHUB_STEP_SUMMARY"
if [ "$failures" != "0" ] || [ "$errors" != "0" ]; then
echo "The result XML reports failures or errors." >&2
exit 1
fi
- name: Keep the test reports on a failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: test-results
path: |
*/build/test-results/test/
*/build/reports/tests/test/
if-no-files-found: ignore
retention-days: 7
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
shell: pwsh
run: |
$version = (Select-String -Path gradle.properties -Pattern '^version=').Line.Split('=')[1]
$shortSha = "${{ github.sha }}".Substring(0, 7)
Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version"
Add-Content -Path $env:GITHUB_ENV -Value "SHORT_SHA=$shortSha"
Add-Content -Path $env:GITHUB_ENV -Value "ASSET_BASENAME=praktiKST-$version-$shortSha"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Install WiX Toolset
shell: pwsh
run: choco install wixtoolset --no-progress -y
- name: Build app-image
shell: pwsh
run: |
.\gradlew.bat -S :app-desktop:createDistributable
New-Item -ItemType Directory -Force -Path dist | Out-Null
# The next step reads dist/KST4Contest, so the Compose output goes
# there. The module list now lives in app-desktop/build.gradle.kts.
if (Test-Path dist/KST4Contest) { Remove-Item -Recurse -Force dist/KST4Contest }
Copy-Item -Recurse app-desktop/build/compose/binaries/main/app/KST4Contest dist/KST4Contest
- name: Create Windows ZIP
shell: pwsh
run: |
if (-not (Test-Path dist/KST4Contest)) {
throw "No Windows app-image produced by createDistributable"
}
Compress-Archive -Path dist/KST4Contest -DestinationPath "dist/$env:ASSET_BASENAME-windows-x64.zip" -Force
- name: Upload Windows artifact
uses: actions/upload-artifact@v6
with:
name: windows-zip
path: dist/praktiKST-*-windows-x64.zip
retention-days: 14
build-linux-appimage:
name: Build Linux AppImage
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Build app-image
run: |
./gradlew -S :app-desktop:createDistributable
mkdir -p dist
# The following steps read dist/KST4Contest, so the Compose output goes
# there. The module list now lives in app-desktop/build.gradle.kts.
rm -rf dist/KST4Contest
cp -a app-desktop/build/compose/binaries/main/app/KST4Contest dist/
- name: Create AppDir metadata
run: |
mkdir -p target
rm -rf target/KST4Contest.AppDir
cp -a dist/KST4Contest target/KST4Contest.AppDir
cat > target/KST4Contest.AppDir/AppRun << 'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/KST4Contest" "$@"
EOF
chmod +x target/KST4Contest.AppDir/AppRun
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=KST4Contest
Exec=KST4Contest
Icon=KST4Contest
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.png
fi
- name: Build AppImage
run: |
mkdir -p target
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x target/appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir "dist/${ASSET_BASENAME}-linux-x86_64.AppImage"
- name: Upload Linux artifact
uses: actions/upload-artifact@v6
with:
name: linux-appimage
path: dist/KST4Contest-*-linux-x86_64.AppImage
retention-days: 14
build-linux-deb:
name: Build Debian package
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends fakeroot
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Build Debian package
run: |
./gradlew -S :app-desktop:packageDeb
mkdir -p dist
DEB="$(ls app-desktop/build/compose/binaries/main/deb/*.deb | head -n 1)"
if [ -z "$DEB" ]; then
echo "No DEB produced by packageDeb" && exit 1
fi
mv "$DEB" "dist/${ASSET_BASENAME}-debian-amd64.deb"
- name: Upload Debian artifact
uses: actions/upload-artifact@v6
with:
name: linux-debian
path: dist/KST4Contest-*-debian-amd64.deb
retention-days: 14
build-linux-rpm:
name: Build Fedora package
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends rpm
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Build Fedora package
run: |
./gradlew -S :app-desktop:packageRpm
mkdir -p dist
RPM="$(ls app-desktop/build/compose/binaries/main/rpm/*.rpm | head -n 1)"
if [ -z "$RPM" ]; then
echo "No RPM produced by packageRpm" && exit 1
fi
mv "$RPM" "dist/${ASSET_BASENAME}-fedora-x86_64.rpm"
- name: Upload Fedora artifact
uses: actions/upload-artifact@v6
with:
name: linux-fedora
path: dist/KST4Contest-*-fedora-x86_64.rpm
retention-days: 14
build-linux-arch:
name: Build Arch Linux package
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends zstd
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Build app-image
run: |
./gradlew -S :app-desktop:createDistributable
mkdir -p dist
# The following steps read dist/KST4Contest, so the Compose output goes
# there. The module list now lives in app-desktop/build.gradle.kts.
rm -rf dist/KST4Contest
cp -a app-desktop/build/compose/binaries/main/app/KST4Contest dist/
- name: Build Arch Linux package artifact
run: |
mkdir -p target
ARCH=$(uname -m)
PKGVER=$(printf '%s' "${VERSION}-${SHORT_SHA}" | sed 's/[^[:alnum:].+_]/_/g')
PKGROOT="target/archpkg"
rm -rf "$PKGROOT"
mkdir -p "$PKGROOT/usr/lib/KST4Contest" "$PKGROOT/usr/bin"
cp -a dist/KST4Contest/. "$PKGROOT/usr/lib/KST4Contest/"
cat > "$PKGROOT/usr/bin/KST4Contest" << 'EOF'
#!/bin/sh
exec /usr/lib/KST4Contest/bin/KST4Contest "$@"
EOF
chmod 755 "$PKGROOT/usr/bin/KST4Contest"
mkdir -p "$PKGROOT/usr/share/applications" "$PKGROOT/usr/share/icons/hicolor/256x256/apps"
cat > "$PKGROOT/usr/share/applications/KST4Contest.desktop" << 'EOF'
[Desktop Entry]
Type=Application
Name=KST4Contest
Comment=ON4KST Chat Client for VHF/UHF contest operation
Exec=KST4Contest
Icon=KST4Contest
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
fi
INSTALLED_SIZE=$(du -sb "$PKGROOT" | cut -f1)
BUILDDATE=$(date +%s)
WORKFLOW_SHA256=$(sha256sum "$GITHUB_WORKSPACE/.github/workflows/nightly-artifacts.yml" | awk '{print $1}')
{
echo "pkgname = kst4contest"
echo "pkgbase = kst4contest"
echo "xdata = pkgtype=pkg"
echo "pkgver = ${PKGVER}-1"
echo "pkgdesc = KST4Contest amateur radio contest logger"
echo "url = https://github.com/${{ github.repository }}"
echo "builddate = ${BUILDDATE}"
echo "packager = GitHub Actions"
echo "size = ${INSTALLED_SIZE}"
echo "arch = ${ARCH}"
echo "license = custom"
echo "depend = java-runtime"
echo "depend = gst-plugins-base"
echo "depend = gst-plugins-good"
} > "$PKGROOT/.PKGINFO"
{
echo "format = 2"
echo "pkgname = kst4contest"
echo "pkgbase = kst4contest"
echo "pkgver = ${PKGVER}-1"
echo "pkgarch = ${ARCH}"
echo "pkgbuild_sha256sum = ${WORKFLOW_SHA256}"
echo "packager = GitHub Actions"
echo "builddate = ${BUILDDATE}"
echo "builddir = /build"
echo "startdir = /build"
echo "buildtool = makepkg"
echo "buildtoolver = 7.0.0-1-x86_64"
echo "buildenv = !distcc !color !ccache check !sign"
echo "options = !strip docs libtool staticlibs emptydirs zipman purge !debug !lto"
} > "$PKGROOT/.BUILDINFO"
tar --zstd --transform 's|^\./||' -cf "dist/${ASSET_BASENAME}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
- name: Upload Arch Linux artifact
uses: actions/upload-artifact@v6
with:
name: linux-arch
path: dist/KST4Contest-*-archlinux-*.pkg.tar.zst
retention-days: 14
build-flatpak:
name: Build Flatpak
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Install Flatpak tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
- name: Build app-image
run: |
./gradlew -S :app-desktop:createDistributable
mkdir -p target/flatpak-src
# The manifest's source is flatpak-src/KST4Contest.
rm -rf target/flatpak-src/KST4Contest
cp -a app-desktop/build/compose/binaries/main/app/KST4Contest target/flatpak-src/
- name: Create Flatpak manifest
run: |
mkdir -p target
mkdir -p dist
cat > target/de.x08.KST4Contest.yml << 'EOF'
app-id: de.x08.KST4Contest
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
command: KST4Contest
finish-args:
- --socket=wayland
- --socket=x11
- --socket=pulseaudio
- --share=network
- --share=ipc
- --device=dri
- --filesystem=~/.praktiKST
- --env=ALSA_CONFIG_PATH=/app/share/alsa/asound.conf
modules:
- name: kst4contest
buildsystem: simple
build-commands:
- install -d /app/lib/KST4Contest /app/bin /app/share/applications /app/share/alsa
- printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf
- cp -a . /app/lib/KST4Contest/
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
- chmod 755 /app/bin/KST4Contest
- echo '[Desktop Entry]' > /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Type=Application' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Name=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Comment=ON4KST Chat Client for VHF/UHF contest operation' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Exec=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Icon=de.x08.KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- printf 'Categories=Network;HamRadio;\n' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Terminal=false' >> /app/share/applications/de.x08.KST4Contest.desktop
- test -f /app/lib/KST4Contest/lib/KST4Contest.png && install -Dm644 /app/lib/KST4Contest/lib/KST4Contest.png /app/share/icons/hicolor/256x256/apps/de.x08.KST4Contest.png || true
sources:
- type: dir
path: flatpak-src/KST4Contest
EOF
- name: Import Flatpak signing key
run: |
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
echo "FLATPAK_GPG_KEY_ID=$FLATPAK_GPG_KEY_ID" >> "$GITHUB_ENV"
- name: Build Flatpak repo (nightly)
run: |
mkdir -p target
flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml
flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build nightly
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo
- name: Create flatpakref (nightly)
run: |
REPO_NAME="${GITHUB_REPOSITORY#*/}"
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
cat > "dist/de.x08.KST4Contest.nightly.flatpakref" << EOF
[Flatpak Ref]
Name=de.x08.KST4Contest
Branch=nightly
Title=KST4Contest (Nightly) – ON4KST Chat Client
Url=${PAGES_URL}
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
GPGKey=${GPG_KEY_B64}
IsRuntime=false
EOF
- name: Upload flatpakref
uses: actions/upload-artifact@v6
with:
name: flatpakref
path: dist/de.x08.KST4Contest.nightly.flatpakref
- name: Upload Flatpak OSTree repo
uses: actions/upload-artifact@v6
with:
name: flatpak-ostree-repo
path: target/flatpak-repo/
publish-flatpak-repo:
name: Publish Flatpak OSTree Repo (nightly)
runs-on: ubuntu-latest
needs: build-flatpak
concurrency:
group: flatpak-repo-publish
cancel-in-progress: false
steps:
- name: Install Flatpak tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends flatpak
- name: Import Flatpak signing key
run: |
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
echo "FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" >> "$GITHUB_ENV"
- name: Download OSTree repo artifact
uses: actions/download-artifact@v8.0.1
with:
name: flatpak-ostree-repo
path: flatpak-ostree-repo/
- name: Checkout existing flatpak-repo branch
uses: actions/checkout@v7
with:
ref: flatpak-repo
path: existing-flatpak-repo
- name: Merge nightly build into flatpak-repo
run: |
cd existing-flatpak-repo
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
# Copy the new OSTree build into the repo
rsync -a ../flatpak-ostree-repo/ ./
# Regenerate summary with all branches
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" .
# Generate .flatpakrepo with embedded GPG key so users can do remote-add without errors
REPO_NAME="${GITHUB_REPOSITORY#*/}"
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
cat > kst4contest.flatpakrepo << EOF
[Flatpak Repo]
Title=KST4Contest
Url=${PAGES_URL}
Homepage=https://github.com/${GITHUB_REPOSITORY}
Comment=KST4Contest – ON4KST Chat Client for VHF/UHF contests
GPGKey=${GPG_KEY_B64}
EOF
# Stage all changes (new/updated refs, summary, objects in OSTree)
git add -A
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "Nightly flatpak: $(echo ${{ github.sha }} | cut -c1-7)"
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo
fi
build-macos-dmg:
name: Build macOS DMG (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: test
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-15-intel]
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '^version=' gradle.properties | cut -d= -f2)
SHORT_SHA="${GITHUB_SHA::7}"
ARCH=$(uname -m)
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
- name: Set up Java 21
uses: actions/setup-java@v6.0.1
with:
distribution: temurin
java-version: "21"
- name: Ensure gradlew is executable
run: chmod +x gradlew
- name: Import signing certificate
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
run: ./packaging/macos/ci-import-cert.sh
# Builds the jar, signs the app bundle and every native library inside it,
# wraps it into a DMG and has Apple notarize the result. Same script the
# local Mac uses, so the two cannot drift apart.
- name: Build signed and notarized DMG
env:
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
run: |
printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
export NOTARY_KEY="$RUNNER_TEMP/notary.p8"
./packaging/macos/build-signed-dmg.sh
- name: Remove signing credentials
if: always()
run: |
rm -f "$RUNNER_TEMP/notary.p8"
if [ -n "${SIGNING_KEYCHAIN:-}" ]; then
security delete-keychain "$SIGNING_KEYCHAIN" || true
fi
- name: Rename DMG artifact
run: |
DMG=$(ls dist/*.dmg | head -n 1)
if [ -z "$DMG" ]; then
echo "No DMG produced by the build" && exit 1
fi
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
- name: Upload macOS artifact
uses: actions/upload-artifact@v6
with:
name: macos-dmg-${{ matrix.os }}
path: dist/KST4Contest-*-macos-*.dmg
retention-days: 14