--- url: /bupha/tutorials/first-run.md --- # Tutorial: your first bupha run Goal: push-triggered CI on a repo, end to end, in ~10 minutes. Prereqs: **Linux + systemd** (shell runner needs bash; macOS/Windows unsupported for now); git; `cargo install bupha` (until 0.1.0 lands on crates.io: `cargo install --git https://github.com/9aia/bupha`); docker only if you use the dagger runner. Also a stable public URL for the webhook — named cloudflared tunnel or a direct box; quick tunnels are convenient but flaky. 1. `cd your-repo && bupha init` — scaffolds the dagger module + delegate (the module builds a Rust workspace — needs `cargo` in the pipeline image; for anything else write `ci.sh` and use the shell runner). 2. `bupha deploy env` → set `BUPHA_WEBHOOK_SECRET` + `BUPHA_TOKEN` (a repo-scoped PAT) — in `~/.config/bupha/env`, or store them in your OS keyring with `secret-tool` (resolution: env → keyring → file). 3. Write `bupha.toml` (see `examples/bupha.toml.example`): your repo's `[[repo]]` entry, `runner.kind`, `tunnel`. 4. `bupha doctor` — checks this box (binaries, secrets, config) before you commit; fix anything red. 5. `bupha deploy service` — systemd unit written + enabled (`loginctl enable-linger` once for boot persistence). 6. `bupha deploy webhook` — registers `POST /hook` on the repo; `bupha deploy check` verifies unit+secrets+endpoint. 7. Push. `ci/bupha` commit status appears; superseded/cancelled shas get honest error statuses. Done. Logs: `journalctl --user -u bupha-watch -f`. --- --- url: /bupha/how-to/multi-repo.md --- # How-to: watch multiple repos Add one `[[repo]]` entry per repository; `name` must equal the payload's `repository.full_name`. Each repo gets its own pending slot; a single global worker serializes runs (deliberate — safe on one box). Same-repo pushes may cancel their own in-flight run (`cancel_running`); a push never cancels another repo's run. ```toml [[repo]] name = "org/api" dir = "/srv/checkouts/api" queue = "all" # every push runs (releases) [[repo]] name = "org/web" dir = "/srv/checkouts/web" # default latest-wins # [repo.runner] # per-repo override; replaces global # kind = "shell" # cmd = "./release.sh" ``` Run `bupha deploy webhook` once — it registers the hook on every watched repo. `status_repo` on an entry redirects statuses (e.g. mirror repos); it defaults to `name`. --- --- url: /bupha/how-to/shell-runner.md --- # How-to: run without containers `runner.kind = "shell"` + `cmd` (alias: `"script"`) — the watcher still does webhook verification, the queue, detached worktrees and statuses; your script does the build. No dagger, no docker, no image pulls. ```toml [runner] kind = "shell" cmd = "./ci.sh" # run with `bash -c` in the worktree ``` The worktree is a detached checkout of the pushed sha at `--`, removed after the run. A killed run reports "cancelled (superseded)", not failure. Use it for: `make ci`, `./ci.sh`, a linter gate, or bupha's own `scripts/release.sh` (debounced release job). For anything heavier — cache volumes, toolchains, parallel shards — use the `dagger` runner and the shared `modules/dagger` module. --- --- url: /bupha/how-to/sccache-backend.md --- # How-to: sccache backend sccache's backend is decoupled via an env file — set `[cache] conf` to a `KEY=VAL` file (mounted into containers as a secret). Without it, sccache uses a local cache volume. ## Local S3 (garage) ``` docker run -d --name garage --restart always \ -v $HOME/bupha-data/garage:/data \ -v $HOME/bupha-data/garage.toml:/etc/garage.toml \ -p 9100:9100 dxflrs/garage:v1.x.x ``` Then in the garage CLI: `bucket create bupha-ci` → `key create sccache` → `bucket allow --read --write --owner bupha-ci --key sccache`. Env file: ``` SCCACHE_ENDPOINT=http://:9100 SCCACHE_BUCKET=bupha-ci SCCACHE_REGION=garage SCCACHE_S3_USE_SSL=false AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... ``` ## Migrating to hosted S3 (R2 / AWS) Same file, new values — `SCCACHE_ENDPOINT` to the provider URL, real region, new creds. No code changes; that's the point of the env file. --- --- url: /bupha/reference/cli.md --- # Command-Line Help for `bupha` This document contains the help content for the `bupha` command-line program. **Command Overview:** * [`bupha`↴](#bupha) * [`bupha watch`↴](#bupha-watch) * [`bupha init`↴](#bupha-init) * [`bupha doctor`↴](#bupha-doctor) * [`bupha run`↴](#bupha-run) * [`bupha deploy`↴](#bupha-deploy) * [`bupha deploy service`↴](#bupha-deploy-service) * [`bupha deploy webhook`↴](#bupha-deploy-webhook) * [`bupha deploy env`↴](#bupha-deploy-env) * [`bupha deploy check`↴](#bupha-deploy-check) * [`bupha deploy release`↴](#bupha-deploy-release) * [`bupha destroy`↴](#bupha-destroy) * [`bupha destroy service`↴](#bupha-destroy-service) * [`bupha destroy webhook`↴](#bupha-destroy-webhook) * [`bupha destroy worktrees`↴](#bupha-destroy-worktrees) * [`bupha runs`↴](#bupha-runs) * [`bupha log`↴](#bupha-log) * [`bupha explain`↴](#bupha-explain) ## `bupha` the oxpecker on your repo's back — headless CI **Usage:** `bupha [OPTIONS] ` ###### **Subcommands:** * `watch` — Serve the webhook → queue → runner loop * `init` — Scaffold dagger CI in a repo (dagger init + pinned module + delegate). For the script runner, a ci.sh is enough * `doctor` — Check what the loaded config actually needs on this box * `run` — Run the pipeline locally — in the watched repo's `dir`, not cwd * `deploy` — Install-time helpers — no manual file copies * `destroy` — Teardown counterparts of `deploy` * `runs` — Recent finished runs (reads `/history.jsonl`) * `log` — Raw run log — last N lines of history.jsonl * `explain` — Why a sha did/didn't run — history + queue + worktree state ###### **Options:** * `--config ` — Path to bupha.toml (default: ./bupha.toml or ~/.config/bupha/) ## `bupha watch` Serve the webhook → queue → runner loop **Usage:** `bupha watch` ## `bupha init` Scaffold dagger CI in a repo (dagger init + pinned module + delegate). For the script runner, a ci.sh is enough **Usage:** `bupha init [DIR]` ###### **Arguments:** * `` — Repo dir to initialize (default: cwd) Default value: `.` ## `bupha doctor` Check what the loaded config actually needs on this box **Usage:** `bupha doctor` ## `bupha run` Run the pipeline locally — in the watched repo's `dir`, not cwd **Usage:** `bupha run [FUNCTION]` ###### **Arguments:** * `` — Function for the dagger runner (default: from config) ## `bupha deploy` Install-time helpers — no manual file copies **Usage:** `bupha deploy ` ###### **Subcommands:** * `service` — Write + enable the systemd user unit (needs enable-linger for boot persistence) * `webhook` — Register/update the repo's push webhook via the REST API (token from env/keyring/env file — no `gh` dependency) * `env` — Seed ~/.config/bupha/env with the secret names * `check` — Verify the whole install: unit, secrets, hook, endpoint, token * `release` — One-time seed: cargo publish in dep order + tag {repo}-v{ver}. Explicit — publishing is the irreversible step in the flow ## `bupha deploy service` Write + enable the systemd user unit (needs enable-linger for boot persistence) **Usage:** `bupha deploy service [OPTIONS]` ###### **Options:** * `--no-start` — Skip enable --now (write the file only) ## `bupha deploy webhook` Register/update the repo's push webhook via the REST API (token from env/keyring/env file — no `gh` dependency) **Usage:** `bupha deploy webhook` ## `bupha deploy env` Seed ~/.config/bupha/env with the secret names **Usage:** `bupha deploy env` ## `bupha deploy check` Verify the whole install: unit, secrets, hook, endpoint, token **Usage:** `bupha deploy check` ## `bupha deploy release` One-time seed: cargo publish in dep order + tag {repo}-v{ver}. Explicit — publishing is the irreversible step in the flow **Usage:** `bupha deploy release [OPTIONS]` ###### **Options:** * `--dry-run` — Print the publish order and tag without doing anything ## `bupha destroy` Teardown counterparts of `deploy` **Usage:** `bupha destroy ` ###### **Subcommands:** * `service` — Disable + remove the systemd user unit * `webhook` — Delete the repo's webhook whose URL matches tunnel.public_url * `worktrees` — Remove detached worktrees and the persisted queue file ## `bupha destroy service` Disable + remove the systemd user unit **Usage:** `bupha destroy service` ## `bupha destroy webhook` Delete the repo's webhook whose URL matches tunnel.public_url **Usage:** `bupha destroy webhook` ## `bupha destroy worktrees` Remove detached worktrees and the persisted queue file **Usage:** `bupha destroy worktrees` ## `bupha runs` Recent finished runs (reads `/history.jsonl`) **Usage:** `bupha runs [REPO]` ###### **Arguments:** * `` — Filter to one repo ("owner/name") ## `bupha log` Raw run log — last N lines of history.jsonl **Usage:** `bupha log [OPTIONS]` ###### **Options:** * `-n`, `--n ` — Lines to show Default value: `50` ## `bupha explain` Why a sha did/didn't run — history + queue + worktree state **Usage:** `bupha explain ` ###### **Arguments:** * `` — The pushed sha (any prefix ≥ 4 chars) This document was generated automatically by clap-markdown. --- --- url: /bupha/reference/config.md --- # Config — `bupha.toml` ## Top level | field | type | required | notes | |---|---|---|---| | `repos` | `Vec` | required | Watched repos — one pending slot each, one global worker. TOML-conventional singular; `repos` kept as alias for existing configs. | | `tunnel` | `TunnelCfg` | optional | How the listener is exposed publicly (cloudflared, direct, none). | | `cache` | `CacheCfg` | optional | Artifact cache backend (`[cache]`); `runner.sccache_conf` is accepted for older configs — `[cache]` wins. | | `access` | `AccessCfg` | optional | Who may trigger (`[access]`). Absent = open. | | `telemetry` | `TelemetryCfg` | optional | Pipeline observability (`[telemetry]`). Absent = none. | | `attest` | `AttestCfg` | optional | Run provenance (`[attest]`). Absent = none. | | `queue` | `QueueCfg` | optional | Queue persistence backend (`[queue]`). Absent = redb. | | `worktree` | `WorktreeCfg` | optional | Where detached git worktrees for runs are created. | ## `[[repo]]` | field | type | required | notes | |---|---|---|---| | `name` | `string` | required | repository.full_name in the webhook payload ("owner/name") — also the status target repo. | | `dir` | `path` | required | Clone to fetch + create detached worktrees from. | | `branch` | `string` | optional | The ref to watch (e.g. "refs/heads/main"). | | `status_repo` | `string` | optional | Status repo override (defaults to `name`). | | `runner` | `RunnerCfg` | optional | Per-repo runner override — a full \[repo.runner] table replaces the global `[runner]` for this repo. | | `queue` | `"latest" | "all"` | optional | Queue policy — `latest` (default) or `all` (run every push). | ## `[trigger]` | field | type | required | notes | |---|---|---|---| | `api_base` | `string` | optional | Forge API base — GHES ("https://ghe.acme/api/v3") or default github.com. Used by the trigger, status, and forge-admin impls. | | `kind` | `string` | required | "github" today; other forges = new impl crates. | | `listen` | `string` | optional | Webhook listener bind address ("127.0.0.1:9000"). | | `secret_env` | `string` | optional | Env var holding the webhook secret. | ## `[runner]` | field | type | required | notes | |---|---|---|---| | `kind` | `string` | required | Runner kind: "dagger" | "shell" | "ssh" | impl crates. | | `token_env` | `string` | optional | Env var name holding the forge token (never the value). | | `cancel_running` | `bool` | optional | Kill an in-flight run when a newer push supersedes it. Invalid with `queue = "all"` (rejected at load). | | `extra` | `toml::Table` | required | Impl-owned fields — the matched impl crate deserializes its own config struct from this; core knows none of them. | ## `[status]` | field | type | required | notes | |---|---|---|---| | `kind` | `string` | optional | "github" | "log" (stdout only) | "none". | | `repo` | `string` | optional | owner/repo for the commit-status API. | | `context` | `string` | optional | Status context label (e.g. "ci/bupha"). | ## `[tunnel]` | field | type | required | notes | |---|---|---|---| | `provider` | `string` | optional | "none" | "cloudflared" | "direct". | | `name` | `string` | optional | Named tunnel (stable URL); empty = quick tunnel. | | `local_url` | `string` | optional | Local URL the tunnel forwards to (listener address). | | `public_url` | `string` | optional | provider=direct: the external URL GitHub should hit. | ## `[cache]` | field | type | required | notes | |---|---|---|---| | `kind` | `string` | optional | Backend id ("sccache"; "none"/empty disables). | | `conf` | `string` | optional | Backend conf value (sccache: "file:///dir", "s3://bucket/…"). | ## `[worktree]` | field | type | required | notes | |---|---|---|---| | `base` | `string` | optional | Worktree parent dir — runs land at `<base>-<repo>-<sha12>`. | ## `[runner]` — kind `dagger` | field | type | required | notes | |---|---|---|---| | `function` | `string` | optional | — | | `shards` | `string` | optional | — | | `jobs` | `string` | optional | — | | `dogfood_jobs` | `string` | optional | — | | `sequential` | `bool` | optional | — | | `sccache_conf` | `string` | optional | sccache conf passed as `--sccache-conf` (resolved from `[cache]` by the composition root). | | `cli_source` | `string` | optional | — | | `extra_args` | `list` | optional | — | ## `[runner]` — kind `shell` | field | type | required | notes | |---|---|---|---| | `cmd` | `string` | optional | — | ## `[runner]` — kind `ssh` | field | type | required | notes | |---|---|---|---| | `remote` | `string` | optional | Remote host ("user@host"; ssh config resolves auth). | | `remote_dir` | `string` | optional | Remote workdir the worktree syncs into. | | `cmd` | `string` | optional | Command run remotely (`bash -c`). | ## `[runner]` — kind `container` | field | type | required | notes | |---|---|---|---| | `image` | `string` | required | Image the pipeline runs in (required). | | `cmd` | `string` | optional | Shell command executed inside the container. | | `engine` | `string` | optional | Engine binary — `"podman"` (default) or `"docker"`. | | `network` | `string` | optional | Network mode — `"none"` default (isolated); `"host"`/`"bridge"` for pipelines that genuinely need outbound. | ## Constraints * `queue = "all"` rejects `cancel_running = true` at load — killing a run would silently drop a promised commit. * `runner.kind` and `trigger.kind`/`status.kind` resolve to impl crates; unknown kinds fail at `bupha doctor`/startup. * `deny_unknown_fields`: typos in field names fail the load. --- --- url: /bupha/reference/crate-api.md --- # Crate API Rustdoc for every workspace crate — public APIs are documented at source (doc comments). * [bupha-core](/bupha/api/bupha_core/index.html) — domain * [bupha-access](/bupha/api/bupha_access/index.html) — port * [bupha-attest](/bupha/api/bupha_attest/index.html) — port * [bupha-cache](/bupha/api/bupha_cache/index.html) — port * [bupha-config](/bupha/api/bupha_config/index.html) — port * [bupha-queue](/bupha/api/bupha_queue/index.html) — port * [bupha-queue-lease](/bupha/api/bupha_queue_lease/index.html) — port * [bupha-runner](/bupha/api/bupha_runner/index.html) — port * [bupha-secret](/bupha/api/bupha_secret/index.html) — port * [bupha-service](/bupha/api/bupha_service/index.html) — port * [bupha-status](/bupha/api/bupha_status/index.html) — port * [bupha-telemetry](/bupha/api/bupha_telemetry/index.html) — port * [bupha-trigger](/bupha/api/bupha_trigger/index.html) — port * [bupha-tunnel](/bupha/api/bupha_tunnel/index.html) — port * [bupha-vcs](/bupha/api/bupha_vcs/index.html) — port * [bupha-access-file](/bupha/api/bupha_access_file/index.html) — adapter * [bupha-access-forge](/bupha/api/bupha_access_forge/index.html) — adapter * [bupha-attest-file](/bupha/api/bupha_attest_file/index.html) — adapter * [bupha-attest-sigstore](/bupha/api/bupha_attest_sigstore/index.html) — adapter * [bupha-cache-sccache](/bupha/api/bupha_cache_sccache/index.html) — adapter * [bupha-cloudflared](/bupha/api/bupha_cloudflared/index.html) — adapter * [bupha-config-toml](/bupha/api/bupha_config_toml/index.html) — adapter * [bupha-container](/bupha/api/bupha_container/index.html) — adapter * [bupha-dagger](/bupha/api/bupha_dagger/index.html) — adapter * [bupha-dotenv](/bupha/api/bupha_dotenv/index.html) — adapter * [bupha-git](/bupha/api/bupha_git/index.html) — adapter * [bupha-github](/bupha/api/bupha_github/index.html) — adapter * [bupha-history](/bupha/api/bupha_history/index.html) — adapter * [bupha-keyring](/bupha/api/bupha_keyring/index.html) — adapter * [bupha-queue-redb](/bupha/api/bupha_queue_redb/index.html) — adapter * [bupha-queue-redis](/bupha/api/bupha_queue_redis/index.html) — adapter * [bupha-shell](/bupha/api/bupha_shell/index.html) — adapter * [bupha-ssh](/bupha/api/bupha_ssh/index.html) — adapter * [bupha-systemd](/bupha/api/bupha_systemd/index.html) — adapter * [bupha-telemetry-otel](/bupha/api/bupha_telemetry_otel/index.html) — adapter * [bupha](/bupha/api/bupha/index.html) — binary --- --- url: /bupha/explanation/architecture.md --- # Architecture bupha is a port/adapter monorepo: every external concern sits behind a trait (a *port*), and each integration is a crate that implements it (an *impl*). `bupha-core` is the pure domain — config shape, queue semantics, shared primitives — and depends on nothing outside itself; the binary is a thin composition root that wires impls to ports. ## Crate map | crate | layer | purpose | |---|---|---| | `bupha-core` | domain | bupha domain core: config types, job queues, shared primitives — no ports, no integrations | | `bupha-access` | port | Access port: who may trigger a repo | | `bupha-attest` | port | Attest port: signed run provenance (SLSA-style predicate) | | `bupha-cache` | port | Cache port: artifact-cache backend for runners | | `bupha-config` | port | Config port: where bupha.toml-shaped config comes from | | `bupha-queue` | port | QueueStore port: queue persistence | | `bupha-queue-lease` | port | Queue-lease port: multi-worker fencing (try_claim + Fence) | | `bupha-runner` | port | Runner port: how a job becomes a spawned process | | `bupha-secret` | port | Secret port: resolve named secrets from storage | | `bupha-service` | port | Service port: OS service lifecycle | | `bupha-status` | port | StatusSink port: where run outcomes are reported | | `bupha-telemetry` | port | Telemetry port: fire-and-forget pipeline events | | `bupha-trigger` | port | Trigger port: where pushes come from | | `bupha-tunnel` | port | Tunnel port: expose the webhook listener publicly | | `bupha-vcs` | port | Vcs port: VCS operations for repos and worktrees | | `bupha-access-file` | adapter | Access adapter: allowlist of forge logins from config | | `bupha-access-forge` | adapter | Access adapter: forge collaborator-permission check | | `bupha-attest-file` | adapter | Attest adapter: SLSA-shape predicate JSON per run (unsigned v1) | | `bupha-attest-sigstore` | adapter | Attest adapter: cosign-signed run predicates (needs cosign + key) | | `bupha-cache-sccache` | adapter | Cache impl: sccache-compatible conf (file/S3/R2/GCS via conf) | | `bupha-cloudflared` | adapter | Tunnel impl: cloudflared (named tunnel preferred, quick tunnel fallback) | | `bupha-config-toml` | adapter | Config impl: bupha.toml file (explicit path, env, ./, ~/.config) | | `bupha-container` | adapter | Runner adapter: unprivileged container (podman/docker), network-off | | `bupha-dagger` | adapter | Runner impl: `dagger call <fn>` per job | | `bupha-dotenv` | adapter | Secret impl: KEY=VAL env file (chmod 600) | | `bupha-git` | adapter | Vcs impl: git via the git CLI | | `bupha-github` | adapter | GitHub impl: push-webhook trigger + commit-status sink | | `bupha-history` | adapter | Run-history adapter: JSONL records per finished run | | `bupha-keyring` | adapter | Secret impl: OS keyring via secret-tool (libsecret) | | `bupha-queue-redb` | adapter | QueueStore impl: redb embedded store (single file, ACID) | | `bupha-queue-redis` | adapter | QueueStore adapter: redis (multi-worker; SET NX claim + INCR fence) | | `bupha-shell` | adapter | Runner impl: bash -c \ per job — no containers | | `bupha-ssh` | adapter | Runner impl: remote execution over ssh (rsync worktree + run) | | `bupha-systemd` | adapter | Service impl: systemd user units | | `bupha-telemetry-otel` | adapter | Telemetry adapter: OpenMetrics textfile for Prometheus | | `bupha` | binary | bupha — the oxpecker on your repo's back: headless CI CLI | `modules/dagger/` (Python SDK) is the shared pipeline module `bupha init` installs into consumer repos — pinned to a sha. ## Request flow ``` POST /hook → 1. path check (404 otherwise) → 2. read body (≤ 2 MiB) → 3. signature verify (Trigger port — github impl: HMAC sha256, constant-time) → 4. dedup delivery id (bounded 10k ring; github impl uses X-GitHub-Delivery) → 5. parse — skip deletes/zero-sha; revision must be path-safe → 6. route — repository.full_name → watched [[repo]] entry; git_ref must equal repo.branch → 7. enqueue — per-repo queue; persist via QueueStore (redb, ACID) → 8. one global worker — Vcs fetch, detached worktree at -- → 9. spawn run handle (token via env:// name, never argv) → 10. status — pending → success/error; superseded and cancelled shas get honest error statuses ``` ## Queue semantics | policy | pending | on new push | use for | | ------------------ | ----------------- | ----------------------------------------------------------------------------------------------------- | -------- | | `latest` (default) | 1 sha | supersedes pending; `cancel_running` may kill in-flight | CI gates | | `all` | bounded FIFO (32) | appends; oldest evicted past cap; `cancel_running` rejected at load — it would drop a promised commit | releases | Persistence: accepted work writes through the `QueueStore` port (`redb`, single embedded file) inside `worktree.base`; on restart the interrupted `running` sha is re-queued at the front. One **global worker** serializes runs across all repos — a second push queues behind the in-flight job. Deliberate (one box, bounded resource use), not a hang: `journalctl --user -u bupha-watch` shows the queue moving. ## Security model * mandatory HMAC (constant-time compare) — unauthenticated payloads never reach the queue * body cap, delivery-id replay dedup, hex-sha validation before the value touches paths or status URLs * webhook takeover is exact-URL only; hook API calls are timed out * config names secrets (`secret_env`/`token_env`) — values resolve env → OS keyring → `~/.config/bupha/env` (0600); the runner gets the token through its own mechanism (`env://` for dagger) ## Extending | to add… | write… | core changes? | | ----------------------- | ----------------------------------- | ------------- | | forge (gitea/gitlab) | `Trigger` + `StatusSink` adapter crate | no | | pipeline backend | `Runner` adapter crate | no | | ingress (fly/tailscale) | `Tunnel` adapter crate | no | --- --- url: /bupha/explanation/release-model.md --- # The release model bupha releases itself — there is no hosted CI and no release tool in the loop. Two deliberate phases: ## Phase 1 — the explicit seed `bupha deploy release` runs once, by hand: it reads cargo metadata, topologically orders the workspace's path-dependency graph, `cargo publish`es every member, tags `{repo}-v{version}`, and pushes the tag. Why explicit? Publishing is the **only irreversible step** in the whole system. Queues, runs, and statuses are all idempotent and local; a crate version on crates.io is forever. Everything else in bupha fires on push — the seed doesn't. (`--dry-run` prints the order and tag without touching anything.) ## Phase 2 — push-driven forever after From then on a release is *just another bupha job*: a push to `main` hits the webhook, queues latest-wins, and runs `scripts/release.sh` in a detached worktree under the shell runner. The script: 1. diffs crate paths since the last `bupha-v*` tag 2. `cargo set-version --bump patch` on touched crates only (the cli republishes on any change — it's the install artifact) 3. `cargo publish`es them in dep order 4. tags `bupha-vX.Y.Z`, builds host-arch binaries with `cargo dist`, uploads to a GitHub Release via `gh` The 5-minute sleep at the top isn't a hack — it's the debounce window. Combined with `queue = "latest"` + `cancel_running`, rapid push bursts collapse to one release of the newest sha; killed runs get an honest "cancelled" status. ## Why this shape * **No release-plz / GHA**: the point of the project is that CI is a local loop — depending on hosted CI to ship it would be self-defeating. `release.sh` is deliberately boring bash so it can run as a plain `cmd` under `runner.kind = "shell"`. * **Latest-wins, not `all`**: a release that misses an intermediate push loses nothing — the tag lands on HEAD, which contains everything. `queue = "all"` is for pipelines where each sha's run is individually meaningful. * **Explicit over automatic**: a wrong tag or half-published workspace is far worse than a delayed release; the boundary sits exactly at irreversibility. Host prerequisites: `cargo-edit`, `cargo-dist`, `gh`, and `CARGO_REGISTRY_TOKEN` (env → keyring → env file). Multi-arch binaries arrive when the remote-runner pair lands — see TODO. --- --- url: /bupha/comparison.md --- # bupha vs hosted CI bupha runs push-triggered CI on self-controlled hardware — not a GHA replacement at fleet scale. ## vs GitHub Actions | | bupha | GitHub Actions | | ------------ | ----------------------------------------------- | --------------------------------- | | compute | your box | managed fleet | | queue | latest-wins + bounded `all`, no cold start | unlimited parallelism, queue wait | | secrets | never leave the machine | hosted on GitHub | | cache | your backend — local disk or your S3/R2/minio | hosted cache | | pipeline def | your runner — `ci.sh` or dagger module | YAML DSL + marketplace actions | | local == CI | `bupha run` is literally the same path | `act` approximates | | batching | supersede built-in, honest "cancelled" statuses | `concurrency` groups | | scale | one worker, serialized runs | matrix/fleet | | cost | your box only | minutes billing | **When GHA is better:** you need parallelism, matrices across OSes, hosted macOS/Windows builds, or marketplace actions. **When bupha fits:** instant feedback on self-owned hardware, secrets that never leave the machine, warm caches on self-owned storage, no YAML. ## vs others * **Jenkins/Buildkite** — bupha is a single binary with a TOML file, not a server product to operate. * **Woodpecker/Forgejo CI** — bupha is forge-webhook driven and much smaller; those are full pipeline platforms. * **self-hosted GHA runner** — bupha is lighter (no runner service, no org config) but has no matrix or marketplace. --- --- url: /bupha/getting-started.md --- # Getting started Pick by what you need to do. * **Tutorials** (learn): [first-run](tutorials/first-run.md) * **How-to** (tasks): [multi-repo](how-to/multi-repo.md) · [shell runner](how-to/shell-runner.md) · [sccache backend](how-to/sccache-backend.md) * **Reference** (lookup): [cli](reference/cli.md) · [config](reference/config.md) · [crate api](reference/crate-api.md) * **Explanation** (why): [architecture](explanation/architecture.md) · [release model](explanation/release-model.md) · [comparison](comparison.md) — vs GHA/hosted CI Project: [CONTRIBUTING](https://github.com/9aia/bupha/blob/main/CONTRIBUTING.md) · [SECURITY](https://github.com/9aia/bupha/blob/main/SECURITY.md) · [project](https://github.com/9aia/bupha/blob/main/project/index.md) — TODO, backlog, ADRs --- --- url: /bupha/public/api/static.files/SourceSerif4-LICENSE-a2cfd9d5.md --- Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name ‘Source’. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL *** ## SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1. Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2. Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5. The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.