Skip to content

Bump the nuget-minor-patch group with 5 updates #808

Bump the nuget-minor-patch group with 5 updates

Bump the nuget-minor-patch group with 5 updates #808

Workflow file for this run

name: Build & Release
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, labeled ]
workflow_dispatch:
inputs:
publish_artifacts:
description: 'Build, sign, notarize, and attach platform artifacts'
required: false
default: false
type: boolean
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
build-test:
name: Build & Test
runs-on: macos-26
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: |
if [ -d "/Applications/Xcode_26.2.app" ]; then
XCODE_PATH="/Applications/Xcode_26.2.app"
elif [ -d "/Applications/Xcode_26.2.0.app" ]; then
XCODE_PATH="/Applications/Xcode_26.2.0.app"
else
XCODE_PATH=$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -rV | head -1)
fi
echo "Selected Xcode: $XCODE_PATH"
sudo xcode-select -s "$XCODE_PATH"
xcodebuild -version
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install MAUI workload
run: dotnet workload install maui --version 10.0.102
- name: Build and test libraries
run: |
dotnet build tests/MauiSherpa.Core.Tests --configuration Release
dotnet build tests/MauiSherpa.Workloads.Tests --configuration Release
dotnet build tests/MauiSherpa.AppInspector.Cli.Tests --configuration Release
- name: Run tests
run: |
dotnet test tests/MauiSherpa.Core.Tests --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx"
dotnet test tests/MauiSherpa.Workloads.Tests --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx"
dotnet test tests/MauiSherpa.AppInspector.Cli.Tests --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx"
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: '**/TestResults/*.trx'
retention-days: 7
publish-macos:
name: Publish macOS App
runs-on: macos-26
needs: build-test
if: >-
github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && inputs.publish_artifacts)
|| (github.event_name == 'pull_request' && (
contains(github.event.pull_request.labels.*.name, 'build-artifacts')
|| contains(github.event.pull_request.labels.*.name, 'build-artifacts-notarized')))
permissions:
statuses: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: |
if [ -d "/Applications/Xcode_26.2.app" ]; then
XCODE_PATH="/Applications/Xcode_26.2.app"
elif [ -d "/Applications/Xcode_26.2.0.app" ]; then
XCODE_PATH="/Applications/Xcode_26.2.0.app"
else
XCODE_PATH=$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -rV | head -1)
fi
echo "Selected Xcode: $XCODE_PATH"
sudo xcode-select -s "$XCODE_PATH"
sudo xcodebuild -license accept 2>/dev/null || true
xcodebuild -version
echo "MD_APPLE_SDK_ROOT=$XCODE_PATH" >> $GITHUB_ENV
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install workloads
run: dotnet workload install maui macos --version 10.0.102
- name: Import signing certificate
if: env.APPLE_CERTIFICATE_P12 != ''
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ github.run_id }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo "Importing signing certificate..."
echo -n "$APPLE_CERTIFICATE_P12" | base64 --decode > $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
echo "Certificate setup complete."
- name: Determine version
id: version
run: |
SHA="${GITHUB_SHA::8}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VER="${GITHUB_REF#refs/tags/v}"
else
VER=$(grep '<AppVersion>' Directory.Build.props | sed 's/.*<AppVersion>\(.*\)<\/AppVersion>.*/\1/')
fi
echo "app_version=$VER" >> $GITHUB_OUTPUT
echo "commit_sha=$SHA" >> $GITHUB_OUTPUT
echo "Version: $VER+$SHA"
- name: Publish macOS App (arm64)
run: |
dotnet publish src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj \
-f net10.0-macos \
-c Release \
-r osx-arm64 \
-p:CreatePackage=false \
-p:AppVersion=${{ steps.version.outputs.app_version }} \
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} \
-p:SentryDsn=${{ secrets.SENTRY_DSN }}
- name: Publish macOS App (x64)
run: |
dotnet publish src/MauiSherpa.MacOS/MauiSherpa.MacOS.csproj \
-f net10.0-macos \
-c Release \
-r osx-x64 \
-p:CreatePackage=false \
-p:AppVersion=${{ steps.version.outputs.app_version }} \
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} \
-p:SentryDsn=${{ secrets.SENTRY_DSN }}
- name: Create universal binary
run: |
ARM64_APP="src/MauiSherpa.MacOS/bin/Release/net10.0-macos/osx-arm64/MAUI Sherpa.app"
X64_APP="src/MauiSherpa.MacOS/bin/Release/net10.0-macos/osx-x64/MAUI Sherpa.app"
mkdir -p ./artifacts/macos
cp -R "$ARM64_APP" ./artifacts/macos/
DEST_APP="./artifacts/macos/MAUI Sherpa.app"
BINARY_NAME="MauiSherpa.MacOS"
# Create universal binary with lipo
lipo -create \
"$ARM64_APP/Contents/MacOS/$BINARY_NAME" \
"$X64_APP/Contents/MacOS/$BINARY_NAME" \
-output "$DEST_APP/Contents/MacOS/$BINARY_NAME"
# Merge any native dylibs that differ between architectures
LIPO_FAILURES=0
while IFS= read -r arm64_lib; do
rel_path="${arm64_lib#$ARM64_APP/}"
x64_lib="$X64_APP/$rel_path"
dest_lib="$DEST_APP/$rel_path"
if [ -f "$x64_lib" ]; then
arm64_arch=$(lipo -info "$arm64_lib" 2>/dev/null | grep -o 'arm64' || true)
x64_arch=$(lipo -info "$x64_lib" 2>/dev/null | grep -o 'x86_64' || true)
if [ -n "$arm64_arch" ] && [ -n "$x64_arch" ]; then
if ! lipo -create "$arm64_lib" "$x64_lib" -output "$dest_lib" 2>/dev/null; then
echo "::warning::lipo failed for $rel_path"
LIPO_FAILURES=$((LIPO_FAILURES + 1))
fi
fi
fi
done < <(find "$ARM64_APP/Contents" -name "*.dylib")
if [ "$LIPO_FAILURES" -gt 0 ]; then
echo "::warning::$LIPO_FAILURES dylib(s) failed lipo merge"
fi
# Copy x64-only dylibs not present in arm64 build
find "$X64_APP/Contents" -name "*.dylib" | while read -r x64_lib; do
rel_path="${x64_lib#$X64_APP/}"
arm64_lib="$ARM64_APP/$rel_path"
dest_lib="$DEST_APP/$rel_path"
if [ ! -f "$arm64_lib" ] && [ ! -f "$dest_lib" ]; then
mkdir -p "$(dirname "$dest_lib")"
cp "$x64_lib" "$dest_lib"
echo "Copied x64-only dylib: $rel_path"
fi
done
# Handle architecture-specific managed assemblies (.dll files).
# dotnet publish produces DLLs with ReadyToRun native code for the target
# architecture. The PE header machine field != 0x14c (IL-only) for these.
# The macOS bootstrapper adds both MonoBundle/ and .xamarin/<RID>/ to the
# Trusted Platform Assemblies list. By moving arch-specific DLLs out of
# MonoBundle into .xamarin/<RID>/ directories, each architecture loads only
# compatible assemblies.
MONO="$DEST_APP/Contents/MonoBundle"
mkdir -p "$MONO/.xamarin/osx-arm64"
mkdir -p "$MONO/.xamarin/osx-x64"
for dll in "$MONO"/*.dll; do
[ -f "$dll" ] || continue
name=$(basename "$dll")
# Read PE machine type: offset 0x3C has PE header offset, then +4 for signature, +0 for machine
machine=$(python3 -c "
import struct, sys
with open(sys.argv[1], 'rb') as f:
sig = f.read(2)
if sig != b'MZ':
print('0')
sys.exit()
f.seek(0x3c)
pe_off = struct.unpack('<I', f.read(4))[0]
f.seek(pe_off + 4)
print(struct.unpack('<H', f.read(2))[0])
" "$dll" 2>/dev/null || echo "0")
# 0x14c (332) = IL-only (works on any architecture) — leave in MonoBundle
# 0 = unreadable/unknown — also leave in MonoBundle
if [ "$machine" != "332" ] && [ "$machine" != "0" ]; then
# Architecture-specific DLL: move arm64 version to .xamarin/osx-arm64/
mv "$dll" "$MONO/.xamarin/osx-arm64/$name"
# Copy x64 version to .xamarin/osx-x64/
x64_dll="$X64_APP/Contents/MonoBundle/$name"
if [ -f "$x64_dll" ]; then
cp "$x64_dll" "$MONO/.xamarin/osx-x64/$name"
fi
fi
done
ARM64_COUNT=$(ls "$MONO/.xamarin/osx-arm64/"*.dll 2>/dev/null | wc -l | tr -d ' ')
X64_COUNT=$(ls "$MONO/.xamarin/osx-x64/"*.dll 2>/dev/null | wc -l | tr -d ' ')
echo "Moved $ARM64_COUNT arch-specific DLLs to .xamarin/osx-arm64/"
echo "Copied $X64_COUNT arch-specific DLLs to .xamarin/osx-x64/"
# Preserve runtime-specific files that aren't part of the lipo merge, such as
# the Copilot CLI and bundled unxip helper under Contents/MonoBundle/runtimes/osx-x64/native/.
if [ -d "$X64_APP/Contents/MonoBundle/runtimes" ]; then
mkdir -p "$DEST_APP/Contents/MonoBundle/runtimes"
cp -R "$X64_APP/Contents/MonoBundle/runtimes/." "$DEST_APP/Contents/MonoBundle/runtimes/"
find "$DEST_APP/Contents/MonoBundle/runtimes" \( -name "copilot" -o -name "unxip" \) -type f -exec chmod +x {} \;
fi
# Merge deps.json from both architectures so the runtime resolver
# knows about assemblies and native libraries for both RIDs.
ARM64_DEPS="$ARM64_APP/Contents/MonoBundle/MauiSherpa.MacOS.deps.json"
X64_DEPS="$X64_APP/Contents/MonoBundle/MauiSherpa.MacOS.deps.json"
DEST_DEPS="$DEST_APP/Contents/MonoBundle/MauiSherpa.MacOS.deps.json"
if [ -f "$ARM64_DEPS" ] && [ -f "$X64_DEPS" ]; then
python3 -c "
import json, sys
with open(sys.argv[1]) as f: a = json.load(f)
with open(sys.argv[2]) as f: b = json.load(f)
def deep_merge(base, overlay):
for k, v in overlay.items():
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
deep_merge(base[k], v)
else:
base.setdefault(k, v)
for section in ['targets', 'libraries', 'runtimes']:
if section in a and section in b:
deep_merge(a[section], b[section])
elif section in b:
a[section] = b[section]
with open(sys.argv[3], 'w') as f: json.dump(a, f, indent=2)
" "$ARM64_DEPS" "$X64_DEPS" "$DEST_DEPS"
echo "Merged deps.json from both architectures"
fi
echo "Universal binary created:"
lipo -info "$DEST_APP/Contents/MacOS/$BINARY_NAME"
- name: Repair Copilot runtime load command
run: |
set -euo pipefail
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
BINARY_PATH="$APP_PATH/Contents/MacOS/MauiSherpa.MacOS"
# The arm64 slice ships two LC_LOAD_DYLIB entries for the Copilot runtime:
# the linker records the dylib's own install name, which is the absolute
# path it was built at on the copilot-agent-runtime CI runner, and the
# macOS packaging adds a second, correct entry pointing into the bundle.
# The absolute one does not exist on user machines, so dyld aborts at
# launch (issue #232).
STALE_PREFIX="/Users/runner/work/copilot-agent-runtime"
BUNDLED="@executable_path/../../Contents/MonoBundle/runtimes/osx-arm64/native/libcopilot_runtime.dylib"
# Equivalent spelling of the same file, relative to Contents/MacOS.
# Used when $BUNDLED is already linked, because rewriting the stale entry
# to that identical string produces a duplicate LC_LOAD_DYLIB, which dyld
# rejects just as hard as the missing library ("duplicate linked dylib").
# install_name_tool cannot delete a load command, so the stale slot has to
# be pointed at something valid rather than removed.
BUNDLED_ALT="@executable_path/../MonoBundle/runtimes/osx-arm64/native/libcopilot_runtime.dylib"
STALE=$(otool -arch arm64 -L "$BINARY_PATH" | awk '{print $1}' | grep -F "$STALE_PREFIX" || true)
if [ -n "$STALE" ]; then
if otool -arch arm64 -L "$BINARY_PATH" | awk '{print $1}' | grep -Fxq "$BUNDLED"; then
TARGET="$BUNDLED_ALT"
else
TARGET="$BUNDLED"
fi
echo "Rewriting stale load command:"
echo " $STALE"
echo " -> $TARGET"
install_name_tool -change "$STALE" "$TARGET" "$BINARY_PATH"
else
echo "No stale Copilot runtime load command found."
fi
- name: Verify Copilot runtime load commands
run: |
set -euo pipefail
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
BINARY_PATH="$APP_PATH/Contents/MacOS/MauiSherpa.MacOS"
FAILED=0
# No build-machine paths may survive into a shipped artifact.
if otool -arch arm64 -L "$BINARY_PATH" | grep -F "/Users/runner/"; then
echo "::error::arm64 slice still links a build-machine path"
FAILED=1
fi
# dyld refuses to launch an image with two identical LC_LOAD_DYLIB paths.
DUPES=$(otool -arch arm64 -l "$BINARY_PATH" \
| awk '/LC_LOAD_DYLIB/,/time stamp/' | awk '/name /{print $2}' \
| sort | uniq -d || true)
if [ -n "$DUPES" ]; then
echo "::error::duplicate LC_LOAD_DYLIB entries in arm64 slice: $DUPES"
FAILED=1
fi
# Every non-system dependency must resolve inside the bundle.
otool -arch arm64 -L "$BINARY_PATH" | tail -n +2 | awk '{print $1}' \
| grep -v '^/usr/lib/' | grep -v '^/System/' | while read -r dep; do
case "$dep" in
@executable_path/*)
resolved="$APP_PATH/Contents/MacOS/${dep#@executable_path/}"
if [ ! -f "$resolved" ]; then
echo "::error::unresolved bundled dependency: $dep"
exit 1
fi
;;
@rpath/*|@loader_path/*) ;;
*)
echo "::error::absolute dependency outside the bundle: $dep"
exit 1
;;
esac
done || FAILED=1
[ "$FAILED" -eq 0 ] || exit 1
echo "arm64 load commands verified."
- name: Re-sign with Hardened Runtime
if: env.APPLE_CERTIFICATE_P12 != ''
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
run: |
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
if [ -n "$APP_PATH" ]; then
echo "Re-signing with hardened runtime: $APP_PATH"
# Sign nested code first, then sign the app bundle last.
# Avoid --deep on the final app sign: it overwrites the Copilot CLI's
# Node/V8 entitlements and causes "Failed to reserve virtual memory
# for CodeRange" crashes in release builds.
if [ -d "$APP_PATH/Contents/MonoBundle/runtimes" ]; then
find "$APP_PATH/Contents/MonoBundle/runtimes" \( -name "copilot" -o -name "unxip" \) -type f | while read -r item; do
chmod +x "$item"
if [ "$(basename "$item")" = "copilot" ]; then
codesign --force --options runtime --timestamp \
--preserve-metadata=entitlements \
--sign "$APPLE_CODESIGN_IDENTITY" "$item"
else
codesign --force --options runtime --timestamp \
--sign "$APPLE_CODESIGN_IDENTITY" "$item"
fi
done
fi
# Sign all nested frameworks and dylibs first
find "$APP_PATH/Contents" \( -name "*.dylib" -o -name "*.framework" \) | while read -r item; do
codesign --force --options runtime --timestamp \
--sign "$APPLE_CODESIGN_IDENTITY" "$item"
done
# Sign the app bundle
codesign --force --options runtime --timestamp \
--entitlements src/MauiSherpa.MacOS/Entitlements.plist \
--sign "$APPLE_CODESIGN_IDENTITY" \
"$APP_PATH"
fi
- name: Verify code signature
if: env.APPLE_CERTIFICATE_P12 != ''
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
run: |
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
if [ -n "$APP_PATH" ]; then
echo "Verifying signature for: $APP_PATH"
codesign -dvv "$APP_PATH" 2>&1 | tee /tmp/codesign-output.txt
if grep -q "flags=0x10000(runtime)" /tmp/codesign-output.txt; then
echo "✅ Hardened Runtime is enabled"
else
echo "::error::Hardened Runtime is NOT enabled — notarization will fail"
exit 1
fi
COPILOT_PATH="$APP_PATH/Contents/MonoBundle/runtimes/osx-arm64/native/copilot"
if [ -f "$COPILOT_PATH" ]; then
codesign -d --entitlements :- "$COPILOT_PATH" 2>&1 | tee /tmp/copilot-entitlements.txt
if grep -q "com.apple.security.cs.allow-jit" /tmp/copilot-entitlements.txt \
&& grep -q "com.apple.security.cs.allow-unsigned-executable-memory" /tmp/copilot-entitlements.txt \
&& grep -q "com.apple.security.cs.disable-library-validation" /tmp/copilot-entitlements.txt; then
echo "✅ Copilot helper entitlements are preserved"
else
echo "::error::Copilot helper entitlements are missing — the CLI will crash in release builds"
exit 1
fi
fi
fi
- name: Create ZIP archive
run: |
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
if [ -n "$APP_PATH" ]; then
ditto -c -k --keepParent "$APP_PATH" ./artifacts/MAUI-Sherpa.macos.zip
echo "Created archive: ./artifacts/MAUI-Sherpa.macos.zip"
ls -lh ./artifacts/MAUI-Sherpa.macos.zip
else
echo "No .app bundle found"
exit 1
fi
- name: Upload macOS App
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.macos
path: ./artifacts/MAUI-Sherpa.macos.zip
retention-days: 30
- name: Set commit status with artifact link
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if [ -f "./artifacts/MAUI-Sherpa.macos.zip" ]; then
STATE="success"
DESC="MAUI Sherpa macOS app built successfully"
else
STATE="failure"
DESC="MAUI Sherpa macOS app build failed"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \
-f state="$STATE" \
-f target_url="$ARTIFACT_URL" \
-f description="$DESC" \
-f context="MAUI Sherpa / Download macOS App"
- name: Cleanup keychain
if: always()
run: |
if [ -f "$RUNNER_TEMP/app-signing.keychain-db" ]; then
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
fi
publish-windows:
name: Publish Windows App
runs-on: windows-latest
needs: build-test
if: >-
github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && inputs.publish_artifacts)
|| (github.event_name == 'pull_request' && (
contains(github.event.pull_request.labels.*.name, 'build-artifacts')
|| contains(github.event.pull_request.labels.*.name, 'build-artifacts-notarized')))
permissions:
statuses: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install MAUI workload
run: dotnet workload install maui --version 10.0.102
- name: Determine version
id: version
shell: bash
run: |
SHA="${GITHUB_SHA::8}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VER="${GITHUB_REF#refs/tags/v}"
else
VER=$(grep '<AppVersion>' Directory.Build.props | sed 's/.*<AppVersion>\(.*\)<\/AppVersion>.*/\1/')
fi
echo "app_version=$VER" >> $GITHUB_OUTPUT
echo "commit_sha=$SHA" >> $GITHUB_OUTPUT
echo "Version: $VER+$SHA"
- name: Publish Windows App (x64)
run: |
dotnet publish src/MauiSherpa/MauiSherpa.csproj `
-f net10.0-windows10.0.19041.0 `
-c Release `
-p:RuntimeIdentifierOverride=win-x64 `
-p:WindowsPackageType=None `
-p:WindowsAppSDKSelfContained=true `
--self-contained true `
-p:AppVersion=${{ steps.version.outputs.app_version }} `
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} `
-p:SentryDsn=${{ secrets.SENTRY_DSN }} `
-o ./artifacts/win-x64
- name: Publish Windows App (arm64)
run: |
dotnet publish src/MauiSherpa/MauiSherpa.csproj `
-f net10.0-windows10.0.19041.0 `
-c Release `
-p:RuntimeIdentifierOverride=win-arm64 `
-p:WindowsPackageType=None `
-p:WindowsAppSDKSelfContained=true `
--self-contained true `
-p:AppVersion=${{ steps.version.outputs.app_version }} `
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} `
-p:SentryDsn=${{ secrets.SENTRY_DSN }} `
-o ./artifacts/win-arm64
- name: Create ZIP archives
run: |
$x64Dir = "./artifacts/win-x64"
$x64Exe = Join-Path $x64Dir "MauiSherpa.exe"
if (-not (Test-Path $x64Exe -PathType Leaf)) {
throw "Expected self-contained executable at $x64Exe"
}
Compress-Archive -Path "$x64Dir/*" -DestinationPath ./artifacts/MAUI-Sherpa.win-x64.zip
Write-Host "Created: MAUI-Sherpa.win-x64.zip"
Get-Item ./artifacts/MAUI-Sherpa.win-x64.zip | Format-List Name, Length
$arm64Dir = "./artifacts/win-arm64"
$arm64Exe = Join-Path $arm64Dir "MauiSherpa.exe"
if (-not (Test-Path $arm64Exe -PathType Leaf)) {
throw "Expected self-contained executable at $arm64Exe"
}
Compress-Archive -Path "$arm64Dir/*" -DestinationPath ./artifacts/MAUI-Sherpa.win-arm64.zip
Write-Host "Created: MAUI-Sherpa.win-arm64.zip"
Get-Item ./artifacts/MAUI-Sherpa.win-arm64.zip | Format-List Name, Length
- name: Create Windows installers
shell: pwsh
run: |
function Find-InnoCompiler {
@(
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
"$env:ProgramFiles\Inno Setup 6\ISCC.exe"
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
) | Where-Object { Test-Path $_ -PathType Leaf } | Select-Object -First 1
}
$compiler = Find-InnoCompiler
if ([string]::IsNullOrWhiteSpace($compiler)) {
winget install --id JRSoftware.InnoSetup --exact --source winget --accept-package-agreements --accept-source-agreements --disable-interactivity
if ($LASTEXITCODE -ne 0) {
throw "Failed to install Inno Setup."
}
$compiler = Find-InnoCompiler
}
if ([string]::IsNullOrWhiteSpace($compiler)) {
throw "Inno Setup compiler was not found after installation."
}
$outputDir = (Resolve-Path "./artifacts").Path
$version = "${{ steps.version.outputs.app_version }}"
foreach ($arch in @("x64", "arm64")) {
$sourceDir = (Resolve-Path "./artifacts/win-$arch").Path
& $compiler `
"/DMyAppVersion=$version" `
"/DMyAppArch=$arch" `
"/DMySourceDir=$sourceDir" `
"/DMyOutputDir=$outputDir" `
"build/windows/MauiSherpa.iss"
if ($LASTEXITCODE -ne 0) {
throw "Failed to create the Windows $arch installer."
}
$installer = "./artifacts/MAUI-Sherpa.Setup.win-$arch.exe"
if (-not (Test-Path $installer -PathType Leaf)) {
throw "Expected installer at $installer."
}
Get-Item $installer | Format-List Name, Length
}
- name: Upload Windows App (x64)
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.win-x64
path: |
./artifacts/MAUI-Sherpa.win-x64.zip
./artifacts/MAUI-Sherpa.Setup.win-x64.exe
retention-days: 30
- name: Upload Windows App (arm64)
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.win-arm64
path: |
./artifacts/MAUI-Sherpa.win-arm64.zip
./artifacts/MAUI-Sherpa.Setup.win-arm64.exe
retention-days: 30
- name: Set commit status with artifact link
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if [ -f "./artifacts/MAUI-Sherpa.win-x64.zip" ]; then
STATE="success"
DESC="MAUI Sherpa Windows built successfully (x64 + arm64)"
else
STATE="failure"
DESC="MAUI Sherpa Windows build failed"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \
-f state="$STATE" \
-f target_url="$ARTIFACT_URL" \
-f description="$DESC" \
-f context="MAUI Sherpa / Download Windows App"
publish-linux:
name: Publish Linux App (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
needs: build-test
if: >-
github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && inputs.publish_artifacts)
|| (github.event_name == 'pull_request' && (
contains(github.event.pull_request.labels.*.name, 'build-artifacts')
|| contains(github.event.pull_request.labels.*.name, 'build-artifacts-notarized')))
permissions:
statuses: write
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-latest
rid: linux-x64
- arch: arm64
runner: ubuntu-24.04-arm
rid: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install GTK4 dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev flatpak flatpak-builder
- name: Install MAUI workload
run: dotnet workload install maui-android --version 10.0.102
- name: Install Flatpak runtimes
run: |
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y flathub org.gnome.Platform//47 org.gnome.Sdk//47
- name: Determine version
id: version
run: |
SHA="${GITHUB_SHA::8}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VER="${GITHUB_REF#refs/tags/v}"
else
VER=$(grep '<AppVersion>' Directory.Build.props | sed 's/.*<AppVersion>\(.*\)<\/AppVersion>.*/\1/')
fi
echo "app_version=$VER" >> $GITHUB_OUTPUT
echo "commit_sha=$SHA" >> $GITHUB_OUTPUT
echo "Version: $VER+$SHA"
- name: Pre-build shared dependencies
run: |
dotnet build src/MauiSherpa.Workloads/MauiSherpa.Workloads.csproj \
-c Release \
--disable-build-servers \
-nodeReuse:false
- name: Publish Linux App (${{ matrix.arch }}) with all packages
run: |
dotnet publish src/MauiSherpa.LinuxGtk/MauiSherpa.LinuxGtk.csproj \
-c Release \
-r ${{ matrix.rid }} \
--self-contained true \
--disable-build-servers \
-p:AppVersion=${{ steps.version.outputs.app_version }} \
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} \
-p:SentryDsn=${{ secrets.SENTRY_DSN }} \
-p:UseSharedCompilation=false \
-nodeReuse:false \
-maxcpucount:1 \
-p:CreateAppImage=false \
-p:CreateDeb=true \
-p:CreateFlatpak=false
- name: Collect artifacts
run: |
mkdir -p ./artifacts
# tar.gz archive from publish output
PUBLISH_DIR=$(find src/MauiSherpa.LinuxGtk/bin/Release -type d -name publish | grep "${{ matrix.rid }}" | head -1)
if [ -n "$PUBLISH_DIR" ]; then
tar -czf "./artifacts/MAUI-Sherpa.${{ matrix.rid }}.tar.gz" -C "$PUBLISH_DIR" .
echo "Created: MAUI-Sherpa.${{ matrix.rid }}.tar.gz ($(du -h ./artifacts/MAUI-Sherpa.${{ matrix.rid }}.tar.gz | cut -f1))"
fi
# Collect generated packages (AppImage, Deb, Flatpak are in bin/{type}/, not bin/Release/)
find src/MauiSherpa.LinuxGtk/bin -name "*.AppImage" -o -name "*.deb" -o -name "*.flatpak" | while read f; do
cp "$f" "./artifacts/"
echo "Collected: $(basename $f) ($(du -h $f | cut -f1))"
done
echo "=== All artifacts ==="
ls -lh ./artifacts/
- name: Upload Linux tar.gz
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.${{ matrix.rid }}
path: ./artifacts/MAUI-Sherpa.${{ matrix.rid }}.tar.gz
retention-days: 30
- name: Upload Linux .deb packages
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.linux-deb-${{ matrix.arch }}
path: ./artifacts/*.deb
retention-days: 30
- name: Upload Linux AppImage
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.linux-appimage-${{ matrix.arch }}
path: ./artifacts/*.AppImage
if-no-files-found: ignore
retention-days: 30
- name: Upload Linux Flatpak
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.linux-flatpak-${{ matrix.arch }}
path: ./artifacts/*.flatpak
if-no-files-found: ignore
retention-days: 30
- name: Set commit status with artifact link
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if [ -f "./artifacts/MAUI-Sherpa.${{ matrix.rid }}.tar.gz" ]; then
STATE="success"
DESC="MAUI Sherpa Linux ${{ matrix.arch }} built successfully"
else
STATE="failure"
DESC="MAUI Sherpa Linux ${{ matrix.arch }} build failed"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \
-f state="$STATE" \
-f target_url="$ARTIFACT_URL" \
-f description="$DESC" \
-f context="MAUI Sherpa / Download Linux App (${{ matrix.arch }})"
publish-inspector-tool:
name: Publish Inspector Tool
runs-on: ubuntu-latest
needs: build-test
if: >-
github.event_name == 'push'
|| (github.event_name == 'workflow_dispatch' && inputs.publish_artifacts)
|| (github.event_name == 'pull_request' && (
contains(github.event.pull_request.labels.*.name, 'build-artifacts')
|| contains(github.event.pull_request.labels.*.name, 'build-artifacts-notarized')))
permissions:
contents: read
statuses: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Determine version
id: version
run: |
SHA="${GITHUB_SHA::8}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VER="${GITHUB_REF#refs/tags/v}"
else
VER=$(grep '<AppVersion>' Directory.Build.props | sed 's/.*<AppVersion>\(.*\)<\/AppVersion>.*/\1/')
fi
echo "app_version=$VER" >> $GITHUB_OUTPUT
echo "commit_sha=$SHA" >> $GITHUB_OUTPUT
echo "Version: $VER+$SHA"
- name: Pack sherpa-inspector dotnet tool
run: |
dotnet pack src/MauiSherpa.AppInspector.Cli/Sherpa.AppInspector.Cli.csproj \
-c Release \
-p:AppVersion=${{ steps.version.outputs.app_version }} \
-p:AppCommitSha=${{ steps.version.outputs.commit_sha }} \
-o ./artifacts/nuget
- name: Upload sherpa-inspector NuGet package
uses: actions/upload-artifact@v4
with:
name: sherpa-inspector-dotnet-tool
path: ./artifacts/nuget/*.nupkg
retention-days: 30
- name: Publish sherpa-inspector to NuGet.org
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
if [ -z "$NUGET_API_KEY" ]; then
echo "::warning::NUGET_API_KEY is not configured; skipping NuGet.org publication"
exit 0
fi
dotnet nuget push "./artifacts/nuget/*.nupkg" \
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
- name: Set commit status with package link
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if compgen -G "./artifacts/nuget/*.nupkg" > /dev/null; then
STATE="success"
DESC="sherpa-inspector dotnet tool package built"
else
STATE="failure"
DESC="sherpa-inspector dotnet tool package failed"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \
-f state="$STATE" \
-f target_url="$ARTIFACT_URL" \
-f description="$DESC" \
-f context="MAUI Sherpa / sherpa-inspector Tool"
notarize-macos:
name: Notarize macOS App
runs-on: macos-26
needs: publish-macos
if: >-
((github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && github.event_name == 'push')
|| (github.event_name == 'workflow_dispatch' && inputs.publish_artifacts)
|| (github.event_name == 'pull_request'
&& !github.event.pull_request.head.repo.fork
&& contains(github.event.pull_request.labels.*.name, 'build-artifacts-notarized'))
permissions:
statuses: write
steps:
- name: Download macOS artifact
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.macos
path: ./artifacts
- name: Notarize with retry
env:
APPLE_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
APPLE_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_NOTARIZATION_TEAM_ID }}
run: |
mkdir -p ./artifacts/macos
ditto -x -k ./artifacts/MAUI-Sherpa.macos.zip ./artifacts/macos
APP_PATH=$(find ./artifacts/macos -name "*.app" -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "::error::No .app bundle found in artifact"
exit 1
fi
rm ./artifacts/MAUI-Sherpa.macos.zip
ditto -c -k --keepParent "$APP_PATH" ./artifacts/notarize.zip
MAX_RETRIES=3
RETRY_DELAY=30
for i in $(seq 1 $MAX_RETRIES); do
echo "Notarization attempt $i of $MAX_RETRIES..."
RESULT_FILE=$(mktemp)
if xcrun notarytool submit ./artifacts/notarize.zip \
--apple-id "$APPLE_NOTARIZATION_APPLE_ID" \
--password "$APPLE_NOTARIZATION_PASSWORD" \
--team-id "$APPLE_NOTARIZATION_TEAM_ID" \
--wait \
--output-format json >"$RESULT_FILE"; then
STATUS=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1], encoding='utf-8')).get('status', ''))" "$RESULT_FILE")
SUBMISSION_ID=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1], encoding='utf-8')).get('id', ''))" "$RESULT_FILE")
rm -f "$RESULT_FILE"
if [ "$STATUS" = "Accepted" ]; then
echo "Notarization succeeded on attempt $i"
break
fi
echo "::error::Notarization returned status '$STATUS'"
if [ -n "$SUBMISSION_ID" ]; then
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_NOTARIZATION_APPLE_ID" \
--password "$APPLE_NOTARIZATION_PASSWORD" \
--team-id "$APPLE_NOTARIZATION_TEAM_ID" || true
fi
exit 1
fi
rm -f "$RESULT_FILE"
if [ $i -lt $MAX_RETRIES ]; then
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2))
else
echo "::error::Notarization failed after $MAX_RETRIES attempts"
exit 1
fi
done
rm ./artifacts/notarize.zip
STAPLER_RETRIES=3
STAPLER_DELAY=15
for i in $(seq 1 $STAPLER_RETRIES); do
if xcrun stapler staple "$APP_PATH"; then
echo "Stapling succeeded on attempt $i"
break
fi
if [ $i -lt $STAPLER_RETRIES ]; then
echo "Stapler attempt $i failed, retrying in ${STAPLER_DELAY}s..."
sleep $STAPLER_DELAY
else
echo "::error::Stapler failed after $STAPLER_RETRIES attempts"
exit 1
fi
done
ditto -c -k --keepParent "$APP_PATH" ./artifacts/MAUI-Sherpa.macos.zip
echo "Created notarized archive"
ls -lh ./artifacts/MAUI-Sherpa.macos.zip
- name: Upload notarized macOS App
uses: actions/upload-artifact@v4
with:
name: MAUI-Sherpa.macos
path: ./artifacts/MAUI-Sherpa.macos.zip
overwrite: true
retention-days: 30
- name: Set commit status
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if [ -f "./artifacts/MAUI-Sherpa.macos.zip" ]; then
STATE="success"
DESC="macOS app notarized"
else
STATE="failure"
DESC="macOS notarization failed"
fi
gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \
-f state="$STATE" \
-f target_url="$ARTIFACT_URL" \
-f description="$DESC" \
-f context="MAUI Sherpa / Notarized macOS App"
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [notarize-macos, publish-windows, publish-linux, publish-inspector-tool]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download macOS artifact
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.macos
path: ./release
- name: Download Windows artifact (x64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.win-x64
path: ./release
- name: Download Windows artifact (arm64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.win-arm64
path: ./release
- name: Download Linux tar.gz (x64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.linux-x64
path: ./release
- name: Download Linux tar.gz (arm64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.linux-arm64
path: ./release
- name: Download Linux .deb packages (x64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.linux-deb-x64
path: ./release
- name: Download Linux .deb packages (arm64)
uses: actions/download-artifact@v4
with:
name: MAUI-Sherpa.linux-deb-arm64
path: ./release
- name: Download Linux AppImage (x64)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: MAUI-Sherpa.linux-appimage-x64
path: ./release
- name: Download Linux AppImage (arm64)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: MAUI-Sherpa.linux-appimage-arm64
path: ./release
- name: Download Linux Flatpak (x64)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: MAUI-Sherpa.linux-flatpak-x64
path: ./release
- name: Download Linux Flatpak (arm64)
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: MAUI-Sherpa.linux-flatpak-arm64
path: ./release
- name: Download sherpa-inspector dotnet tool package
uses: actions/download-artifact@v4
with:
name: sherpa-inspector-dotnet-tool
path: ./release
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
./release/MAUI-Sherpa.macos.zip
./release/MAUI-Sherpa.win-x64.zip
./release/MAUI-Sherpa.win-arm64.zip
./release/MAUI-Sherpa.Setup.win-x64.exe
./release/MAUI-Sherpa.Setup.win-arm64.exe
./release/MAUI-Sherpa.linux-x64.tar.gz
./release/MAUI-Sherpa.linux-arm64.tar.gz
./release/*.deb
./release/*.AppImage
./release/*.flatpak
./release/*.nupkg
generate_release_notes: false
draft: false
prerelease: ${{ contains(github.ref, '-') }}
publish-winget:
name: Publish WinGet Manifest
needs: [create-release]
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
uses: ./.github/workflows/publish-winget.yml
with:
tag: ${{ github.ref_name }}
secrets: inherit
update-homebrew:
name: Update Homebrew Tap
runs-on: macos-latest
needs: [create-release]
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
steps:
- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download release asset and compute SHA256
run: |
curl -fSL -o MAUI-Sherpa.macos.zip \
"https://github.com/Redth/MAUI.Sherpa/releases/download/v${VERSION}/MAUI-Sherpa.macos.zip"
SHA256=$(shasum -a 256 MAUI-Sherpa.macos.zip | awk '{print $1}')
echo "SHA256=$SHA256" >> $GITHUB_ENV
- name: Update cask in tap repo
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
git clone https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/Redth/homebrew-tap.git
cd homebrew-tap
cat > Casks/maui-sherpa.rb << CASK
cask "maui-sherpa" do
version "${VERSION}"
sha256 "${SHA256}"
url "https://github.com/Redth/MAUI.Sherpa/releases/download/v#{version}/MAUI-Sherpa.macos.zip"
name "MAUI Sherpa"
desc "Desktop app for managing .NET MAUI developer tools"
homepage "https://github.com/Redth/MAUI.Sherpa"
depends_on macos: :ventura
app "MAUI Sherpa.app"
zap trash: [
"~/.maui-sherpa",
]
end
CASK
# Remove leading whitespace from heredoc
sed -i '' 's/^ //' Casks/maui-sherpa.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/maui-sherpa.rb
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Update maui-sherpa to v${VERSION}"
git push