返回资讯中心
外部精选
软件工程
#开发者实时流

Joggo:在自用 Windows PC 上运行的自托管 GitHub Actions runner

来源标题:Show HN: Joggo – a self-hosted GitHub Actions runner for the Windows PC you use

Joggo 是一个自托管的 GitHub Actions runner,可以安装在你日常使用的 Windows 电脑上运行 CI 任务。该项目在 Show HN 上发布,面向希望利用本地 Windows 机器执行 GitHub Actions 工作流的用户。

Hacker News 最新AmirCat2026/11/8 · 今天30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

A self-hosted GitHub Actions runner built to run for weeks on a machine someone is also using.

Not a Kubernetes operator, not an autoscaler, not a fleet. One ordinary Windows PC with 16 GB of RAM and WSL2, taking production builds since June 2026: one runner at first, containerised runner slots since September, two of them from the end of September and three since late October. Through clock drift, power loss, Windows Update reboots, memory exhaustion down to 445 MB available, and a Docker daemon that occasionally stops answering.

The code is maybe half of it. The other half is the comments: most files carry the failure that produced them, what was measured, and what was tried and did not work. That is deliberate. docs/memory-pressure.md is the single most useful page here even if you never run any of this.

If you are an AI agent operating this box, skip to For agents. It has the invariants, the paths, the safe/destructive command split, and the failure-mode discriminators in one place.

If you landed here from an error message, these are the ones joggo has a measured cause and a working fix for. Each link goes to the write-up, whether or not you use joggo.

GitHub-hosted minutes are metered. Private repositories get a monthly allowance and then bill per minute, and the rate is multiplied by the runner: Linux is the cheapest, Windows costs several times more per minute, macOS more again. Check your own numbers at Settings → Billing → Plans and usage — that page tells you what you are actually spending, and whether any of this is worth your evening.

Minutes on a runner you own are not metered at all. You pay electricity and the hardware you already bought. For a handful of repositories that rebuild a Docker image on every push to main, that is usually where the bill is, and moving it to a desktop that is idle most of the day removes it.

What this does not save, so the sums are honest:

install.ps1 The installer: wizard for a human, flags for an agent, -PreflightOnly to install/ check the box without touching it. WSL2 on Windows is the only target. CONTRACT.md config keys, flags, steps, exit codes

slots/ Two containerised runner slots. Each is a runner container plus its own Docker daemon, on its own network, with a throwaway work volume. run.sh --once per job, so no job ever sees another's workspace. run-containerized-slot.sh the launcher (read its header first) add-slot.sh add slot N slot-pause.sh drain/hold a slot without killing its job slots-status.sh one-line state of every slot dind-image/ the per-slot Docker-in-Docker image systemd/joggo-slot@.service the template unit

guard/ Memory guard. Samples every 5 min and escalates: drop the VM page cache, then HOLD the slots so no NEW job starts while running jobs finish, then restart the VM only if the slots are idle. wsl-mem-guard.ps1 the guard mem-watch.ps1 the 5-minute sampler + snapshots

watchdog/ The three failure modes that actually took this runner down, each with a self-heal: a frozen clock that invalidates every OAuth token, a listener that hangs alive at 0% CPU, and a Windows Update reboot that kills an in-flight job. runner-watchdog.sh wedge detector (65-min threshold; see docs) rerun-after-reboot.sh re-dispatches jobs killed by a reboot take-restore-point.sh ~450 MB rollback point before upgrades

monitor/ deploy-monitor.ps1 WPF monitor that reconstructs deploys from the runner's own journal. No GitHub token needed. day-report.ps1 daily "what failed, and why" page

network/ filter-aaaa.conf dnsmasq drop-in for hosts that advertise AAAA records they cannot route (see Known limits)

tailscale/ acl.hujson default-deny tailnet policy; admin devices, SSH only. OPTIONAL — only for managing a headless box remotely.

.github/ build-and-push.yml reusable workflow_call image build (buildx → GHCR) node-test.yml reusable containerised Node test job, with a no-host-port Postgres service

Setup

docs/quickstart.md — shortest real path from nothing to a green build. Windows 10 2004+ or 11 with WSL2 is the only supported target: every Windows-side tool here (guard, sampler, monitor, day report) talks to the WSL VM from the host, so there is no native-Linux or VM path.

./install.ps1 -PreflightOnly # checks everything, changes nothing ./install.ps1 -Wizard # a human ./install.ps1 -NonInteractive -Json -Yes -Scope repo -Repo me/my-app -Token $env:JOGGO_TOKEN

-Scope repo is the personal-account path and is fully supported: runner groups are an organisation-only feature, so a personal account registers the runner per repository instead. -Scope org -Org <org> is the organisation path. Slots and the memory guard's thresholds scale with RAM — 1 slot under 12 GB, 2 for 12–32 GB, 3 above; guard warn at 15% of RAM and critical at 7.5%, which is the measured 2500/1200 MB on a 16 GB box. The honest floor is 8 GB. Tailscale is optional and only for remote management of a headless box. Exit codes: 0 success, 1 failure, 2 bad usage, 3 preflight failed.

The registration token is short-lived (~1 hour) and must never be committed or stored. Minting it is deliberately a human step — SECURITY.md and docs/recovery.md §5 explain why automating it is left undone.

The runner group — or, on a personal account, the set of repositories you registered to — is the actual security boundary. On an org, Settings → Actions → Runner groups:

Allow public repositories MUST stay unchecked Repository access private repos only, chosen explicitly Fork PR workflows require approval for outside collaborators

A self-hosted runner executes whatever the allowed repositories tell it to, on your hardware, inside your network. That list of repositories is the whole threat model.

To understand or deviate from any step, the long-form reference is docs/runner-setup.md; to add a slot by hand, docs/adding-a-slot.md. Neither pins a runner version: take the current release, and the slot launcher resolves the installed runner's version-stamped bin/externals symlinks at start rather than hardcoding a path.

Full runbook: docs/onboarding-a-repo.md. The fast path calls the reusable workflow, which keeps a consuming repo to a few lines:

.github/workflows/ci.yml in YOUR project repo

name: CI on: push: branches: [main] paths: ['apps/', 'packages/', 'Dockerfile', 'package.json'] workflow_dispatch:

jobs: build: uses: <your-org>/joggo/.github/workflows/build-and-push.yml@main with: image-name: my-service # defaults to the repo name context: . dockerfile: ./Dockerfile

publish-latest: false # see the sharp edge below

permissions: contents: read packages: write # REQUIRED for the job's GITHUB_TOKEN to push to GHCR

That is all. The reusable workflow already sets runs-on: [self-hosted, linux, x64, joggo], logs into GHCR with the job's own GITHUB_TOKEN, and tags the image.

One sharp edge, worth reading before you rely on :latest. build-and-push.yml appends :latest itself, gated on enable={{is_default_branch}}. Two consequences:

If your repo publishes several images and anything downstream keys off :latest, pass publish-latest: false and retag deliberately once every image for the commit exists. Otherwise the :latest pushes race each other.

If your deploy branch is not your default branch, that gate never fires and :latest silently freezes. Check before trusting it:

gh api repos/<owner>/<repo> --jq .default_branch # vs the branch in your on:

Equal → a separate retag job is redundant. Different → that retag job is the only thing publishing :latest, and deleting it breaks your deploys.

slots/slots-status.sh # what every slot is doing slots/slot-pause.sh status --slot 1 # one slot, in detail slots/slot-pause.sh pause --slot 1 "why" # drain: no NEW jobs; running job finishes slots/slot-pause.sh resume --slot 1 # take jobs again pwsh guard/wsl-mem-guard.ps1 -DryRun # what the guard would do, changing nothing

Day to day: docs/operations.md. When it stops taking jobs: docs/recovery.md — it opens with a triage table, because the failure modes have opposite fixes and guessing makes things worse.

If you are an AI agent asked to run or repair this box, this section is for you.

Claude Code users: install the operator skills as a plugin. They load the right runbook for the problem at hand (supervising the box, a failed job, a runner not taking jobs, adding a slot, risky upgrades):

/plugin marketplace add telluric-dev/joggo /plugin install joggo@joggo

There is a better version of this, specific to your box. The installer renders agent/SUPERVISOR.md to C:\ProgramData\joggo\SUPERVISOR.md with the real distro name, paths, slot count and thresholds substituted — so it says -d Ubuntu-24.04 rather than -d <your distro>, and warn 2450 MB rather than 15% of RAM. Paste that file as the opening prompt of a session that will supervise the runner, or point the agent at it: it carries the standing duties, these invariants, the command-safety split and the diagnosis tests, already resolved for one machine.

Rendered by default; -NoAgentSupervisor skips it. What follows is the generic form — what you get reading the repo rather than running on a box.

Invariant Why

No privileged secrets on the box. No org-admin PAT; never store a registration token. A self-hosted runner executes other repositories' code. See SECURITY.md.

Never point a public repo, or fork PRs, at this runner. Arbitrary code on the operator's hardware, inside their network.

A hold must never kill a running job. pause writes a flag and the job finishes. Only --now interrupts, and it names the job first. A hold that destroys work is worse than no hold at all.

Ask before restarting the runner, the VM, or anything that drops a job. Hard to reverse, and the cost lands on other people's builds.

Do not move the guard's thresholds to make a symptom go away. They came from measurements — see docs/memory-pressure.md.

Where state lives

/var/lib/joggo-slot/<N>.paused the pause flag — the contract between slot-pause, the launcher and the guard /var/log/joggo-slots/<N>/diag/ that slot's Runner.log and Worker_.log C:\ProgramData\joggo\mem-watch.csv the 5-minute sample trail C:\ProgramData\joggo\snapshots\ full snapshots, written when free memory trips C:\ProgramData\joggo\wsl-mem-guard.log every guard decision, with its reasoning

Command safety

READ-ONLY slots-status.sh · slot-pause.sh status · wsl-mem-guard.ps1 -DryRun systemctl is-active|is-enabled · docker ps · journalctl gh run list · gh api .../jobs

MUTATING slot-pause.sh pause|resume reversible; loses no job systemctl restart joggo-slot@N CHECK for an in-flight job first

DESTRUCTIVE slot-pause.sh --now kills the running job wsl --shutdown kills every job on the box config.sh remove deregisters; needs a fresh token ASK FIRST. Each of these costs somebody a build.

Discriminators — read these before diagnosing

Several failure modes here are indistinguishable without the right test. These are the tests, each learned by getting it wrong first.

A job that never ran, versus a job that failed. From the payload, no logs needed — all four conditions together:

conclusion cancelled|failure · runner_name empty · steps 0 · labels include self-hosted

Every clause excludes a real case. Zero failed steps alone also matches a job a human cancelled mid-flight, which reports its runner and its executed steps. Without the self-hosted clause, every hosted job killed by a billing problem gets misattributed to your runner.

Queued forever — four causes with opposite fixes. docs/recovery.md has the table: clock drift (re-register), wedged listener (restarting is the fix), Actions outage (do not restart), or the box rebooted (already retried automatically).

--once makes "inactive" normal. A slot is genuinely inactive between every job. Read is-enabled and the age of the newest Runner_*.log instead.

A run's conclusion says nothing about whether anything was published, in either direction: a red run may have pushed its image already, and

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →