Build Multi-Platform #37
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 Multi-Platform | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| pull_request: | |
| branches: [ main ] | |
| schedule: | |
| # 21:00 UTC = 23:00 Europe/Athens — runs on days 1, 5, 9, 13, 17, 21, 25, 29 (every ~4 days) | |
| - cron: '0 21 1,5,9,13,17,21,25,29 * *' | |
| workflow_dispatch: | |
| inputs: | |
| nightly_dry_run: | |
| description: 'Nightly: preview retention/upload actions without changing the release' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| jobs: | |
| detect-nightly-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_changes: ${{ steps.detect.outputs.has_changes }} | |
| run_builds: ${{ steps.detect.outputs.run_builds }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - id: detect | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| NIGHTLY_CONTEXT="false" | |
| if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] || [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${GITHUB_REF}" == "refs/heads/main" ]]; then | |
| NIGHTLY_CONTEXT="true" | |
| fi | |
| if [[ "${NIGHTLY_CONTEXT}" != "true" ]]; then | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "run_builds=true" >> "$GITHUB_OUTPUT" | |
| echo "Non-nightly event; builds will run." | |
| exit 0 | |
| fi | |
| CURRENT_SHA="${GITHUB_SHA}" | |
| LAST_SHA="" | |
| if gh release view nightly >/dev/null 2>&1; then | |
| BODY=$(gh release view nightly --json body --jq '.body' || true) | |
| LAST_SHA=$(printf '%s\n' "$BODY" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true) | |
| fi | |
| if [[ -z "${LAST_SHA}" ]]; then | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "run_builds=true" >> "$GITHUB_OUTPUT" | |
| echo "No recorded nightly commit found; builds will run." | |
| elif [[ "${LAST_SHA}" == "${CURRENT_SHA}" ]]; then | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| echo "run_builds=false" >> "$GITHUB_OUTPUT" | |
| echo "No new commits since last nightly (${CURRENT_SHA}); builds will be skipped." | |
| else | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "run_builds=true" >> "$GITHUB_OUTPUT" | |
| echo "New commit detected: current=${CURRENT_SHA}, last-nightly=${LAST_SHA}. Builds will run." | |
| fi | |
| nightly-logic-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate nightly versioning and retention logic | |
| run: | | |
| set -euo pipefail | |
| RAW_VERSION=$(grep '^version:' ScrcpyGui/pubspec.yaml | awk '{print $2}') | |
| BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/\+.*$//' | sed -E 's/-.*$//') | |
| TEST_DATE="20260304" | |
| NIGHTLY_VERSION="${BASE_VERSION}-nightly.${TEST_DATE}.1" | |
| [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] | |
| [[ "$NIGHTLY_VERSION" =~ ^${BASE_VERSION}-nightly\.[0-9]{8}\.1$ ]] | |
| EXISTING=$(printf '%s\n' \ | |
| "${BASE_VERSION}-nightly.20260303.1" \ | |
| "${BASE_VERSION}-nightly.20260304.1" \ | |
| "${BASE_VERSION}-nightly.20260302.1") | |
| PREVIOUS_VERSION=$( | |
| printf '%s\n' "$EXISTING" \ | |
| | grep -v "^${NIGHTLY_VERSION}$" \ | |
| | sort -Vr \ | |
| | head -n1 | |
| ) | |
| test "$PREVIOUS_VERSION" = "${BASE_VERSION}-nightly.20260303.1" | |
| # Mirror detect-nightly-changes release-body SHA parsing behavior. | |
| BODY_WITH_SHA="Latest nightly: v${NIGHTLY_VERSION}; Commit: abcdef1234567890" | |
| PARSED_SHA=$(printf '%s\n' "$BODY_WITH_SHA" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true) | |
| test "$PARSED_SHA" = "abcdef1234567890" | |
| BODY_WITHOUT_SHA="Latest nightly: v${NIGHTLY_VERSION}" | |
| PARSED_EMPTY=$(printf '%s\n' "$BODY_WITHOUT_SHA" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true) | |
| test -z "$PARSED_EMPTY" | |
| # Skip behavior checks used by detect-nightly-changes. | |
| CURRENT_SHA="abcdef1234567890" | |
| LAST_SHA_SAME="abcdef1234567890" | |
| LAST_SHA_DIFF="1234567abcdef1234" | |
| LAST_SHA_MISSING="" | |
| if [[ -z "${LAST_SHA_MISSING}" ]]; then HAS_CHANGES_MISSING=true; else HAS_CHANGES_MISSING=false; fi | |
| if [[ "${LAST_SHA_SAME}" == "${CURRENT_SHA}" ]]; then HAS_CHANGES_SAME=false; else HAS_CHANGES_SAME=true; fi | |
| if [[ "${LAST_SHA_DIFF}" == "${CURRENT_SHA}" ]]; then HAS_CHANGES_DIFF=false; else HAS_CHANGES_DIFF=true; fi | |
| test "${HAS_CHANGES_MISSING}" = "true" | |
| test "${HAS_CHANGES_SAME}" = "false" | |
| test "${HAS_CHANGES_DIFF}" = "true" | |
| echo "Nightly logic tests passed: base=$BASE_VERSION nightly=$NIGHTLY_VERSION previous=$PREVIOUS_VERSION" | |
| build-windows: | |
| needs: [detect-nightly-changes] | |
| if: needs.detect-nightly-changes.outputs.run_builds == 'true' | |
| runs-on: windows-latest | |
| defaults: | |
| run: | |
| working-directory: ScrcpyGui | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: 'stable' | |
| cache: true | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Build Windows | |
| run: flutter build windows --release | |
| - name: Archive Windows build | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-build | |
| path: ScrcpyGui/build/windows/x64/runner/Release/ | |
| build-linux: | |
| needs: [detect-nightly-changes] | |
| if: needs.detect-nightly-changes.outputs.run_builds == 'true' | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: ScrcpyGui | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: 'stable' | |
| cache: true | |
| - name: Install Linux dependencies | |
| run: | | |
| sudo apt-get update -y | |
| sudo apt-get install -y ninja-build pkg-config libgtk-3-dev liblzma-dev | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Build Linux | |
| run: flutter build linux --release | |
| - name: Create Linux installation package | |
| run: bash package_linux.sh | |
| - name: Archive Linux build | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-build | |
| path: ScrcpyGui/artifacts/linux_package/ | |
| build-macos: | |
| needs: [detect-nightly-changes] | |
| if: needs.detect-nightly-changes.outputs.run_builds == 'true' | |
| runs-on: macos-latest | |
| defaults: | |
| run: | |
| working-directory: ScrcpyGui | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: 'stable' | |
| cache: true | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Build macOS | |
| run: flutter build macos --release | |
| - name: Fix macOS app permissions and code signing | |
| run: | | |
| # Set the app name | |
| APP_NAME="scrcpy_gui_prod" | |
| APP_PATH="build/macos/Build/Products/Release/${APP_NAME}.app" | |
| # Ensure the main executable has execute permissions | |
| chmod +x "${APP_PATH}/Contents/MacOS/${APP_NAME}" | |
| # Remove resource forks and extended attributes that prevent signing | |
| dot_clean -m "${APP_PATH}" | |
| find "${APP_PATH}" -name "._*" -delete | |
| xattr -cr "${APP_PATH}" | |
| # Sign all frameworks first (don't use --deep on frameworks) | |
| find "${APP_PATH}/Contents/Frameworks" -name "*.framework" -maxdepth 1 | while read framework; do | |
| echo "Signing framework: ${framework}" | |
| /usr/bin/codesign --force --sign - --timestamp=none "${framework}" | |
| done | |
| # Sign the main app bundle | |
| echo "Signing app bundle" | |
| /usr/bin/codesign --force --sign - --timestamp=none "${APP_PATH}" | |
| # Verify the signature | |
| /usr/bin/codesign --verify --verbose "${APP_PATH}" | |
| # Display app info | |
| echo "App signed successfully:" | |
| ls -lh "${APP_PATH}/Contents/MacOS/" | |
| du -sh "${APP_PATH}" | |
| - name: Create DMG and installation package | |
| run: | | |
| # Set the app name | |
| APP_NAME="scrcpy_gui_prod" | |
| APP_PATH="build/macos/Build/Products/Release/${APP_NAME}.app" | |
| DMG_NAME="${APP_NAME}.dmg" | |
| # Create a temporary directory for DMG contents | |
| mkdir -p dmg_temp | |
| # Use ditto instead of cp to preserve all metadata, permissions, and signatures | |
| ditto "${APP_PATH}" "dmg_temp/${APP_NAME}.app" | |
| # Create the DMG with proper flags to preserve code signatures | |
| hdiutil create -volname "${APP_NAME}" -srcfolder dmg_temp -ov -format UDZO "${DMG_NAME}" | |
| # Create package directory for final distribution | |
| mkdir -p macos_package | |
| # Move DMG to package directory | |
| mv "${DMG_NAME}" macos_package/ | |
| # Create installation script | |
| cat > macos_package/install.sh << 'SCRIPT_EOF' | |
| #!/bin/bash | |
| # macOS App Installer for scrcpy_gui_prod | |
| # This script removes quarantine flags and installs the app | |
| set -e | |
| APP_NAME="scrcpy_gui_prod" | |
| DMG_FILE="${APP_NAME}.dmg" | |
| INSTALL_DIR="/Applications" | |
| echo "======================================" | |
| echo " macOS App Installer" | |
| echo "======================================" | |
| echo "" | |
| # Check if DMG exists | |
| if [ ! -f "$DMG_FILE" ]; then | |
| echo "Error: $DMG_FILE not found in current directory" | |
| echo "Please run this script from the extracted folder" | |
| exit 1 | |
| fi | |
| echo "Step 1: Removing quarantine flags from DMG..." | |
| xattr -cr "$DMG_FILE" 2>/dev/null || true | |
| echo "✓ Quarantine removed" | |
| echo "" | |
| echo "Step 2: Mounting DMG..." | |
| MOUNT_POINT=$(hdiutil attach "$DMG_FILE" | grep Volumes | awk '{print $3}') | |
| if [ -z "$MOUNT_POINT" ]; then | |
| echo "Error: Failed to mount DMG" | |
| exit 1 | |
| fi | |
| echo "✓ DMG mounted at: $MOUNT_POINT" | |
| echo "" | |
| echo "Step 3: Removing quarantine from app..." | |
| xattr -cr "$MOUNT_POINT/${APP_NAME}.app" 2>/dev/null || true | |
| echo "✓ App quarantine removed" | |
| echo "" | |
| echo "Step 4: Copying app to Applications folder..." | |
| if [ -d "${INSTALL_DIR}/${APP_NAME}.app" ]; then | |
| echo "Warning: App already exists in Applications. Removing old version..." | |
| rm -rf "${INSTALL_DIR}/${APP_NAME}.app" | |
| fi | |
| cp -R "$MOUNT_POINT/${APP_NAME}.app" "$INSTALL_DIR/" | |
| echo "✓ App copied to $INSTALL_DIR" | |
| echo "" | |
| echo "Step 5: Unmounting DMG..." | |
| hdiutil detach "$MOUNT_POINT" -quiet | |
| echo "✓ DMG unmounted" | |
| echo "" | |
| echo "======================================" | |
| echo " Installation Complete!" | |
| echo "======================================" | |
| echo "" | |
| echo "The app has been installed to:" | |
| echo " $INSTALL_DIR/${APP_NAME}.app" | |
| echo "" | |
| echo "You can now open it from:" | |
| echo " - Spotlight: Press Cmd+Space and type '${APP_NAME}'" | |
| echo " - Applications folder in Finder" | |
| echo " - Or run: open '${INSTALL_DIR}/${APP_NAME}.app'" | |
| echo "" | |
| echo "Opening the app now..." | |
| sleep 1 | |
| open "${INSTALL_DIR}/${APP_NAME}.app" | |
| SCRIPT_EOF | |
| # Make the script executable | |
| chmod +x macos_package/install.sh | |
| # Create README | |
| cat > macos_package/README.txt << 'README_EOF' | |
| macOS Installation Instructions | |
| ================================ | |
| EASY INSTALLATION (Recommended): | |
| --------------------------------- | |
| 1. Open Terminal (Applications -> Utilities -> Terminal) | |
| 2. Type: cd ~/Downloads/scrcpy-gui-macos | |
| (or wherever you extracted this folder) | |
| 3. Type: chmod +x install.sh | |
| 4. Type: ./install.sh | |
| 5. The app will be installed and launched automatically! | |
| ALTERNATIVE - One-line installation: | |
| ------------------------------------ | |
| Copy and paste this into Terminal: | |
| cd ~/Downloads/scrcpy-gui-macos && chmod +x install.sh && ./install.sh | |
| MANUAL INSTALLATION: | |
| -------------------- | |
| 1. Open Terminal and run: | |
| xattr -cr scrcpy_gui_prod.dmg | |
| 2. Double-click the DMG file | |
| 3. Drag the app to your Applications folder | |
| 4. Right-click the app -> Open (first time only) | |
| WHY IS THIS NECESSARY? | |
| ---------------------- | |
| This app is not signed with an Apple Developer ID certificate ($99/year). | |
| macOS adds "quarantine" flags to downloaded files for security. | |
| The install.sh script safely removes these flags. | |
| This is normal for free/open-source macOS apps. | |
| README_EOF | |
| # Move to artifacts directory | |
| mkdir -p artifacts | |
| mv macos_package artifacts/ | |
| # Show package contents | |
| echo "Package contents:" | |
| ls -lh artifacts/macos_package/ | |
| - name: Archive macOS build | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-build | |
| path: ScrcpyGui/artifacts/macos_package/ | |
| create-release: | |
| needs: [build-windows, build-linux, build-macos] | |
| runs-on: ubuntu-latest | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Create ZIP archives | |
| run: | | |
| # Extract version from tag (e.g., refs/tags/v1.0.0 -> 1.0.0) | |
| VERSION=${GITHUB_REF#refs/tags/v} | |
| cd artifacts | |
| # Windows build | |
| zip -r ../scrcpy-gui-windows-v${VERSION}.zip windows-build/ | |
| # Linux build | |
| zip -r ../scrcpy-gui-linux-v${VERSION}.zip linux-build/ | |
| # macOS build - create ZIP with DMG, install script, and README | |
| cd macos-build | |
| zip -r ../../scrcpy-gui-macos-v${VERSION}.zip . | |
| cd .. | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| scrcpy-gui-windows-v*.zip | |
| scrcpy-gui-linux-v*.zip | |
| scrcpy-gui-macos-v*.zip | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| update-nightly-release: | |
| needs: [detect-nightly-changes, build-windows, build-linux, build-macos] | |
| runs-on: ubuntu-latest | |
| if: (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && needs.detect-nightly-changes.outputs.has_changes == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Compute nightly version | |
| run: | | |
| RAW_VERSION=$(grep '^version:' ScrcpyGui/pubspec.yaml | awk '{print $2}') | |
| BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/\+.*$//' | sed -E 's/-.*$//') | |
| NIGHTLY_VERSION="${BASE_VERSION}-nightly.$(date -u +%Y%m%d).1" | |
| echo "RAW_VERSION=$RAW_VERSION" >> "$GITHUB_ENV" | |
| echo "BASE_VERSION=$BASE_VERSION" >> "$GITHUB_ENV" | |
| echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> "$GITHUB_ENV" | |
| - name: Create nightly ZIP archives | |
| run: | | |
| cd artifacts | |
| zip -r ../scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip windows-build/ | |
| zip -r ../scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip linux-build/ | |
| cd macos-build | |
| zip -r ../../scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip . | |
| - name: Ensure nightly pre-release exists | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if ! gh release view nightly >/dev/null 2>&1; then | |
| gh release create nightly \ | |
| --title "Nightly Builds" \ | |
| --notes "Automated nightly pre-release builds. Assets keep only the current nightly and one previous nightly." \ | |
| --prerelease | |
| fi | |
| - name: Keep only current + previous nightly versions, then upload current | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NIGHTLY_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.nightly_dry_run || false }} | |
| run: | | |
| set -euo pipefail | |
| # Discover existing nightly versions from asset names: | |
| # scrcpy-gui-<platform>-v<version>.zip | |
| mapfile -t EXISTING_VERSIONS < <( | |
| gh release view nightly --json assets --jq '.assets[].name' \ | |
| | sed -nE 's/^scrcpy-gui-(windows|linux|macos)-v(.+)\.zip$/\2/p' \ | |
| | sort -Vu | |
| ) | |
| PREVIOUS_VERSION="" | |
| if [ ${#EXISTING_VERSIONS[@]} -gt 0 ]; then | |
| PREVIOUS_VERSION=$( | |
| printf '%s\n' "${EXISTING_VERSIONS[@]}" \ | |
| | grep -v "^${NIGHTLY_VERSION}$" \ | |
| | sort -Vr \ | |
| | head -n1 || true | |
| ) | |
| fi | |
| echo "Nightly dry run: ${NIGHTLY_DRY_RUN}" | |
| echo "Current nightly version: ${NIGHTLY_VERSION}" | |
| echo "Previous nightly version: ${PREVIOUS_VERSION:-none}" | |
| # Remove any nightly asset that is neither current nor previous | |
| mapfile -t EXISTING_ASSETS < <(gh release view nightly --json assets --jq '.assets[].name') | |
| for asset in "${EXISTING_ASSETS[@]}"; do | |
| if [[ "$asset" =~ ^scrcpy-gui-(windows|linux|macos)-v(.+)\.zip$ ]]; then | |
| asset_version="${BASH_REMATCH[2]}" | |
| if [[ "$asset_version" != "$NIGHTLY_VERSION" && "$asset_version" != "$PREVIOUS_VERSION" ]]; then | |
| if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then | |
| echo "[DRY RUN] Would delete asset: $asset" | |
| else | |
| gh release delete-asset nightly "$asset" --yes || true | |
| fi | |
| fi | |
| fi | |
| done | |
| # Upload current nightly assets (idempotent for same-day reruns) | |
| if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then | |
| echo "[DRY RUN] Would upload: scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip" | |
| echo "[DRY RUN] Would upload: scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip" | |
| echo "[DRY RUN] Would upload: scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip" | |
| else | |
| gh release upload nightly "scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip" --clobber | |
| gh release upload nightly "scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip" --clobber | |
| gh release upload nightly "scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip" --clobber | |
| fi | |
| # Update release metadata each run for clarity | |
| CURRENT_BODY=$(gh release view nightly --json body --jq '.body' || true) | |
| STATIC_SECTION=$(printf '%s' "$CURRENT_BODY" | tr -d '\r' | awk '/^---$/{found=1; next} found{print}' || true) | |
| DYNAMIC_NOTES=$(printf 'Latest nightly: v%s\nPrevious nightly: %s\nSource branch: main\nCommit: %s' \ | |
| "${NIGHTLY_VERSION}" "${PREVIOUS_VERSION:-none}" "${GITHUB_SHA}") | |
| if [[ -n "$STATIC_SECTION" ]]; then | |
| RELEASE_NOTES=$(printf '%s\n\n---\n%s' "$DYNAMIC_NOTES" "$STATIC_SECTION") | |
| else | |
| RELEASE_NOTES="${DYNAMIC_NOTES}" | |
| fi | |
| if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then | |
| echo "[DRY RUN] Would edit release title/notes for nightly" | |
| echo "[DRY RUN] Notes would be:" | |
| printf '%s\n' "$RELEASE_NOTES" | |
| else | |
| gh release edit nightly \ | |
| --title "Nightly Builds (v${NIGHTLY_VERSION})" \ | |
| --notes "$RELEASE_NOTES" | |
| fi |