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>
This commit is contained in:
2026-08-26 23:44:05 +09:00
parent 32575530dc
commit 86b290d978
2 changed files with 215 additions and 87 deletions
+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: