diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1d6707d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + # Keep the SHA-pinned GitHub Actions current. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + # NuGet packages across the solution. + - package-ecosystem: nuget + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8f63c2..6403bd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 533a359..f6bde2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,42 +1,173 @@ name: Release +# Pushing a version tag (v0.2.0) builds and publishes the Windows + Linux installers. Building on a +# private repo bills Actions minutes with steep multipliers (macOS 10x, Windows 2x), so releases are +# deliberate — tag when you mean it — rather than one per merge. macOS carries the 10x multiplier and +# is built only on demand via workflow_dispatch, which attaches the .dmg to the release for that +# version. on: push: - tags: [ 'v*' ] + tags: ['v*'] + workflow_dispatch: + inputs: + version: + description: Version to build the macOS .dmg for (e.g. 0.2.0), matching an existing v-tag release. + required: true permissions: contents: write +env: + PUBLISH_FLAGS: >- + -c Release --self-contained true + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true + -p:DebugType=none -p:DebugSymbols=false + jobs: - publish: - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - rid: win-x64 - asset: lagebuch-win-x64.exe - artifact: Feuerwehr.App.exe - - os: ubuntu-latest - rid: linux-x64 - asset: lagebuch-linux-x64 - artifact: Feuerwehr.App - runs-on: ${{ matrix.os }} + version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + steps: + # tag push -> strip the leading 'v' from the tag; dispatch -> the supplied input. + - id: v + env: + REF_NAME: ${{ github.ref_name }} + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ "${{ github.event_name }}" = "push" ]; then + echo "version=${REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT" + fi + + windows: + needs: version + if: github.event_name == 'push' + runs-on: windows-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '8.0.x' + - name: Publish + run: > + dotnet publish src/Feuerwehr.App/Feuerwehr.App.csproj -r win-x64 ${{ env.PUBLISH_FLAGS }} + -p:Version=${{ needs.version.outputs.version }} -o publish + - name: Install WiX + run: | + dotnet tool install --global wix --version 5.0.2 + echo "$env:USERPROFILE\.dotnet\tools" >> $env:GITHUB_PATH + - name: Build MSI + # Absolute paths: WiX resolves relative to the .wxs file's own directory, + # not the working directory, so a bare "publish" would look under packaging/windows. + run: > + wix build packaging/windows/Lagebuch.wxs + -d Version=${{ needs.version.outputs.version }} + -d "PublishDir=${{ github.workspace }}\publish" + -d "IconFile=${{ github.workspace }}\src\Feuerwehr.App\Assets\icon.ico" + -o "lagebuch-${{ needs.version.outputs.version }}-x64.msi" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: msi + path: '*.msi' + if-no-files-found: error + + linux: + needs: version + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: '8.0.x' + - name: Publish + run: > + dotnet publish src/Feuerwehr.App/Feuerwehr.App.csproj -r linux-x64 ${{ env.PUBLISH_FLAGS }} + -p:Version=${{ needs.version.outputs.version }} -o publish + - name: Build .deb + run: | + chmod +x packaging/linux/build-deb.sh + packaging/linux/build-deb.sh \ + "${{ needs.version.outputs.version }}" \ + publish \ + src/Feuerwehr.App/Assets/icon-1024.png \ + dist + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: deb + path: dist/*.deb + if-no-files-found: error + + release: + needs: [version, windows, linux] + runs-on: ubuntu-latest + # Publish the Windows + Linux installers to the release for the pushed tag. + if: github.event_name == 'push' steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: dist + - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + tag_name: v${{ needs.version.outputs.version }} + name: Lagebuch v${{ needs.version.outputs.version }} + prerelease: true + files: dist/**/* + body: | + Getaggter Release-Build. + + | Plattform | Datei | + |-----------|-------| + | Windows | `lagebuch-${{ needs.version.outputs.version }}-x64.msi` | + | Linux | `lagebuch_${{ needs.version.outputs.version }}_amd64.deb` | + + Ein macOS-Build (Apple Silicon, `.dmg`) wird auf Anfrage bereitgestellt und dann hier + angehängt. + + **Die Pakete sind nicht signiert.** Beim ersten Start: + - **Windows**: SmartScreen → *Weitere Informationen* → *Trotzdem ausführen*. + - **Linux**: `sudo dpkg -i lagebuch_*.deb` (oder `sudo apt install ./lagebuch_*.deb`). + - **macOS**: Rechtsklick auf Lagebuch.app → *Öffnen* (einmalig bestätigen), + oder `xattr -dr com.apple.quarantine /Applications/Lagebuch.app`. + + # macOS carries the 10x minute multiplier, so it is not built on every tag. Run this workflow + # manually (workflow_dispatch) with the version of an existing release to add the .dmg to it. + macos: + needs: version + if: github.event_name == 'workflow_dispatch' + runs-on: macos-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: '8.0.x' - name: Publish run: > - dotnet publish src/Feuerwehr.App/Feuerwehr.App.csproj - -c Release -r ${{ matrix.rid }} --self-contained true - -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true - -o publish - - name: Rename artifact - shell: bash - run: cp "publish/${{ matrix.artifact }}" "${{ matrix.asset }}" - - name: Upload to release - uses: softprops/action-gh-release@v2 - with: - files: ${{ matrix.asset }} + dotnet publish src/Feuerwehr.App/Feuerwehr.App.csproj -r osx-arm64 ${{ env.PUBLISH_FLAGS }} + -p:Version=${{ needs.version.outputs.version }} -o publish + - name: Build .dmg + run: | + chmod +x packaging/macos/build-dmg.sh + packaging/macos/build-dmg.sh \ + "${{ needs.version.outputs.version }}" \ + publish \ + src/Feuerwehr.App/Assets/icon-1024.png \ + dist + # Always downloadable from the run; also attach to the matching release if one exists. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dmg + path: dist/*.dmg + if-no-files-found: error + - name: Attach .dmg to the release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ needs.version.outputs.version }} + run: | + if gh release view "v${VERSION}" >/dev/null 2>&1; then + gh release upload "v${VERSION}" dist/*.dmg --clobber + else + echo "No release v${VERSION} yet — .dmg is available as a workflow artifact only." + fi diff --git a/README.md b/README.md index f186fb1..8618af7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,34 @@ Replaces a legacy macro-enabled Excel template with a robust, offline, cross-pla Under construction. See plans under `docs/` (note: design/plan artifacts are not committed). +## Install + +Pushing a version tag publishes a release under [Releases](../../releases) with an installer per +platform: + +```bash +git tag v0.2.0 && git push origin v0.2.0 +``` + +| Platform | File | Built | +|----------|------|-------| +| Windows | `lagebuch--x64.msi` | on every tag | +| Linux (Debian/Ubuntu) | `lagebuch__amd64.deb` | on every tag | +| macOS (Apple Silicon) | `lagebuch--macos-arm64.dmg` | on request | + +macOS runners bill Actions minutes at 10× on a private repo, so the `.dmg` is built on demand rather +than on every tag: run the **Release** workflow manually (Actions → Release → *Run workflow*) with +the release version, and the `.dmg` is attached to that release. + +The packages are **not code-signed**, so the OS warns on first launch: + +- **Windows** — run the `.msi`; if SmartScreen appears, *More info → Run anyway*. +- **macOS** — open the `.dmg`, drag Lagebuch to Applications, then **right-click the app → Open** + once (or `xattr -dr com.apple.quarantine /Applications/Lagebuch.app`). +- **Linux** — `sudo dpkg -i lagebuch_*.deb` (or `sudo apt install ./lagebuch_*.deb`). + +All builds are self-contained; no .NET runtime needs to be installed separately. + ## Build & Test ```bash diff --git a/packaging/linux/build-deb.sh b/packaging/linux/build-deb.sh new file mode 100755 index 0000000..9ef4b5e --- /dev/null +++ b/packaging/linux/build-deb.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Build a .deb from a published self-contained Lagebuch. Reproducible on any Linux box with +# dpkg-deb, so the CI leg and a local smoke test run the exact same steps. +# +# build-deb.sh +set -euo pipefail + +VERSION="$1" +PUBLISH_DIR="$2" +ICON_PNG="$3" +OUT_DIR="$4" + +PKG="lagebuch" +ROOT="$(mktemp -d)" +trap 'rm -rf "$ROOT"' EXIT + +# --- filesystem layout ----------------------------------------------------------------------- +# The self-contained payload lives under /usr/lib/lagebuch; /usr/bin/lagebuch is a thin launcher +# so the binary is on PATH under a friendly name regardless of the publish output's filename. +install -d "$ROOT/usr/lib/$PKG" +cp -r "$PUBLISH_DIR"/. "$ROOT/usr/lib/$PKG/" +chmod +x "$ROOT/usr/lib/$PKG/Feuerwehr.App" + +install -d "$ROOT/usr/bin" +cat > "$ROOT/usr/bin/$PKG" <<'LAUNCH' +#!/bin/sh +exec /usr/lib/lagebuch/Feuerwehr.App "$@" +LAUNCH +chmod +x "$ROOT/usr/bin/$PKG" + +install -Dm644 "$ICON_PNG" "$ROOT/usr/share/icons/hicolor/512x512/apps/$PKG.png" + +install -d "$ROOT/usr/share/applications" +cat > "$ROOT/usr/share/applications/$PKG.desktop" < "$ROOT/DEBIAN/control" < +Installed-Size: $SIZE_KB +Description: Lagebuch — Einsatzdokumentation + Digitales Einsatztagebuch und Lagedarstellung für die Feuerwehr. + Self-contained; no .NET runtime required. +CONTROL + +mkdir -p "$OUT_DIR" +DEB="$OUT_DIR/${PKG}_${VERSION}_amd64.deb" +# dpkg-deb's progress line goes to stderr so stdout is only the artifact path. +dpkg-deb --root-owner-group --build "$ROOT" "$DEB" 1>&2 +echo "$DEB" diff --git a/packaging/macos/build-dmg.sh b/packaging/macos/build-dmg.sh new file mode 100644 index 0000000..46d468a --- /dev/null +++ b/packaging/macos/build-dmg.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Build an unsigned Lagebuch.app and wrap it in a drag-to-Applications .dmg. macOS-only +# (iconutil / hdiutil / sips / codesign). Runs on the macos-latest CI runner. +# +# build-dmg.sh +set -euo pipefail + +VERSION="$1" +PUBLISH_DIR="$2" +ICON_PNG="$3" +OUT_DIR="$4" + +APP="Lagebuch.app" +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +APP_DIR="$STAGE/$APP" +install -d "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources" + +# --- payload --------------------------------------------------------------------------------- +cp -R "$PUBLISH_DIR"/. "$APP_DIR/Contents/MacOS/" +chmod +x "$APP_DIR/Contents/MacOS/Feuerwehr.App" + +# --- icon: 1024 png -> multi-size .icns ------------------------------------------------------ +ICONSET="$STAGE/Lagebuch.iconset" +mkdir -p "$ICONSET" +for s in 16 32 64 128 256 512; do + sips -z $s $s "$ICON_PNG" --out "$ICONSET/icon_${s}x${s}.png" >/dev/null + sips -z $((s*2)) $((s*2)) "$ICON_PNG" --out "$ICONSET/icon_${s}x${s}@2x.png" >/dev/null +done +iconutil -c icns "$ICONSET" -o "$APP_DIR/Contents/Resources/Lagebuch.icns" + +# --- Info.plist ------------------------------------------------------------------------------ +cat > "$APP_DIR/Contents/Info.plist" < + + + + CFBundleName Lagebuch + CFBundleDisplayName Lagebuch + CFBundleIdentifier de.feuerwehr.lagebuch + CFBundleVersion $VERSION + CFBundleShortVersionString$VERSION + CFBundleExecutable Feuerwehr.App + CFBundleIconFile Lagebuch + CFBundlePackageType APPL + LSMinimumSystemVersion 11.0 + NSHighResolutionCapable + + +PLIST + +# --- ad-hoc sign ----------------------------------------------------------------------------- +# No Developer ID, so sign with the ad-hoc identity "-". This is what lets the app launch at all +# once the user allows it in Gatekeeper; without any signature Gatekeeper reports "damaged". +codesign --force --deep --sign - "$APP_DIR" + +# --- .dmg (drag-to-Applications) ------------------------------------------------------------- +ln -s /Applications "$STAGE/Applications" +mkdir -p "$OUT_DIR" +DMG="$OUT_DIR/lagebuch-${VERSION}-macos-arm64.dmg" +hdiutil create -volname "Lagebuch" -srcfolder "$STAGE" -ov -format UDZO "$DMG" 1>&2 +echo "$DMG" diff --git a/packaging/windows/Lagebuch.wxs b/packaging/windows/Lagebuch.wxs new file mode 100644 index 0000000..e462629 --- /dev/null +++ b/packaging/windows/Lagebuch.wxs @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Feuerwehr.App/Assets/icon-1024.png b/src/Feuerwehr.App/Assets/icon-1024.png new file mode 100644 index 0000000..8ba99ba Binary files /dev/null and b/src/Feuerwehr.App/Assets/icon-1024.png differ diff --git a/src/Feuerwehr.App/Assets/icon.ico b/src/Feuerwehr.App/Assets/icon.ico new file mode 100644 index 0000000..e272733 Binary files /dev/null and b/src/Feuerwehr.App/Assets/icon.ico differ diff --git a/src/Feuerwehr.App/Assets/icon.svg b/src/Feuerwehr.App/Assets/icon.svg new file mode 100644 index 0000000..f08c7b0 --- /dev/null +++ b/src/Feuerwehr.App/Assets/icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Feuerwehr.App/Feuerwehr.App.csproj b/src/Feuerwehr.App/Feuerwehr.App.csproj index 1615c0e..d79f3fa 100644 --- a/src/Feuerwehr.App/Feuerwehr.App.csproj +++ b/src/Feuerwehr.App/Feuerwehr.App.csproj @@ -6,7 +6,13 @@ enable true app.manifest + Assets\icon.ico true + + Lagebuch + Lagebuch + Feuerwehr + 0.1.0