Visual Guide

GitHub Actions
Self-Hosted Runner

A visual guide to CI/CD on your own infrastructure

CI (Continuous Integration) = every time you push code, a machine automatically checks if your code still works. No manual testing. No forgetting. Here's the difference:

Without CI

👨‍💻 You write code
🤔 "Did I break anything?"
🤷 You push anyway (forgot to test)
💥 Bug in production

With CI (Your Setup)

👨‍💻 You write code
🚀 You push (git push)
🤖 Runner auto-tests
You know instantly if it works

Three machines are involved. Here's how they connect:

💻
Your Computer
git push origin main
you type commands here
push code →
← status check
🐙
GitHub Cloud
github.com
stores code + queues jobs
jojomojo-org
← runner polls "any jobs?"
reports results →
🖥️
Iqra Server
89.144.2.96
Debian 13 + Rust 1.95
runs your CI jobs

Key insight: Your server reaches out to GitHub (outbound HTTPS). GitHub never reaches into your server. This means no ports need to be opened on your firewall.

The full journey from typing git push to seeing your result:

👨‍💻 git push
⚡ GitHub detects
📋 reads .yml file
📦 queues job
🔨 iqra builds & tests
✅ or ❌
YOUR COMPUTER
💻
Steps 1: You push code
GITHUB CLOUD
☁️
Steps 2-4: Detects push, reads YAML, queues job
IQRA SERVER
🖥️
Steps 5-6: Runs build/test, reports back

Your runner doesn't sit and wait for GitHub to call it. Instead, it polls every few seconds: "Hey GitHub, got any jobs for me?" Here's the exact conversation:

💻
You
🐙
GitHub
🖥️
Iqra Runner
🔄 "any jobs?" → "no"
🔄 "any jobs?" → "no" (every ~3s)
🚀 You run: git push origin main
code pushed to GitHub
📋 GitHub reads ci.yml → queues job
🔄 "any jobs?" → "YES! Here's a build job"
🔨 checkout code...
🏗️ cargo build --release...
🧪 cargo test...
🔍 cargo clippy...
← "job complete: all steps passed ✅"
✅ GitHub shows green checkmark on your commit
🔄 back to polling... "any jobs?" → "no"

Organization structure: The runner is registered at the org level, so it serves every repo in the org.

👤 jojomojo786
personal account (owner)
owns
🏢 jojomojo-org
organization
📁
beads
📁
repo-2
📁
repo-N
repos (each with ci.yml)
🖥️
iqra-runner
shared by ALL repos
🧰
Toolchain
Installed on iqra
rust1.95.0
cargo1.95.0
git2.47.3
gcc14.2.0
🏷️
Labels & Service
How GitHub finds this runner
self-hosted Linux X64 iqra
systemd enabled survives reboot
runs-on: self-hosted ← matches these labels

The workflow file must be in a specific location. GitHub only looks here:

beads/ ← your repo root ├── .github/ ← hidden folder (the dot makes it hidden) │ └── workflows/ ← GitHub looks here for YAML files │ └── ci.yml ← YOUR WORKFLOW FILE (the important one!) ├── src/ │ └── main.rs ← your Rust source code ├── Cargo.toml ← Rust project config (name, dependencies) ├── Cargo.lock └── README.md

Rule: No .github/workflows/*.yml file = no CI. The runner sits idle even if it's online. The YAML file is the instruction manual that tells the runner what to do.

.github/workflows/ci.yml
name: Rust CI
Display name — shows in the Actions tab. Call it whatever.
on: push: branches: [main]
Trigger: run this workflow when someone pushes to main branch.
pull_request: branches: [main]
Also trigger when a PR targeting main is opened or updated.
jobs: build: runs-on: self-hosted
THE key line. Tells GitHub: run this on YOUR server, not GitHub's cloud.
steps: - uses: actions/checkout@v4
Download code from the repo onto the runner's disk.
- name: Build run: cargo build --release
Compile your Rust project. Fails on syntax/type errors.
- name: Test run: cargo test
Run tests — every #[test] function. Fails if assertions fail.
- name: Clippy run: cargo clippy -- -D warnings
Lint check — catches code smells the compiler misses.

Steps run one after another (sequentially). Each bar below shows how long that step takes. If one fails, everything after it is skipped:

Successful Run
📥 Checkout
2s
2s
🏗️ Build
18s
18s
🧪 Test
5s
5s
🔍 Clippy
6s
6s
Total: ~31s ✔
Failed Run (Build Error)
📥 Checkout
2s
2s ✔
💥 Build
8s
FAILED
🧪 Test
SKIPPED
🔍 Clippy
SKIPPED
Failed at step 2 — steps 3 & 4 never ran
🚀
Push to main
git push origin main
✅ RUNS
🔀
Open a PR to main
feature → main PR
✅ RUNS
🔇
Push to other branch
git push origin dev
❌ DOES NOT RUN
💡 Bottom line: Just git push origin main like normal. The runner picks it up within seconds — no manual action needed.

Syntax Error (Build Fails)

Mistyped a variable name. Compiler catches it.

error[E0425]: cannot find value `nme` --> src/main.rs:5:20 | 5 | println!("{}", nme); | ^^^ help: did you mean `name`? Build FAILED

Test Failure (Logic Bug)

Code compiles, but your function returns wrong results.

test test_add ... ok test test_negative ... FAILED assertion `left == right` failed left: -1 right: 1 test result: FAILED. 1 passed; 1 failed

Clippy Warning (Code Smell)

Works, but Clippy spots a likely mistake.

warning: unnecessary reference --> src/main.rs:12:10 | 12 | foo(&*bar) | ^^^^ help: use `bar` Clippy FAILED (warnings = errors)

All Passed

Everything works. This is what healthy output looks like.

Compiling beads v0.1.0 Finished release target in 2.31s test test_add ... ok test test_negative ... ok test result: ok. 2 passed; 0 failed Checking beads v0.1.0 — 0 warnings All steps passed ✅
📝
1. Commit List

Small ✅ or ❌ icon next to each commit message on the repo page

▶️
2. Actions Tab

Full list of every CI run with detailed logs for each step

🔀
3. PR Checks

Status shown at the bottom of every Pull Request page

Code Issues Pull requests ▶ Actions
Rust CI — fix: handle negative numbers
✔ 31s
Rust CI — add new parser module
✘ failed at Build
🟡
Rust CI — update readme
running...
passed
failed
🟡 running
1

Transfer to Org

Move repo from personal account to jojomojo-org. Private repos stay private.

gh api --method POST repos/jojomojo786/REPO/transfer -f new_owner="jojomojo-org"
2

Add Workflow File

Create the ci.yml file in .github/workflows/. Copy the YAML from section 07.

mkdir -p .github/workflows && cp ci.yml .github/workflows/
3

Push & Done

Every push to main now auto-builds and tests. No cloud minutes used.

git push origin main → ✅ CI runs

Current setup — what happens right now:

🚀 git push
🔨 build + test
✅ status badge
🛑 done

The runner only validates your code. It reports ✅ or ❌. It does not deploy, block pushes, or send notifications.

Could add later (not active):

🚢
Auto-Deploy
Deploy after passing
🛡️
Block Merging
Require CI to pass
🔔
Notifications
Alert on failure
📦
Build Releases
Upload binaries
CI (Continuous Integration)
Auto-test code every time someone pushes.
CD (Continuous Deployment)
Auto-deploy after CI passes. Not set up yet.
Runner
Machine that executes CI jobs. Yours is iqra.
Workflow (.yml file)
YAML instructions telling the runner what to do.
Job
A group of steps running on one runner.
Step
One task within a job (e.g., cargo build).
Action
Pre-built reusable code (e.g., actions/checkout).
Trigger (on:)
Event that starts a workflow (push, PR, etc.).
Organization
GitHub group that holds repos. Runner shared across all.
Labels
Tags on runner (self-hosted, linux, x64, iqra) for matching.
cargo build
Compiles Rust code. Fails on syntax/type errors.
cargo test
Runs #[test] functions. Fails if assertions panic.
cargo clippy
Linter catching common mistakes the compiler misses.
systemd
Linux service manager. Keeps runner alive 24/7.
Pull Request (PR)
Request to merge branch into main. CI checks it first.
Status Check
The ✅, ❌, or 🟡 icon on commits/PRs.