Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ LINUX_DOCKER_PROJECT_ENV_FILE=.env
LINUX_DOCKER_INSTALL_PULL_IMAGES=true
LINUX_DOCKER_INSTALL_FORCE_RECREATE=true
LINUX_DOCKER_INSTALL_REMOVE_ORPHANS=true
LINUX_DOCKER_ALLOW_ALL_REPOSITORIES=false
WINDOWS_DOCKER_RUNNER_BASE_DIR=C:\github-runner-fleet\windows-docker
WINDOWS_DOCKER_HOST=windows-host.internal
WINDOWS_DOCKER_PORT=22
Expand All @@ -35,9 +36,11 @@ WINDOWS_DOCKER_PROJECT_ENV_FILE=.env
WINDOWS_DOCKER_INSTALL_PULL_IMAGES=true
WINDOWS_DOCKER_INSTALL_FORCE_RECREATE=true
WINDOWS_DOCKER_INSTALL_REMOVE_ORPHANS=true
WINDOWS_DOCKER_ALLOW_ALL_REPOSITORIES=false
LUME_RUNNER_BASE_DIR=~/Library/Application Support/github-runner-fleet/lume
LUME_RUNNER_ENV_FILE=~/Library/Application Support/github-runner-fleet/lume/runner.env
LUME_RUNNER_IPSW_PATH=~/Library/Application Support/github-runner-fleet/lume/cache/latest.ipsw
LUME_GUEST_PASSWORD=change-me
COMPOSE_PROJECT_NAME=github-runner-fleet
RUNNER_VERSION=2.333.0
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com:4318
Expand Down
57 changes: 54 additions & 3 deletions .github/workflows/pr-fast-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ defaults:
jobs:
changes:
name: Detect Relevant Changes
runs-on: ['self-hosted', 'synology', 'shell-only', 'public']
runs-on: ubuntu-latest
outputs:
app: ${{ steps.filter.outputs.app }}
ci: ${{ steps.filter.outputs.ci }}
Expand Down Expand Up @@ -62,6 +62,7 @@ jobs:
needs: changes
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository &&
(needs.changes.outputs.app == 'true' || needs.changes.outputs.ci == 'true')
steps:
- uses: actions/checkout@v6
Expand All @@ -75,29 +76,79 @@ jobs:
name: Validate Secrets
runs-on: ['self-hosted', 'synology', 'shell-only', 'public']
timeout-minutes: 10
if: github.event.pull_request.draft == false
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan repository for secret patterns
run: bash scripts/check-detect-secrets.sh --all-files

hosted-fork-fast-checks:
name: Hosted Fork Fast Checks
runs-on: ubuntu-latest
timeout-minutes: 15
needs: changes
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name != github.repository &&
(needs.changes.outputs.app == 'true' || needs.changes.outputs.ci == 'true')
steps:
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}

- uses: pnpm/action-setup@v6
with:
version: 10.32.1

- uses: actions/setup-node@v6
with:
node-version: "24"
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm build

hosted-fork-validate-secrets:
name: Hosted Fork Validate Secrets
runs-on: ubuntu-latest
timeout-minutes: 10
if: >-
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v6
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan repository for secret patterns
run: bash scripts/check-detect-secrets.sh --all-files

ci-gate:
name: CI Gate
runs-on: ['self-hosted', 'synology', 'shell-only', 'public']
runs-on: ubuntu-latest
if: always()
needs:
- changes
- fast-checks
- validate-secrets
- hosted-fork-fast-checks
- hosted-fork-validate-secrets
steps:
- name: Check required PR jobs
env:
RESULTS: >-
changes=${{ needs.changes.result }}
fast-checks=${{ needs.fast-checks.result }}
validate-secrets=${{ needs.validate-secrets.result }}
hosted-fork-fast-checks=${{ needs.hosted-fork-fast-checks.result }}
hosted-fork-validate-secrets=${{ needs.hosted-fork-validate-secrets.result }}
run: |
failed=0
for entry in $RESULTS; do
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,17 @@ Keep the Lume runner env file outside git and locked down with `chmod 600`. The
- Do not add Compose `init: true` for these services. The image already uses `tini`, and double-init setups on Synology produce noisy subreaper warnings.
- For public pools, use DSM firewall rules to reduce unnecessary LAN reachability.
- Keep the Docker socket restricted to the dedicated Linux Docker host. Do not mount it into the Synology shell-only plane.
- Keep external fork pull requests on GitHub-hosted runners; self-hosted runner groups are for trusted same-repository or explicitly allowed private workflows.
- Treat Docker-capable runner groups as host-control boundaries because they mount the Docker socket or Windows named pipe.
- Keep `LUME_GUEST_PASSWORD` out of git and rotate the base VM guest credential when rebuilding or resealing Lume images.

## Useful Commands

```bash
pnpm doctor -- full --env .env
pnpm doctor -- synology --env .env
pnpm doctor -- linux-docker --env .env
pnpm doctor -- windows-docker --env .env
pnpm doctor -- lume --env .env
pnpm validate-linux-docker-config -- --config config/linux-docker-runners.yaml --env .env
pnpm validate-linux-docker-github -- --config config/linux-docker-runners.yaml --env .env
Expand Down Expand Up @@ -437,7 +442,7 @@ SMOKE_KEEP_ARTIFACTS=1 pnpm smoke-test

## Troubleshooting Starting Points

- `pnpm doctor -- full --env .env` for one preflight/status summary across Synology and Lume checks
- `pnpm doctor -- full --env .env` for one preflight/status summary across Synology, Linux Docker, Windows Docker, and Lume checks
- `pnpm validate-linux-docker-config -- --config config/linux-docker-runners.yaml --env .env` for Docker-capable Linux pool schema and label validation
- `pnpm validate-linux-docker-github -- --config config/linux-docker-runners.yaml --env .env` for Docker-capable Linux runner-group verification
- `pnpm render-linux-docker-project-manifest -- --config config/linux-docker-runners.yaml --env .env` for the remote Linux Docker install plan before you push it
Expand Down
17 changes: 17 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,20 @@ We will acknowledge receipt, validate the report, and coordinate a fix and discl
## Scope Notes

This repository publishes software intended to manage self-hosted GitHub Actions runners. Public repositories should not route untrusted workflow code to privileged or host-sensitive runner environments.

External fork pull requests must stay on GitHub-hosted runners. Self-hosted
runner groups are for trusted same-repository or explicitly allowed private
repository workflows only.

Runner registration currently uses `GITHUB_PAT` to mint short-lived runner
registration and removal tokens. Treat that PAT as fleet-wide infrastructure
auth: keep it out of images and base VMs, stage it only through generated
environment files or GitHub environments, rotate it after runner-host incidents,
and prefer narrowly scoped/fine-grained credentials where GitHub supports the
required runner APIs.

Docker-capable runner planes mount the host Docker socket or Windows Docker
named pipe. Any repository allowed onto those runner groups can effectively
control the Docker host, so `repositoryAccess: all` requires an explicit
break-glass environment flag and should not be used for public or untrusted
repositories.
2 changes: 1 addition & 1 deletion config/lume-runners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pool:
diskSize: 80GB
network: nat
guestUser: lume
guestPassword: lume
guestPassword: ${LUME_GUEST_PASSWORD}
guestRunnerRoot: /Users/lume/actions-runner
guestWorkRoot: /Users/lume/actions-runner/_work
runnerVersion: 2.333.0
Expand Down
4 changes: 2 additions & 2 deletions docs/bootstrap/claude-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
## Claude Code On The Web

- Hosted entrypoint: `https://claude.ai/code`
- Repo: `OMT-Global/synology-github-runner`
- Repo: `OMT-Global/github-runner-fleet`
- Setup script: `bash scripts/claude-cloud/setup.sh`
- Network access: start with limited access; only expand it when a task truly needs more than registries and GitHub
- Environment variables: configure them in the Claude environment UI as `.env`-style key-value pairs
Expand Down Expand Up @@ -52,5 +52,5 @@

## Project

- Repository: `OMT-Global/synology-github-runner`
- Repository: `OMT-Global/github-runner-fleet`
- Default branch: `main`
2 changes: 1 addition & 1 deletion docs/bootstrap/codex-cloud-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Configure the Codex Web environment in Codex settings for:

- Repo: `OMT-Global/synology-github-runner`
- Repo: `OMT-Global/github-runner-fleet`
- Base image: `universal`
- Setup mode: manual setup script
- Setup script: `bash scripts/codex-cloud/setup.sh`
Expand Down
2 changes: 1 addition & 1 deletion docs/bootstrap/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Repo Governance

- Confirm the repository exists at `OMT-Global/synology-github-runner`.
- Confirm the repository exists at `OMT-Global/github-runner-fleet`.
- Confirm branch protection or rulesets on `main` require one approval and code owner review.
- Confirm branch protection points at the `CI Gate` status.
- Confirm `delete branch on merge` and `allow auto-merge` are enabled.
Expand Down
42 changes: 22 additions & 20 deletions docs/workflow-cookbook.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
# Shell-safe workflow cookbook

This guide is for downstream repositories that want to consume the runner contracts from this repo without guessing which jobs belong on self-hosted Synology runners, which jobs belong on the Lume macOS pool, and which jobs should stay on GitHub-hosted runners.
This guide is for downstream repositories that want to consume the runner contracts from this repo without guessing which jobs belong on self-hosted Synology runners, Linux Docker runners, Windows Docker runners, the Lume macOS pool, and GitHub-hosted runners.

## Runner compatibility matrix

| Job class | Synology shell-only pool | Lume macOS pool | GitHub-hosted runners | Notes |
| --- | --- | --- | --- | --- |
| Node install, lint, test, build | Yes | Usually unnecessary | Yes | On Synology, use `OMT-Global/synology-github-runner/actions/setup-shell-safe-node` instead of `actions/setup-node`. |
| Python 3.12 lint/test | Yes | Optional | Yes | `actions/setup-python@v6` with `python-version: '3.12'` resolves locally on the Synology image. Other Python versions should stay hosted unless you control the full toolchain. |
| Terraform fmt/validate/init without cloud sidecars | Yes | Optional | Yes | Keep plugin cache under `RUNNER_TEMP` or another writable container-local path. |
| Docs checks, markdown lint, shell validation | Yes | Optional | Yes | Good fit for the shell-only pool when the job only needs baked-in CLI tools. |
| Release image builds, Buildx, QEMU, registry publish | No | No | Yes | Keep these on GitHub-hosted runners. |
| Docker daemon, `docker build`, `docker compose`, service containers | No | No | Yes | The Synology runner class intentionally avoids Docker socket mounts and does not support service containers. |
| `container:` jobs | No | No | Yes | Route these back to GitHub-hosted runners. |
| Browser/UI/E2E jobs needing extra distro packages | No | Sometimes | Yes | Prefer hosted runners unless the macOS requirement is explicit and owned. |
| macOS signing, Xcode builds, Swift/macOS validation | No | Yes | Yes | Use the Lume pool when you need a self-hosted macOS environment. |
| Public fork pull requests | No | No | Yes | Keep fork PRs on GitHub-hosted runners so untrusted code does not land on self-hosted infrastructure. |
| Job class | Synology shell-only pool | Linux Docker pool | Windows Docker pool | Lume macOS pool | GitHub-hosted runners | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| Node install, lint, test, build | Yes | Yes | Case-by-case | Usually unnecessary | Yes | On Synology, use `OMT-Global/github-runner-fleet/actions/setup-shell-safe-node` instead of `actions/setup-node`. |
| Python 3.12 lint/test | Yes | Yes | Case-by-case | Optional | Yes | `actions/setup-python@v6` with `python-version: '3.12'` resolves locally on the Synology image. Other Python versions should stay hosted unless you control the full toolchain. |
| Terraform fmt/validate/init without cloud sidecars | Yes | Yes | Case-by-case | Optional | Yes | Keep plugin cache under `RUNNER_TEMP` or another writable container-local path. |
| Docs checks, markdown lint, shell validation | Yes | Yes | Case-by-case | Optional | Yes | Good fit for the shell-only pool when the job only needs baked-in CLI tools. |
| Release image builds, Buildx, QEMU, registry publish | No | Yes | No | No | Yes | Use Linux Docker for trusted private workloads; keep untrusted or public fork builds hosted. |
| Docker daemon, `docker build`, `docker compose`, service containers | No | Yes | Windows containers only | No | Yes | The Synology runner class intentionally avoids Docker socket mounts and does not support service containers. |
| `container:` jobs | No | Yes | Windows containers only | No | Yes | Use Docker-capable private planes for trusted repos; use hosted runners for untrusted code. |
| Browser/UI/E2E jobs needing extra distro packages | No | Case-by-case | Case-by-case | Sometimes | Yes | Prefer hosted runners unless the self-hosted requirement is explicit and owned. |
| macOS signing, Xcode builds, Swift/macOS validation | No | No | No | Yes | Yes | Use the Lume pool when you need a self-hosted macOS environment. |
| Public fork pull requests | No | No | No | No | Yes | Keep fork PRs on GitHub-hosted runners so untrusted code does not land on self-hosted infrastructure. |

## Routing rules

Use these rules when deciding where a workflow job should run:

- Use `runs-on: [self-hosted, synology, shell-only, public]` for trusted shell-safe jobs that can run with the baked-in Linux toolchain.
- Use `runs-on: [self-hosted, linux, docker-capable, private]` for trusted private Linux jobs that need Docker, `container:`, or service containers.
- Use `runs-on: [self-hosted, windows, docker-capable, private]` only for trusted private Windows container work.
- Use `runs-on: [self-hosted, macos, arm64]` only when you intentionally target the Lume macOS pool and control the repo trust boundary.
- Keep pull requests from forks on GitHub-hosted runners.
- Keep any workflow using `container:`, `services:`, browsers, Docker daemon access, Buildx, or extra distro package assumptions on GitHub-hosted runners.
- Keep any untrusted workflow using `container:`, `services:`, browsers, Docker daemon access, Buildx, or extra distro package assumptions on GitHub-hosted runners.
- Prefer a split workflow over forcing one runner class to handle incompatible jobs.

## Recipe: trusted Node job on the Synology shell-only pool
Expand Down Expand Up @@ -57,7 +59,7 @@ jobs:
- uses: pnpm/action-setup@v5
with:
version: 10.32.1
- uses: OMT-Global/synology-github-runner/actions/setup-shell-safe-node@main
- uses: OMT-Global/github-runner-fleet/actions/setup-shell-safe-node@main
with:
node-version: 24.14.1
- run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -100,7 +102,7 @@ jobs:
- uses: pnpm/action-setup@v5
with:
version: 10.32.1
- uses: OMT-Global/synology-github-runner/actions/setup-shell-safe-node@main
- uses: OMT-Global/github-runner-fleet/actions/setup-shell-safe-node@main
with:
node-version: 24.14.1
- run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -180,7 +182,7 @@ jobs:
- run: terraform validate
```

This works well for pure CLI Terraform jobs. If the workflow also builds containers, talks to Docker, or needs sidecar services, split those parts back to GitHub-hosted runners.
This works well for pure CLI Terraform jobs. If the workflow also builds containers, talks to Docker, or needs sidecar services, split those parts onto the Linux Docker private plane for trusted repos or back to GitHub-hosted runners for untrusted code.

## Recipe: Lume macOS contract job

Expand Down Expand Up @@ -213,9 +215,9 @@ Use the hosted `macos-latest` image instead when the repository does not need se

## Force jobs back to GitHub-hosted runners when

- the workflow uses `container:`
- the workflow uses `services:`
- the job requires Docker daemon access, Buildx, or QEMU
- the workflow uses `container:` and is not trusted private work assigned to a Docker-capable plane
- the workflow uses `services:` and is not trusted private work assigned to a Docker-capable plane
- the job requires Docker daemon access, Buildx, or QEMU and is not trusted private Linux Docker work
- the job needs browsers or large sets of distro packages not already present in the runner contract
- the change comes from a public fork or another untrusted source
- the job depends on a language/version combination outside the documented self-hosted contract
6 changes: 3 additions & 3 deletions project.bootstrap.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: 1
project:
name: synology-github-runner
description: Shell-only GitHub self-hosted runner pools for Synology NAS deployments.
name: github-runner-fleet
description: Self-hosted GitHub runner fleet for Synology, Linux Docker, Windows Docker, and Lume macOS planes.
visibility: public
owner: OMT-Global
defaultBranch: main
Expand All @@ -24,7 +24,7 @@ repo:
archetype:
kind: generic-empty
packageManager: npm
moduleName: synology_github_runner
moduleName: github_runner_fleet
github:
createRepo: false
reviewers:
Expand Down
5 changes: 3 additions & 2 deletions scripts/smoke/mock-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "node:path";

const logPath = process.env.MOCK_LOG_PATH ?? "/tmp/mock-api.log";
const port = Number(process.env.MOCK_PORT ?? "8080");
const host = process.env.MOCK_HOST ?? "0.0.0.0";
fs.mkdirSync(path.dirname(logPath), { recursive: true });

const server = http.createServer((req, res) => {
Expand All @@ -29,10 +30,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ error: "not found" }));
});

server.listen(port, "0.0.0.0", () => {
server.listen(port, host, () => {
fs.appendFileSync(
logPath,
`${new Date().toISOString()} listening 0.0.0.0:${port}\n`,
`${new Date().toISOString()} listening ${host}:${port}\n`,
"utf8"
);
});
Loading