App Build and Release #156
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: App Build and Release | |
| concurrency: | |
| group: build-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| actions: read | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| target_branch: | |
| description: 'Branch to build' | |
| default: 'dev' | |
| required: true | |
| push: | |
| tags: | |
| - 'release-v*' | |
| jobs: | |
| build: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest, windows-latest, ubuntu-latest] | |
| arch: [x64, arm64] | |
| steps: | |
| - name: Free Disk Space (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| df -h | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true | |
| docker system prune -af || true | |
| sudo apt clean | |
| df -h | |
| - name: Free Disk Space (macOS) | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| df -h | |
| sudo rm -rf /usr/local/lib/android /opt/hostedtoolcache /usr/local/share/powershell /usr/local/share/chromium /opt/CodeQL || true | |
| brew cleanup || true | |
| brew autoremove || true | |
| df -h | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| # If manually triggered, use the input branch; otherwise use the tag ref | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target_branch || github.ref_name }} | |
| fetch-depth: 0 | |
| - name: Ensure full git history | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if git rev-parse --is-shallow-repository | grep -q true; then | |
| git fetch --prune --unshallow --tags | |
| else | |
| git fetch --prune --tags | |
| fi | |
| - name: Derive Release Version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_REF:-}" == refs/tags/release-v* ]]; then | |
| RELEASE_VERSION="${GITHUB_REF#refs/tags/release-v}" | |
| elif [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then | |
| RELEASE_VERSION="${GITHUB_REF#refs/tags/v}" | |
| else | |
| RELEASE_VERSION="0.0.0-dev+$(git rev-parse --short HEAD)" | |
| fi | |
| echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$GITHUB_ENV" | |
| echo "RELEASE_VERSION_WITH_V=v$RELEASE_VERSION" >> "$GITHUB_ENV" | |
| - name: Setup .NET (8.x) | |
| uses: actions/setup-dotnet@v3 | |
| with: | |
| dotnet-version: '8.0.x' | |
| - name: Restore dependencies | |
| run: dotnet restore Companion.Desktop/Companion.Desktop.csproj | |
| - name: Clean build directories | |
| shell: bash | |
| run: | | |
| rm -rf ./Companion.Desktop/bin ./Companion.Desktop/obj || true | |
| - name: Build project | |
| run: dotnet build Companion.Desktop/Companion.Desktop.csproj --configuration Release | |
| - name: Run Tests | |
| if: matrix.os == 'ubuntu-latest' && matrix.arch == 'x64' | |
| run: dotnet test Companion.Tests/Companion.Tests.csproj --configuration Release --logger "trx;LogFileName=test-results.trx" | |
| - name: Publish Project | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| dotnet publish Companion.Desktop/Companion.Desktop.csproj \ | |
| -c Release -r win-${{ matrix.arch }} --self-contained true \ | |
| -p:PublishSingleFile=false -p:PublishReadyToRun=true -p:IncludeNativeLibrariesForSelfExtract=true | |
| elif [[ "${{ runner.os }}" == "macOS" ]]; then | |
| dotnet publish Companion.Desktop/Companion.Desktop.csproj \ | |
| -c Release -r osx-${{ matrix.arch }} --self-contained true \ | |
| -p:PublishSingleFile=true -p:UseAppHost=true | |
| else | |
| dotnet publish Companion.Desktop/Companion.Desktop.csproj \ | |
| -c Release -r linux-${{ matrix.arch }} --self-contained true \ | |
| -p:PublishSingleFile=true | |
| fi | |
| - name: Normalize VERSION in publish output | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| PUBLISH_DIR="./Companion.Desktop/bin/Release/net8.0/win-${{ matrix.arch }}/publish" | |
| elif [[ "${{ runner.os }}" == "macOS" ]]; then | |
| PUBLISH_DIR="./Companion.Desktop/bin/Release/net8.0/osx-${{ matrix.arch }}/publish" | |
| else | |
| PUBLISH_DIR="./Companion.Desktop/bin/Release/net8.0/linux-${{ matrix.arch }}/publish" | |
| fi | |
| if [[ -d "$PUBLISH_DIR" ]]; then | |
| echo "${RELEASE_VERSION_WITH_V}" > "${PUBLISH_DIR}/VERSION" | |
| echo "Wrote ${RELEASE_VERSION_WITH_V} to ${PUBLISH_DIR}/VERSION" | |
| fi | |
| # ----- macOS: bundle .app + sign + dmg ----- | |
| - name: Decode signing certificate | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then | |
| echo "No APPLE_CERTIFICATE; skipping signing setup." | |
| exit 0 | |
| fi | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > macos_signing_cert.p12 | |
| - name: Create and manage custom keychain | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| MY_KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then | |
| echo "No APPLE_CERTIFICATE; skipping keychain setup." | |
| exit 0 | |
| fi | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| security create-keychain -p "$MY_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$MY_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import macos_signing_cert.p12 \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| # Let codesign access the key without UI prompts | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$MY_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Ensure codesign searches this keychain | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| security default-keychain -d user -s "$KEYCHAIN_PATH" | |
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV | |
| - name: Verify codesigning identity is present | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then | |
| echo "No APPLE_CERTIFICATE; skipping identity verification." | |
| exit 0 | |
| fi | |
| if [[ -z "${KEYCHAIN_PATH:-}" ]]; then | |
| echo "ERROR: KEYCHAIN_PATH not set. Keychain setup likely did not run." | |
| exit 1 | |
| fi | |
| echo "Searching identities in: $KEYCHAIN_PATH" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true | |
| if [[ -z "${CODESIGN_IDENTITY:-}" ]]; then | |
| echo "ERROR: MACOS_CODESIGN_IDENTITY is empty" | |
| exit 1 | |
| fi | |
| if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -Fq "$CODESIGN_IDENTITY"; then | |
| echo "ERROR: Identity not found in keychain: $CODESIGN_IDENTITY" | |
| exit 1 | |
| fi | |
| - name: Package (and sign) macOS App | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} # "Developer ID Application: Mike Carr (EQKLR945TW)" | |
| run: | | |
| set -euo pipefail | |
| APP_NAME="Companion" | |
| # NOTE: project paths changed — icons now live under Companion.Desktop | |
| ICON_SRC="./Companion/Assets/Icons/OpenIPC.icns" | |
| PUBLISH_DIR="./Companion.Desktop/bin/Release/net8.0/osx-${{ matrix.arch }}/publish" | |
| APP_DIR="${APP_NAME}.app" | |
| DMG_NAME="Companion-macos-${{ matrix.arch }}.dmg" | |
| echo "Publish dir:" | |
| ls -la "$PUBLISH_DIR" | |
| rm -rf "$APP_DIR" | |
| mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" | |
| cp -R "${PUBLISH_DIR}/"* "${APP_DIR}/Contents/MacOS/" | |
| [[ -f "$ICON_SRC" ]] && cp "$ICON_SRC" "${APP_DIR}/Contents/Resources/OpenIPC.icns" || true | |
| if [[ -f "${APP_DIR}/Contents/MacOS/Companion.Desktop" ]]; then | |
| chmod +x "${APP_DIR}/Contents/MacOS/Companion.Desktop" | |
| fi | |
| cat > "${APP_DIR}/Contents/Info.plist" <<EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>CFBundleName</key><string>${APP_NAME}</string> | |
| <key>CFBundleDisplayName</key><string>${APP_NAME}</string> | |
| <key>CFBundleExecutable</key><string>Companion.Desktop</string> | |
| <key>CFBundleIdentifier</key><string>org.openipc.Companion</string> | |
| <key>CFBundleVersion</key><string>${{ env.RELEASE_VERSION }}</string> | |
| <key>CFBundleShortVersionString</key><string>${{ env.RELEASE_VERSION }}</string> | |
| <key>CFBundlePackageType</key><string>APPL</string> | |
| <key>LSMinimumSystemVersion</key><string>13.0</string> | |
| <key>CFBundleIconFile</key><string>OpenIPC.icns</string> | |
| </dict> | |
| </plist> | |
| EOF | |
| # Ensure runtime version check reads the tagged release version. | |
| echo "${RELEASE_VERSION_WITH_V}" > "${APP_DIR}/Contents/MacOS/VERSION" | |
| # Sign only if everything required is present | |
| if [[ -n "${CODESIGN_IDENTITY:-}" && -n "${KEYCHAIN_PATH:-}" ]]; then | |
| echo "Signing app with identity: $CODESIGN_IDENTITY" | |
| codesign --keychain "$KEYCHAIN_PATH" \ | |
| --deep --force --verify --verbose \ | |
| --sign "${CODESIGN_IDENTITY}" "${APP_DIR}" | |
| codesign --verify --verbose=4 "${APP_DIR}" | |
| spctl --assess --type execute --verbose "${APP_DIR}" || true | |
| else | |
| echo "Signing skipped (missing CODESIGN_IDENTITY or KEYCHAIN_PATH)" | |
| fi | |
| rm -f "${DMG_NAME}" | |
| mkdir -p dmg_build && cp -R "${APP_DIR}" dmg_build/ && ln -s /Applications dmg_build/Applications | |
| hdiutil create -volname "Companion" -srcfolder dmg_build -ov -format UDZO -fs HFS+ -size 500m "${DMG_NAME}" | |
| rm -rf dmg_build | |
| rm -f "Companion-macos-${{ matrix.arch }}.zip" | |
| zip -r "Companion-macos-${{ matrix.arch }}.zip" "${APP_DIR}" | |
| # ----- Linux packaging ----- | |
| - name: Linux - zip publish folder | |
| if: matrix.os == 'ubuntu-latest' | |
| shell: bash | |
| run: | | |
| OUTDIR="Companion-linux-${{ matrix.arch }}" | |
| SRC="./Companion.Desktop/bin/Release/net8.0/linux-${{ matrix.arch }}/publish" | |
| mkdir -p "$OUTDIR" | |
| cp -R "$SRC/"* "$OUTDIR/" | |
| zip -r "${OUTDIR}.zip" "$OUTDIR" | |
| ls -la | |
| # ----- Windows icon (optional) ----- | |
| - name: Windows - set icon | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| $PublishDir = "./Companion.Desktop/bin/Release/net8.0/win-${{ matrix.arch }}/publish" | |
| # NOTE: icons now live under Companion.Desktop | |
| $Icon = "./Companion/Assets/Icons/OpenIPC.ico" | |
| choco install rcedit -y | |
| if (Test-Path "$PublishDir/Companion.Desktop.exe") { | |
| rcedit "$PublishDir/Companion.Desktop.exe" --set-icon "$Icon" | |
| } | |
| # ----- Upload artifacts ----- | |
| - name: Upload macOS Artifact | |
| if: matrix.os == 'macos-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| # Name without extension keeps folders sane when downloaded | |
| name: Companion-macos-${{ matrix.arch }} | |
| path: Companion-macos-${{ matrix.arch }}.dmg | |
| compression-level: 9 | |
| - name: Upload macOS App Zip | |
| if: matrix.os == 'macos-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Companion-macos-${{ matrix.arch }}-app | |
| path: Companion-macos-${{ matrix.arch }}.zip | |
| compression-level: 9 | |
| - name: Upload Windows Artifact | |
| if: matrix.os == 'windows-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Companion-windows-${{ matrix.arch }} | |
| path: ./Companion.Desktop/bin/Release/net8.0/win-${{ matrix.arch }}/publish/** | |
| - name: Upload Linux Artifact | |
| if: matrix.os == 'ubuntu-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Companion-linux-${{ matrix.arch }} | |
| path: ./Companion.Desktop/bin/Release/net8.0/linux-${{ matrix.arch }}/publish/** | |
| compression-level: 0 | |
| release: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Read Version from tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| VERSION=${VERSION#release-v} | |
| echo "VERSION=$VERSION" >> $GITHUB_ENV | |
| - name: Download All Artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: ./artifacts | |
| - name: List Downloaded Files | |
| run: ls -R ./artifacts | |
| - name: Prepare Release Assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release_files | |
| # Re-zip Windows folders to a single file per arch | |
| for arch in x64 arm64; do | |
| if [ -d "./artifacts/Companion-windows-${arch}" ]; then | |
| (cd "./artifacts/Companion-windows-${arch}" && zip -r "../../release_files/Companion-windows-${arch}.zip" .) | |
| fi | |
| done | |
| # Re-zip Linux folders to a single file per arch | |
| for arch in x64 arm64; do | |
| if [ -d "./artifacts/Companion-linux-${arch}" ]; then | |
| (cd "./artifacts/Companion-linux-${arch}" && zip -r "../../release_files/Companion-linux-${arch}.zip" .) | |
| fi | |
| done | |
| # Copy macOS DMGs (also inside artifact folders) | |
| find ./artifacts -type f -name "Companion-macos-*.dmg" -exec cp {} ./release_files/ \; | |
| # Copy macOS app zips | |
| find ./artifacts -type f -name "Companion-macos-*.zip" -exec cp {} ./release_files/ \; | |
| ls -la ./release_files | |
| - name: Generate latest.json | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cat << EOF > ./release_files/latest.json | |
| { | |
| "version": "${{ env.VERSION }}", | |
| "release_notes": "Generated release notes.", | |
| "download_url": "https://github.com/${{ github.repository }}/releases/latest" | |
| } | |
| EOF | |
| - name: Create GitHub Release and Upload Assets | |
| uses: softprops/action-gh-release@v1 | |
| if: startsWith(github.ref, 'refs/tags/') | |
| with: | |
| draft: true | |
| generate_release_notes: true | |
| body: | | |
| ## Companion (OpenIPC Multiplatform Configurator) | |
| ### Downloads | |
| * **Windows**: `Companion-windows-x64.zip` / `Companion-windows-arm64.zip` | |
| * **macOS**: `Companion-macos-x64.dmg` / `Companion-macos-arm64.dmg` | |
| * **macOS (App ZIP)**: `Companion-macos-x64.zip` / `Companion-macos-arm64.zip` | |
| * **Linux**: `Companion-linux-x64.zip` / `Companion-linux-arm64.zip` | |
| ### Install | |
| * **Windows**: unzip and run `Companion.Desktop.exe`. | |
| * **macOS**: open `.dmg` and drag **Companion.app** to Applications. (ZIP contains the `.app` bundle.) | |
| * **Linux**: unzip and run `./Companion.Desktop` (chmod +x if needed). | |
| files: ./release_files/* |