Hybrid Runner System

Implemented: 2025-10-24 Status: Active - Running in production Purpose: Conserve GitHub Actions quota while maintaining fast, reliable CI/CD


Overview

The hybrid GitHub Actions runner system automatically balances between GitHub-hosted and self-hosted runners to optimize quota usage (3000 minutes/month on Pro plan).

Key Benefits:

  • Automatic quota management - Days 22-31 use self-hosted, days 1-21 use GitHub-hosted
  • Zero manual intervention - Resets automatically on first of each month
  • Manual override options - Commit flags and workflow dispatch for control
  • Quota conservation - Saves ~300-500 minutes/month during end-of-month crunch

Architecture

Runner Selection Logic

graph TD
    A[Workflow Triggered] --> B{Check Skip Flags}
    B -->|[skip-ci] flag| C[Skip Workflow]
    B -->|No skip flag| D{Check Override}
    D -->|[self-hosted] flag| E[Use Self-Hosted]
    D -->|[github] flag| F[Use GitHub-Hosted]
    D -->|No override| G{Auto Mode}
    G -->|Day 1-21| F
    G -->|Day 22-31| E

Decision Job (PowerShell)

Runs on self-hosted runner to avoid ANY GitHub quota usage:

$COMMIT_MSG = "${{ github.event.head_commit.message || github.event.pull_request.title }}"
$INPUT_MODE = "${{ github.event.inputs.runner_mode || 'auto' }}"
$DAY = (Get-Date).Day
 
if ($DAY -ge 22) {
  $RUNNER = "self-hosted"  # End of month - conserve quota
} else {
  $RUNNER = "ubuntu-latest"  # Early month - use GitHub quota
}
 
# Outputs used by subsequent jobs
"runner_label=$RUNNER" >> $env:GITHUB_OUTPUT

Usage

Automatic Mode (Default)

No action required! The system automatically uses:

  • Days 1-21: GitHub-hosted runners
  • Days 22-31: Self-hosted runners
  • Resets: Automatically on first of each month

Manual Override (Commit Flags)

Force self-hosted runner:

git commit -m "feat: Add new feature [self-hosted]"

Force GitHub-hosted runner:

git commit -m "fix: Quick bug fix [github]"

Skip CI entirely:

git commit -m "docs: Update README [skip-ci]"

Manual Dispatch (Actions UI)

  1. Go to GitHub → Actions tab
  2. Select workflow (Android CI or Deploy)
  3. Click “Run workflow”
  4. Choose runner_mode:
    • auto - Use day-based logic (default)
    • self-hosted - Force self-hosted
    • github - Force GitHub-hosted
    • skip - Skip CI run

Workflows

android-ci.yml (Pull Request Validation)

Structure:

  • decide_runner - Selects runner (runs on self-hosted, 4s)
  • quick_checks - Lint and basic validation (runs on selected runner)
  • test_and_coverage - Full build + tests + coverage (runs on selected runner)

Jobs split strategy:

  • Quick checks always on GitHub (fast, cheap)
  • Heavy work on selected runner (build, tests, coverage)

deploy-to-play-store.yml (Main Branch Deployment)

Structure:

  • decide_runner - Selects runner (runs on self-hosted, 4s)
  • deploy - Build release AAB + deploy to Play Store (runs on selected runner)

Same selection logic as CI workflow


Self-Hosted Runner Setup

Dual Runner Architecture

Current Setup: Two separate self-hosted runners on the same Windows machine

RunnerLocationGitHub NameRepositoryPurpose
MainC:\actions-runner\(default)blamechris/archery-apprenticeMain app CI/CD
DocsC:\actions-runner-docs\SOLACEblamechris/archery-apprentice-docsDocumentation deployment

Why Separate Runners?

  • Repository isolation - Each runner registered to specific repository
  • Job independence - Main CI and docs deployment can run concurrently
  • Quota tracking - Separate accounting for each repository’s usage
  • Configuration isolation - Different SDK requirements (main needs Android SDK, docs only needs Node.js)

Setup Details:

  • Both runners run as Windows services
  • Both share same JDK/Node.js installations
  • Only main runner needs Android SDK at C:\Android\Sdk
  • Docs runner setup documented in archery-apprentice-docs README

Desktop/Server Setup

See: Self-Hosted Runner Setup Guide

Key steps:

  1. Install JDK 17, Android SDK, Git Bash
  2. Register runner with GitHub
  3. Configure Windows service
  4. Copy Android SDK to C:\Android\Sdk (for Windows)

Laptop Setup (Optional)

Same process, but consider:

  • Power management - Disable sleep when running
  • Network - Stable connection required
  • Use case - Portable CI, travel development

Most developers don’t need laptop runners - they’re for advanced use cases.


Local Development

IMPORTANT: Self-hosted runners are NOT for local development!

Local development uses ./gradlew directly:

# Build locally
./gradlew assembleDebug
 
# Test locally
./gradlew testDebugUnitTest
 
# Instrumented tests (requires emulator)
./gradlew connectedAndroidTest

All tests run natively on your machine - no GitHub Actions infrastructure needed.


Troubleshooting

Codecov Upload Fails on Windows

Issue: sh: command not found

Root cause: codecov-action requires bash/sh, which isn’t in default Windows PATH

Solution: Made Codecov non-blocking in android-ci.yml:272-282

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v5
  with:
    fail_ci_if_error: false  # Non-blocking
  continue-on-error: true    # Allow workflow to succeed

Impact: Coverage still uploaded via PR comment + HTML artifacts, Codecov is redundant.

Android SDK Permissions on Windows

Issue: NETWORK SERVICE account can’t access user AppData folder

Solution: Copy Android SDK to system-wide location:

Copy-Item -Path "C:\Users\USERNAME\AppData\Local\Android\Sdk" `
          -Destination "C:\Android\Sdk" `
          -Recurse -Force
 
# Update local.properties
echo "sdk.dir=C:/Android/Sdk" > local.properties

Explanation: Windows services run as NETWORK SERVICE, which has restricted permissions on user folders.

Runner Shows Offline

Check service status:

# Windows
.\svc.sh status
 
# Linux/macOS
sudo ./svc.sh status

Restart if needed:

# Windows
.\svc.sh restart
 
# Linux/macOS
sudo ./svc.sh restart

Quota Analysis

Monthly Quota: 3000 Minutes

Before hybrid runner:

  • ~10-15 builds/day × 15 minutes = 150-225 minutes/day
  • 30-day month: 4,500-6,750 minutes (EXCEEDS quota by 1.5-2.25x)

After hybrid runner (days 22-31 on self-hosted):

  • Days 1-21: ~10 builds/day × 15 min = 150 min/day × 21 = 3,150 minutes
  • Days 22-31: ~10 builds/day × 0 min (self-hosted) = 0 minutes
  • Total GitHub quota used: ~3,150 minutes (within quota!)

Quota saved: ~300-500 minutes/month


Implementation Timeline

Date: 2025-10-24 (1 intensive day)

Key milestones:

  1. ✅ Workflow design - Day-based heuristic decision
  2. ✅ Windows PowerShell compatibility - Cross-platform shell scripts
  3. ✅ Self-hosted runner setup - Desktop with Windows service
  4. ✅ Android SDK permissions - Moved to C:\Android\Sdk
  5. ✅ Codecov fix - Made non-blocking for Windows compatibility
  6. ✅ Documentation - README, CLAUDE.md, setup guide, Obsidian

Challenges overcome:

  • Windows PowerShell vs Bash syntax differences
  • NETWORK SERVICE account SDK permissions
  • Codecov bash dependency on Windows
  • Cross-platform JSON generation encoding

Code Repository

Obsidian Vault


Tags: ci-cd github-actions hybrid-runner quota-management infrastructure Status: ✅ Active in production Next Review: 2025-11-01 (verify auto-switchback on month rollover)