From f21953e39b95920248b55f4d37448cb80ebf37d2 Mon Sep 17 00:00:00 2001 From: per Date: Tue, 14 Apr 2026 20:40:36 +0200 Subject: [PATCH 1/5] fixade windows-specifika testfel i dist-bygget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppPathsTest: använd @TempDir istället för hårdkodad /tmp, så pathen blir absolut på Windows - ReportServicesTest: normalisera radslut före SHA-256 så golden-hashen håller även med CRLF - .gitattributes: tvinga LF för *.ftl och *.sql för stabila checksummor över plattformar - AlipsaAccounting --verify-launch: stäng H2 efter verifiering så @TempDir kan städa på Windows - DatabaseService.shutdown(): ny metod som släpper H2:s filhandtag Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 4 ++++ .../alipsa/accounting/AlipsaAccounting.groovy | 3 +++ .../accounting/service/DatabaseService.groovy | 21 +++++++++++++++++++ .../service/ReportServicesTest.groovy | 3 ++- .../accounting/support/AppPathsTest.groovy | 9 ++++++-- createDist.sh | 0 6 files changed, 37 insertions(+), 3 deletions(-) mode change 100644 => 100755 createDist.sh diff --git a/.gitattributes b/.gitattributes index f91f646..ba44a8c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,10 @@ # These are Windows script files and should use crlf *.bat text eol=crlf +# Report templates and SQL must be byte-stable across platforms (checksums, golden masters) +*.ftl text eol=lf +*.sql text eol=lf + # Binary files should be left untouched *.jar binary diff --git a/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy b/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy index 56cebe0..6fa88df 100644 --- a/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy +++ b/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy @@ -57,6 +57,9 @@ final class AlipsaAccounting { log.warning("Launch verification completed with warnings: ${startupReport.warnings.join(' | ')}") } System.out.println("Launch verification OK: ${versionLine()} [home=${AppPaths.applicationHome()}]") + // Release the H2 file handles so the caller (tests, packaging verification) can delete + // the verification home directory on Windows. + DatabaseService.instance.shutdown() return } if (!startupReport.ok || !startupReport.warnings.isEmpty()) { diff --git a/app/src/main/groovy/se/alipsa/accounting/service/DatabaseService.groovy b/app/src/main/groovy/se/alipsa/accounting/service/DatabaseService.groovy index cf1663c..91527d5 100644 --- a/app/src/main/groovy/se/alipsa/accounting/service/DatabaseService.groovy +++ b/app/src/main/groovy/se/alipsa/accounting/service/DatabaseService.groovy @@ -114,6 +114,27 @@ final class DatabaseService { } } + /** + * Issues an H2 {@code SHUTDOWN} so the engine releases its file handles. Required on Windows + * before deleting the database directory (e.g. JUnit {@code @TempDir} cleanup), since H2 keeps + * the {@code .mv.db} file open between connections. + */ + void shutdown() { + Sql sql = Sql.newInstance(databaseUrl(), USERNAME, PASSWORD, DRIVER) + try { + sql.execute('shutdown') + } catch (java.sql.SQLException ignored) { + // H2 closes the connection as part of SHUTDOWN and reports "Database is already closed" + // when the Statement tries to finalize. The shutdown itself succeeded. + } finally { + try { + sql.close() + } catch (java.sql.SQLException ignored) { + // Connection is already closed by SHUTDOWN. + } + } + } + private String defaultDatabaseUrl() { embeddedDatabaseUrl(AppPaths.applicationHome()) } diff --git a/app/src/test/groovy/integration/se/alipsa/accounting/service/ReportServicesTest.groovy b/app/src/test/groovy/integration/se/alipsa/accounting/service/ReportServicesTest.groovy index e29d634..290bfc3 100644 --- a/app/src/test/groovy/integration/se/alipsa/accounting/service/ReportServicesTest.groovy +++ b/app/src/test/groovy/integration/se/alipsa/accounting/service/ReportServicesTest.groovy @@ -132,7 +132,8 @@ class ReportServicesTest { assertEquals(ReportType.INCOME_STATEMENT, archive.reportType) assertTrue(new String(pdf, 0, 5, StandardCharsets.US_ASCII).startsWith('%PDF-')) assertTrue(Files.size(reportArchiveService.resolveStoredPath(archive)) > 500L) - assertEquals('ccf4d53a601b18ee472bdb2c9b945f7b8bb1c297239e58cb3f7bc2ebbf56e19d', sha256(journoReportService.renderHtml(preview).getBytes(StandardCharsets.UTF_8))) + String html = journoReportService.renderHtml(preview).replace('\r\n', '\n').replace('\r', '\n') + assertEquals('ccf4d53a601b18ee472bdb2c9b945f7b8bb1c297239e58cb3f7bc2ebbf56e19d', sha256(html.getBytes(StandardCharsets.UTF_8))) } @Test diff --git a/app/src/test/groovy/unit/se/alipsa/accounting/support/AppPathsTest.groovy b/app/src/test/groovy/unit/se/alipsa/accounting/support/AppPathsTest.groovy index c9a4cb9..e0b24ce 100644 --- a/app/src/test/groovy/unit/se/alipsa/accounting/support/AppPathsTest.groovy +++ b/app/src/test/groovy/unit/se/alipsa/accounting/support/AppPathsTest.groovy @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import java.nio.file.Paths @@ -14,6 +15,9 @@ import java.nio.file.Paths // interfering with other tests that call AppPaths.applicationHome() concurrently. class AppPathsTest { + @TempDir + Path tempDir + private String previousOsName private String previousUserHome private String previousHomeOverride @@ -59,11 +63,12 @@ class AppPathsTest { @Test void applicationHomeOverrideTakesPrecedenceForAllDerivedDirectories() { - System.setProperty(AppPaths.HOME_OVERRIDE_PROPERTY, '/tmp/accounting-home') + Path overrideHome = tempDir.resolve('accounting-home') + System.setProperty(AppPaths.HOME_OVERRIDE_PROPERTY, overrideHome.toString()) Path home = AppPaths.applicationHome() - assertEquals(Paths.get('/tmp/accounting-home'), home) + assertEquals(overrideHome.toAbsolutePath().normalize(), home) assertEquals(home.resolve('data'), AppPaths.dataDirectory()) assertEquals(home.resolve('logs'), AppPaths.logDirectory()) assertEquals(home.resolve('attachments'), AppPaths.attachmentsDirectory()) diff --git a/createDist.sh b/createDist.sh old mode 100644 new mode 100755 From a6ffbc56ab448767a442e9832245e59f927483a5 Mon Sep 17 00:00:00 2001 From: per Date: Tue, 14 Apr 2026 20:47:21 +0200 Subject: [PATCH 2/5] tvinga lf-radslut globalt via .gitattributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows-runners checkar ut filer med CRLF (core.autocrlf), vilket får Spotless att misslyckas på Groovy-reglerna som kräver LF. Sätter text=auto eol=lf för alla textfiler, med *.bat/*.cmd som CRLF och binära filer explicit markerade. Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.gitattributes b/.gitattributes index ba44a8c..a54f28a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,16 +1,19 @@ # # https://help.github.com/articles/dealing-with-line-endings/ # -# Linux start script should use lf -/gradlew text eol=lf +# All text files use LF so Spotless checks and checksum-based tests are stable +# on Windows runners (where core.autocrlf would otherwise convert to CRLF). +* text=auto eol=lf -# These are Windows script files and should use crlf +# Windows script files keep CRLF. *.bat text eol=crlf +*.cmd text eol=crlf -# Report templates and SQL must be byte-stable across platforms (checksums, golden masters) -*.ftl text eol=lf -*.sql text eol=lf - -# Binary files should be left untouched +# Binary files should be left untouched. *.jar binary - +*.png binary +*.ico binary +*.icns binary +*.pdf binary +*.xlsx binary +*.zip binary From c3ac77df16bdb66b6de0f251ef20e1f8ed807e62 Mon Sep 17 00:00:00 2001 From: per Date: Tue, 14 Apr 2026 21:00:14 +0200 Subject: [PATCH 3/5] =?UTF-8?q?st=C3=A4ng=20=C3=A4ven=20fileloggern=20i=20?= =?UTF-8?q?--verify-launch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AcceptanceCriteriaTest's @TempDir-städning på Windows blockerades av att LoggingConfigurer's FileHandler höll verify-home/logs/accounting-0.log öppen. DatabaseService.shutdown() räckte inte — även loggern behövde stängas innan main() returnerar. Co-Authored-By: Claude Opus 4.6 --- .../main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy b/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy index 6fa88df..975caaa 100644 --- a/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy +++ b/app/src/main/groovy/se/alipsa/accounting/AlipsaAccounting.groovy @@ -57,9 +57,10 @@ final class AlipsaAccounting { log.warning("Launch verification completed with warnings: ${startupReport.warnings.join(' | ')}") } System.out.println("Launch verification OK: ${versionLine()} [home=${AppPaths.applicationHome()}]") - // Release the H2 file handles so the caller (tests, packaging verification) can delete - // the verification home directory on Windows. + // Release the H2 and logging file handles so the caller (tests, packaging verification) + // can delete the verification home directory on Windows. DatabaseService.instance.shutdown() + LoggingConfigurer.shutdown() return } if (!startupReport.ok || !startupReport.warnings.isEmpty()) { From 876f9c403c4cedd960658449ee2c79c4824c8f5c Mon Sep 17 00:00:00 2001 From: per Date: Tue, 14 Apr 2026 22:43:52 +0200 Subject: [PATCH 4/5] =?UTF-8?q?linux-paket:=20l=C3=A4gg=20till=20install.s?= =?UTF-8?q?h=20och=20uninstall.sh=20i=20zip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tidigare genererades en .desktop-fil med hårdkodad /opt/-sökväg som packades direkt i zip:en, vilket inte fungerar när användaren extraherar zip:en någon annanstans. Launchern saknade dessutom exekveringsrätt. Nu genererar bygget install.sh/uninstall.sh från mallar i packaging/linux och packar dem i zip:en med 0755. install.sh skapar en .desktop-genväg i ~/.local/share/applications som pekar på faktisk extraktionsplats, gör launchern exekverbar och registrerar ikonen. bin/AlipsaAccounting packas också med 0755 så att den går att starta direkt utan install.sh. --- app/build.gradle | 36 +++++++++++++++- packaging/linux/install.sh.template | 59 +++++++++++++++++++++++++++ packaging/linux/uninstall.sh.template | 22 ++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packaging/linux/install.sh.template create mode 100644 packaging/linux/uninstall.sh.template diff --git a/app/build.gradle b/app/build.gradle index d22e444..ddd10fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,6 +58,10 @@ def licenseFilePath = rootProject.file('LICENSE').absolutePath def linuxDesktopTemplate = releaseConfig.packagingRoot.dir('linux/desktop').file("${releaseConfig.appName}.desktop.template") def linuxDesktopEntry = releaseConfig.generatedRoot.map { it.file("linux-desktop/${releaseConfig.appName}.desktop") } +def linuxInstallTemplate = releaseConfig.packagingRoot.dir('linux').file('install.sh.template') +def linuxUninstallTemplate = releaseConfig.packagingRoot.dir('linux').file('uninstall.sh.template') +def linuxInstallScript = releaseConfig.generatedRoot.map { it.file('linux-scripts/install.sh') } +def linuxUninstallScript = releaseConfig.generatedRoot.map { it.file('linux-scripts/uninstall.sh') } def currentOsName = System.getProperty('os.name', 'unknown').toLowerCase(Locale.ROOT) def currentPlatform = currentOsName.contains('win') ? 'windows' : currentOsName.contains('mac') ? 'macos' : 'linux' @@ -186,6 +190,26 @@ tasks.register('generateLinuxDesktopEntry') { } } +tasks.register('generateLinuxInstallScripts') { + inputs.file(linuxInstallTemplate) + inputs.file(linuxUninstallTemplate) + outputs.file(linuxInstallScript) + outputs.file(linuxUninstallScript) + doLast { + [ + (linuxInstallTemplate.asFile): linuxInstallScript.get().asFile, + (linuxUninstallTemplate.asFile): linuxUninstallScript.get().asFile + ].each { File source, File target -> + target.parentFile.mkdirs() + target.text = source.getText('UTF-8') + .replace('@DISPLAY_NAME@', releaseConfig.displayName) + .replace('@DESCRIPTION@', releaseConfig.description) + .replace('@APP_NAME@', releaseConfig.appName) + .replace('@PACKAGE_NAME@', releaseConfig.packageName) + } + } +} + tasks.register('prepareLinuxPackagingResources', Copy) { dependsOn 'generateReleaseMetadata', 'generateLinuxDesktopEntry' from(releaseConfig.packagingRoot.dir('linux')) @@ -242,15 +266,23 @@ tasks.register('packageLinuxAppImage', Exec) { tasks.register('packageLinuxReleaseZip', Zip) { onlyIf { isLinux } - dependsOn 'packageLinuxAppImage', 'generateLinuxDesktopEntry' + dependsOn 'packageLinuxAppImage', 'generateLinuxInstallScripts' archiveBaseName = releaseConfig.packageName archiveVersion = releaseConfig.version archiveClassifier = 'linux' destinationDirectory = releaseConfig.releaseRoot.map { it.dir('linux') } from(releaseConfig.releaseRoot.map { it.dir("linux/${releaseConfig.appName}") }) { into(releaseConfig.appName) + filesMatching("bin/${releaseConfig.appName}") { + it.permissions { unix(0755) } + } + } + from(linuxInstallScript) { + filePermissions { unix(0755) } + } + from(linuxUninstallScript) { + filePermissions { unix(0755) } } - from(linuxDesktopEntry) } tasks.register('packageWindowsAppImage', Exec) { diff --git a/packaging/linux/install.sh.template b/packaging/linux/install.sh.template new file mode 100644 index 0000000..397c482 --- /dev/null +++ b/packaging/linux/install.sh.template @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Installationsskript för @DISPLAY_NAME@. +# Registrerar en desktop-genväg mot den plats där zip:en har extraherats, +# och säkerställer att launchern är exekverbar. +# +# Användning: kör ./install.sh från samma katalog som @APP_NAME@/ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="${SCRIPT_DIR}/@APP_NAME@" + +if [ ! -d "${INSTALL_DIR}" ]; then + echo "Fel: hittade inte ${INSTALL_DIR}." >&2 + echo "Kör install.sh från samma katalog som @APP_NAME@/." >&2 + exit 1 +fi + +LAUNCHER="${INSTALL_DIR}/bin/@APP_NAME@" +if [ ! -f "${LAUNCHER}" ]; then + echo "Fel: launchern saknas: ${LAUNCHER}" >&2 + exit 1 +fi +chmod +x "${LAUNCHER}" + +ICON="${INSTALL_DIR}/lib/@APP_NAME@.png" +if [ ! -f "${ICON}" ]; then + ICON="${SCRIPT_DIR}/@APP_NAME@.png" +fi + +APPLICATIONS_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/applications" +mkdir -p "${APPLICATIONS_DIR}" + +DESKTOP_FILE="${APPLICATIONS_DIR}/@APP_NAME@.desktop" +cat > "${DESKTOP_FILE}" </dev/null 2>&1; then + update-desktop-database "${APPLICATIONS_DIR}" >/dev/null 2>&1 || true +fi + +echo "Installerat." +echo " Launcher: ${LAUNCHER}" +echo " Genväg: ${DESKTOP_FILE}" +echo +echo "Starta från applikationsmenyn, eller direkt via launchern ovan." diff --git a/packaging/linux/uninstall.sh.template b/packaging/linux/uninstall.sh.template new file mode 100644 index 0000000..3770c91 --- /dev/null +++ b/packaging/linux/uninstall.sh.template @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# +# Avinstallationsskript för @DISPLAY_NAME@. +# Tar bort desktop-genvägen. Radera sedan katalogen @APP_NAME@/ manuellt. + +set -euo pipefail + +APPLICATIONS_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/applications" +DESKTOP_FILE="${APPLICATIONS_DIR}/@APP_NAME@.desktop" + +if [ -f "${DESKTOP_FILE}" ]; then + rm -f "${DESKTOP_FILE}" + echo "Desktop-genvägen borttagen: ${DESKTOP_FILE}" + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "${APPLICATIONS_DIR}" >/dev/null 2>&1 || true + fi +else + echo "Ingen genväg att ta bort (${DESKTOP_FILE} finns inte)." +fi + +echo "Radera katalogen @APP_NAME@/ manuellt om du vill ta bort applikationen helt." +echo "Användardata ligger under \${XDG_DATA_HOME:-\$HOME/.local/share}/alipsa-accounting — ta bort det separat om det inte ska sparas." From e064f5d96a10b7bf1e23386f13dc56b5cb9f590d Mon Sep 17 00:00:00 2001 From: per Date: Tue, 14 Apr 2026 22:47:45 +0200 Subject: [PATCH 5/5] =?UTF-8?q?dist:=20h=C3=A5ll=20dist-roten=20ren=20?= =?UTF-8?q?=E2=80=94=20flytta=20macos-artefakt-skr=C3=A4p=20till=20extras/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS-bygget laddade tidigare upp hela app/build/release/macos/*, vilket inkluderade den uppackade AlipsaAccounting.app-katalogen (alla .dylib, .jar, cacerts, LICENSE, etc.). När release.sh flattnade dist/ hamnade allt skräp i roten tillsammans med själva zip:arna. - CI: macOS-artefakten är nu bara AlipsaAccounting-macos.zip. - release.sh: laddar ned till dist/extras/ och promoverar endast kända distribution-filer (linux-zip, windows-exe, macos-zip, generic-zip) till dist/-roten. Eventuella oväntade filer stannar i dist/extras/ istället för att skräpa ned roten. - gh release create får nu bara toppnivåfiler via mapfile. --- .github/workflows/build-distributions.yml | 2 +- release.sh | 25 +++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-distributions.yml b/.github/workflows/build-distributions.yml index 588e784..7e5e5be 100644 --- a/.github/workflows/build-distributions.yml +++ b/.github/workflows/build-distributions.yml @@ -19,7 +19,7 @@ jobs: - os: macos-latest platform: macos tasks: packageMacosAppImage - artifact: app/build/release/macos/* + artifact: app/build/release/macos/AlipsaAccounting-macos.zip runs-on: ${{ matrix.os }} diff --git a/release.sh b/release.sh index bf9ac1a..951bea5 100755 --- a/release.sh +++ b/release.sh @@ -68,12 +68,24 @@ if [ "$BUILD" = true ]; then echo "Downloading artifacts to $DIST_DIR/" rm -rf "$DIST_DIR" - mkdir -p "$DIST_DIR" - gh run download "$run_id" --repo "$REPO" --dir "$DIST_DIR" + mkdir -p "$DIST_DIR/extras" + gh run download "$run_id" --repo "$REPO" --dir "$DIST_DIR/extras" + + # Promote known distribution files to the dist root; leave anything else + # behind in extras/ so the root stays clean. + shopt -s nullglob + for f in "$DIST_DIR"/extras/*/alipsa-accounting-*.zip \ + "$DIST_DIR"/extras/*/AlipsaAccounting-*.exe \ + "$DIST_DIR"/extras/*/AlipsaAccounting-*.zip \ + "$DIST_DIR"/extras/*/app-*.zip; do + mv "$f" "$DIST_DIR/" + done + shopt -u nullglob - # Flatten: move files out of per-platform subdirectories (no-clobber to avoid overwrites) - find "$DIST_DIR" -mindepth 2 -type f -exec mv -n {} "$DIST_DIR/" \; - find "$DIST_DIR" -mindepth 1 -type d -empty -delete + # Flatten any residual platform subdirs inside extras/ and drop empties. + find "$DIST_DIR/extras" -mindepth 2 -type f -exec mv -n {} "$DIST_DIR/extras/" \; + find "$DIST_DIR/extras" -mindepth 1 -type d -empty -delete + rmdir "$DIST_DIR/extras" 2>/dev/null || true echo "" echo "Generating SHA-256 checksums..." @@ -143,11 +155,12 @@ gpg --verify .asc fi echo "Creating GitHub release $TAG..." + mapfile -t release_assets < <(find "$DIST_DIR" -maxdepth 1 -type f) gh release create "$TAG" \ --repo "$REPO" \ --title "Alipsa Accounting ${VERSION}" \ --notes "$release_body" \ - "$DIST_DIR"/* + "${release_assets[@]}" echo "" release_url=$(gh release view "$TAG" --repo "$REPO" --json url --jq '.url')