Build and Release App #23
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: Build and Release App | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: "Release type" | |
| required: true | |
| default: "release" | |
| type: choice | |
| options: | |
| - release | |
| - prerelease | |
| workflow_run: | |
| workflows: ["Bump Version & Release"] | |
| types: | |
| - completed | |
| jobs: | |
| get-version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.get_version.outputs.version }} | |
| prerelease: ${{ steps.get_version.outputs.prerelease }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get version | |
| id: get_version | |
| run: | | |
| VERSION=$(cat pubspec.yaml | grep version: | awk '{print $2}') | |
| VERSION="v$VERSION" | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.release_type }}" == "prerelease" ]]; then | |
| PRERELEASE=true | |
| else | |
| PRERELEASE=false | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT | |
| build: | |
| needs: get-version | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| # One platform failing should not hide whether the others work. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: android | |
| os: ubuntu-latest | |
| artifact: android | |
| - platform: windows | |
| os: windows-latest | |
| artifact: windows | |
| - platform: macos | |
| os: macos-latest | |
| artifact: macos | |
| - platform: linux | |
| os: ubuntu-latest | |
| artifact: linux-amd64 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: 3.47.0 | |
| # Flutter's Linux build needs the GTK toolchain, which the runner image | |
| # does not carry by default. | |
| - name: Install Linux build dependencies | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev libsecret-1-dev libjsoncpp-dev | |
| - name: Install dependencies | |
| run: flutter pub get | |
| # A single timestamp shared by every dart-define below, so all platforms | |
| # in one release report the same build time. | |
| - name: Stamp build time | |
| shell: bash | |
| run: echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" | |
| # Without a stable key the APK is signed with a debug key that runners | |
| # regenerate on every run, so each release refuses to install over the last. | |
| - name: Prepare Android signing | |
| if: matrix.platform == 'android' | |
| shell: bash | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| run: | | |
| if [ -z "$KEYSTORE_BASE64" ]; then | |
| echo "::warning::ANDROID_KEYSTORE_BASE64 is not set. The APK will be debug-signed and will not install over an existing Admincraft." | |
| exit 0 | |
| fi | |
| echo "$KEYSTORE_BASE64" | base64 -d > android/app/release.jks | |
| { | |
| echo "storeFile=release.jks" | |
| echo "storePassword=$STORE_PASSWORD" | |
| echo "keyAlias=$KEY_ALIAS" | |
| echo "keyPassword=$KEY_PASSWORD" | |
| } > android/key.properties | |
| echo "Release signing configured." | |
| - name: Build Android | |
| if: matrix.platform == 'android' | |
| run: >- | |
| flutter build apk --release | |
| --dart-define=ADMINCRAFT_BUILD_TIME=${{ env.BUILD_TIME }} | |
| --dart-define=ADMINCRAFT_GOOGLE_WEB_CLIENT_ID=${{ secrets.ADMINCRAFT_GOOGLE_WEB_CLIENT_ID }} | |
| - name: Build Windows | |
| if: matrix.platform == 'windows' | |
| run: >- | |
| flutter build windows --release | |
| --dart-define=ADMINCRAFT_BUILD_TIME=${{ env.BUILD_TIME }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }} | |
| - name: Build macOS | |
| if: matrix.platform == 'macos' | |
| run: >- | |
| flutter build macos --release | |
| --dart-define=ADMINCRAFT_BUILD_TIME=${{ env.BUILD_TIME }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }} | |
| - name: Build Linux | |
| if: matrix.platform == 'linux' | |
| run: >- | |
| flutter build linux --release | |
| --dart-define=ADMINCRAFT_BUILD_TIME=${{ env.BUILD_TIME }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }} | |
| --dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }} | |
| - name: Package Android | |
| if: matrix.platform == 'android' | |
| shell: bash | |
| run: | | |
| mkdir -p dist | |
| mv build/app/outputs/flutter-apk/app-release.apk "dist/admincraft-${VERSION}-android.apk" | |
| env: | |
| VERSION: ${{ needs.get-version.outputs.version }} | |
| # Portable zip plus an Inno Setup installer, so the download page offers | |
| # the same pair as every other desktop platform. | |
| # PowerShell rather than bash: Git bash rewrites arguments that look like | |
| # paths, so ISCC received "//DSourceDir=/d/a/..." and rejected it. | |
| - name: Package Windows | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ needs.get-version.outputs.version }}" | |
| $dist = Join-Path $PWD 'dist' | |
| $releaseDir = Join-Path $PWD 'build\windows\x64\runner\Release' | |
| New-Item -ItemType Directory -Force $dist | Out-Null | |
| Compress-Archive -Path (Join-Path $releaseDir '*') ` | |
| -DestinationPath (Join-Path $dist "admincraft-$version-windows-portable.zip") | |
| # Inno Setup ships on the runner image, but the path has moved between | |
| # image versions, so look for it before falling back to installing it. | |
| $iscc = @( | |
| "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", | |
| "$env:ProgramFiles\Inno Setup 6\ISCC.exe" | |
| ) | Where-Object { Test-Path $_ } | Select-Object -First 1 | |
| if (-not $iscc) { | |
| choco install innosetup -y --no-progress | |
| $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" | |
| } | |
| Write-Host "Using $iscc" | |
| & $iscc "/DAppVersion=$($version.TrimStart('v'))" "/DSourceDir=$releaseDir" "/DOutputDir=$dist" 'windows\packaging\admincraft.iss' | |
| if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE" } | |
| Move-Item (Join-Path $dist 'admincraft-setup.exe') ` | |
| (Join-Path $dist "admincraft-$version-windows-installer.exe") | |
| # Unsigned: without an Apple Developer certificate Gatekeeper asks the user | |
| # to allow the app on first launch. Signing would need a MAC_CSC_LINK | |
| # secret and an Apple Developer account. | |
| - name: Package macOS | |
| if: matrix.platform == 'macos' | |
| shell: bash | |
| run: | | |
| mkdir -p dist | |
| APP=build/macos/Build/Products/Release/admincraft.app | |
| BASE="dist/admincraft-${VERSION}-${{ matrix.artifact }}" | |
| echo "Architectures in the built binary:" | |
| lipo -archs "$APP/Contents/MacOS/admincraft" || true | |
| ditto -c -k --keepParent "$APP" "${BASE}-portable.zip" | |
| hdiutil create -volname Admincraft -srcfolder "$APP" -ov -format UDZO "${BASE}-installer.dmg" | |
| env: | |
| VERSION: ${{ needs.get-version.outputs.version }} | |
| - name: Package Linux | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| run: | | |
| bash linux/packaging/build-packages.sh "$VERSION" build/linux/x64/release/bundle dist | |
| env: | |
| VERSION: ${{ needs.get-version.outputs.version }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: admincraft-${{ needs.get-version.outputs.version }}-${{ matrix.artifact }} | |
| path: dist/* | |
| if-no-files-found: error | |
| release: | |
| needs: [get-version, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| # Collected by pattern rather than one step per platform, so adding a | |
| # platform to the matrix above needs no change here. | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: admincraft-${{ needs.get-version.outputs.version }}-* | |
| path: dist | |
| merge-multiple: true | |
| - name: List collected files | |
| run: ls -lh dist | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ needs.get-version.outputs.version }} | |
| target_commitish: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} | |
| name: Release ${{ needs.get-version.outputs.version }} | |
| prerelease: ${{ needs.get-version.outputs.prerelease }} | |
| generate_release_notes: true | |
| files: dist/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Delete build artifacts from GitHub | |
| uses: geekyeggo/delete-artifact@v5 | |
| with: | |
| name: admincraft-${{ needs.get-version.outputs.version }}-* |