Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Android Release Build

on:
workflow_dispatch:
inputs:
package_format:
description: Android package output to build
required: true
type: choice
default: both
options:
- both
- aab
- apk

jobs:
build-android:
runs-on: windows-latest

env:
PROJECT_PATH: src/app/ShadersCamera.csproj
TARGET_FRAMEWORK: net10.0-android
OUTPUT_DIR: artifacts/android
MANIFEST_PATH: src/app/Platforms/Android/AndroidManifest.xml
KEYSTORE_FILE_NAME: release.keystore

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Install MAUI workload
shell: pwsh
run: dotnet workload install maui

- name: Decode Android keystore
shell: pwsh
run: |
$keystorePath = Join-Path $pwd $env:KEYSTORE_FILE_NAME
[IO.File]::WriteAllBytes(
$keystorePath,
[Convert]::FromBase64String("${{ secrets.ANDROID_KEYSTORE }}")
)
"KEYSTORE_PATH=$keystorePath" >> $env:GITHUB_ENV

- name: Set version variables
shell: pwsh
run: |
[xml]$project = Get-Content $env:PROJECT_PATH
[xml]$manifest = Get-Content $env:MANIFEST_PATH
$androidNs = "http://schemas.android.com/apk/res/android"

$projectNode = $project.Project.PropertyGroup | Where-Object { $_.ApplicationDisplayVersion } | Select-Object -First 1
$displayBase = if ($projectNode -and $projectNode.ApplicationDisplayVersion) { "$($projectNode.ApplicationDisplayVersion)" } else { "1.0" }

$manifestVersionCode = [int]$manifest.manifest.GetAttribute("versionCode", $androidNs)
$runNumber = [int]"${{ github.run_number }}"
$versionCode = $manifestVersionCode + $runNumber
$versionDisplay = "$displayBase.$runNumber"

"VERSION_DISPLAY=$versionDisplay" >> $env:GITHUB_ENV
"VERSION_CODE=$versionCode" >> $env:GITHUB_ENV

- name: Resolve package formats
shell: pwsh
run: |
$packageFormat = "${{ inputs.package_format }}"
"PACKAGE_FORMAT=$packageFormat" >> $env:GITHUB_ENV

- name: Restore NuGet packages
shell: pwsh
run: dotnet restore "$env:PROJECT_PATH"

- name: Publish signed Android packages
shell: pwsh
run: |
function Publish-AndroidPackage([string]$packageFormat) {
$publishArgs = @(
'publish'
$env:PROJECT_PATH
'-f'
$env:TARGET_FRAMEWORK
'-c'
'Release'
'-p:AndroidKeyStore=true'
"-p:AndroidPackageFormats=$packageFormat"
"-p:AndroidVersionCode=$env:VERSION_CODE"
"-p:ApplicationVersion=$env:VERSION_CODE"
"-p:ApplicationDisplayVersion=$env:VERSION_DISPLAY"
"-p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }}"
"-p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }}"
"-p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"
"-p:AndroidSigningKeyStore=$env:KEYSTORE_PATH"
)

& dotnet @publishArgs
if ($LASTEXITCODE -ne 0) {
throw "dotnet publish failed for package format '$packageFormat'."
}
}

if ($env:PACKAGE_FORMAT -eq 'both') {
Publish-AndroidPackage 'aab'
Publish-AndroidPackage 'apk'
}
else {
Publish-AndroidPackage $env:PACKAGE_FORMAT
}

- name: Collect Android artifacts
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "$env:OUTPUT_DIR" | Out-Null
$files = Get-ChildItem -Path (Split-Path $env:PROJECT_PATH -Parent) -Recurse -File |
Where-Object {
$_.Extension -in '.aab', '.apk' -and
$_.BaseName -like '*-Signed'
}

if (-not $files) {
throw "No signed Android artifacts were produced."
}

foreach ($file in $files) {
Copy-Item $file.FullName -Destination "$env:OUTPUT_DIR" -Force
}

- name: Upload Android artifacts
uses: actions/upload-artifact@v4
with:
name: android-${{ inputs.package_format }}-release
path: artifacts/android
if-no-files-found: error
28 changes: 16 additions & 12 deletions .github/workflows/dotnet-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ on:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:

jobs:
build:

runs-on: windows-latest

steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 9.0.x

- name: Install MAUI workload
run: dotnet workload install maui

- name: Build
run: dotnet build src/app/ShadersCamera.csproj -c Release -f:net9.0-windows10.0.19041.0
- uses: actions/checkout@v4

- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Install MAUI workload
run: dotnet workload install maui

- name: Restore
run: dotnet restore src/app/ShadersCamera.csproj

- name: Build
run: dotnet build src/app/ShadersCamera.csproj -c Release -f:net10.0-windows10.0.19041.0 --no-restore


197 changes: 197 additions & 0 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: iOS IPA Build

on:
workflow_dispatch:

jobs:
validate-signing:
runs-on: macos-15

env:
PROJECT_PATH: src/app/ShadersCamera.csproj
CERTIFICATE_PATH: ${{ github.workspace }}/ios-signing.p12
PROFILE_PATH: ${{ github.workspace }}/ios-signing.mobileprovision
PROFILE_PLIST_PATH: ${{ github.workspace }}/ios-signing-profile.plist
KEYCHAIN_PATH: ${{ github.workspace }}/ios-signing.keychain-db

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Decode signing assets
shell: bash
run: |
echo "${{ secrets.IOS_P12_BASE64 }}" | base64 --decode > "$CERTIFICATE_PATH"
echo "${{ secrets.IOS_MOBILEPROVISION_BASE64 }}" | base64 --decode > "$PROFILE_PATH"

- name: Validate signing materials
shell: bash
run: |
KEYCHAIN_PASSWORD="$(uuidgen)"

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 "${{ secrets.IOS_P12_PASSWORD }}" -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -F "${{ secrets.IOS_CODESIGN_KEY }}" >/dev/null; then
echo "Configured IOS_CODESIGN_KEY was not found in the imported certificate."
exit 1
fi

security cms -D -i "$PROFILE_PATH" > "$PROFILE_PLIST_PATH"

PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$PROFILE_PLIST_PATH")"
PROFILE_APP_ID="$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' "$PROFILE_PLIST_PATH")"
APP_BUNDLE_ID="$(python3 -c 'import os, xml.etree.ElementTree as ET; root = ET.parse(os.environ["PROJECT_PATH"]).getroot(); print(next((node.text.strip() for group in root.findall("PropertyGroup") for node in [group.find("ApplicationId")] if node is not None and node.text), ""))')"

PROFILE_BUNDLE_PATTERN="${PROFILE_APP_ID#*.}"

if [[ "$PROFILE_BUNDLE_PATTERN" == '*' ]]; then
:
elif [[ "$PROFILE_BUNDLE_PATTERN" == *'.*' ]]; then
PROFILE_PREFIX="${PROFILE_BUNDLE_PATTERN%.*}"
if [[ "$APP_BUNDLE_ID" != "$PROFILE_PREFIX".* ]]; then
echo "Provisioning profile wildcard '$PROFILE_APP_ID' does not match bundle id '$APP_BUNDLE_ID'."
exit 1
fi
elif [[ "$PROFILE_BUNDLE_PATTERN" != "$APP_BUNDLE_ID" ]]; then
echo "Provisioning profile app identifier '$PROFILE_APP_ID' does not match bundle id '$APP_BUNDLE_ID'."
exit 1
fi

echo "Validated signing identity and provisioning profile: $PROFILE_NAME"

- name: Cleanup validation assets
if: always()
shell: bash
run: |
security delete-keychain "$KEYCHAIN_PATH" || true
rm -f "$CERTIFICATE_PATH" "$PROFILE_PATH" "$PROFILE_PLIST_PATH"

build-ios:
needs: validate-signing
runs-on: macos-15

env:
PROJECT_PATH: src/app/ShadersCamera.csproj
TARGET_FRAMEWORK: net10.0-ios
OUTPUT_DIR: artifacts/ios
CERTIFICATE_PATH: ${{ github.workspace }}/ios-signing.p12
PROFILE_PATH: ${{ github.workspace }}/ios-signing.mobileprovision
PROFILE_PLIST_PATH: ${{ github.workspace }}/ios-signing-profile.plist
KEYCHAIN_PATH: ${{ github.workspace }}/ios-signing.keychain-db

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Select Xcode 16.4
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.4'

- name: Show Xcode version
shell: bash
run: xcodebuild -version

- name: Install MAUI workload
shell: bash
run: dotnet workload install maui

- name: Decode signing assets
shell: bash
run: |
echo "${{ secrets.IOS_P12_BASE64 }}" | base64 --decode > "$CERTIFICATE_PATH"
echo "${{ secrets.IOS_MOBILEPROVISION_BASE64 }}" | base64 --decode > "$PROFILE_PATH"

- name: Import certificate and provisioning profile
shell: bash
run: |
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"

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 "${{ secrets.IOS_P12_PASSWORD }}" -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security default-keychain -d user -s "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db

mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"

security cms -D -i "$PROFILE_PATH" > "$PROFILE_PLIST_PATH"

PROFILE_UUID="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$PROFILE_PLIST_PATH")"
PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$PROFILE_PLIST_PATH")"

cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.mobileprovision"

echo "IOS_PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"

- name: Set version variables
shell: bash
run: |
DISPLAY_BASE="$(python3 -c 'import os, xml.etree.ElementTree as ET; root = ET.parse(os.environ["PROJECT_PATH"]).getroot(); print(next((node.text.strip() for group in root.findall("PropertyGroup") for node in [group.find("ApplicationDisplayVersion")] if node is not None and node.text), "1.0"))')"

BUILD_NUMBER="${{ github.run_number }}"
echo "VERSION_DISPLAY=$DISPLAY_BASE.$BUILD_NUMBER" >> "$GITHUB_ENV"
echo "VERSION_BUILD=$BUILD_NUMBER" >> "$GITHUB_ENV"

- name: Restore NuGet packages
shell: bash
run: |
dotnet restore "$PROJECT_PATH" \
-p:RuntimeIdentifier=ios-arm64

- name: Publish signed IPA
shell: bash
run: |
dotnet publish "$PROJECT_PATH" \
-f "$TARGET_FRAMEWORK" \
-c Release \
-p:RuntimeIdentifier=ios-arm64 \
-p:ArchiveOnBuild=true \
-p:BuildIpa=true \
-p:ProvisioningType=manual \
-p:CodesignKey="${{ secrets.IOS_CODESIGN_KEY }}" \
-p:CodesignProvision="$IOS_PROFILE_NAME" \
-p:ApplicationDisplayVersion="$VERSION_DISPLAY" \
-p:ApplicationVersion="$VERSION_BUILD"

- name: Collect IPA artifact
shell: bash
run: |
mkdir -p "$OUTPUT_DIR"
PROJECT_DIR="$(dirname "$PROJECT_PATH")"
IPA_PATH="$(find "$PROJECT_DIR/bin/Release/net10.0-ios" -type f -name "*.ipa" | head -n 1)"

if [[ -z "$IPA_PATH" ]]; then
echo "No .ipa file was produced."
exit 1
fi

cp "$IPA_PATH" "$OUTPUT_DIR/"

- name: Upload iOS IPA artifact
uses: actions/upload-artifact@v4
with:
name: ios-ipa-release
path: artifacts/ios
if-no-files-found: error

- name: Cleanup signing assets
if: always()
shell: bash
run: |
security delete-keychain "$KEYCHAIN_PATH" || true
rm -f "$CERTIFICATE_PATH" "$PROFILE_PATH" "$PROFILE_PLIST_PATH"
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/$IOS_PROFILE_UUID.mobileprovision" || true
Loading
Loading