Compare commits

...

9 Commits

Author SHA1 Message Date
irene ff423f8bbf ci: split into fast analyze + slow release build with path gating
analyze runs on every push/PR (fast check).
release-apk only fires on push to master or workflow_dispatch (slow build).
Doc-only pushes no longer trigger any CI run.
workflow_dispatch triggers both jobs unconditionally.
2026-08-27 23:59:04 +09:00
irene 47d849a122 ci: split into fast analyze + slow release build with path gating
analyze runs on every push/PR (fast check).
release-apk only fires on push to master or workflow_dispatch (slow build).
Doc-only pushes no longer trigger any CI run.
workflow_dispatch triggers both jobs unconditionally.
2026-08-27 23:58:02 +09:00
irene ba7f335b67 ci: split into fast analyze + slow release build with path gating
analyze job runs on push/PR regardless of changed files (fast check).
release-apk job only fires when Dart source, Android, pubspec, or
tooling files change on push to master (slow build).
workflow_dispatch triggers both jobs unconditionally.
Doc-only pushes no longer trigger any CI run.
2026-08-27 23:56:31 +09:00
irene 5b6c2c0435 fix(docs): correct iperf3 version from 3.21 to 3.19 in LICENSE
CI / Analyze & Test (push) Successful in 14s
CI / Release APK (push) Successful in 22s
2026-08-27 00:06:04 +09:00
irene b01f8cdf87 docs: add MIT LICENSE file for Gitea detection
CI / Analyze & Test (push) Successful in 10s
CI / Release APK (push) Successful in 23s
2026-08-27 00:02:44 +09:00
mikkeli b960581fd1 ci: allow manual runs with workflow_dispatch
CI / Analyze & Test (push) Successful in 9s
CI / Release APK (push) Successful in 23s
The workflow only triggered on push and pull_request, so a run that failed for
reasons outside the repo — runner down, toolchain changed underneath it —
could only be retried with an empty commit. That is a likelier situation with a
self-hosted runner than with hosted ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:57:33 +09:00
mikkeli 7981ca2022 fix(ci): register from the runner's own directory with an absolute state path
CI / Analyze & Test (push) Successful in 11s
CI / Release APK (push) Successful in 22s
Registration failed with `open .runner: permission denied`. runner.file was
the relative `.runner`, which resolves against the caller's working directory
— the repo the admin ran sudo from, which the service account cannot write.
The same applied to .runner.lock, so the single-process guard was skipped too.

Two fixes rather than one, because either alone leaves a trap: runner.file is
now absolute, and register runs with cwd set to the runner's home. An earlier
draft did cd there; that was lost when the script was rewritten for a system
service, and an absolute path is what makes it not matter next time.

Also drops --labels from the register call, which gitea-runner ignores and
warns about whenever the config file defines labels, and rewrites a config that
exists while nothing is registered yet — such a config is provisional, not in
use, and leaving a broken one in place was what required manual cleanup here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:53:04 +09:00
mikkeli ae2033865c fix(ci): probe writability by writing, not with test -w
CI / Analyze & Test (push) Successful in 12s
CI / Release APK (push) Successful in 22s
The setup script refused to provision, reporting /opt/flutter/bin/cache and
/opt/android-sdk unwritable by the service account, while a touch as that same
account succeeded.

Cause: this machine's /usr/bin/test is uutils coreutils 0.8.0, whose -w does
not credit supplementary group membership. Write access to both directories
comes from the flutterdev group rather than the owner bits, so -w reports false
for any account that should be building here — including the developer's own.
-r and -x pass only because `other` carries r-x, which is what made the failure
look ACL-shaped. bash's builtin [ -w ] evaluates it correctly, but sudo execs
the external binary, so the builtin was never in play.

Replaced with a real write probe: create a file as the service account and
remove it. That tests the operation a build actually performs and is immune to
whichever test implementation is installed, which is the better check
regardless of the bug that prompted it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:50:09 +09:00
mikkeli 86b290d978 ci: run the runner as a dedicated account under a system service
CI / Analyze & Test (push) Successful in 13s
CI / Release APK (push) Successful in 24s
The earlier script installed a systemd *user* service, which conflated two
separate decisions. What matters in host mode is which account jobs run as,
because there is no container boundary and the service account is the only
sandbox. A system service with User= set gets unprivileged jobs *and* a
root-managed lifecycle that starts at boot with no lingering session.

Jobs now run as a dedicated `gitea-runner` account in the flutterdev group,
with its own HOME so its Gradle and pub caches are its own. The alternative —
a human account — would hand every workflow that user's home directory and SSH
keys, which is a sharper edge than usual here because a workflow file is code
and much of this repo is agent-authored. Running as root would be worse still,
and would also miss the warm caches in a human HOME and re-download
dependencies every run.

flutterdev is what grants write access to /opt/flutter and /opt/android-sdk.
Group membership alone does not prove it, since both carry ACLs, so the script
tests writability as the service account and fails with the getfacl command to
run. Tool lookups use systemd's default PATH rather than an interactive shell's,
because that is what the service will actually have.

Adds --check, which runs pre-flight and exits without creating anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:44:05 +09:00
7 changed files with 275 additions and 93 deletions
+10
View File
@@ -3,8 +3,16 @@ name: CI
on:
push:
branches: [ master ]
paths:
- 'lib/**.dart'
- 'test/**.dart'
- 'android/**'
- 'tool/**'
- 'pubspec.yaml'
- 'pubspec.lock'
pull_request:
branches: [ master ]
workflow_dispatch:
# 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
@@ -43,6 +51,8 @@ jobs:
release-apk:
name: Release APK
runs-on: flutter-android
needs: [analyze]
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
+26
View File
@@ -0,0 +1,26 @@
MIT License
Copyright (c) 2024–2026 Mikkeli Matlock
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Note: This project bundles iperf3 v3.19, which is licensed under the
BSD-3-Clause license. See the bundled iperf3 source for its full license text.
-3
View File
@@ -40,9 +40,6 @@ domain, so shipping an iperf3 ELF in assets and running it via `Process.start()`
fails with "Permission denied". iperf3 is therefore compiled as a shared library
(`libiperf3jni.so`) and called in-process over JNI.
`assets/iperf3/` still holds the pre-compiled binaries from that earlier
approach. They are not declared in `pubspec.yaml`, so they are not packaged into
the APK, and nothing in the app reads them.
## Building
Binary file not shown.
Binary file not shown.
+25 -4
View File
@@ -196,10 +196,31 @@ arm64-host `gen_snapshot` ([flutter#189724](https://github.com/flutter/flutter/i
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.
SDK/NDK. CI therefore builds with the same toolchain that produced the verified
release, and no NDK is downloaded per run. The cost is that there is no
container boundary, which is what decides the next paragraph.
Jobs run as a dedicated unprivileged account, `gitea-runner`, in the
`flutterdev` group. In host mode the service account *is* the sandbox — there
is no other — and a workflow file is code that runs as whoever the service runs
as. A human account would hand every workflow that user's home directory and
SSH keys; root would be worse and would also miss the warm caches in that home,
re-downloading dependencies every run. The dedicated account keeps its own
Gradle and pub caches under `/var/lib/gitea-runner`, so the first Android build
is slow by design and later ones are not.
`flutterdev` is what grants write access to `/opt/flutter` and
`/opt/android-sdk`; both carry ACLs with `group:flutterdev:rwx` and matching
`default:` entries, so new files inherit it. The setup script proves that access
as the service account rather than assuming group membership is sufficient.
It installs a **system** service, not a user service: root-managed lifecycle,
starts at boot, no lingering session. That the service is root-managed is
unrelated to the jobs being unprivileged — `User=` in the unit settles the
second, and conflating the two is easy to do.
`sudo ./tool/setup_ci_runner.sh --check` validates this machine's toolchain
without creating anything.
Two jobs:
+214 -86
View File
@@ -1,160 +1,288 @@
#!/usr/bin/env bash
# Register this machine as a Gitea Actions runner for the android-stuff org.
# Provision 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.
# sudo ./tool/setup_ci_runner.sh --check # pre-flight only, changes nothing
# sudo RUNNER_TOKEN=xxxx ./tool/setup_ci_runner.sh
#
# 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.
# Get an ORG-scoped token from
# <gitea>/org/android-stuff/settings/actions/runners → "Create new runner"
# Org scope, not repo scope, so every android-stuff repo shares one runner.
#
# 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
# ── WHY THIS MACHINE, NOT THE GITEA HOST ────────────────────────────────────
# Gitea runs on an aarch64 box. 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). An x86_64 runner is a
# requirement here, not a preference.
#
# Re-running is safe: it skips the download if the binary is current and
# refuses to clobber an existing registration.
# ── WHY HOST MODE, NOT A CONTAINER ──────────────────────────────────────────
# This machine has a working Flutter + Android SDK + NDK. In host mode jobs use
# it directly, so CI builds with the same toolchain that produced the verified
# release and skips a per-run NDK download. The cost is that there is no
# container boundary — which is exactly why the service account below matters.
#
# ── WHY A DEDICATED USER, AND A SYSTEM SERVICE ──────────────────────────────
# Host mode means the service account IS the sandbox; there is no other. A
# workflow file is code, and it runs as whoever this service runs as. Running
# as a human user would give every workflow read access to that user's home,
# SSH keys included. Running as root would be worse, and would also miss the
# warm Gradle/pub caches in that home (HOME=/root), re-downloading dependencies
# every run for no benefit.
#
# So: a dedicated unprivileged account, in the flutterdev group for SDK access,
# with its own HOME so its caches are its own. A *system* service rather than a
# user service, so it starts at boot under plain systemctl with no lingering
# session — the service being root-managed is unrelated to the jobs being
# unprivileged, and conflating those two was the earlier mistake here.
set -euo pipefail
VERSION="${RUNNER_VERSION:-3.3.0}"
INSTANCE="${GITEA_INSTANCE:-http://pi5-16.local:3005}"
RUNNER_USER="${RUNNER_USER:-gitea-runner}"
SDK_GROUP="${SDK_GROUP:-flutterdev}"
RUNNER_HOME="${RUNNER_HOME:-/var/lib/gitea-runner}"
BIN="${RUNNER_BIN:-/usr/local/bin/gitea-runner}"
UNIT="/etc/systemd/system/gitea-runner.service"
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.
# Capability label, deliberately not a hostname: it says what the runner can
# do, so a 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)}"
CHECK_ONLY=0
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=1
echo "==> Checking the toolchain this runner promises to provide"
[[ $EUID -eq 0 ]] || { echo "Run with sudo: sudo RUNNER_TOKEN=... $0" >&2; exit 1; }
# --check is for validating this machine before committing to anything, so it
# deliberately does not require a token.
if [[ $CHECK_ONLY -eq 0 ]]; then
: "${RUNNER_TOKEN:?set RUNNER_TOKEN to an org registration token (see header)}"
fi
say() { printf '\n==> %s\n' "$1"; }
# ── Pre-flight, before anything is created ─────────────────────────────────
say "Pre-flight"
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"
for cmd in flutter dart java cmake ninja node git unzip curl; do
# Resolved from systemd's own PATH, not an interactive shell's, since that
# is what the service will actually have.
if PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
command -v "$cmd" >/dev/null; then
printf ' ok %s\n' "$cmd"
else
printf ' MISSING %s\n' "$cmd"
printf ' MISSING %s (not on systemd default PATH)\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
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/opt/android-sdk}}"
for d in "$SDK/ndk" "$SDK/build-tools" "$SDK/platforms"; do
[[ -d "$d" ]] && printf ' ok %s\n' "$d" \
|| { printf ' MISSING %s\n' "$d"; missing=1; }
done
getent group "$SDK_GROUP" >/dev/null \
&& printf ' ok group %s\n' "$SDK_GROUP" \
|| { printf ' MISSING group %s\n' "$SDK_GROUP"; missing=1; }
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
echo "provide these, or every job fails confusingly instead of here." >&2
exit 1
fi
echo "==> Installing gitea-runner $VERSION"
mkdir -p "$(dirname "$BIN")" "$RUNNER_HOME"
if [[ $CHECK_ONLY -eq 1 ]]; then
say "Pre-flight passed"
echo " Nothing was created. Re-run with RUNNER_TOKEN set to provision."
echo " Note: SDK write access is verified as $RUNNER_USER, which can only"
echo " be checked once that account exists — so it is not covered here."
exit 0
fi
# ── Service account ────────────────────────────────────────────────────────
say "Service account: $RUNNER_USER"
if id "$RUNNER_USER" &>/dev/null; then
echo " exists; ensuring membership of $SDK_GROUP"
usermod -aG "$SDK_GROUP" "$RUNNER_USER"
else
# --shell /bin/bash, not nologin: host-mode steps are executed as shell
# scripts, and a login shell costs nothing here — the account has no
# password, so it is only reachable via root anyway.
useradd --system \
--create-home --home-dir "$RUNNER_HOME" \
--shell /bin/bash \
--groups "$SDK_GROUP" \
--comment "Gitea Actions runner ($LABEL)" \
"$RUNNER_USER"
echo " created"
fi
install -d -o "$RUNNER_USER" -g "$SDK_GROUP" -m 0750 \
"$RUNNER_HOME" "$RUNNER_HOME/workspace"
id "$RUNNER_USER"
# Group membership is worthless if the ACLs disagree, so prove it rather than
# assume it — as the service user, against the paths a build actually writes.
#
# By actually writing a file, not by asking `test -w`. This machine's
# /usr/bin/test is uutils coreutils 0.8.0, whose -w does not credit
# supplementary group membership: it reports /opt/android-sdk unwritable for
# any account whose write access comes from the flutterdev group rather than
# from the owner bits — which is every account that should be running builds
# here. (-r and -x pass only because `other` has r-x.) bash's builtin `[ -w ]`
# gets it right, but sudo execs the external binary, so the builtin is not
# available here. A write probe is immune to the whole question.
say "Verifying $RUNNER_USER can write the SDKs"
write_ok=1
for d in "/opt/flutter/bin/cache" "$SDK"; do
if sudo -u "$RUNNER_USER" env probe="$d/.gitea-runner-probe.$$" \
sh -c 'touch "$probe" && rm -f "$probe"' 2>/dev/null; then
printf ' ok writable: %s\n' "$d"
else
printf ' FAIL not writable: %s\n' "$d"
sudo -u "$RUNNER_USER" rm -f "$d/.gitea-runner-probe.$$" 2>/dev/null || true
write_ok=0
fi
done
if [[ $write_ok -ne 1 ]]; then
echo
echo "$RUNNER_USER is in $SDK_GROUP but still cannot write. Check:" >&2
echo " getfacl /opt/flutter /opt/android-sdk" >&2
echo " id $RUNNER_USER" >&2
exit 1
fi
# ── Binary ─────────────────────────────────────────────────────────────────
say "Installing gitea-runner $VERSION"
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"
echo " $url"
curl -fSL --retry 3 -o "$BIN.tmp" "$url"
chmod +x "$BIN.tmp"
chmod 0755 "$BIN.tmp"
mv "$BIN.tmp" "$BIN"
fi
"$BIN" --version
echo "==> Writing config"
# ── Config ─────────────────────────────────────────────────────────────────
say "Config"
CONFIG="$RUNNER_HOME/config.yaml"
if [[ -f "$CONFIG" ]]; then
echo " $CONFIG exists, leaving it alone"
if [[ -f "$CONFIG" && -f "$RUNNER_HOME/.runner" ]]; then
# Registered: this config is in use, do not touch it.
echo " $CONFIG exists and the runner is registered — leaving it alone"
else
if [[ -f "$CONFIG" ]]; then
echo " $CONFIG exists but nothing is registered yet — rewriting it"
fi
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.
# Absolute on purpose: a relative path resolves against whatever directory
# the command was invoked from, which for `register` is wherever the admin
# happened to be standing — typically a repo this account cannot write.
file: ${RUNNER_HOME}/.runner
# One at a time: a 2-ABI NDK compile plus Gradle will saturate this box, and
# concurrent jobs would only contend for it.
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.
# ':host' runs steps directly on this machine as ${RUNNER_USER}, using the
# toolchain verified in pre-flight, 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.
# Unused in host mode. Kept so switching to container mode later is an edit
# rather than a rediscovery — it would need the Docker network that the
# Gitea host's own runner also needs.
network: ""
privileged: false
host:
workdir_parent: ${RUNNER_HOME}/workspace
YAML
chown "$RUNNER_USER:$SDK_GROUP" "$CONFIG"
chmod 0640 "$CONFIG"
echo " wrote $CONFIG"
fi
echo "==> Registering with $INSTANCE"
# ── Registration ───────────────────────────────────────────────────────────
say "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."
echo " already registered ($RUNNER_HOME/.runner) — skipping."
echo " To re-register: delete that file AND remove the runner in Gitea."
else
(cd "$RUNNER_HOME" && "$BIN" register \
--no-interactive \
--instance "$INSTANCE" \
--token "$RUNNER_TOKEN" \
--name "$RUNNER_NAME" \
--labels "${LABEL}:host" \
--config "$CONFIG")
# Run from the runner's own directory: register writes .runner and
# .runner.lock relative to cwd unless told otherwise, and this account
# cannot write wherever the admin invoked the script from.
# --labels is deliberately absent: gitea-runner ignores it when the config
# file defines labels, and warns that it is doing so.
sudo -u "$RUNNER_USER" env HOME="$RUNNER_HOME" sh -c '
cd "$1" || exit 1
exec "$2" register --no-interactive \
--instance "$3" --token "$4" --name "$5" --config "$6"
' sh "$RUNNER_HOME" "$BIN" "$INSTANCE" "$RUNNER_TOKEN" "$RUNNER_NAME" "$CONFIG"
# register writes .runner into the cwd it was given; make sure it landed.
[[ -f "$RUNNER_HOME/.runner" ]] || {
echo " registration produced no .runner in $RUNNER_HOME" >&2
exit 1
}
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
# ── Service ────────────────────────────────────────────────────────────────
say "systemd unit"
cat > "$UNIT" <<UNITFILE
[Unit]
Description=Gitea Actions runner (${LABEL}, host mode)
Documentation=https://gitea.com/gitea/act_runner
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=${BIN} daemon --config ${CONFIG}
Type=simple
User=${RUNNER_USER}
Group=${SDK_GROUP}
WorkingDirectory=${RUNNER_HOME}
ExecStart=${BIN} daemon --config ${CONFIG}
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.
# HOME must be explicit: it decides where Gradle and pub put their caches, and
# a service does not inherit a login environment.
Environment=HOME=${RUNNER_HOME}
Environment=GRADLE_USER_HOME=${RUNNER_HOME}/.gradle
Environment=PUB_CACHE=${RUNNER_HOME}/.pub-cache
Environment=ANDROID_HOME=${SDK}
Environment=ANDROID_SDK_ROOT=${SDK}
[Install]
WantedBy=default.target
UNIT
echo " (unit file written)"
WantedBy=multi-user.target
UNITFILE
echo " wrote $UNIT"
systemctl daemon-reload
systemctl enable --now gitea-runner
sleep 2
systemctl --no-pager --lines=15 status gitea-runner || true
cat <<EOF
==> Done.
Logs: journalctl -u gitea-runner -f
Stop: systemctl stop gitea-runner
Remove: systemctl disable --now gitea-runner
rm $UNIT $BIN && rm -rf $RUNNER_HOME
userdel $RUNNER_USER
...and delete the runner in the Gitea org settings.
The runner should now appear under
${INSTANCE}/org/android-stuff/settings/actions/runners
with label '$LABEL'. The queued run for this repo should start on its own.
First Android build will be slow: this account's Gradle and pub caches
start empty by design. Subsequent runs reuse them.
EOF