mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-18 21:34:03 +02:00
* 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>
710 lines
27 KiB
YAML
710 lines
27 KiB
YAML
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 |