name: Release on: push: tags: - 'v*' workflow_dispatch: inputs: tag: description: 'Release tag' required: true type: string is_prerelease: description: 'Pre-release' required: false type: boolean default: false incremental_strategy: description: 'Incremental strategy' required: false type: choice default: release-payload options: - release-payload - commit-range publish_incremental_release: description: 'Publish as incremental release' required: false type: boolean default: true baseline_ref: description: 'Optional baseline tag/version/commit' required: false type: string env: DOTNET_VERSION: '10.0.x' Solution_Name: LanMountainDesktop.slnx DOTNET_gcServer: 1 jobs: prepare: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} assembly_version: ${{ steps.version.outputs.assembly_version }} informational_version: ${{ steps.version.outputs.informational_version }} tag: ${{ steps.version.outputs.tag }} checkout_ref: ${{ steps.version.outputs.checkout_ref }} steps: - name: Checkout repository metadata uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get release info id: version run: | if [[ "${{ github.event_name }}" == "push" ]]; then TAG="${GITHUB_REF#refs/tags/}" CHECKOUT_REF="${GITHUB_REF}" else RAW_TAG="${{ github.event.inputs.tag }}" if [[ "${RAW_TAG}" == refs/tags/* ]]; then TAG="${RAW_TAG#refs/tags/}" elif [[ "${RAW_TAG}" == v* ]]; then TAG="${RAW_TAG}" else TAG="v${RAW_TAG}" fi if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then CHECKOUT_REF="refs/tags/${TAG}" else CHECKOUT_REF="${GITHUB_SHA}" fi fi VERSION="${TAG#v}" IFS='.' read -r -a VERSION_PARTS <<< "${VERSION}" while [ "${#VERSION_PARTS[@]}" -lt 4 ]; do VERSION_PARTS+=("0") done ASSEMBLY_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}.${VERSION_PARTS[3]}" echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "assembly_version=${ASSEMBLY_VERSION}" >> $GITHUB_OUTPUT echo "informational_version=${VERSION}" >> $GITHUB_OUTPUT echo "checkout_ref=${CHECKOUT_REF}" >> $GITHUB_OUTPUT build-windows: needs: prepare runs-on: windows-latest strategy: fail-fast: false matrix: include: - arch: x64 self_contained: true suffix: '' - arch: x86 self_contained: true suffix: '' name: Build_Windows_${{ matrix.arch }}${{ matrix.suffix }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-quality: 'preview' - name: Restore run: dotnet restore ${{ env.Solution_Name }} - name: Build run: > dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal -p:Version=${{ needs.prepare.outputs.version }} -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} - name: Publish Launcher (AOT) run: | $version = "${{ needs.prepare.outputs.version }}" $arch = "${{ matrix.arch }}" $launcherPublishDir = "publish/launcher-win-$arch" Write-Host "Publishing Launcher with AOT for Windows $arch..." # AOT publish dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj ` -c Release ` -o ./$launcherPublishDir ` --self-contained ` -r win-$arch ` -p:PublishAot=true ` -p:PublishSingleFile=true ` -p:IncludeNativeLibrariesForSelfExtract=true ` -p:EnableCompressionInSingleFile=true ` -p:DebugType=none ` -p:DebugSymbols=false ` -p:Version=${{ needs.prepare.outputs.version }} ` -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} if ($LASTEXITCODE -ne 0) { Write-Error "Launcher AOT publish failed" exit 1 } # 鏄剧ず鍙戝竷缁撴? Write-Host "Launcher published to: $launcherPublishDir" $exeFile = Get-ChildItem -Path $launcherPublishDir -Filter "*.exe" | Select-Object -First 1 if ($exeFile) { $size = [Math]::Round($exeFile.Length / 1MB, 2) Write-Host "Launcher executable: $($exeFile.Name) ($size MB)" } # Warn if unexpected extra files are produced $files = Get-ChildItem -Path $launcherPublishDir -File if ($files.Count -gt 1) { Write-Host "Warning: Expected single file but found $($files.Count) files" $files | ForEach-Object { Write-Host " - $($_.Name)" } } shell: pwsh - name: Publish Main App run: | $selfContained = "${{ matrix.self_contained }}" -eq "true" $publishDir = if ($selfContained) { "publish/windows-${{ matrix.arch }}" } else { "publish/windows-${{ matrix.arch }}-lite" } if ($selfContained) { dotnet publish LanMountainDesktop/LanMountainDesktop.csproj ` -c Release ` -o ./$publishDir ` --self-contained ` -r win-${{ matrix.arch }} ` -p:PublishSingleFile=false ` -p:DebugType=none ` -p:DebugSymbols=false ` -p:PublishTrimmed=false ` -p:PublishReadyToRun=false ` -p:Version=${{ needs.prepare.outputs.version }} ` -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} } else { dotnet publish LanMountainDesktop/LanMountainDesktop.csproj ` -c Release ` -o ./$publishDir ` --self-contained:false ` -p:PublishSingleFile=false ` -p:DebugType=none ` -p:DebugSymbols=false ` -p:PublishTrimmed=false ` -p:PublishReadyToRun=false ` -p:Version=${{ needs.prepare.outputs.version }} ` -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} ` -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} } Write-Host "Published to: $publishDir" Write-Host "Self-contained: $selfContained" shell: pwsh - name: Restructure for Launcher run: | $version = "${{ needs.prepare.outputs.version }}" $arch = "${{ matrix.arch }}" $selfContained = "${{ matrix.self_contained }}" -eq "true" $publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" } $launcherPublishDir = "publish/launcher-win-$arch" $appDir = "app-$version" Write-Host "Restructuring for Launcher mode..." Write-Host "Version: $version" Write-Host "Publish dir: $publishDir" $newStructure = "publish-launcher/windows-$arch" New-Item -ItemType Directory -Path $newStructure -Force | Out-Null $appPath = Join-Path $newStructure $appDir Move-Item -Path $publishDir -Destination $appPath -Force $launcherSource = $launcherPublishDir if (Test-Path $launcherSource) { Write-Host "Copying Launcher to root..." Copy-Item -Path "$launcherSource\*" -Destination $newStructure -Recurse -Force } else { Write-Warning "Launcher publish dir not found: $launcherSource" } New-Item -ItemType File -Path (Join-Path $appPath ".current") -Force | Out-Null Write-Host "New directory structure:" Get-ChildItem -Path $newStructure -Recurse -Depth 2 | Select-Object FullName Remove-Item -Path $publishDir -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path $launcherPublishDir -Recurse -Force -ErrorAction SilentlyContinue Move-Item -Path $newStructure -Destination $publishDir -Force shell: pwsh - name: Install Inno Setup and 7z run: | choco install innosetup -y --no-progress choco install 7zip -y --no-progress shell: pwsh - name: Build Installer run: | $version = "${{ needs.prepare.outputs.version }}" $arch = "${{ matrix.arch }}" $selfContained = "${{ matrix.self_contained }}" -eq "true" $suffix = "${{ matrix.suffix }}" $publishDir = if ($selfContained) { "publish\windows-$arch" } else { "publish\windows-$arch-lite" } $installerScript = "LanMountainDesktop\installer\LanMountainDesktop.iss" $outputDir = "build-installer" if (-not (Test-Path -Path $publishDir)) { Write-Error "Publish directory not found: $publishDir" Get-ChildItem -Path "publish" -Directory -ErrorAction SilentlyContinue | Select-Object Name exit 1 } New-Item -ItemType Directory -Path $outputDir -Force | Out-Null if (-not (Test-Path -Path $installerScript)) { Write-Error "Installer script not found: $installerScript" exit 1 } $isccPath = $null $isccCommand = Get-Command ISCC.exe -ErrorAction SilentlyContinue if ($isccCommand) { $isccPath = $isccCommand.Source } $candidatePaths = @( "C:\Program Files (x86)\Inno Setup 6\ISCC.exe", "C:\Program Files\Inno Setup 6\ISCC.exe", "$env:ChocolateyInstall\bin\ISCC.exe", "$env:ChocolateyInstall\lib\innosetup\tools\ISCC.exe" ) if (-not $isccPath) { foreach ($candidate in $candidatePaths) { if ($candidate -and (Test-Path -Path $candidate)) { $isccPath = $candidate break } } } if (-not $isccPath) { Write-Host "ISCC.exe was not found in PATH or known locations." Write-Host "Checked locations:" $candidatePaths | ForEach-Object { Write-Host " - $_" } Write-Host "Chocolatey bin listing (if exists):" Get-ChildItem "$env:ChocolateyInstall\bin" -Filter "*iscc*" -ErrorAction SilentlyContinue | Select-Object FullName Write-Error "Inno Setup compiler not found." exit 1 } Write-Host "Found Inno Setup at: $isccPath" Write-Host "Building installer for Windows $arch with version $version..." $publishDir = (Resolve-Path $publishDir).Path $outputDir = (Resolve-Path $outputDir).Path $installerScript = (Resolve-Path $installerScript).Path $compileArgs = @( "/DMyAppVersion=$version", "/DPublishDir=$publishDir", "/DMyOutputDir=$outputDir", "/DMyAppArch=$arch", "/DMyAppSuffix=$suffix", "/DIsSelfContained=$selfContained", $installerScript ) Write-Host "Compile command: `"$isccPath`" $($compileArgs -join ' ')" & $isccPath @compileArgs if ($LASTEXITCODE -ne 0) { Write-Error "Inno Setup compiler exited with code $LASTEXITCODE" exit 1 } $installerFile = Get-ChildItem -Path $outputDir -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $installerFile) { Write-Error "Failed to create installer" exit 1 } Write-Host "Successfully created: $($installerFile.Name)" Write-Host "Installer size: $([Math]::Round($installerFile.Length / 1MB, 2)) MB" shell: pwsh - name: Upload App Payload uses: actions/upload-artifact@v4 with: name: app-payload-windows-${{ matrix.arch }} path: | publish/windows-${{ matrix.arch }}/** if-no-files-found: error retention-days: 30 - name: Upload Installer uses: actions/upload-artifact@v4 with: name: installer-windows-${{ matrix.arch }} path: build-installer/*.exe if-no-files-found: error retention-days: 30 build-linux: needs: prepare runs-on: ubuntu-latest name: Build_Linux steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y \ libfontconfig1 libfreetype6 \ libx11-6 libxrandr2 libxinerama1 \ libxi6 libxcursor1 libxext6 \ libxrender1 libxkbcommon-x11-0 \ clang zlib1g-dev # Ubuntu 24.04+ moved several packages to t64 names. sudo apt-get install -y libasound2t64 || sudo apt-get install -y libasound2 sudo apt-get install -y libportaudio2t64 || sudo apt-get install -y libportaudio2 # Prefer modern WebKit package, fallback for older images. sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-quality: 'preview' - name: Restore run: dotnet restore ${{ env.Solution_Name }} - name: Build run: > dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal -p:Version=${{ needs.prepare.outputs.version }} -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} - name: Publish Launcher (AOT) run: | echo "Publishing Launcher with AOT for Linux x64..." dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj \ -c Release \ -o ./publish/launcher-linux-x64 \ --self-contained \ -r linux-x64 \ -p:PublishAot=true \ -p:PublishSingleFile=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ -p:EnableCompressionInSingleFile=true \ -p:DebugType=none \ -p:DebugSymbols=false \ -p:Version=${{ needs.prepare.outputs.version }} \ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} if [ $? -ne 0 ]; then echo "Launcher AOT publish failed" exit 1 fi echo "Launcher published to: ./publish/launcher-linux-x64" ls -lh ./publish/launcher-linux-x64/ - name: Publish Main App run: | dotnet publish LanMountainDesktop/LanMountainDesktop.csproj \ -c Release \ -o ./publish/linux-x64-app \ --self-contained \ -r linux-x64 \ -p:PublishSingleFile=false \ -p:SelfContained=true \ -p:DebugType=none \ -p:DebugSymbols=false \ -p:PublishTrimmed=false \ -p:PublishReadyToRun=false \ -p:Version=${{ needs.prepare.outputs.version }} \ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} - name: Restructure for Launcher run: | version="${{ needs.prepare.outputs.version }}" publishDir="publish/linux-x64" appDir="app-$version" launcherDir="publish/launcher-linux-x64" echo "Restructuring for Launcher mode..." echo "Version: $version" mkdir -p "$publishDir" mv "publish/linux-x64-app" "$publishDir/$appDir" if [ -d "$launcherDir" ]; then echo "Copying Launcher to root..." cp -r "$launcherDir"/* "$publishDir/" chmod +x "$publishDir/LanMountainDesktop.Launcher" 2>/dev/null || true else echo "Warning: Launcher publish dir not found: $launcherDir" fi touch "$publishDir/$appDir/.current" echo "New directory structure:" find "$publishDir" -maxdepth 2 | head -50 rm -rf "$launcherDir" - name: Package as DEB run: | version="${{ needs.prepare.outputs.version }}" source="publish/linux-x64" package_name="LanMountainDesktop" package_version="${version}" arch="amd64" desktop_template="LanMountainDesktop/packaging/linux/LanMountainDesktop.desktop" icon_source="LanMountainDesktop/packaging/linux/lanmountaindesktop.png" if [ ! -d "$source" ]; then echo "Error: Source directory not found: $source" ls -la publish/ || echo "publish directory not found" exit 1 fi mkdir -p "build-deb/DEBIAN" mkdir -p "build-deb/usr/local/bin" mkdir -p "build-deb/usr/share/applications" mkdir -p "build-deb/usr/share/pixmaps" mkdir -p "build-deb/usr/share/icons/hicolor/256x256/apps" cp -r "$source"/* "build-deb/usr/local/bin/" item_count=$(find build-deb/usr/local/bin -type f 2>/dev/null | wc -l) echo "DEB package contains $item_count files" if [ "$item_count" -eq 0 ]; then echo "Error: DEB package is empty after copy" exit 1 fi if [ ! -f "$desktop_template" ] || [ ! -f "$icon_source" ]; then echo "Error: Linux desktop resources are missing" ls -la "LanMountainDesktop/packaging/linux" || true exit 1 fi sed \ -e "s|@@EXEC@@|/usr/local/bin/LanMountainDesktop.Launcher|g" \ -e "s|@@ICON@@|lanmountaindesktop|g" \ "$desktop_template" > "build-deb/usr/share/applications/LanMountainDesktop.desktop" cp "$icon_source" "build-deb/usr/share/pixmaps/lanmountaindesktop.png" cp "$icon_source" "build-deb/usr/share/icons/hicolor/256x256/apps/lanmountaindesktop.png" { printf '%s\n' '#!/bin/sh' printf '%s\n' 'set -e' printf '%s\n' 'if command -v update-desktop-database >/dev/null 2>&1; then' printf '%s\n' ' update-desktop-database /usr/share/applications >/dev/null 2>&1 || true' printf '%s\n' 'fi' printf '%s\n' 'if command -v gtk-update-icon-cache >/dev/null 2>&1; then' printf '%s\n' ' gtk-update-icon-cache /usr/share/icons/hicolor >/dev/null 2>&1 || true' printf '%s\n' 'fi' } > "build-deb/DEBIAN/postinst" { printf '%s\n' "Package: $package_name" printf '%s\n' "Version: $package_version" printf '%s\n' "Architecture: $arch" printf '%s\n' "Maintainer: LanMountain Team " printf '%s\n' "Description: LanMountain Desktop Application" printf '%s\n' " A desktop application for LanMountain." } > "build-deb/DEBIAN/control" chmod 755 "build-deb/usr/local/bin/LanMountainDesktop.Launcher" 2>/dev/null || chmod 755 "build-deb/usr/local/bin"/* chmod 644 "build-deb/usr/share/applications/LanMountainDesktop.desktop" chmod 644 "build-deb/usr/share/pixmaps/lanmountaindesktop.png" chmod 644 "build-deb/usr/share/icons/hicolor/256x256/apps/lanmountaindesktop.png" chmod 755 "build-deb/DEBIAN/postinst" if dpkg-deb --build "build-deb" "${package_name}_${package_version}_${arch}.deb"; then echo "Successfully created: ${package_name}_${package_version}_${arch}.deb" ls -lh "${package_name}_${package_version}_${arch}.deb" else echo "Error: Failed to build DEB package" exit 1 fi - name: Upload App Payload uses: actions/upload-artifact@v4 with: name: app-payload-linux-x64 path: | publish/linux-x64/** if-no-files-found: error retention-days: 30 - name: Upload Installer uses: actions/upload-artifact@v4 with: name: installer-linux-x64 path: "*.deb" if-no-files-found: error retention-days: 30 build-macos: needs: prepare runs-on: macos-latest strategy: matrix: arch: [x64, arm64] name: Build_macOS_${{ matrix.arch }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Install dependencies run: brew install portaudio - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-quality: 'preview' - name: Restore run: dotnet restore ${{ env.Solution_Name }} - name: Build run: > dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal -p:Version=${{ needs.prepare.outputs.version }} -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} - name: Publish Launcher (AOT) run: | echo "Publishing Launcher with AOT for macOS ${{ matrix.arch }}..." dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj \ -c Release \ -o ./publish/launcher-macos-${{ matrix.arch }} \ --self-contained \ -r osx-${{ matrix.arch }} \ -p:PublishAot=true \ -p:PublishSingleFile=true \ -p:IncludeNativeLibrariesForSelfExtract=true \ -p:EnableCompressionInSingleFile=true \ -p:DebugType=none \ -p:DebugSymbols=false \ -p:Version=${{ needs.prepare.outputs.version }} \ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} if [ $? -ne 0 ]; then echo "Launcher AOT publish failed" exit 1 fi echo "Launcher published to: ./publish/launcher-macos-${{ matrix.arch }}" ls -lh ./publish/launcher-macos-${{ matrix.arch }}/ - name: Publish Main App run: | dotnet publish LanMountainDesktop/LanMountainDesktop.csproj \ -c Release \ -o ./publish/macos-${{ matrix.arch }}-app \ --self-contained \ -r osx-${{ matrix.arch }} \ -p:PublishSingleFile=false \ -p:SelfContained=true \ -p:DebugType=none \ -p:DebugSymbols=false \ -p:PublishTrimmed=false \ -p:PublishReadyToRun=false \ -p:Version=${{ needs.prepare.outputs.version }} \ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }} - name: Restructure and Package as DMG run: | version="${{ needs.prepare.outputs.version }}" arch="${{ matrix.arch }}" app_name="LanMountainDesktop" package_name="${app_name}-${version}-macos-${arch}" launcherDir="publish/launcher-macos-$arch" appSourceDir="publish/macos-$arch-app" echo "Restructuring for Launcher mode..." echo "Version: $version" mkdir -p "${app_name}.app/Contents/MacOS" appDir="app-$version" mkdir -p "${app_name}.app/Contents/MacOS/$appDir" if [ -d "$appSourceDir" ]; then cp -r "$appSourceDir"/* "${app_name}.app/Contents/MacOS/$appDir/" else echo "Error: Main app source directory not found: $appSourceDir" exit 1 fi if [ -d "$launcherDir" ]; then echo "Copying Launcher to root..." cp -r "$launcherDir"/* "${app_name}.app/Contents/MacOS/" chmod +x "${app_name}.app/Contents/MacOS/LanMountainDesktop.Launcher" 2>/dev/null || true else echo "Warning: Launcher publish dir not found: $launcherDir" fi touch "${app_name}.app/Contents/MacOS/$appDir/.current" mkdir -p "${app_name}.app/Contents/Resources" item_count=$(find "${app_name}.app/Contents/MacOS" -type f | wc -l) echo "App bundle contains $item_count files" if [ "$item_count" -eq 0 ]; then echo "Error: App bundle is empty after copy" exit 1 fi { printf '%s\n' '' printf '%s\n' '' printf '%s\n' '' printf '%s\n' '' printf '%s\n' ' CFBundleExecutable' printf '%s\n' ' LanMountainDesktop.Launcher' printf '%s\n' ' CFBundleName' printf '%s\n' ' LanMountain Desktop' printf '%s\n' ' CFBundleVersion' printf '%s\n' " $version" printf '%s\n' ' CFBundleShortVersionString' printf '%s\n' " $version" printf '%s\n' ' CFBundleIdentifier' printf '%s\n' ' com.lanmountain.desktop' printf '%s\n' ' CFBundlePackageType' printf '%s\n' ' APPL' printf '%s\n' '' printf '%s\n' '' } > "${app_name}.app/Contents/Info.plist" mkdir -p dmg-temp cp -r "${app_name}.app" dmg-temp/ if hdiutil create -volname "${app_name}" -srcfolder dmg-temp -ov -format UDZO "${package_name}.dmg" 2>&1; then echo "Successfully created: ${package_name}.dmg" ls -lh "${package_name}.dmg" else echo "Error: Failed to create DMG" exit 1 fi rm -rf dmg-temp "${app_name}.app" - name: Upload uses: actions/upload-artifact@v4 with: name: installer-macos-${{ matrix.arch }} path: "*.dmg" if-no-files-found: error retention-days: 30 publish-plonds: needs: [ prepare, build-windows, build-linux ] runs-on: ubuntu-latest permissions: contents: read env: VERSION: ${{ needs.prepare.outputs.version }} S3_ENDPOINT: ${{ vars.S3_ENDPOINT }} S3_BUCKET: ${{ vars.S3_BUCKET }} S3_REGION: ${{ vars.S3_REGION }} UPDATE_PRIVATE_KEY_PEM: ${{ secrets.UPDATE_PRIVATE_KEY_PEM }} PLONDS_SIGNING_KEY: ${{ secrets.PLONDS_SIGNING_KEY }} PDC_SIGNING_KEY: ${{ secrets.PDC_SIGNING_KEY }} S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} AWS_DEFAULT_REGION: ${{ vars.S3_REGION }} AWS_REGION: ${{ vars.S3_REGION }} AWS_EC2_METADATA_DISABLED: "true" AWS_REQUEST_CHECKSUM_CALCULATION: "WHEN_REQUIRED" AWS_RESPONSE_CHECKSUM_VALIDATION: "WHEN_REQUIRED" steps: - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive ref: ${{ needs.prepare.outputs.checkout_ref }} - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-quality: 'preview' - name: Download app payload artifacts uses: actions/download-artifact@v4 with: path: artifacts/app-payload pattern: app-payload-* - name: Download installer artifacts uses: actions/download-artifact@v4 with: path: artifacts/installers pattern: installer-* - name: Prepare signing key shell: pwsh run: | $ErrorActionPreference = "Stop" function Test-PemKey { param([string]$PemText) if ([string]::IsNullOrWhiteSpace($PemText)) { return $false } $rsa = [System.Security.Cryptography.RSA]::Create() try { $rsa.ImportFromPem($PemText) return $true } catch { return $false } finally { $rsa.Dispose() } } $candidates = @( $env:PLONDS_SIGNING_KEY, $env:UPDATE_PRIVATE_KEY_PEM, $env:PDC_SIGNING_KEY ) $key = $null foreach ($candidate in $candidates) { if (Test-PemKey $candidate) { $key = $candidate break } } if ([string]::IsNullOrWhiteSpace($key)) { throw "Missing a valid PEM signing key in PLONDS_SIGNING_KEY, UPDATE_PRIVATE_KEY_PEM, or PDC_SIGNING_KEY." } $keyPath = Join-Path $PWD "update-private-key.pem" [System.IO.File]::WriteAllText($keyPath, $key, [System.Text.Encoding]::ASCII) Add-Content -Path $env:GITHUB_ENV -Value "UPDATE_PRIVATE_KEY_PATH=$keyPath" - name: Probe S3 access if: ${{ env.S3_ENDPOINT != '' && env.S3_BUCKET != '' && env.S3_ACCESS_KEY != '' && env.S3_SECRET_KEY != '' }} shell: bash run: | set -euo pipefail aws --version aws --endpoint-url "$S3_ENDPOINT" --region "$S3_REGION" s3 ls "s3://$S3_BUCKET" >/dev/null echo "S3 access probe succeeded for $S3_BUCKET" - name: Build PLONDS assets shell: pwsh run: | $ErrorActionPreference = "Stop" $incrementalStrategy = if ("${{ github.event_name }}" -eq "workflow_dispatch" -and -not [string]::IsNullOrWhiteSpace("${{ github.event.inputs.incremental_strategy }}")) { "${{ github.event.inputs.incremental_strategy }}" } else { "release-payload" } $publishIncrementalRelease = if ("${{ github.event_name }}" -eq "workflow_dispatch" -and -not [string]::IsNullOrWhiteSpace("${{ github.event.inputs.publish_incremental_release }}")) { "${{ github.event.inputs.publish_incremental_release }}" } else { "true" } $baselineRef = if ("${{ github.event_name }}" -eq "workflow_dispatch") { "${{ github.event.inputs.baseline_ref }}" } else { "" } ./scripts/Publish-Plonds.ps1 ` -Version $env:VERSION ` -AppArtifactsRoot (Join-Path $PWD "artifacts/app-payload") ` -InstallerArtifactsRoot (Join-Path $PWD "artifacts/installers") ` -OutputDir (Join-Path $PWD "plonds-output") ` -PrivateKeyPath $env:UPDATE_PRIVATE_KEY_PATH ` -Channel "stable" ` -S3Endpoint $env:S3_ENDPOINT ` -S3Bucket $env:S3_BUCKET ` -S3Region $env:S3_REGION ` -IncrementalStrategy $incrementalStrategy ` -PublishIncrementalRelease $publishIncrementalRelease ` -BaselineRef $baselineRef ` -GitHubRepository "${{ github.repository }}" ` -GitHubTag "${{ needs.prepare.outputs.tag }}" ` -MirrorInstallersToS3 "false" ` -UploadMetaToS3 "false" - name: Upload PLONDS assets uses: actions/upload-artifact@v4 with: name: plonds-assets path: | plonds-output/release-assets/** plonds-output/published/** if-no-files-found: error retention-days: 90 github-release: needs: [ prepare, build-windows, build-linux, build-macos, publish-plonds ] runs-on: ubuntu-latest permissions: contents: write steps: - name: Download installer artifacts uses: actions/download-artifact@v4 with: path: artifacts/installers pattern: installer-* - name: Download PLONDS artifacts uses: actions/download-artifact@v4 with: path: artifacts/plonds pattern: plonds-assets - name: List artifacts structure run: | echo "Artifact directory structure:" find artifacts -type f -o -type d | sort echo "" echo "Files found:" find artifacts -type f -exec ls -lh {} \; echo "" echo "Full tree:" tree artifacts || find artifacts -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g' - name: Flatten artifacts for release run: | echo "Organizing artifacts..." mkdir -p release-files find artifacts/installers -type f \( -name "*.exe" -o -name "*.deb" -o -name "*.dmg" \) -exec cp -v {} release-files/ \; find artifacts/plonds -type f \( -name "files-*.json" -o -name "files-*.json.sig" -o -name "update-*.zip" -o -name "plonds-*.json" -o -name "plonds-*.json.sig" -o -name "plonds-payload-*.zip" \) -exec cp -v {} release-files/ \; echo "" echo "Files ready for release:" ls -lh release-files/ || echo "No files found in release-files" echo "" echo "Total files:" file_count=$(find release-files -type f | wc -l) echo "$file_count" if [ "$file_count" -eq 0 ]; then echo "Error: No release files found" exit 1 fi - name: Create Release uses: ncipollo/release-action@v1 with: tag: ${{ needs.prepare.outputs.tag }} name: ${{ needs.prepare.outputs.tag }} commit: ${{ github.sha }} allowUpdates: true draft: false prerelease: ${{ github.event.inputs.is_prerelease == 'true' }} artifacts: "release-files/**" body: | ## Release ${{ needs.prepare.outputs.version }} ### Windows - **LanMountainDesktop-Setup-${{ needs.prepare.outputs.version }}-x64.exe** - 64-bit installer (includes .NET runtime) - **LanMountainDesktop-Setup-${{ needs.prepare.outputs.version }}-x86.exe** - 32-bit installer (includes .NET runtime) **Note:** The Launcher is now built with AOT (Ahead-of-Time) compilation as a single executable file for faster startup and smaller footprint. Installation: Double-click the .exe file and follow the wizard. ### Incremental Update Assets - **plonds-filemap-windows-x64.json / plonds-filemap-windows-x64.json.sig** - **plonds-filemap-windows-x86.json / plonds-filemap-windows-x86.json.sig** - **plonds-filemap-linux-x64.json / plonds-filemap-linux-x64.json.sig** - **plonds-payload-windows-x64.zip** - **plonds-payload-windows-x86.zip** - **plonds-payload-linux-x64.zip** ### Legacy Fallback Assets - **files-windows-x64.json / files-windows-x64.json.sig / update-windows-x64.zip** - **files-windows-x86.json / files-windows-x86.json.sig / update-windows-x86.zip** - **files-linux-x64.json / files-linux-x64.json.sig / update-linux-x64.zip** Existing users: Host will prefer staged PLONDS payloads and keep the Launcher responsible for apply + rollback. Legacy signed file-map assets remain attached as a fallback path. ### Linux - **LanMountainDesktop-${{ needs.prepare.outputs.version }}-linux-x64.deb** - Debian package (x64) ### macOS - **LanMountainDesktop-${{ needs.prepare.outputs.version }}-macos-x64.dmg** - Intel processor - **LanMountainDesktop-${{ needs.prepare.outputs.version }}-macos-arm64.dmg** - Apple Silicon (M1/M2/M3) See commits for changes. token: ${{ secrets.GITHUB_TOKEN }} publish-plonds-meta: needs: [ prepare, publish-plonds, github-release ] runs-on: ubuntu-latest permissions: contents: read env: S3_ENDPOINT: ${{ vars.S3_ENDPOINT }} S3_BUCKET: ${{ vars.S3_BUCKET }} S3_REGION: ${{ vars.S3_REGION }} S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_KEY }} AWS_DEFAULT_REGION: ${{ vars.S3_REGION }} AWS_REGION: ${{ vars.S3_REGION }} AWS_EC2_METADATA_DISABLED: "true" AWS_REQUEST_CHECKSUM_CALCULATION: "WHEN_REQUIRED" AWS_RESPONSE_CHECKSUM_VALIDATION: "WHEN_REQUIRED" steps: - name: Download PLONDS artifacts uses: actions/download-artifact@v4 with: path: artifacts/plonds pattern: plonds-assets - name: Publish PLONDS meta to S3 if: ${{ env.S3_ENDPOINT != '' && env.S3_BUCKET != '' && env.S3_ACCESS_KEY != '' && env.S3_SECRET_KEY != '' }} shell: bash run: | set -euo pipefail meta_dir="$(find artifacts/plonds -type d -path '*/published/meta' | head -n 1)" if [ -z "${meta_dir}" ]; then echo "Unable to locate published/meta inside PLONDS artifacts" exit 1 fi echo "Publishing PLONDS meta from ${meta_dir}" aws --endpoint-url "$S3_ENDPOINT" --region "$S3_REGION" s3 cp "$meta_dir" "s3://$S3_BUCKET/lanmountain/update/meta/" --recursive --only-show-errors --no-progress