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
122 changes: 109 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@ name: Build
# macOS apps/macos universal .app → DMG + ZIP
# Android apps/mobile release APK (installable, signed with the debug key)
# iOS apps/mobile unsigned IPA, for sideloading with AltStore / SideStore / Sideloadly
# Pushing a v* tag also publishes a GitHub Release with all of them attached.
# A GitHub Release with the builds and a changelog is published when either:
# - a v* tag is pushed, or
# - the workflow is run manually (Actions → Build → Run workflow) with "release" ticked;
# it then creates the v<version> tag on that commit itself.
# A version with a suffix (1.2.0-alpha.1, 1.2.0-beta.2) is published as a pre-release, for
# testing from a feature branch before it is merged. Every release builds all three apps; a
# manual run can tick "macos_only" to skip the mobile builds (e.g. a quick Mac test build).

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
inputs:
release:
description: Publish a GitHub Release with the builds and changelog
type: boolean
default: false
version:
description: Release version without the v (1.2.0, or 1.2.0-alpha.1 for a pre-release)
type: string
default: ""
macos_only:
description: Build only the macOS app (skips Android and iOS)
type: boolean
default: false

permissions:
contents: write
Expand All @@ -25,12 +44,30 @@ jobs:
outputs:
version: ${{ steps.v.outputs.version }}
app-version: ${{ steps.v.outputs.app-version }}
prerelease: ${{ steps.v.outputs.prerelease }}
mobile: ${{ !inputs.macos_only }}
steps:
- id: v
env:
EVENT: ${{ github.event_name }}
RELEASE: ${{ inputs.release }}
INPUT_VERSION: ${{ inputs.version }}
run: |
VERSION=""
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
echo "app-version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
VERSION="${GITHUB_REF_NAME#v}"
elif [[ "$EVENT" == "workflow_dispatch" && "$RELEASE" == "true" ]]; then
VERSION="$INPUT_VERSION"
fi
if [[ -n "$VERSION" ]]; then
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
echo "::error::Version must look like 1.2.0 or 1.2.0-alpha.1 (got '$VERSION')"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# iOS needs a purely numeric version, so the app itself gets 1.2.0 for 1.2.0-alpha.1.
echo "app-version=${VERSION%%-*}" >> "$GITHUB_OUTPUT"
[[ "$VERSION" == *-* ]] && echo "prerelease=true" >> "$GITHUB_OUTPUT" || echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "version=0.0.0-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
# iOS requires a numeric version, so untagged builds keep package.json's.
Expand All @@ -48,6 +85,41 @@ jobs:
steps:
- uses: actions/checkout@v4

# A stand-in Android TV (Tests/fake_tv.py) for the end-to-end pairing test.
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Start fake Android TV
run: |
python -m pip install --quiet androidtvremote2 pyopenssl
python Tests/fake_tv.py --code-file "$RUNNER_TEMP/tv-code" --keys-file "$RUNNER_TEMP/tv-keys" \
> "$RUNNER_TEMP/fake-tv.log" 2>&1 &
for i in $(seq 1 20); do nc -z 127.0.0.1 6466 && nc -z 127.0.0.1 6467 && exit 0; sleep 0.5; done
cat "$RUNNER_TEMP/fake-tv.log"; exit 1

- name: Run tests
env:
MIREMOTE_FAKE_TV_CODE: ${{ runner.temp }}/tv-code
MIREMOTE_FAKE_TV_KEYS: ${{ runner.temp }}/tv-keys
run: |
set +e
swift test 2>&1 | tee "$RUNNER_TEMP/swift-test.log"
status=${PIPESTATUS[0]}
if [[ $status != 0 ]]; then
# Surface failures as annotations, readable without the raw log.
enc() { python -c 'import sys; print(sys.stdin.read()[-6000:].replace("%","%25").replace("\r","").replace("\n","%0A"))'; }
echo "::error title=swift test failures::$(grep -E 'error:|failed|Timed out|XCTAssert|fatal' "$RUNNER_TEMP/swift-test.log" | head -40 | enc)"
echo "::error title=fake TV log::$(tail -60 "$RUNNER_TEMP/fake-tv.log" | enc)"
fi
enc() { python -c 'import sys; print(sys.stdin.read()[-6000:].replace("%","%25").replace("\r","").replace("\n","%0A"))'; }
echo "::notice title=swift test summary::$(grep -E "Test Case .*(passed|failed|skipped)|Executed [0-9]+ test" "$RUNNER_TEMP/swift-test.log" | tail -30 | enc)"
exit $status

- name: Fake TV log
if: always()
run: cat "$RUNNER_TEMP/fake-tv.log" || true

- name: Build universal app
run: UNIVERSAL=1 ./build-app.sh

Expand All @@ -67,6 +139,7 @@ jobs:

android:
needs: version
if: needs.version.outputs.mobile == 'true'
runs-on: ubuntu-latest
defaults:
run:
Expand Down Expand Up @@ -128,6 +201,7 @@ jobs:

ios:
needs: version
if: needs.version.outputs.mobile == 'true'
runs-on: macos-26
defaults:
run:
Expand Down Expand Up @@ -205,11 +279,16 @@ jobs:
path: apps/mobile/out/*

release:
if: startsWith(github.ref, 'refs/tags/v')
# Mobile jobs may be skipped (macos_only); everything that ran must have succeeded.
if: >-
!cancelled() && !failure()
&& (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release))
needs: [version, macos, android, ios]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.version.outputs.version }}
TAG: v${{ needs.version.outputs.version }}
PRERELEASE: ${{ needs.version.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -220,24 +299,36 @@ jobs:
path: out
merge-multiple: true

# Uses the commit SHA (not the tag) so it also works on a manual run, before the tag exists.
- name: Write release notes
run: |
PREV=$(git describe --tags --abbrev=0 "$GITHUB_REF_NAME^" 2>/dev/null || true)
RANGE=${PREV:+$PREV..}$GITHUB_REF_NAME
PREV=$(git describe --tags --abbrev=0 "$GITHUB_SHA^" 2>/dev/null || true)
RANGE=${PREV:+$PREV..}$GITHUB_SHA
{
if [[ "$PRERELEASE" == "true" ]]; then
echo "> [!WARNING]"
echo "> **Pre-release for testing.** It may have bugs. Grab the latest stable release if you just want a working remote."
echo
fi
echo "## Download"
echo
echo "| Platform | File | How to install |"
echo "|---|---|---|"
echo "| macOS 14+ | \`Mi-Remote-$VERSION-macOS.dmg\` | Open it and drag **Mi Remote** to Applications |"
echo "| Android 7+ | \`Mi-Remote-$VERSION-android.apk\` | Open it on the phone and allow installing from this source |"
echo "| iPhone (iOS 16.4+) | \`Mi-Remote-$VERSION-ios-unsigned.ipa\` | Sideload with AltStore, SideStore or Sideloadly using your Apple ID |"
if [[ -f "out/Mi-Remote-$VERSION-android.apk" ]]; then
echo "| Android 7+ | \`Mi-Remote-$VERSION-android.apk\` | Open it on the phone and allow installing from this source |"
fi
if [[ -f "out/Mi-Remote-$VERSION-ios-unsigned.ipa" ]]; then
echo "| iPhone (iOS 16.4+) | \`Mi-Remote-$VERSION-ios-unsigned.ipa\` | Sideload with AltStore, SideStore or Sideloadly using your Apple ID |"
fi
echo
echo "## First launch"
echo
echo "- **macOS**: the app is not notarized. Open it once, then go to **System Settings → Privacy & Security** and click **Open Anyway**."
echo "- **macOS, after updating**: if the app asks to use its key in your keychain, click **Always Allow**. That key is what the TV paired with."
echo "- **All platforms**: allow **Local Network** access when asked; the app needs it to find and reach the TV."
echo "- **iPhone**: apps sideloaded with a free Apple ID expire after 7 days; refresh them in AltStore/SideStore, or reinstall."
echo "- **Mi TVs without MiLink** (newer Android TV / Google TV models): the apps connect through the Android TV remote service instead. The first time, the TV shows a 6-character code; type it into the app."
[[ -f "out/Mi-Remote-$VERSION-ios-unsigned.ipa" ]] && echo "- **iPhone**: apps sideloaded with a free Apple ID expire after 7 days; refresh them in AltStore/SideStore, or reinstall." || true
echo
echo "See the [README](https://github.com/$GITHUB_REPOSITORY#install) for step-by-step instructions."
echo
Expand All @@ -246,9 +337,9 @@ jobs:
git log --no-merges --pretty='- %s (%h)' "$RANGE"
echo
if [[ -n "$PREV" ]]; then
echo "**Full changelog**: https://github.com/$GITHUB_REPOSITORY/compare/$PREV...$GITHUB_REF_NAME"
echo "**Full changelog**: https://github.com/$GITHUB_REPOSITORY/compare/$PREV...$TAG"
else
echo "**Full changelog**: https://github.com/$GITHUB_REPOSITORY/commits/$GITHUB_REF_NAME"
echo "**Full changelog**: https://github.com/$GITHUB_REPOSITORY/commits/$TAG"
fi
} > notes.md
cat notes.md
Expand All @@ -258,6 +349,11 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" out/* \
FLAGS=()
[[ "$PRERELEASE" == "true" ]] && FLAGS+=(--prerelease)
# --target creates the tag on this commit when it doesn't exist yet (manual run).
gh release create "$TAG" out/* \
--target "$GITHUB_SHA" \
--title "Mi Remote $VERSION" \
--notes-file notes.md
--notes-file notes.md \
"${FLAGS[@]}"
Loading
Loading