Add ci-relay (VPS webhook receiver) and local-runner (Thinkpad CI executor) crates that communicate over iroh. Includes .ci.yml smoke test pipeline, simulation tests, and development docs. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
10 KiB
How to Test Drive the Local CI Runner over LAN
This guide walks through running the local CI runner on the Thinkpad and triggering it from Forgejo on your main laptop. By the end, pushes to your Forgejo repos will automatically run CI pipelines on the Thinkpad.
Network Layout
┌──────────────────────┐ LAN ┌──────────────────────┐
│ Main Laptop │◄───────────────────►│ Thinkpad │
│ │ │ │
│ Forgejo instance │ webhook POST ───► │ local-runner │
│ (e.g. :3000) │ ◄── status API ──── │ (e.g. :8787) │
│ │ │ dashboard (:9090) │
└──────────────────────┘ └──────────────────────┘
Prerequisites
- Main laptop: Forgejo running and accessible on LAN (e.g.
http://192.168.1.100:3000) - Thinkpad: Rust toolchain installed, this repo cloned, network-reachable from the main laptop
- Both machines on the same LAN (or routable to each other)
Find each machine's LAN IP:
# On each machine
ip addr show | grep 'inet ' | grep -v 127.0.0.1
# or
hostname -I
We'll use these example IPs throughout the guide:
- Main laptop (Forgejo):
192.168.1.100 - Thinkpad (CI runner):
192.168.1.200
Replace them with your actual IPs.
Step 1: Create a Forgejo API Token
On your main laptop, open Forgejo in a browser:
- Go to Settings > Applications (top-right user menu > Settings > Applications)
- Create a new token with at least these permissions:
repo: read/write (needed to post commit statuses)
- Copy the token (e.g.
abc123def456)
Step 2: Write a .ci.yml for Your Repository
Create a .ci.yml file on the Thinkpad. This defines what pipelines and jobs to run.
Example for a Rust project:
pipelines:
check:
triggers:
- event: push
branches: ["*"]
exclude: ["master"]
jobs:
fmt:
run: cargo fmt -- --check
clippy:
run: cargo clippy -- -D warnings
test:
needs: [fmt, clippy]
run: cargo test
timeout: 300
release:
triggers:
- event: push
branches: ["master"]
jobs:
test:
run: cargo test --all-features
timeout: 600
bench:
needs: [test]
run: cargo bench
Key points:
triggers[].event: one ofpush,tag,mergetriggers[].branches: glob patterns ("*"matches all,"feature-*"matches prefixed)triggers[].exclude: branches to skipjobs[].run: a single command string, or a list of commands (run sequentially)jobs[].needs: list of jobs that must pass first (DAG dependencies)jobs[].timeout: max seconds before the job is killed (default: 300)jobs[].env: extra environment variables as key-value pairs
Save this file somewhere accessible on the Thinkpad, e.g. /home/user/ci/my-project.ci.yml.
Step 3: Build the Local Runner on the Thinkpad
SSH into the Thinkpad (or work directly on it):
# Clone the repo if not already present
git clone <repo-url> ~/swactor
cd ~/swactor
# Build the local-runner binary
cargo build --release -p local-runner
The binary will be at target/release/local-runner.
Step 4: Create a Working Directory
The runner clones your repo into a working directory for each pipeline. Create it:
mkdir -p ~/ci-work
Step 5: Start the Local Runner
./target/release/local-runner \
--port 8787 \
--forgejo-url http://192.168.1.100:3000 \
--forgejo-token abc123def456 \
--secret my-webhook-secret \
--yaml /home/user/ci/my-project.ci.yml \
--work-dir /home/user/ci-work \
--repo-url http://192.168.1.100:3000/user/repo.git \
--dashboard-port 9090
| Flag | Description |
|---|---|
--port |
Port the webhook listener binds to (default: 8787) |
--forgejo-url |
URL of your Forgejo instance on the main laptop |
--forgejo-token |
API token created in Step 1 |
--secret |
Webhook secret (must match what you set in Forgejo, see Step 6) |
--yaml |
Path to the .ci.yml file on disk |
--work-dir |
Base directory for git clones (one subdirectory per pipeline) |
--repo-url |
Git clone URL for the repository (HTTP or SSH) |
--dashboard-port |
Optional: enables the web dashboard on this port |
You should see:
Webhook listener on http://0.0.0.0:8787
Local CI runner started
Webhook: http://0.0.0.0:8787
YAML: /home/user/ci/my-project.ci.yml
Workdir: /home/user/ci-work
Dashboard: http://0.0.0.0:9090
The runner is now listening for webhooks.
Step 6: Configure the Forgejo Webhook
On your main laptop, open Forgejo and go to the repo's settings:
- Navigate to Settings > Webhooks > Add Webhook > Forgejo
- Fill in:
- Target URL:
http://192.168.1.200:8787(Thinkpad's LAN IP and runner port) - Secret:
my-webhook-secret(must match--secretfrom Step 5) - Trigger On: Choose which events to send:
- Push Events (for
pushtriggers) - Create Events (for
tagtriggers) - Pull Request Events (for
mergetriggers)
- Push Events (for
- Branch filter: leave blank to send all branches, or set a pattern
- Active: checked
- Target URL:
- Click Add Webhook
Test the webhook connection
After adding the webhook, Forgejo shows a Test Delivery button. Click it to send a test ping. Check the Thinkpad terminal for output. Forgejo also shows the response status — you should see 200 OK.
Step 7: Push and Watch
From your main laptop (or anywhere with push access):
cd ~/my-project
git checkout -b test-ci
echo "// test" >> src/main.rs
git add src/main.rs
git commit -m "test CI"
git push origin test-ci
On the Thinkpad terminal, you'll see the runner:
- Receive the webhook
- Match triggers in
.ci.yml - Queue the pipeline
- Clone the repo and checkout the commit SHA
- Execute jobs one at a time, respecting the DAG order
- Stream stdout/stderr in real time
- Report commit statuses back to Forgejo
Back in Forgejo, the commit will show status checks (pending, then success/failure) next to the SHA.
Step 8: View the Dashboard (Optional)
If you started with --dashboard-port 9090, open a browser on any LAN machine:
http://192.168.1.200:9090
This shows the swactor runtime dashboard with CI-specific panels: active pipelines, recent pipelines, job statuses, and actor system metrics.
Behavior Reference
One-at-a-Time Execution
Jobs run serially — only one job executes at any moment across all pipelines. This guarantees benchmark isolation with no resource contention.
Queue Supersede
If you push twice to the same branch quickly:
- First push is already running: it finishes normally
- Second push is queued: it runs after the first finishes
- Third push arrives while second is still queued: the second is superseded (marked as error, skipped), and the third takes its place in the queue
Only queued pipelines get superseded — a running pipeline always runs to completion.
DAG Dependencies
Within a pipeline, jobs respect their needs dependencies. If test needs [fmt, clippy], then fmt runs first, then clippy, then test. If fmt fails, test is skipped.
CI Environment Variables
Every job command has these injected:
| Variable | Value |
|---|---|
CI |
true |
CI_COMMIT_SHA |
The commit being tested |
CI_BRANCH |
The branch name |
CI_PIPELINE_ID |
Numeric pipeline identifier |
CI_JOB_NAME |
Name of the current job |
Plus any env keys from the job definition in .ci.yml.
Commit Status Reporting
The runner posts status updates to the Forgejo API for each pipeline and each job:
pendingwhen a pipeline/job is queuedsuccesswhen all jobs passfailurewhen a job failserrorwhen a pipeline is superseded
These appear as commit status checks in Forgejo's UI.
Troubleshooting
Webhook not reaching the Thinkpad
- Verify the Thinkpad's firewall allows inbound on the webhook port:
# On Thinkpad sudo ufw allow 8787/tcp # if using ufw # or sudo iptables -A INPUT -p tcp --dport 8787 -j ACCEPT - Confirm connectivity from main laptop:
curl -v http://192.168.1.200:8787 # Should get "method not allowed" (405) — that means the server is reachable
Signature mismatch (401)
- The
--secretflag on the runner must exactly match the Secret field in Forgejo's webhook config - If you don't want signature verification, set both to empty strings (omit
--secretand leave Secret blank in Forgejo)
Git clone fails
- Make sure
--repo-urlis reachable from the Thinkpad:# On Thinkpad git ls-remote http://192.168.1.100:3000/user/repo.git - If the repo is private, use an authenticated URL:
Or use SSH:http://user:password@192.168.1.100:3000/user/repo.gitgit@192.168.1.100:user/repo.git
Status updates not appearing in Forgejo
- Verify the API token has
repowrite permissions - Check the Thinkpad terminal for
StatusReporter: failed to post statuserrors - Test the token manually:
curl -H "Authorization: token abc123def456" \ http://192.168.1.100:3000/api/v1/user
Jobs failing unexpectedly
- Check that the Thinkpad has the necessary toolchain (cargo, rustup, etc.)
- The working directory for each pipeline is
{work-dir}/pipeline-{id}/— you can inspect it - Job stdout/stderr is streamed to the runner's terminal output
Quick-Start Cheat Sheet
# === Thinkpad ===
cd ~/swactor
cargo build --release -p local-runner
mkdir -p ~/ci-work
./target/release/local-runner \
--port 8787 \
--forgejo-url http://LAPTOP_IP:3000 \
--forgejo-token YOUR_TOKEN \
--secret YOUR_SECRET \
--yaml /path/to/.ci.yml \
--work-dir ~/ci-work \
--repo-url http://LAPTOP_IP:3000/user/repo.git \
--dashboard-port 9090
# === Main Laptop (Forgejo) ===
# Repo > Settings > Webhooks > Add Webhook:
# URL: http://THINKPAD_IP:8787
# Secret: YOUR_SECRET
# Events: Push, Create, Pull Request
# === Test it ===
git push origin my-branch # triggers CI on the Thinkpad