CI setup
Register the GitHub App, connect repositories, write .runyard/ci.yml, and set up CI runners (native + Dagger).
This guide takes you from nothing to green checks: GitHub App registration, hub configuration, repository enablement, the .runyard/ci.yml reference, runner setup, and troubleshooting. Read Concepts β CI first for the model and trust boundary.
1. Register a GitHub App
Create a GitHub App (Settings β Developer settings β GitHub Apps β New) owned by your user or org:
- Webhook URL:
https://<your-hub>/api/ci/webhooks/github - Webhook secret: generate a long random value; you will hand the same value to the hub.
- Permissions (the exact minimum):
- Repository β Contents: Read-only (fetch
.runyard/ci.yml+ git checkout) - Repository β Checks: Read & write (create/update check runs)
- Repository β Metadata: Read-only (implicit)
- Repository β Pull requests: Read-only (PR event payloads)
- Repository β Contents: Read-only (fetch
- Subscribe to events:
Push,Pull request,Check run, and the installation events are delivered automatically. - Generate a private key (
.pem) and note the App ID.
Install the App on the repositories you want CI for. Installation is connection only β nothing runs until an operator enables each repository.
2. Configure the hub
RUNYARD_GITHUB_APP_ID=123456
RUNYARD_GITHUB_APP_PRIVATE_KEY_PATH=/etc/runyard/github-app.pem # or RUNYARD_GITHUB_APP_PRIVATE_KEY with inline PEM
RUNYARD_GITHUB_WEBHOOK_SECRET=<the webhook secret>
# Optional (GitHub Enterprise / tests): RUNYARD_GITHUB_API_BASE=https://ghe.example.com/api/v3The key and webhook secret never enter the database; installation tokens are minted just in time and held only in memory. Verify with GET /api/ci/github-app (admin) or the CI page in the web app β it reports configuration presence without any secret values.
3. Connect and enable repositories
runyard repo sync # pull installations + repositories from the App (admin)
runyard repo list
runyard repo enable yolo-maxi/runyard # nothing runs until this
runyard repo trust yolo-maxi/runyard --level trusted --allow-nativeTrust policy governs secrets and native execution (see the trust model). --runner-tags build,fast additionally restricts this repository's jobs to runners carrying those tags.
4. Write .runyard/ci.yml
Committed to the repository (loaded from the trusted base revision, pinned by SHA). The schema is deliberately small β it is not GitHub Actions:
version: 1 # required; 1 is the only schema version
name: ci # optional pipeline name (lowercase identifier)
on: # at least one trigger
push:
branches: [main, "release/*"] # glob: *, **, ? (deterministic, no expressions)
tags: ["v*"] # tag pushes (path filters don't apply to tags)
paths: ["src/**", "tests/**"] # optional: only fire when a changed file matches
pull_request:
branches: [main] # target-branch filter
manual: true # allow `runyard ci dispatch` (default true)
concurrency:
# default group: per-PR for pull requests, per-ref for pushes
cancelInProgress: true # newer trigger cancels the older pipeline (default true)
jobs:
lint:
executor: native # native | dagger
commands: # native: one bash script, `set -euo pipefail`
- pnpm install
- pnpm lint
timeoutMinutes: 10 # 1..360, default 30
test:
needs: [lint] # validated DAG (cycles/unknown refs rejected)
commands: ["pnpm test"]
env: { NODE_ENV: test } # non-secret literals only
secrets: [NPM_TOKEN] # hub secret NAMES; values never appear in config/logs
artifacts: ["coverage/**"] # repo-relative globs, uploaded as run artifacts
package:
executor: dagger
needs: [test]
dagger: { module: ".", function: build }
required: false # optional job: its failure doesn't fail the pipelineValidation errors are shown by GET /api/ci/repos/:id/config, the repository page in the web app, and β for triggered pipelines β as a failed runyard/ci check on the commit.
RunYard's own repository carries the canonical dogfood baseline at .runyard/ci.yml:
quality: frozen root install,pnpm lint, andpnpm typecheck.tests: frozen root install and the fullpnpm testsuite.build_docs: frozen root and docs installs, hub build, static docs build, thengit diff --exit-code -- public/ docs-site/out/so generated web/docs output cannot drift from source.sandbox_smoke: optional, prerequisite-aware real Bubblewrap/AppArmor smoke. It runstests/runner-sandbox.test.jswithRUNYARD_REQUIRE_BWRAP=1only when the host can actually create a Bubblewrap user namespace; otherwise it prints the exact prerequisite path (sudo deploy/apparmor/install.shon AppArmor-restricted hosts) and remains optional until the runner fleet is ready.security_artifact_hygiene: deterministic repo-contained check for high-confidence tracked secrets plus forbidden publishable artifacts (.map, source files, env files, private keys) inpublic/anddocs-site/out/. It intentionally does not run a network dependency audit.
The dogfood graph fans these jobs out independently and lets the existing CI orchestrator aggregate the result. There is no automatic agent repair in this baseline; implement-change-gated stays an explicit operator workflow, not a CI side effect.
Container validation is a next-phase gate: add it only after the runner hosts can build or inspect images without publishing and without touching repo.box deploy configuration. The permanent GitHub Actions release.yml and images.yml workflows stay enabled as the independent lifeboat for releases/images while RunYard CI rolls out in shadow.
5. Set up a CI runner
Any RunYard runner becomes a CI runner by opting in:
RUNYARD_RUNNER_CI=1 # advertise the `ci` tag; claim CI jobs
RUNYARD_RUNNER_CI_NATIVE=1 # additionally allow native host execution (trusted repos only)
RUNYARD_RUNNER_CI_DIR=/var/lib/runyard-ci # job workspaces (default <workspace>/.runyard-ci)
RUNYARD_RUNNER_CI_RETAIN_MS=86400000 # failed-workspace evidence retention (24h default)Job workspaces are isolated per run, path-contained, removed on success, and retained (bounded) on failure for debugging.
Native executor
commands join into a single bash -c script running under set -euo pipefail β the first failing command fails the job. The child environment is a strict allowlist baseline plus the job's env, the hub-delivered secrets, and CI/RUNYARD_CI_* context variables. Cancellation and timeouts kill the whole process group.
Dagger executor
Install the open-source Dagger engine on the runner host β RunYard never auto-installs it and has no Dagger Cloud dependency. The runner probes dagger version at job start; if unavailable, the job fails as infra_unavailable with a clear message (checks show action_required), and native trusted jobs keep working. A dagger job runs dagger call -m <module> <function> [--arg valueβ¦] against the checked-out source; secrets ride environment variables through the same non-logging channel as native jobs.
6. Dogfood and recovery surfaces
runyard ci dispatch yolo-maxi/runyard --ref main # manual pipeline at an exact resolved sha
runyard ci pipelines --repo yolo-maxi/runyard
runyard ci status <pipeline-or-parent-run-id>
runyard ci cancel <pipeline>
runyard ci rerun <pipeline>Equivalent HTTP endpoints live in the ci API group (/api/v1/ci/...), and MCP tools list_ci_repositories, get_ci_pipeline, dispatch_ci_run cover agent use. Admin diagnostics: GET /api/ci/deliveries (webhook ledger) and GET /api/ci/diagnostics (signature failures, duplicates, queue latency, check lag). After a GitHub outage, POST /api/ci/pipelines/:id/sync-checks resets the bounded check-retry counters and reconciles.
Shadow rollout for RunYard itself
For the RunYard repository, enable the App connection first but leave branch protection on the GitHub Actions lifeboat while the RunYard checks run in shadow. A production-candidate commit must satisfy the stable-production-tests-candidate invariant: the checked-in .runyard/ci.yml is loaded from the trusted base, PR pipelines test the exact merge candidate, main/tag pipelines test the pushed SHA, required jobs are deterministic and non-LLM, and the GitHub Actions lifeboat remains green for the same source. Only after repeated shadow greens should operators consider making runyard/ci checks required.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Webhook answers 401 invalid signature | RUNYARD_GITHUB_WEBHOOK_SECRET doesn't match the App's webhook secret. |
Webhook answers 503 | The hub has no GitHub App configuration β see step 2. |
Delivery recorded as ignored: repository β¦ not enabled | Run runyard repo enable <repo> β installation alone never starts CI. |
ignored: no .runyard/ci.yml at trusted revision | Commit the config to the default branch (PRs read it from the base). |
Job blocked: native CI execution is disabled on this runner | Set RUNYARD_RUNNER_CI_NATIVE=1 on the runner and mark the repo trusted with allowNative. |
Job failed infra_unavailable: Dagger is not available | Install Dagger on the runner or switch the job to executor: native. |
Check stuck queued on GitHub | See GET /api/ci/diagnostics β check lag; after an outage run sync-checks. Reporter failures never affect the run itself. |
PR check action_required: merge candidate conflicted | The PR no longer merges cleanly into its base β rebase/update the PR. |
Migrating from GitHub Actions
Translate each workflow's on: triggers and jobs into the schema above; move repository secrets into the hub's secrets and reference them by name in secrets:; replace runs-on with runner tags + trust policy; keep the GitHub merge-gate by marking the runyard/* checks required in branch protection. Explicitly out of scope for now (planned follow-ups, not silently emulated): act-style local Actions compatibility, merge queues / speculative gating (Zuul-style), Kubernetes/Tekton executors, and release automation.