diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index f7da8d1..61f5868 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,5 +1,5 @@
{
- "name": "template-appliance",
+ "name": "runner",
"build": {
"dockerfile": "Dockerfile"
},
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 2aad126..6b45a99 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,4 +1,4 @@
-# GitHub Copilot instructions (template-appliance)
+# GitHub Copilot instructions (runner)
These instructions describe how this repo is structured, how CI runs, and what “good changes” look like.
@@ -6,7 +6,7 @@ If anything here conflicts with an explicit user request in the chat, follow the
## 1) What this repo is
-`template-appliance` is a Bash-first skeleton project that ships scripts + systemd units for a generic appliance with a primary mode, a secondary mode, and a healthcheck-based failover.
+`runner` is a Bash-first appliance project that ships scripts + systemd units for running a single GitHub Actions self-hosted runner.
The codebase emphasizes:
- Script correctness and predictable behavior under `set -euo pipefail`
@@ -23,7 +23,7 @@ Preferred ways to run things:
- One CI part: `./scripts/ci.sh lint-sh` (or other part names)
- Make targets (when available): `make lint`, `make test-unit`, `make test-integration`, `make ci`
-The Makefile runs commands inside a Docker devcontainer image (`template-appliance-devcontainer:local`). If `docker` isn’t available, the Makefile may fall back to running locally.
+The Makefile runs commands inside a Docker devcontainer image (`runner-devcontainer:local`). If `docker` isn’t available, the Makefile may fall back to running locally.
## 3) CI/router model (important)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9e2262d..b18af62 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -37,7 +37,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-permissions'
lint-naming:
@@ -54,7 +54,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -66,7 +66,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-naming'
lint-sh:
@@ -83,7 +83,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -95,7 +95,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-sh'
test-all:
@@ -112,7 +112,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -124,7 +124,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh tests'
test-coverage:
@@ -142,7 +142,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -155,7 +155,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh coverage'
- name: Show kcov output paths
@@ -217,7 +217,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -229,7 +229,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-yaml'
lint-systemd:
@@ -246,7 +246,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -258,7 +258,7 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-systemd'
lint-markdown:
@@ -275,7 +275,7 @@ jobs:
with:
context: .
file: .devcontainer/Dockerfile
- tags: template-appliance-devcontainer:ci
+ tags: runner-devcontainer:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
@@ -287,5 +287,5 @@ jobs:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer:ci \
+ runner-devcontainer:ci \
bash -lc './scripts/ci.sh lint-markdown'
diff --git a/.gitignore b/.gitignore
index e568c02..eac9207 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,10 @@ tests/.tmp/
.tmp/
.tmp-bats-trace.txt
+# Junk dirs accidentally created by misquoted paths
+"mp_dir/
+"PPLIANCE_ROOT/
+
# Coverage artifacts (generated by kcov runs)
coverage/
coverage-devcontainer*/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f6f9ab8..ebdd162 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -63,7 +63,7 @@ That is the same pipeline GitHub CI uses.
Build the devcontainer image:
```bash
-docker build -t template-appliance-devcontainer -f .devcontainer/Dockerfile .
+docker build -t runner-devcontainer -f .devcontainer/Dockerfile .
```
Run the full pipeline inside it:
@@ -72,7 +72,7 @@ Run the full pipeline inside it:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer \
+ runner-devcontainer \
bash -lc './scripts/ci.sh'
```
diff --git a/Makefile b/Makefile
index fd60316..f0e66fe 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ SHELL := /usr/bin/env bash
ci
DOCKER ?= docker
-DEVCONTAINER_IMAGE ?= template-appliance-devcontainer:local
+DEVCONTAINER_IMAGE ?= runner-devcontainer:local
DEVCONTAINER_DOCKERFILE ?= .devcontainer/Dockerfile
DEVCONTAINER_CONTEXT ?= .
DEVCONTAINER_WORKDIR ?= /work
diff --git a/README.md b/README.md
index 8986c76..bc3e134 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
-# template-appliance
+# runner
-Reusable Bash + systemd appliance skeleton built around a simple model:
+Bash + systemd appliance for running a single GitHub Actions self-hosted runner on Linux (including Raspberry Pi).
-- Primary mode: run your main workload (`APPLIANCE_PRIMARY_CMD`)
-- Secondary mode: run a fallback workload (`APPLIANCE_SECONDARY_CMD`)
-- Healthcheck timer: if primary is not active, start secondary
+Goal:
-This repo is intentionally minimal: you provide the commands and any extra packages.
+- Keep exactly one host runner process managed by systemd.
+- Route job execution into an ephemeral `systemd-nspawn` guest (systemd PID1 semantics)
+ via GitHub Actions runner container hooks.
## Documentation
@@ -19,7 +19,7 @@ This repo is intentionally minimal: you provide the commands and any extra packa
Build the devcontainer image:
```bash
-docker build -t template-appliance-devcontainer -f .devcontainer/Dockerfile .
+docker build -t runner-devcontainer -f .devcontainer/Dockerfile .
```
Run the full CI pipeline inside it:
@@ -28,7 +28,7 @@ Run the full CI pipeline inside it:
docker run --rm \
-v "$PWD:/work" \
-w /work \
- template-appliance-devcontainer \
+ runner-devcontainer \
bash -lc './scripts/ci.sh'
```
@@ -42,27 +42,18 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the required pre-PR checks.
## Runtime model
-At a glance, systemd manages three key units:
+At a glance, systemd manages two key units:
-- `template-appliance-primary.service`
-- `template-appliance-secondary.service`
-- `template-appliance-healthcheck.timer` (runs `template-appliance-healthcheck.service`)
-
-```mermaid
-flowchart TD
- PRIMARY["template-appliance-primary.service
runs APPLIANCE_PRIMARY_CMD"]
- SECONDARY["template-appliance-secondary.service
runs APPLIANCE_SECONDARY_CMD"]
- TIMER["template-appliance-healthcheck.timer"] --> CHECK["template-appliance-healthcheck.service"]
- CHECK -->|"primary inactive"| SECONDARY
-```
+- `runner-install.service` (first-boot installer; retried until it succeeds)
+- `runner.service` (runs the configured GitHub runner)
## Installation (cloud-init / Pi Imager)
The recommended install flow is:
-1. cloud-init writes `/etc/template-appliance/config.env`.
+1. cloud-init writes `/etc/runner/config.env`.
2. cloud-init installs a one-time installer unit + bootstrap script.
-3. systemd runs `template-appliance-install.service` until install succeeds.
+3. systemd runs `runner-install.service` until install succeeds.
Examples:
@@ -71,40 +62,44 @@ Examples:
## Configuration
-Runtime configuration lives in `/etc/template-appliance/config.env`.
+Runtime configuration lives in `/etc/runner/config.env`.
Required (first-boot bootstrap):
- `APPLIANCE_REPO_URL`
- `APPLIANCE_REPO_REF` (branch/tag/commit; pinning to a tag/commit is recommended)
-Required (runtime modes):
-
-- `APPLIANCE_PRIMARY_CMD`
-- `APPLIANCE_SECONDARY_CMD`
-
Optional:
-- `APPLIANCE_CHECKOUT_DIR` (default: `/opt/template-appliance`)
-- `APPLIANCE_INSTALLED_MARKER` (default: `/var/lib/template-appliance/installed`)
+- `APPLIANCE_CHECKOUT_DIR` (default: `/opt/runner`)
+- `APPLIANCE_INSTALLED_MARKER` (default: `/var/lib/runner/installed`)
- `APPLIANCE_APT_PACKAGES` (space-separated extra packages for install)
-- `APPLIANCE_PRIMARY_SERVICE` / `APPLIANCE_SECONDARY_SERVICE` (override healthcheck targets)
- `APPLIANCE_DRY_RUN=1` (do not modify system; record intended actions)
+Runner:
+
+- `RUNNER_ACTIONS_RUNNER_DIR` (default: `/opt/runner/actions-runner`)
+- `RUNNER_HOOKS_DIR` (default: `/usr/local/lib/runner`)
+
+Job isolation (`systemd-nspawn`):
+
+- `RUNNER_NSPAWN_BASE_ROOTFS` (default: `/var/lib/runner/nspawn/base-rootfs`)
+- `RUNNER_NSPAWN_READY_TIMEOUT_S` (default: `20`)
+- `RUNNER_NSPAWN_BIND` / `RUNNER_NSPAWN_BIND_RO` (space-separated bind mount entries)
+
## Day-2 operations
-Switch modes:
+Inspect service status:
```bash
-systemctl start template-appliance-primary.service
-systemctl start template-appliance-secondary.service
+systemctl status runner.service --no-pager
```
Inspect install/boot status:
```bash
-systemctl status template-appliance-install.service --no-pager
-ls -l /var/lib/template-appliance/installed || true
+systemctl status runner-install.service --no-pager
+ls -l /var/lib/runner/installed || true
```
## Manual install (no cloud-init)
@@ -118,18 +113,18 @@ sudo apt-get update
sudo apt-get install -y --no-install-recommends ca-certificates curl git
```
-1. Create `/etc/template-appliance/config.env` (start from the example):
+1. Create `/etc/runner/config.env` (start from the example):
```bash
-sudo mkdir -p /etc/template-appliance
-sudo cp /path/to/template-appliance/examples/config.env.example /etc/template-appliance/config.env
-sudo nano /etc/template-appliance/config.env
+sudo mkdir -p /etc/runner
+sudo cp /path/to/runner/examples/config.env.example /etc/runner/config.env
+sudo nano /etc/runner/config.env
```
1. Clone the repo and run the installer as root:
```bash
-git clone https://github.com/your-org/template-appliance.git /opt/template-appliance
-cd /opt/template-appliance
+git clone https://github.com/your-org/github-runner.git /opt/runner
+cd /opt/runner
sudo ./scripts/install.sh
```
diff --git a/ci/05-lint-permissions.sh b/ci/05-lint-permissions.sh
index c482dfd..dec4622 100755
--- a/ci/05-lint-permissions.sh
+++ b/ci/05-lint-permissions.sh
@@ -13,24 +13,37 @@ echo "== lint-permissions: executable bits =="
fail=0
-check_tree() {
+check_git_mode() {
local prefix="$1"
-
- # Only consider tracked files to avoid false positives from local build artifacts.
- mapfile -t files < <(git ls-files "$prefix" | grep -E '\.sh$' || true)
-
- local f
- for f in "${files[@]}"; do
- if [[ ! -x "$f" ]]; then
- echo "lint-permissions [error]: not executable (expected +x): $f" >&2
- fail=1
+ local pattern_re="$2"
+ local expected_mode="$3"
+
+ # Check the *git index* mode, not the filesystem mode. In some dev/test
+ # environments the worktree may be mounted 0777 which would mask missing +x.
+ #
+ # git ls-files --stage emits: