diff --git a/.github/workflows/xcode-benchmark.yaml b/.github/workflows/xcode-benchmark.yaml index 20568ec..7fb3c90 100644 --- a/.github/workflows/xcode-benchmark.yaml +++ b/.github/workflows/xcode-benchmark.yaml @@ -5,7 +5,10 @@ on: jobs: benchmark: - runs-on: macos-latest + strategy: + matrix: + runner: [macos-15-xlarge] + runs-on: ${{ matrix.runner }} steps: - name: Show runner info run: | diff --git a/README.md b/README.md index dfe161e..9f9362b 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ Here's what some open source projects would save: Local builds are also faster. Based on [XcodeBenchmark](https://github.com/devMEremenko/XcodeBenchmark): -| Runner | Time | vs GitHub | -|--------|------|-----------| -| GitHub macos-latest | [967s](https://github.com/bfulton/localmost/actions/runs/20388833445/job/58594848226) | — | -| MacBook Air M2 (2022) | 202s | **4.8x faster** | -| MacBook Pro M4 Max (2024) | 77s | **12.6x faster** | +| Runner | Time | +|--------|------| +| GitHub macos-latest M1 x3 ($0.06/m) | [838s](https://github.com/bfulton/localmost/actions/runs/20525324038/job/58967518701) | +| GitHub macos-15-large Intel x12 ($0.08/m) | [955s](https://github.com/bfulton/localmost/actions/runs/20525324038/job/58967518696) | +| GitHub macos-15-xlarge M2 Pro x5 ($0.10/m) | [339s](https://github.com/bfulton/localmost/actions/runs/20525919481/job/58969135831) | +| MacBook Air M2 x8 (2022) | 202s | +| MacBook Pro M4 Max x16 (2024) | 77s | ## Why else use localmost? @@ -39,7 +41,7 @@ Features: - **Automatic fallback** — workflows detect when your Mac is available; fall back to hosted runners when it's not - **One-click setup** — no terminal commands, no manually generating registration tokens - **Lid-close protection** — close your laptop without killing in-progress jobs -- **Multi-runner parallelism** — run 1-16 concurrent jobs +- **Multi-runner parallelism** — run 1-8 concurrent jobs - **Network isolation** — runner traffic is proxied through an allowlist (GitHub, npm, PyPI, etc.) - **Filesystem sandboxing** — runner processes can only write to their working directory - **Resource-aware scheduling** — automatically pause runners when on battery or during video calls @@ -48,12 +50,14 @@ Features: localmost is a macOS app that manages GitHub's official [actions-runner](https://github.com/actions/runner) binary. It handles authentication, registration, runner process lifecycle, and automatic fallback — the tedious parts of self-hosted runners. +**Security note:** Running CI jobs on your local machine has inherent risks—especially for public repos that accept external contributions. localmost sandboxes runner processes and restricts network access, but these are not VM-level isolation. See [SECURITY.md](SECURITY.md) for details on the threat model and recommendations. + ## Architecture localmost architecture diagram - **Runner proxy** — maintains long-poll sessions with GitHub's broker to receive job assignments -- **Runner pool** — 1-16 worker instances that execute jobs in sandboxed environments +- **Runner pool** — 1-8 worker instances that execute jobs in sandboxed environments - **HTTP proxy** — allowlist-based network isolation for runner traffic (GitHub, npm, PyPI, etc.) - **Build cache** — persistent tool cache shared across job runs (Node.js, Python, etc.) @@ -215,6 +219,8 @@ npm run make Future feature ideas: +- **Trusted contributors for public repos** - Control which repos can run on your machine based on their contributor list. Options: never build public repos, only build repos where all contributors are trusted (default: you + known bots, customizable), or always build (with high-friction confirmation). Repos with untrusted contributors fail with a clear error. +- **Graceful heartbeat shutdown** - On clean exit, immediately mark heartbeat stale so workflows fall back to cloud without waiting for the 90s timeout. - **Quick actions** - Re-run failed job, cancel all jobs. - **Audit logging** - Detailed logs of what each job accessed. - **Network policy customization** - User-defined network allowlists per repo. @@ -224,6 +230,9 @@ Future feature ideas: - **Disk space monitoring** - Warn or pause when disk is low, auto-clean old work dirs. - **Runner handoff** - Transfer a running job to GitHub-hosted if you need to leave. - **Reactive state management** - Unify disk state, React state, and state machine into a single reactive store to prevent synchronization bugs. +- **Linux and Windows host support** - Run self-hosted runners on non-Mac machines for projects that need them. +- **Higher parallelism cap** - Parallelize proxy registration to support 16+ concurrent runners (currently capped at 8 due to serial registration time). +- **Ephemeral VM isolation** - Run each job in a fresh lightweight VM for stronger isolation between jobs. Bugs and quick improvements: diff --git a/src/main/config.ts b/src/main/config.ts index f24009b..0abb117 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -50,7 +50,7 @@ export interface AppConfig { orgName?: string; runnerName?: string; labels?: string; - runnerCount?: number; // Number of parallel runners (1-16) + runnerCount?: number; // Number of parallel runners (1-8) }; theme?: string; launchAtLogin?: boolean; @@ -62,7 +62,7 @@ export interface AppConfig { userFilter?: UserFilterConfig; /** Multi-target configuration - list of repos/orgs to register runners for */ targets?: Target[]; - /** Maximum concurrent jobs across all targets (1-16, defaults to 4) */ + /** Maximum concurrent jobs across all targets (1-8, defaults to 4) */ maxConcurrentJobs?: number; /** Power settings (battery/video call pausing) */ power?: PowerConfig; diff --git a/src/main/target-manager.test.ts b/src/main/target-manager.test.ts index fc6fc85..eaf093a 100644 --- a/src/main/target-manager.test.ts +++ b/src/main/target-manager.test.ts @@ -307,10 +307,10 @@ describe('TargetManager', () => { expect(mockSaveConfig).toHaveBeenCalledWith({ maxConcurrentJobs: 1 }); }); - it('should clamp value to maximum of 16', () => { + it('should clamp value to maximum of 8', () => { mockLoadConfig.mockReturnValue({}); manager.setMaxConcurrentJobs(100); - expect(mockSaveConfig).toHaveBeenCalledWith({ maxConcurrentJobs: 16 }); + expect(mockSaveConfig).toHaveBeenCalledWith({ maxConcurrentJobs: 8 }); }); }); diff --git a/src/main/target-manager.ts b/src/main/target-manager.ts index 8bbaced..93f3a19 100644 --- a/src/main/target-manager.ts +++ b/src/main/target-manager.ts @@ -213,7 +213,7 @@ export class TargetManager { const log = () => getLogger(); const config = loadConfig(); const oldValue = config.maxConcurrentJobs ?? 4; - const newValue = Math.max(1, Math.min(16, count)); + const newValue = Math.max(1, Math.min(8, count)); if (oldValue !== newValue) { log()?.info(`[Settings] maxConcurrentJobs: ${oldValue} -> ${newValue}`); } diff --git a/src/renderer/components/SettingsPage.tsx b/src/renderer/components/SettingsPage.tsx index 41cbd2d..b411fb2 100644 --- a/src/renderer/components/SettingsPage.tsx +++ b/src/renderer/components/SettingsPage.tsx @@ -364,7 +364,7 @@ const SettingsPage: React.FC = ({ onBack, scrollToSection, on updateRunnerConfig({ runnerCount: parseInt(e.target.value, 10) })} /> diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 03515d6..f1cb1b9 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -90,7 +90,7 @@ export const DEFAULT_RUNNER_COUNT = 4; export const MIN_RUNNER_COUNT = 1; /** Maximum number of runner instances */ -export const MAX_RUNNER_COUNT = 16; +export const MAX_RUNNER_COUNT = 8; /** Default max job history entries to keep */ export const DEFAULT_MAX_JOB_HISTORY = 10; diff --git a/src/shared/types.ts b/src/shared/types.ts index 8dac09b..19e11b6 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -246,7 +246,7 @@ export interface ConfigureOptions { orgName?: string; // Required for org level runnerName: string; labels: string[]; - runnerCount?: number; // Number of parallel runners (1-16), defaults to 1 + runnerCount?: number; // Number of parallel runners (1-8), defaults to 1 } export type SleepProtection = 'never' | 'when-busy' | 'always';