Merge branch 'ci/self-hosted-x86-64-runner'
Move CI to a self-hosted x86_64 runner registered at android-stuff org scope. Flutter cannot build an Android APK on a Linux arm64 host, so the aarch64 Gitea box could never have run this build regardless of its container networking. Adds tool/setup_ci_runner.sh to provision it in host mode, using the machine's existing proven toolchain, and points both jobs at the capability label flutter-android. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+17
-18
@@ -6,18 +6,28 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
|
# Both jobs target a self-hosted runner, by capability rather than by hostname.
|
||||||
|
# See tool/setup_ci_runner.sh for how that runner is provisioned and why it is
|
||||||
|
# not the Gitea host: Flutter cannot build an Android APK on a Linux arm64 host
|
||||||
|
# (flutter/flutter#189724), and the Gitea box is aarch64. The runner is
|
||||||
|
# registered at android-stuff org scope so any repo in the org can use it.
|
||||||
|
#
|
||||||
|
# The runner is host-mode, so Flutter, the JDK and the Android SDK/NDK are the
|
||||||
|
# machine's own — the same toolchain that produced the verified release, and no
|
||||||
|
# multi-hundred-MB download per run. Nothing here installs a toolchain; the
|
||||||
|
# Toolchain step fails loudly if one is missing.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze & Test
|
name: Analyze & Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: flutter-android
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
- name: Toolchain
|
||||||
with:
|
run: |
|
||||||
flutter-version: '3.47.1'
|
flutter --version
|
||||||
channel: 'stable'
|
echo "ANDROID_HOME=${ANDROID_HOME:?not set on this runner}"
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -32,21 +42,10 @@ jobs:
|
|||||||
|
|
||||||
release-apk:
|
release-apk:
|
||||||
name: Release APK
|
name: Release APK
|
||||||
runs-on: ubuntu-latest
|
runs-on: flutter-android
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: '3.47.1'
|
|
||||||
channel: 'stable'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,41 @@ Dart uses `Completer<Iperf3Result>` to await native completion. The MethodChanne
|
|||||||
| `isTestRunning()` | `isTestRunning` | Check if test is in progress |
|
| `isTestRunning()` | `isTestRunning` | Check if test is in progress |
|
||||||
| `setTempDir(String)` | `setTempDir` | Point `mkstemp()` at the app cache dir |
|
| `setTempDir(String)` | `setTempDir` | Point `mkstemp()` at the app cache dir |
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Both jobs run on a **self-hosted runner registered at `android-stuff` org scope**,
|
||||||
|
targeted by the capability label `flutter-android` rather than by hostname.
|
||||||
|
`tool/setup_ci_runner.sh` provisions it.
|
||||||
|
|
||||||
|
It does not run on the Gitea host. Gitea lives on an aarch64 box, and Flutter
|
||||||
|
cannot build an Android APK on a Linux arm64 host at all — the SDK ships no
|
||||||
|
arm64-host `gen_snapshot` ([flutter#189724](https://github.com/flutter/flutter/issues/189724),
|
||||||
|
[#75864](https://github.com/flutter/flutter/issues/75864), open as of July 2026).
|
||||||
|
An x86_64 runner is a requirement, not a preference.
|
||||||
|
|
||||||
|
The runner is **host mode**: jobs use the machine's own Flutter, JDK and Android
|
||||||
|
SDK/NDK. That means CI builds with the same toolchain that produced the verified
|
||||||
|
release, and no NDK download per run. The trade is isolation — workflows run as
|
||||||
|
that user on that machine — so the runner is only for repos whose workflows are
|
||||||
|
trusted.
|
||||||
|
|
||||||
|
Two jobs:
|
||||||
|
|
||||||
|
| Job | What it protects |
|
||||||
|
|---|---|
|
||||||
|
| `analyze` | `flutter analyze`, then `flutter test` — which includes `architecture_test.dart`, failing the build if a layer boundary is crossed |
|
||||||
|
| `release-apk` | builds the **minified** APK and runs `tool/check_jni_keep_rules.sh` |
|
||||||
|
|
||||||
|
The second exists because of a specific escape. A release APK that aborted on
|
||||||
|
every test shipped once, while every on-device check passed — because those
|
||||||
|
checks all used the debug build, which does not minify. Building the release APK
|
||||||
|
is necessary but not sufficient: a renamed JNI callback still produces a
|
||||||
|
*successful build* and a broken app. So the guard reads R8's `mapping.txt` for
|
||||||
|
the callback classes and methods, and the DEX string pool for the native entry
|
||||||
|
points. It is verified to fail: delete the keep rules from
|
||||||
|
`android/app/proguard-rules.pro`, rebuild, and it reports `MainThreadCallback`,
|
||||||
|
`onOutput`, `onError` and `onComplete` renamed.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|||||||
Executable
+160
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Register this machine as a Gitea Actions runner for the android-stuff org.
|
||||||
|
#
|
||||||
|
# WHY THIS MACHINE, AND NOT THE GITEA HOST
|
||||||
|
# Gitea runs on pi5-16 (aarch64). Flutter cannot build an Android APK on a
|
||||||
|
# Linux arm64 host at all — the SDK ships no arm64-host gen_snapshot
|
||||||
|
# (flutter/flutter#189724, #75864, open as of July 2026). So an Android build
|
||||||
|
# runner has to be x86_64 regardless of how the Gitea host is configured.
|
||||||
|
#
|
||||||
|
# WHY HOST MODE, AND NOT A CONTAINER
|
||||||
|
# This machine already has a proven Flutter + Android SDK + NDK toolchain. In
|
||||||
|
# host mode the job uses it directly: no SDK download, no ~1GB NDK pull per
|
||||||
|
# run, and CI builds with the exact toolchain that produced the verified
|
||||||
|
# release. The trade is isolation — a workflow in this org runs commands as
|
||||||
|
# this user on this machine. That is the normal self-hosted-runner bargain, but
|
||||||
|
# it is a real one: only grant it for repos whose workflows you trust.
|
||||||
|
#
|
||||||
|
# USAGE
|
||||||
|
# 1. Get an ORG-scoped registration token:
|
||||||
|
# http://pi5-16.local:3005/org/android-stuff/settings/actions/runners
|
||||||
|
# → "Create new runner" → copy the token
|
||||||
|
# Org scope, not repo scope, so every android-stuff repo can use it.
|
||||||
|
# 2. RUNNER_TOKEN=xxxx ./tool/setup_ci_runner.sh
|
||||||
|
#
|
||||||
|
# Re-running is safe: it skips the download if the binary is current and
|
||||||
|
# refuses to clobber an existing registration.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${RUNNER_VERSION:-3.3.0}"
|
||||||
|
INSTANCE="${GITEA_INSTANCE:-http://pi5-16.local:3005}"
|
||||||
|
RUNNER_NAME="${RUNNER_NAME:-$(hostname)-android}"
|
||||||
|
RUNNER_HOME="${RUNNER_HOME:-$HOME/.local/share/gitea-runner}"
|
||||||
|
BIN="${RUNNER_BIN:-$HOME/.local/bin/gitea-runner}"
|
||||||
|
|
||||||
|
# Capability-based label, deliberately not host-based: it says what the runner
|
||||||
|
# can do, so the workflow does not encode which machine happens to provide it.
|
||||||
|
LABEL="${RUNNER_LABEL:-flutter-android}"
|
||||||
|
|
||||||
|
: "${RUNNER_TOKEN:?set RUNNER_TOKEN to an org registration token (see header)}"
|
||||||
|
|
||||||
|
echo "==> Checking the toolchain this runner promises to provide"
|
||||||
|
missing=0
|
||||||
|
for cmd in flutter dart java cmake ninja node git unzip; do
|
||||||
|
if command -v "$cmd" >/dev/null; then
|
||||||
|
printf ' ok %s\n' "$cmd"
|
||||||
|
else
|
||||||
|
printf ' MISSING %s\n' "$cmd"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
||||||
|
if [[ -n "$SDK" && -d "$SDK/ndk" && -d "$SDK/build-tools" ]]; then
|
||||||
|
echo " ok Android SDK at $SDK (ndk: $(ls "$SDK/ndk" | tr '\n' ' '))"
|
||||||
|
else
|
||||||
|
echo " MISSING Android SDK with ndk/ and build-tools/ (ANDROID_HOME=${SDK:-unset})"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
if [[ $missing -ne 0 ]]; then
|
||||||
|
echo
|
||||||
|
echo "Refusing to register: a runner advertising '$LABEL' must actually" >&2
|
||||||
|
echo "provide these, or every job will fail confusingly instead of here." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Installing gitea-runner $VERSION"
|
||||||
|
mkdir -p "$(dirname "$BIN")" "$RUNNER_HOME"
|
||||||
|
if [[ -x "$BIN" ]] && "$BIN" --version 2>/dev/null | grep -qF "$VERSION"; then
|
||||||
|
echo " already at $VERSION"
|
||||||
|
else
|
||||||
|
url="https://dl.gitea.com/gitea-runner/${VERSION}/gitea-runner-${VERSION}-linux-amd64"
|
||||||
|
echo " downloading $url"
|
||||||
|
curl -fSL --retry 3 -o "$BIN.tmp" "$url"
|
||||||
|
chmod +x "$BIN.tmp"
|
||||||
|
mv "$BIN.tmp" "$BIN"
|
||||||
|
fi
|
||||||
|
"$BIN" --version
|
||||||
|
|
||||||
|
echo "==> Writing config"
|
||||||
|
CONFIG="$RUNNER_HOME/config.yaml"
|
||||||
|
if [[ -f "$CONFIG" ]]; then
|
||||||
|
echo " $CONFIG exists, leaving it alone"
|
||||||
|
else
|
||||||
|
cat > "$CONFIG" <<YAML
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
runner:
|
||||||
|
file: .runner
|
||||||
|
# One Android build at a time: gradle + a 2-ABI NDK compile will happily
|
||||||
|
# saturate this box, and concurrent runs would just contend.
|
||||||
|
capacity: 1
|
||||||
|
timeout: 45m
|
||||||
|
# ':host' runs the job directly on this machine using the toolchain above,
|
||||||
|
# rather than in a container that would have to fetch its own.
|
||||||
|
labels:
|
||||||
|
- "${LABEL}:host"
|
||||||
|
|
||||||
|
container:
|
||||||
|
# Unused in host mode, kept so switching to container mode later is a
|
||||||
|
# one-line change rather than a rediscovery.
|
||||||
|
network: ""
|
||||||
|
privileged: false
|
||||||
|
|
||||||
|
host:
|
||||||
|
workdir_parent: ${RUNNER_HOME}/workspace
|
||||||
|
YAML
|
||||||
|
echo " wrote $CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Registering with $INSTANCE"
|
||||||
|
if [[ -f "$RUNNER_HOME/.runner" ]]; then
|
||||||
|
echo " already registered (found $RUNNER_HOME/.runner) — skipping."
|
||||||
|
echo " To re-register: remove that file and the runner in the Gitea UI first."
|
||||||
|
else
|
||||||
|
(cd "$RUNNER_HOME" && "$BIN" register \
|
||||||
|
--no-interactive \
|
||||||
|
--instance "$INSTANCE" \
|
||||||
|
--token "$RUNNER_TOKEN" \
|
||||||
|
--name "$RUNNER_NAME" \
|
||||||
|
--labels "${LABEL}:host" \
|
||||||
|
--config "$CONFIG")
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
==> Done. Start it with:
|
||||||
|
|
||||||
|
cd $RUNNER_HOME && $BIN daemon --config $CONFIG
|
||||||
|
|
||||||
|
Or install a user service that survives logout:
|
||||||
|
|
||||||
|
systemctl --user enable --now gitea-runner
|
||||||
|
loginctl enable-linger $USER # so it runs without an active session
|
||||||
|
|
||||||
|
A unit file is written alongside this script's output at:
|
||||||
|
$RUNNER_HOME/gitea-runner.service
|
||||||
|
Copy it to ~/.config/systemd/user/ and 'systemctl --user daemon-reload'.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$RUNNER_HOME/gitea-runner.service" <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea Actions runner (${LABEL}, host mode)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=${BIN} daemon --config ${CONFIG}
|
||||||
|
WorkingDirectory=${RUNNER_HOME}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
# Host mode uses the login environment's toolchain; make the SDK explicit so a
|
||||||
|
# job does not depend on which shell rc happened to be sourced.
|
||||||
|
Environment=ANDROID_HOME=${SDK}
|
||||||
|
Environment=ANDROID_SDK_ROOT=${SDK}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
UNIT
|
||||||
|
echo " (unit file written)"
|
||||||
Reference in New Issue
Block a user