mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
1037 lines
39 KiB
YAML
1037 lines
39 KiB
YAML
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
|
||
}
|
||
|
||
# é<>„剧ã<C2A7>šé<C5A1>™æˆ<C3A6>ç«·ç¼<C3A7>æ’´ç<C2B4>?
|
||
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 <dev@example.com>"
|
||
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' '<?xml version="1.0" encoding="UTF-8"?>'
|
||
printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
|
||
printf '%s\n' '<plist version="1.0">'
|
||
printf '%s\n' '<dict>'
|
||
printf '%s\n' ' <key>CFBundleExecutable</key>'
|
||
printf '%s\n' ' <string>LanMountainDesktop.Launcher</string>'
|
||
printf '%s\n' ' <key>CFBundleName</key>'
|
||
printf '%s\n' ' <string>LanMountain Desktop</string>'
|
||
printf '%s\n' ' <key>CFBundleVersion</key>'
|
||
printf '%s\n' " <string>$version</string>"
|
||
printf '%s\n' ' <key>CFBundleShortVersionString</key>'
|
||
printf '%s\n' " <string>$version</string>"
|
||
printf '%s\n' ' <key>CFBundleIdentifier</key>'
|
||
printf '%s\n' ' <string>com.lanmountain.desktop</string>'
|
||
printf '%s\n' ' <key>CFBundlePackageType</key>'
|
||
printf '%s\n' ' <string>APPL</string>'
|
||
printf '%s\n' '</dict>'
|
||
printf '%s\n' '</plist>'
|
||
} > "${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
|