Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 252 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

176 changes: 176 additions & 0 deletions docs/guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,179 @@ enables TRACE (e.g. `RUST_LOG=trace`).
| `--no-fail-fast` | | Run all tests even if some fail |
| `--recreate` | | Stop and recreate the VM |
| `-- <args>` | | Extra args passed to cargo |

## Running in CI

If you run a `patchbay-serve` instance (see [patchbay-serve](#patchbay-serve)
below), you can push test results from GitHub Actions and get a link
posted as a PR comment.

Set two repository secrets: `PATCHBAY_URL` (e.g. `https://patchbay.example.com`)
and `PATCHBAY_API_KEY`.

Add this to your workflow **after** the test step:

```yaml
- name: Push patchbay results
if: always()
env:
PATCHBAY_URL: ${{ secrets.PATCHBAY_URL }}
PATCHBAY_API_KEY: ${{ secrets.PATCHBAY_API_KEY }}
run: |
set -euo pipefail

PROJECT="${{ github.event.repository.name }}"
TESTDIR="$(cargo metadata --format-version=1 --no-deps | jq -r .target_directory)/testdir-current"

if [ ! -d "$TESTDIR" ]; then
echo "No testdir output found, skipping push"
exit 0
fi

# Create run.json manifest
cat > "$TESTDIR/run.json" <<MANIFEST
{
"project": "$PROJECT",
"branch": "${{ github.head_ref || github.ref_name }}",
"commit": "${{ github.sha }}",
"pr": ${{ github.event.pull_request.number || 'null' }},
"pr_url": "${{ github.event.pull_request.html_url || '' }}",
"title": "${{ github.event.pull_request.title || github.event.head_commit.message || '' }}",
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
MANIFEST

# Upload as tar.gz
RESPONSE=$(tar -czf - -C "$TESTDIR" . | \
curl -s -w "\n%{http_code}" \
-X POST \
-H "Authorization: Bearer $PATCHBAY_API_KEY" \
-H "Content-Type: application/gzip" \
--data-binary @- \
"$PATCHBAY_URL/api/push/$PROJECT")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)

if [ "$HTTP_CODE" != "200" ]; then
echo "Push failed ($HTTP_CODE): $BODY"
exit 1
fi

INVOCATION=$(echo "$BODY" | jq -r .invocation)
VIEW_URL="$PATCHBAY_URL/#/inv/$INVOCATION"
echo "PATCHBAY_VIEW_URL=$VIEW_URL" >> "$GITHUB_ENV"
echo "Results uploaded: $VIEW_URL"

- name: Comment on PR
if: always() && github.event.pull_request && env.PATCHBAY_VIEW_URL
uses: actions/github-script@v7
with:
script: |
const marker = '<!-- patchbay-results -->';
const body = `${marker}\n**patchbay results:** ${process.env.PATCHBAY_VIEW_URL}`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
```

The PR comment is auto-updated on each push, so you always see the latest run.

## patchbay-serve

`patchbay-serve` is a standalone server for hosting run results. CI
pipelines push test output to it; the devtools UI lets you browse them.

### Install

```bash
cargo install --git https://github.com/n0-computer/patchbay patchbay-server --bin patchbay-serve
```

### Quick start

```bash
patchbay-serve \
--accept-push \
--api-key "$(openssl rand -hex 32)" \
--http-bind 0.0.0.0:8080 \
--retention 10GB
```

With automatic TLS:

```bash
patchbay-serve \
--accept-push \
--api-key "$(openssl rand -hex 32)" \
--acme-domain patchbay.example.com \
--acme-email you@example.com \
--retention 10GB
```

This will:
- Serve the runs index at `/runs`
- Accept pushed runs at `POST /api/push/{project}`
- Auto-provision TLS via Let's Encrypt (when `--acme-domain` is set)
- Store data in `~/.local/share/patchbay-serve/` (runs + ACME certs)
- Delete oldest runs when total size exceeds the retention limit

### Flags

| Flag | Description |
|------|-------------|
| `--run-dir <path>` | Override run storage location |
| `--data-dir <path>` | Override data directory (default: `~/.local/share/patchbay-serve`) |
| `--accept-push` | Enable the push API |
| `--api-key <key>` | Required with `--accept-push`; also reads `PATCHBAY_API_KEY` env |
| `--acme-domain <d>` | Enable automatic TLS for domain |
| `--acme-email <e>` | Contact email for Let's Encrypt (required with `--acme-domain`) |
| `--retention <size>` | Max total run storage (e.g. `500MB`, `10GB`) |
| `--http-bind <addr>` | HTTP listen address (default: `0.0.0.0:8080`; redirect when ACME is active) |
| `--https-bind <addr>` | HTTPS listen address (default: `0.0.0.0:4443`; only with `--acme-domain`) |

### systemd

A unit file is included at `patchbay-server/patchbay-serve.service`.
To install:

```bash
# Create service user and data directory
sudo useradd -r -s /usr/sbin/nologin patchbay
sudo mkdir -p /var/lib/patchbay-serve
sudo chown patchbay:patchbay /var/lib/patchbay-serve

# Install the binary
cargo install --git https://github.com/n0-computer/patchbay patchbay-server --bin patchbay-serve
sudo cp ~/.cargo/bin/patchbay-serve /usr/local/bin/

# Install and configure the unit file
sudo cp patchbay-server/patchbay-serve.service /etc/systemd/system/
sudo systemctl edit patchbay-serve # set PATCHBAY_API_KEY, --acme-domain, --acme-email
sudo systemctl enable --now patchbay-serve
```

Check status:

```bash
sudo systemctl status patchbay-serve
journalctl -u patchbay-serve -f
```
16 changes: 15 additions & 1 deletion patchbay-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ authors.workspace = true
repository.workspace = true
build = "build.rs"

[[bin]]
name = "patchbay-serve"
path = "src/main.rs"

[dependencies]
axum = { version = "0.8", features = ["tokio"] }
tokio = { version = "1", features = ["rt", "macros", "sync", "time", "fs", "io-util"] }
tokio = { version = "1", features = ["rt-multi-thread", "rt", "macros", "sync", "time", "fs", "io-util", "signal"] }
tokio-stream = { version = "0.1", features = ["sync"] }
tokio-rustls-acme = { version = "0.7", features = ["axum"] }
anyhow = "1"
async-stream = "0.3"
clap = { version = "4", features = ["derive", "env"] }
dirs = "6"
flate2 = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tar = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["v4"] }
chrono = "0.4"
axum-server = "0.7"
rustls = "0.23"
Loading