Merge nucleic/mellow-dewy-falcon-rjhr into main
This commit is contained in:
+479
@@ -0,0 +1,479 @@
|
||||
# Setup
|
||||
|
||||
This walkthrough covers everything needed to go from a bare Apple Silicon Mac and a self-hosted
|
||||
Gitea instance to a working macOS CI runner: Gitea-side configuration, host build and signing,
|
||||
base image creation, service installation, and verification.
|
||||
|
||||
Work through it in order. The Gitea side can be done from any machine; the host side must be done
|
||||
on the Mac that will run the VMs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Gitea-side configuration
|
||||
|
||||
### 1.1 Version requirements
|
||||
|
||||
| Component | Minimum | Notes |
|
||||
| --- | --- | --- |
|
||||
| Gitea server | **1.25** | 1.25 added `GET /api/v1/admin/actions/jobs`, including the `labels` field that carries the job's `runs-on`. The daemon cannot work without it. |
|
||||
| Gitea server | 1.26+ recommended | Later fixes to Actions job dispatch and runner cleanup. |
|
||||
| Runner binary in the guest | **`gitea-runner` v3.x** | This is Gitea's own runner. It is *not* the older `act_runner`; do not substitute it. |
|
||||
|
||||
Actions must be enabled on the instance (`[actions] ENABLED = true` in `app.ini`) and on any repo
|
||||
that will use the runner.
|
||||
|
||||
### 1.2 Create the admin token (PAT)
|
||||
|
||||
The daemon needs a personal access token belonging to a **site administrator** so it can read the
|
||||
queued-jobs list and delete stale runner registrations.
|
||||
|
||||
1. Sign in as a site-admin user.
|
||||
2. **Settings → Applications → Manage Access Tokens → Generate New Token.**
|
||||
3. Grant the token **`read:admin` and `write:admin`** scopes. Nothing else is required.
|
||||
4. Copy the token immediately — Gitea shows it once.
|
||||
|
||||
Store it on the host in a file readable only by the runner user rather than inline in the config:
|
||||
|
||||
```sh
|
||||
install -m 600 /dev/null ~/.config/gitea-macos-runner/admin-token
|
||||
printf '%s' 'PASTE_TOKEN_HERE' > ~/.config/gitea-macos-runner/admin-token
|
||||
```
|
||||
|
||||
Then set `gitea.adminTokenFile` to that path. See [security.md](security.md) for why the file form
|
||||
is preferred.
|
||||
|
||||
### 1.3 Choose a registration token strategy
|
||||
|
||||
Every VM registers itself as a runner before it can accept a job, and registration requires a
|
||||
registration token. Gitea's registration tokens are **reusable**, and creating a new token for a
|
||||
given scope **invalidates the previous one** — so a distinct token per VM is impossible by design.
|
||||
You have two options.
|
||||
|
||||
**Option A (recommended): seed a stable instance-wide token on the Gitea server.**
|
||||
|
||||
Set an environment variable on the Gitea server process before it starts:
|
||||
|
||||
```sh
|
||||
# Must be at least 32 characters.
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=<32+ character random string>
|
||||
```
|
||||
|
||||
For example, in a systemd unit:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment=GITEA_RUNNER_REGISTRATION_TOKEN=<32+ character random string>
|
||||
```
|
||||
|
||||
or in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea:
|
||||
environment:
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=<32+ character random string>
|
||||
```
|
||||
|
||||
Restart Gitea, then put the same value on the host in `gitea.registrationTokenFile`. This token is
|
||||
stable across restarts and is not invalidated by someone clicking "create new token" in the UI for
|
||||
a different scope.
|
||||
|
||||
**Option B: let the daemon fetch a token via the admin API.**
|
||||
|
||||
Set `gitea.fetchRegistrationTokenViaAPI: true` and omit `gitea.registrationToken`/`…File`. The
|
||||
daemon requests a token with the admin PAT when it needs one. This is simpler to set up, but any
|
||||
out-of-band token creation for the same scope invalidates the token the daemon is holding, and the
|
||||
daemon must re-fetch. Option A is more predictable for an unattended host.
|
||||
|
||||
Generate a token value with:
|
||||
|
||||
```sh
|
||||
openssl rand -hex 24
|
||||
```
|
||||
|
||||
### 1.4 Target the runner from a workflow
|
||||
|
||||
```yaml
|
||||
# .gitea/workflows/macos.yml
|
||||
name: macOS build
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-arm64 # bare label name — must match runner.labels
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Show host
|
||||
run: sw_vers && uname -m
|
||||
- name: Build
|
||||
run: swift build
|
||||
```
|
||||
|
||||
Two things to know about labels:
|
||||
|
||||
- Use **bare label names** in `runs-on`. The runner registers with `--labels "macos-arm64:host"`;
|
||||
the `:host` suffix is runner-side only and declares that the label executes directly on the host
|
||||
OS rather than in a container. It never appears in workflow YAML.
|
||||
- Every label in `runs-on` must be present in the runner's `runner.labels`. A typo means the job
|
||||
sits queued forever with no error.
|
||||
|
||||
Because no runner exists until a job appears, jobs necessarily wait for a VM to boot. Gitea holds a
|
||||
queued job for `ABANDONED_JOB_TIMEOUT` (default **24 hours**) before marking it abandoned, so
|
||||
scale-from-zero is safe. If your queue can legitimately back up for more than a day — a long
|
||||
maintenance window, for example — raise that setting:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ABANDONED_JOB_TIMEOUT = 72h
|
||||
```
|
||||
|
||||
### 1.5 Optionally restrict which repos may use the runner
|
||||
|
||||
The runner is registered instance-wide by default, meaning any repo whose workflow says
|
||||
`runs-on: macos-arm64` can execute code on it. If that is broader than you want, register the
|
||||
runner at the org or repo level instead, or restrict via Gitea's runner settings. See
|
||||
[security.md](security.md#limiting-who-can-use-the-runner).
|
||||
|
||||
---
|
||||
|
||||
## 2. Host-side setup
|
||||
|
||||
### 2.1 Host requirements
|
||||
|
||||
| Requirement | Value |
|
||||
| --- | --- |
|
||||
| Architecture | Apple Silicon (arm64). Intel Macs cannot run macOS guests. |
|
||||
| Host macOS | 26 minimum; **27+ strongly recommended** (see below). |
|
||||
| Guest macOS | 27+ if you want unattended image builds. |
|
||||
| Free disk | ~60 GB for a vanilla image; **140 GB+** with Xcode installed. |
|
||||
| RAM | 16 GB minimum; each guest defaults to 8 GB. |
|
||||
| Concurrent VMs | **2 maximum**, enforced by the macOS kernel. `scheduler.maxConcurrentVMs` must be ≤ 2. |
|
||||
|
||||
The macOS 27 recommendation is not cosmetic. The automated image builder uses
|
||||
`VZMacGuestProvisioningOptions` — introduced in macOS 27 — to create the admin account and skip
|
||||
Setup Assistant during first boot. Both host and guest must be 27+. An older guest **silently
|
||||
ignores** the options: the install succeeds, the VM boots, and then sits at Setup Assistant with no
|
||||
SSH server, so the build appears to hang. See
|
||||
[troubleshooting.md](troubleshooting.md).
|
||||
|
||||
### 2.2 Build, sign, and install
|
||||
|
||||
Virtualization.framework refuses to run unless the calling binary carries the
|
||||
`com.apple.security.virtualization` entitlement, and entitlements are only honoured on a signed
|
||||
binary inside a proper `.app` bundle. Ad-hoc signing (`codesign -s -`) satisfies this — **no paid
|
||||
Apple developer account is needed.**
|
||||
|
||||
```sh
|
||||
git clone <this repo> && cd gitea-macos-runner
|
||||
make install
|
||||
```
|
||||
|
||||
`make install` runs the full chain and places the result:
|
||||
|
||||
| Target | What it does |
|
||||
| --- | --- |
|
||||
| `make build` | `swift build -c release --arch arm64` |
|
||||
| `make bundle` | Assemble `GiteaMacosRunner.app` around the binary: `Contents/MacOS/gitea-macos-runner`, `Contents/Info.plist`, and `Contents/Resources/` (`provision.sh`, `launchd.plist.template`, `config.example.json`) |
|
||||
| `make sign` | `codesign --sign - --entitlements …` (ad-hoc) and print the resulting entitlements |
|
||||
| `make all` | `build` + `bundle` + `sign`. The default target. |
|
||||
| `make install` | Runs `all`, copies the app to `~/Applications`, and symlinks the CLI to `/usr/local/bin/gitea-macos-runner` |
|
||||
| `make dev` | Debug build + bundle + sign, for fast iteration. Does **not** install. |
|
||||
| `make test` | `swift test` |
|
||||
| `make uninstall` | Remove the installed app and symlink |
|
||||
| `make clean` | Remove `.build/` |
|
||||
| `make help` | List the targets |
|
||||
|
||||
The three files under `Contents/Resources/` are not decoration. `image build`
|
||||
uploads `provision.sh` into the guest, `service install` renders
|
||||
`launchd.plist.template`, and `config init` writes `config.example.json`. The
|
||||
code looks in `Contents/Resources` first and only then falls back to
|
||||
repo-relative paths, so an installed `.app` missing them is a working binary
|
||||
with three broken commands.
|
||||
|
||||
If `/usr/local/bin` isn't writable, `make install` says so and prints the `sudo ln -sf …` command to
|
||||
run yourself.
|
||||
|
||||
Never run the raw binary from `.build/release/` — it is outside the signed bundle, so every VM
|
||||
operation fails with an entitlement error. Always invoke the symlink (or
|
||||
`~/Applications/GiteaMacosRunner.app/Contents/MacOS/gitea-macos-runner`).
|
||||
|
||||
### 2.3 Create the configuration file
|
||||
|
||||
```sh
|
||||
gitea-macos-runner config init # add --force to overwrite an existing file
|
||||
$EDITOR "$(gitea-macos-runner config path)"
|
||||
gitea-macos-runner config show # parse + validate, printing secrets redacted
|
||||
```
|
||||
|
||||
`config init` writes the annotated example shipped in the app bundle, so the
|
||||
file you land in has a `_comment` key explaining each section; those keys are
|
||||
ignored when the config is read back. Pass `--instance-url https://…` to seed
|
||||
`gitea.instanceURL` instead of the placeholder. Every subcommand takes
|
||||
`--config PATH` (`-c`) if you keep the file somewhere else, and `--verbose`.
|
||||
|
||||
A minimal working config:
|
||||
|
||||
```json
|
||||
{
|
||||
"gitea": {
|
||||
"instanceURL": "https://gitea.example.com",
|
||||
"adminTokenFile": "~/.config/gitea-macos-runner/admin-token",
|
||||
"registrationTokenFile": "~/.config/gitea-macos-runner/registration-token"
|
||||
},
|
||||
"runner": {
|
||||
"labels": ["macos-arm64"],
|
||||
"namePrefix": "macos-vm-"
|
||||
},
|
||||
"scheduler": {
|
||||
"maxConcurrentVMs": 2
|
||||
},
|
||||
"guest": {
|
||||
"username": "admin",
|
||||
"password": "CHANGE_ME",
|
||||
"cpuCount": 4,
|
||||
"memoryGB": 8,
|
||||
"diskGB": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuration reference
|
||||
|
||||
**`gitea`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `instanceURL` | — (required) | Base URL of the Gitea instance, e.g. `https://gitea.example.com`. Must be `http://` or `https://` with a host. A trailing slash is harmless; a subpath is preserved. |
|
||||
| `adminToken` | — | Site-admin PAT with `read:admin` + `write:admin`, inline. |
|
||||
| `adminTokenFile` | — | Path to a file containing the PAT (tilde-expanded). Should be `chmod 600`. |
|
||||
| `registrationToken` | — | Runner registration token, inline. Prefer `registrationTokenFile`. |
|
||||
| `registrationTokenFile` | — | Path to a file containing the registration token (tilde-expanded). Takes precedence over `registrationToken` if both are set. |
|
||||
| `fetchRegistrationTokenViaAPI` | `false` | Fetch a registration token with the admin PAT when no static one is configured. A *fallback*, not an override: a configured token still wins. |
|
||||
|
||||
Set **exactly one** of `adminToken` and `adminTokenFile`. Setting both is a
|
||||
validation error — a stale inline token next to a live token file is precisely
|
||||
the ambiguity that turns into a baffling 401 later — and setting neither is too.
|
||||
|
||||
For the registration token the rule is looser: configure `registrationToken`,
|
||||
`registrationTokenFile`, or `fetchRegistrationTokenViaAPI: true`. At least one
|
||||
is required; the file form wins over the inline form when both are present.
|
||||
|
||||
**`runner`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `labels` | `["macos-arm64"]` | Labels this runner offers. A queued job runs here only if its `runs-on` labels are all in this set. **Bare names only** — a `:schema` suffix here is a validation error; the `:host` suffix is appended automatically at registration. |
|
||||
| `namePrefix` | `"macos-vm-"` | Prefix for generated runner names in the Gitea UI; a unique suffix is appended per VM. |
|
||||
| `runnerDownloadURL` | `https://gitea.com/gitea/runner/releases/download/v{version}/gitea-runner-{version}-darwin-arm64` | Release asset installed into the guest. `{version}` is substituted with `version`. |
|
||||
| `version` | `"3.0.2"` | The `gitea-runner` version to install. |
|
||||
|
||||
**`scheduler`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `maxConcurrentVMs` | `2` | Simultaneous VMs. **Hard-capped at 2 by macOS.** A larger value is silently clamped to 2 at load rather than rejected; the kernel is not something a config file gets to negotiate. |
|
||||
| `pollIntervalSeconds` | `5` | How often to poll the queued-jobs API. |
|
||||
| `reconcileIntervalSeconds` | `300` | How often to sweep Gitea for orphaned runner registrations from uncleanly-killed VMs. |
|
||||
| `jobTimeoutMinutes` | `120` | Wall-clock limit for one job; the VM is destroyed when exceeded. |
|
||||
| `bootTimeoutSeconds` | `300` | Time allowed from VM start to a usable SSH connection. |
|
||||
|
||||
**`guest`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `username` | `"admin"` | Guest admin account created during image build. Has passwordless sudo. |
|
||||
| `password` | — (required) | Password for that account. Also used for SSH if key auth is unavailable. |
|
||||
| `cpuCount` | `4` | vCPUs per guest. |
|
||||
| `memoryGB` | `8` | RAM per guest. Two concurrent guests at 8 GB need a 16 GB+ host with headroom. |
|
||||
| `diskGB` | `64` | Guest disk size. Sparse, so this is a ceiling, not immediate consumption. Raise for Xcode. |
|
||||
|
||||
**`storage`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `storeDir` | `~/Library/Application Support/gitea-macos-runner` | Base images (`images/`) and running clones (`vms/`) live here. |
|
||||
| `minFreeDiskGB` | `20` | The scheduler refuses to start a VM below this free-space threshold rather than filling the disk mid-job. |
|
||||
|
||||
### 2.4 Build the base image
|
||||
|
||||
Download a macOS **27 or newer** IPSW for Apple Silicon (Apple's restore images; the URL for the
|
||||
current release is also discoverable from Virtualization.framework's latest-supported-restore-image
|
||||
endpoint), then:
|
||||
|
||||
```sh
|
||||
gitea-macos-runner image build \
|
||||
--ipsw ~/Downloads/UniversalMac_27.0_XXXXX_Restore.ipsw \
|
||||
--disk-gb 64
|
||||
```
|
||||
|
||||
`--name` defaults to `default`, which is also what `daemon` and `vm boot` look
|
||||
for. If you name the image something else, pass the same name to
|
||||
`daemon --image NAME` (and to `vm boot --image NAME`) or the daemon will not
|
||||
find it. `--ipsw` is optional: omit it and the latest supported restore image is
|
||||
downloaded into `storeDir/ipsw/` first, which is most of the build's wall-clock
|
||||
time. `--disk-gb` overrides `guest.diskGB` for this image only.
|
||||
|
||||
This takes a long time — macOS installs from the IPSW, boots, and is then provisioned over SSH.
|
||||
**Do not interrupt it during the install phase.** Stopping a VM mid-install leaves the disk image
|
||||
in an undefined state; delete the image and start over rather than trying to resume.
|
||||
|
||||
The finished image contains:
|
||||
|
||||
- macOS installed from the IPSW.
|
||||
- The `guest.username` admin account, auto-created via provisioning options, with SSH (Remote
|
||||
Login) enabled and **passwordless sudo**.
|
||||
- Sleep, screensaver, and Spotlight indexing disabled — a sleeping guest stalls a job, and Spotlight
|
||||
wastes I/O on a throwaway machine.
|
||||
- Raised file-descriptor limits, which Node- and Xcode-based builds routinely exhaust at the
|
||||
default.
|
||||
- **Node.js — required, not optional.** Gitea Actions' JavaScript actions (`actions/checkout` and
|
||||
most of the ecosystem) are executed by spawning `node` in the guest. Without it, every workflow
|
||||
fails on its first step.
|
||||
- `git`, verified present.
|
||||
- The `gitea-runner` v3.x binary.
|
||||
|
||||
Verify:
|
||||
|
||||
```sh
|
||||
gitea-macos-runner image list
|
||||
```
|
||||
|
||||
#### Adding Xcode (optional)
|
||||
|
||||
Xcode is not installed automatically. Apple requires an authenticated Apple ID for the download and
|
||||
its download endpoints are impractical to drive unattended, so you fetch the `.xip` yourself from
|
||||
[developer.apple.com/download](https://developer.apple.com/download/) and hand it to the
|
||||
provisioner:
|
||||
|
||||
```sh
|
||||
gitea-macos-runner image provision default --xcode-xip ~/Downloads/Xcode_XX.xip
|
||||
```
|
||||
|
||||
Budget disk accordingly: **~60 GB free is enough for a vanilla image, but plan on 140 GB+ with
|
||||
Xcode**, and remember each running VM is a copy-on-write clone whose divergence from the base
|
||||
consumes additional space while it runs. Raise `guest.diskGB` (e.g. to 120) before building if the
|
||||
image will carry Xcode.
|
||||
|
||||
### 2.5 Install the service
|
||||
|
||||
```sh
|
||||
gitea-macos-runner service install
|
||||
gitea-macos-runner service status
|
||||
```
|
||||
|
||||
This installs a **LaunchAgent in the logged-in user's GUI session — not a LaunchDaemon.** Two
|
||||
reasons this is not negotiable:
|
||||
|
||||
1. Virtualization.framework requires a GUI session; a system-context daemon cannot start VMs.
|
||||
2. macOS 15 and later require an **unlocked `login.keychain`** for key operations the VM lifecycle
|
||||
performs. In a locked or headless session these fail with `SecKeyCreateRandomKey` /
|
||||
"Interaction is not allowed" errors.
|
||||
|
||||
**Recommended: enable auto-login for the runner user on a dedicated CI Mac.**
|
||||
**System Settings → Users & Groups → Automatically log in as → \<runner user\>.** Combine with
|
||||
disabling sleep (`sudo pmset -a sleep 0 disablesleep 1`) so the Mac comes back into a live session
|
||||
after a power event without a human present. This does mean the disk is effectively unlocked at
|
||||
boot — appropriate for a dedicated CI machine, not for a shared workstation.
|
||||
|
||||
`service uninstall` removes the LaunchAgent; it does not delete images or config.
|
||||
|
||||
### 2.6 macOS 15+ Local Network privacy prompt
|
||||
|
||||
Starting with macOS 15, a process that contacts other hosts on the local network triggers a
|
||||
one-time Local Network permission prompt. A LaunchAgent that is denied (or that never gets a human
|
||||
to click Allow) cannot reach the guest's NAT address, so VMs boot but SSH never connects.
|
||||
|
||||
Grant it interactively the first time — run `gitea-macos-runner vm boot` from a
|
||||
Terminal in the GUI session and click **Allow** — or pre-authorize the VM subnet:
|
||||
|
||||
```sh
|
||||
sudo defaults write com.apple.network.local-network \
|
||||
AllowedEthernetLocalNetworkAddresses -array "192.168.0.0/16"
|
||||
```
|
||||
|
||||
Adjust the range to match the subnet Virtualization.framework's NAT hands out on your host (check
|
||||
`/var/db/dhcpd_leases` after a VM boots). Reboot, or restart the service, for the change to take
|
||||
effect. Also confirm the runner is enabled under **System Settings → Privacy & Security → Local
|
||||
Network**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Verification
|
||||
|
||||
### 3.1 Preflight
|
||||
|
||||
```sh
|
||||
gitea-macos-runner doctor
|
||||
```
|
||||
|
||||
It reports one line per check, each `✓` pass, `✗` fail, `!` warn, or `·` note,
|
||||
with a remediation hint under anything that is not a pass, and exits non-zero if
|
||||
anything failed. `--json` emits the same results machine-readably; `--no-fail`
|
||||
exits zero regardless, which is what you want when running it from a script that
|
||||
handles the results itself.
|
||||
|
||||
The checks, in order:
|
||||
|
||||
| Check | What it looks at |
|
||||
| --- | --- |
|
||||
| `host capability` | Apple Silicon, and host macOS ≥ 26 |
|
||||
| `Virtualization.framework` | `VZVirtualMachine.isSupported` |
|
||||
| `virtualization entitlement` | `com.apple.security.virtualization` on the *running* executable — this is the check that catches running from `.build/` instead of the signed `.app` |
|
||||
| `configuration` | The config file loads, parses, and passes validation |
|
||||
| `free disk space` | Free space on the `storeDir` volume against `storage.minFreeDiskGB` |
|
||||
| `login.keychain unlocked` | `security show-keychain-info login.keychain` |
|
||||
| `gitea admin token` / `gitea admin api` | The admin token resolves, and an admin-only endpoint answers with it (so a non-admin PAT fails here rather than at 3am) |
|
||||
| `registration token` | A static token resolves, or one can be fetched when `fetchRegistrationTokenViaAPI` is on |
|
||||
| `runner download url` | The `gitea-runner` release asset is reachable |
|
||||
| `token file permissions` | Warns — not fails — when a token file is group- or world-readable |
|
||||
| `local network access` | An informational note about the macOS 15+ Local Network prompt |
|
||||
|
||||
If the config file is missing or invalid, the host checks still run and the rest
|
||||
are skipped — which is exactly the state a first-time operator is in. Resolve
|
||||
everything it reports before going further.
|
||||
|
||||
There is deliberately **no** "a base image exists" check; use `image list`.
|
||||
|
||||
### 3.2 Boot a VM by hand
|
||||
|
||||
```sh
|
||||
gitea-macos-runner vm boot
|
||||
```
|
||||
|
||||
This clones the base image and boots it without registering a runner — the fastest way to confirm
|
||||
that virtualization, networking, and SSH all work. Once it is up, check that the guest got a lease:
|
||||
|
||||
```sh
|
||||
cat /var/db/dhcpd_leases
|
||||
```
|
||||
|
||||
and that you can reach it:
|
||||
|
||||
```sh
|
||||
ssh admin@<guest-ip> 'sw_vers; node --version; git --version; which gitea-runner'
|
||||
```
|
||||
|
||||
All four should answer. A guest at Setup Assistant instead of a login window means the image was
|
||||
built from a pre-27 IPSW; rebuild.
|
||||
|
||||
### 3.3 Watch a real job
|
||||
|
||||
Start the service, push the example workflow from §1.4, and watch both sides:
|
||||
|
||||
```sh
|
||||
# Host: daemon logs
|
||||
log stream --predicate 'process == "gitea-macos-runner"' --info
|
||||
|
||||
# Host: VM lifecycle
|
||||
gitea-macos-runner service status
|
||||
```
|
||||
|
||||
In the Gitea UI, the job should move from queued to running within roughly one poll interval plus
|
||||
boot time; a runner named `macos-vm-<suffix>` appears under **Site Administration → Actions →
|
||||
Runners** while the job runs and disappears when it finishes. That disappearance is Gitea deleting
|
||||
the ephemeral registration itself, and it is the signal that the whole loop worked.
|
||||
|
||||
If the job stays queued, or the runner appears but the job fails immediately, go to
|
||||
[troubleshooting.md](troubleshooting.md).
|
||||
Reference in New Issue
Block a user