Skip to content
Merged
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
146 changes: 74 additions & 72 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
name: Release

# Tag vX.Y.Z (matching the workspace version) → build the frontend, cross-build
# the device binary inside the rk3588-buildkit image, and publish the assets
# that app/src/update.rs installs over the air.
# Builds the application package in the published devkit image (the same
# environment as `scripts/devkit.sh run scripts/build-app.sh`), signs the
# checksums, and creates or updates a DRAFT GitHub release. Nothing is
# published: a maintainer reviews the draft, attaches the firmware images
# (scripts/release-firmware.sh) and clicks Publish, which creates the tag.
#
# Trigger: Actions → Release → Run workflow with the version (no tag needed),
# or push a vX.Y.Z tag. Either way the version must equal Cargo.toml's.
#
# Signing: the RELEASE_SIGNING_KEY secret holds an ASCII-armored private key
# without a passphrase (see docs/firmware-release.md). Without it the draft is
# created unsigned and the run prints a warning.

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: Version without the leading v, e.g. 0.1.0
required: true

permissions:
contents: read
Expand All @@ -16,93 +30,81 @@ env:
ASSET: rustkvm_app-aarch64-unknown-linux-gnu

jobs:
web:
name: trunk build
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: crates/rkvm-web
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: jetli/trunk-action@v0.5.1
with:
version: "v0.21.14"
- run: trunk build --release --locked
- uses: actions/upload-artifact@v4
with:
name: web-dist
path: crates/rkvm-web/dist/
if-no-files-found: error

app:
name: device binary
needs: web
build:
name: application package
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: read
packages: read
container:
image: ghcr.io/${{ github.repository_owner }}/rk3588-buildkit:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
env:
# Same pin as ci/devkit/Dockerfile.
RUSTUP_TOOLCHAIN: nightly-2026-06-14
CC_aarch64_unknown_linux_gnu: /opt/rk3588-buildkit/bin/aarch64-buildroot-linux-gnu-gcc
image: ghcr.io/rust-kvm/rustkvm-devkit:nightly-2026-06-14
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
# build.rs reads git metadata; the container user differs from the checkout owner.
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Check tag matches workspace version
- name: Check version
id: version
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' Cargo.toml | head -n1)
test "${GITHUB_REF_NAME}" = "v${version}" || {
echo "::error::tag ${GITHUB_REF_NAME} does not match workspace version ${version}"
if [ "$GITHUB_REF_TYPE" = tag ]; then want=${GITHUB_REF_NAME#v}; else want=$INPUT_VERSION; fi
have=$(sed -n 's/^version *= *"\(.*\)"/\1/p' Cargo.toml | head -n1)
test "$want" = "$have" || {
echo "::error::requested version $want does not match workspace version $have"
exit 1
}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-06-14
components: rust-src
targets: aarch64-unknown-linux-gnu
- uses: actions/download-artifact@v8
with:
name: web-dist
path: crates/rkvm-web/dist
echo "version=$have" >> "$GITHUB_OUTPUT"
- name: Build
run: |
cargo build -Z build-std=std,panic_abort --locked --release \
--target aarch64-unknown-linux-gnu -p rustkvm --bin rustkvm_app
cp target/aarch64-unknown-linux-gnu/release/rustkvm_app "$ASSET"
sha256sum "$ASSET" > "$ASSET.sha256"
run: scripts/build-app.sh
- name: GPL source bundle
run: OUT=out/app scripts/gpl-source-bundle.sh
- uses: actions/upload-artifact@v4
with:
name: device-binary
path: |
${{ env.ASSET }}
${{ env.ASSET }}.sha256
name: release-assets
path: out/app/
if-no-files-found: error

publish:
name: GitHub release
needs: app
draft:
name: draft release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{ needs.build.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: device-binary
name: release-assets
path: assets
- name: Checksums and signature
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
run: |
cd assets
sha256sum -- * > SHA256SUMS
if [ -n "$RELEASE_SIGNING_KEY" ]; then
export GNUPGHOME="$RUNNER_TEMP/gnupg"
mkdir -m 700 "$GNUPGHOME"
printf '%s\n' "$RELEASE_SIGNING_KEY" | gpg --batch --import
gpg --batch --armor --detach-sign --output SHA256SUMS.asc SHA256SUMS
gpg --batch --verify SHA256SUMS.asc SHA256SUMS
rm -rf "$GNUPGHOME"
else
echo "::warning::RELEASE_SIGNING_KEY is not set; the draft is unsigned"
fi
ls -l
- name: Release notes
id: notes
run: |
f="docs/release-notes/v$VERSION.md"
if [ -f "$f" ]; then echo "path=$f" >> "$GITHUB_OUTPUT"; fi
- uses: softprops/action-gh-release@v2
with:
files: |
${{ env.ASSET }}
${{ env.ASSET }}.sha256
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
draft: true
tag_name: v${{ env.VERSION }}
target_commitish: ${{ github.sha }}
name: RustKVM v${{ env.VERSION }}
body_path: ${{ steps.notes.outputs.path }}
generate_release_notes: ${{ steps.notes.outputs.path == '' }}
prerelease: ${{ contains(env.VERSION, '-') }}
files: assets/*
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ trunk serve # local dev server on :9000 (Trunk.toml)

- **`.github/workflows/ci.yml`** — workspace `rustfmt` (nightly) + `cargo deny` + host `clippy --workspace --release --all-targets -D warnings` + aarch64 `clippy --target aarch64-unknown-linux-gnu` (Ubuntu cross gcc, no link) + `cargo test --workspace` (GStreamer dev libs, stub `rkvm-web/dist`) + `actionlint` over every workflow, gated by a `CI Success` summary check.
- **`.github/workflows/buildkit.yml`** — manual: downloads the Buildroot SDK tar (Google Drive file ID), runs `./build.sh rk3588.mk`, publishes `/opt/rk3588-buildkit` as `ghcr.io/<owner>/rk3588-buildkit:{sdk_tag,latest}` (via the `rk3588-buildkit` named build context of `ci/buildkit/Dockerfile`) and uploads the system images as an artifact.
- **`.github/workflows/release.yml`** — tag `vX.Y.Z` (must equal the workspace version): Trunk frontend → `cargo +nightly-2026-06-14 build -Z build-std` in the buildkit image → GitHub Release with `rustkvm_app-aarch64-unknown-linux-gnu` + `.sha256` (the assets OTA installs).
- **`.github/workflows/release.yml`** — manual (*Run workflow* with the version) or tag `vX.Y.Z`; the version must equal the workspace version. Runs `scripts/build-app.sh` in the published devkit image, adds the GPL source tarball, writes `SHA256SUMS` signed with the `RELEASE_SIGNING_KEY` secret (unsigned with a warning if absent) and creates or updates a **draft** release; publishing (which creates the tag) is manual. Release notes come from `docs/release-notes/vX.Y.Z.md`.
- **`.github/workflows/device-health.yml`** — every 6h and on demand: runs `app/tests/device_live.rs` against a live device (`RKVM_DEVICE_URL` variable, `RKVM_DEVICE_TOKEN` secret; optional `RKVM_PROBE_RUNNER` self-hosted runner label); a no-op when no device is configured. The same test runs locally with those env vars and is skipped when `RKVM_DEVICE_URL` is unset.
- **`.github/workflows/web.yml`** — `rkvm-web` only (triggers on `crates/rkvm-web/**`, `crates/rkvm-proto/**`): `rustfmt` (nightly) → `clippy` (stable, `--target wasm32-unknown-unknown -D warnings`) → `trunk build --locked` → `cargo deny`, gated by a `Web CI Success` summary check.

Expand All @@ -242,6 +242,8 @@ ci/firmware/Dockerfile # Ubuntu 22.04 SDK host + repo; runs --privileged (
scripts/devkit.sh # buildkit | image | shell | run CMD | firmware-image | firmware STEPS
scripts/build-app.sh # trunk + device binary → out/app/ (binary, .sha256, BUILD_INFO)
scripts/build-firmware.sh # sync | patch | build | collect → out/firmware/ (sd + eflasher images)
scripts/gpl-source-bundle.sh # release source tarball (+ Buildroot legal-info with LEGAL_INFO=1)
scripts/release-firmware.sh # sign out/firmware checksums and attach them to the draft release
os/rk3588/manifest.xml # FriendlyElec SDK projects pinned to exact commits
os/rk3588/patches/<project>/ # every local SDK change, applied with git apply after a reset
os/rk3588/rootfs-overlay/ # S99rustkvm, dhcpcd/network config, dropbear devmode wrapper
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

| Guide | For |
| --- | --- |
| [Quick start: flash and first boot](quickstart.md) | Installing a release image and first login |
| [Getting started on RK3588](getting-started.md) | Flashing, deploying, wiring and first login |
| [API reference](api.md) | HTTP routes, JSON-RPC, MCP tools, HID-RPC, MQTT, Prometheus metrics |
| [Building](building.md) | Docker devkit, application package and firmware image, pinned versions |
Expand Down
56 changes: 45 additions & 11 deletions docs/firmware-release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Releasing and shipping firmware

How a RustKVM version goes from a tag to a flashed unit in a box. Building itself is
How a RustKVM version goes from a draft release to a flashed unit in a box. Building itself is
in [building.md](building.md).

## Versions
Expand All @@ -11,24 +11,58 @@ in [building.md](building.md).
| Firmware image | `X.Y.Z-YYYYMMDD-<rev>` | `FW_VERSION` in `scripts/build-firmware.sh`, written to `/etc/rustkvm-release` |
| SDK | commit per project | `os/rk3588/manifest.xml` |

The application updates itself over the air from GitHub Releases (`app/src/update.rs`);
the firmware image is what a unit leaves the factory with. A firmware release therefore
always embeds a released application version.
The firmware image is what a unit leaves the factory with, and it always embeds a
released application version. Over-the-air application updates from GitHub Releases
are pending (PR #29); until they land, updates mean reflashing or `dev_deploy.sh`.

## Release assets

| Asset | Built by |
| --- | --- |
| `rustkvm_app-aarch64-unknown-linux-gnu` + `.sha256` | `release.yml` (the pair OTA will install) |
| `BUILD_INFO` | `release.yml` |
| `rustkvm-X.Y.Z-source.tar.gz` | `release.yml` via `scripts/gpl-source-bundle.sh` |
| `SHA256SUMS` + `SHA256SUMS.asc` | `release.yml`, signed with the release key |
| `rustkvm-rk3588-<fw>-sd.img.gz`, `-eflasher.img.gz`, `.release` | `scripts/release-firmware.sh` from a local firmware build |
| `rustkvm-rk3588-<fw>.SHA256SUMS` + `.asc` | `scripts/release-firmware.sh` |
| `rustkvm-X.Y.Z-legal-info.tar.gz` | `LEGAL_INFO=1 scripts/release-firmware.sh` (Buildroot license texts and package sources) |

## Release checklist

1. Bump `version` in `Cargo.toml`, update `CHANGELOG.md`, merge to `main`.
2. Tag `vX.Y.Z`. `release.yml` builds the application and publishes the OTA assets.
3. Build the firmware from that tag with the released binary:
Nothing is public until step 6; every earlier step only touches a draft.

1. Bump `version` in `Cargo.toml`, update `CHANGELOG.md`, write
`docs/release-notes/vX.Y.Z.md`, merge to `dev`.
2. Actions → **Release** → *Run workflow* with `X.Y.Z`. It builds the application in the
published devkit image and creates the draft release `vX.Y.Z` (the tag does not exist
yet).
3. Build the firmware from the same commit and attach it to the draft:
```bash
git checkout vX.Y.Z
scripts/devkit.sh run scripts/build-app.sh
FW_VERSION=X.Y.Z-$(date -u +%Y%m%d) scripts/devkit.sh firmware
LEGAL_INFO=1 scripts/release-firmware.sh X.Y.Z
```
Check that `out/app/*.sha256` matches the one on the GitHub release.
4. Flash a unit with the eflasher image and run the acceptance test below.
5. Archive `out/firmware/` (images, `.release`, `SHA256SUMS`) with the release; it is
what the production line flashes.
5. Review the draft: assets, notes, `gpg --verify SHA256SUMS.asc SHA256SUMS`.
6. **Publish** the draft on GitHub. That creates the tag `vX.Y.Z` on the drafted commit.

### Release signing key (one-time)

Generate a dedicated signing key on a trusted machine, store the private half only as
the `RELEASE_SIGNING_KEY` Actions secret (and an offline backup), and commit the public
half so users can verify downloads:

```bash
gpg --quick-gen-key "RustKVM Release Signing" ed25519 sign 3y
fpr=$(gpg --list-keys --with-colons "RustKVM Release Signing" | awk -F: '/^fpr/{print $10; exit}')
gpg --armor --export-secret-keys "$fpr" | gh secret set RELEASE_SIGNING_KEY
gpg --armor --export "$fpr" > docs/release-signing-key.asc
```

The workflow needs the key without a passphrase; leave it empty when gpg asks, or
strip it afterwards with `gpg --passwd "$fpr"`. Set `RELEASE_GPG_KEY=$fpr` for
`scripts/release-firmware.sh` so the firmware checksums carry the same signature.
Without the secret the workflow still creates the draft, unsigned, with a warning.

## Flashing

Expand Down
60 changes: 60 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Quick start: flash and first boot

From a release download to controlling a computer in your browser. Developers building
from source want [getting-started.md](getting-started.md) instead.

## You need

- The RustKVM RK3588 board, its power supply, an HDMI cable and a USB data cable
- A microSD card (8 GB or larger) and a computer to write it
- Ethernet on the same network as the browser you will use

## 1. Write the installer card

Download `rustkvm-rk3588-<version>-eflasher.img.gz` and `SHA256SUMS` from the release.
Write the image to the microSD card with [balenaEtcher](https://etcher.balena.io/)
(it reads `.gz` directly), or on Linux:

```bash
sha256sum -c SHA256SUMS --ignore-missing
gzip -dc rustkvm-rk3588-<version>-eflasher.img.gz | sudo dd of=/dev/sdX bs=4M conv=fsync
```

## 2. Install to the board

1. Power off the board, insert the card, power on.
2. The card installs RustKVM to the internal eMMC. This takes a few minutes; with a
monitor on the board's HDMI output you can watch the progress.
3. Power off, **remove the card**, power on. The board now boots RustKVM from eMMC.

To run from the card without touching the eMMC, write the `-sd.img.gz` image instead
and leave the card in.

## 3. Connect the target computer

| Cable | From | To |
| --- | --- | --- |
| HDMI | Target's video output | Board's **HDMI IN** |
| USB | Board's **OTG** port | A USB port on the target (keyboard, mouse, virtual media) |
| Ethernet | Board | Your network |
| ATX extension (optional) | Board | Target's front-panel power / reset header |

## 4. First login

1. Open `https://rustkvm.local/`, or the address your router shows for the board.
2. Accept the browser's certificate warning (the certificate is self-signed).
3. The setup wizard asks you to set a device password. Choose no-password mode only
on a network you fully trust.
4. Click the video to capture keyboard and mouse; the toolbar has Ctrl+Alt+Del,
fullscreen, quality and audio.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| `rustkvm.local` does not resolve | Use the IP from your router's DHCP list |
| Black video | Target is on and outputs HDMI; try another resolution in the target's OS |
| No keyboard / mouse | The USB cable goes to the board's OTG port and carries data |
| Forgot the password | Reflash, or remove `/userdata/rustkvm/config.toml` over SSH in developer mode |

Firmware version: Settings → About, or `/etc/rustkvm-release` on the board.
44 changes: 44 additions & 0 deletions docs/release-notes/v0.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
First release of RustKVM: a browser-based KVM-over-IP for RK3588 boards, written in Rust,
with hardware H.264/H.265 encoding and a JSON-RPC / MCP API for automation and AI agents.

## Highlights

- **Remote console** in the browser over WebRTC: hardware-encoded video via Rockchip MPP,
Opus audio, keyboard and five-button mouse, EDID control, screenshots.
- **Virtual media**: mount an ISO by URL or from device storage, bundled netboot.xyz.
- **Power control**: ATX power / reset and DC power, Wake-on-LAN.
- **Automation**: 130+ JSON-RPC methods over WebRTC or `POST /device/rpc`, an MCP server at
`POST /mcp`, API tokens, MQTT with Home Assistant discovery, Prometheus `/metrics`,
`/device/health`.
- **Reliability**: hardware watchdog, self-healing video pipeline.
- **Reproducible builds**: Docker devkit and a pinned firmware build; this release's
application and firmware were built with them.

## Downloads

| File | What it is |
| --- | --- |
| `rustkvm-rk3588-*-eflasher.img.gz` | Writes RustKVM to the board's eMMC (recommended) |
| `rustkvm-rk3588-*-sd.img.gz` | Runs RustKVM from a microSD card |
| `rustkvm_app-aarch64-unknown-linux-gnu` | The application binary alone, for existing installs |
| `rustkvm-*-source.tar.gz`, `*-legal-info.tar.gz` | Corresponding source and licenses |
| `SHA256SUMS`, `*.SHA256SUMS` and `.asc` | Checksums and their GPG signatures |

Flashing and first boot: [docs/quickstart.md](https://github.com/Rust-KVM/rustkvm/blob/dev/docs/quickstart.md).

Verify a download:

```bash
gpg --import release-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS && sha256sum -c SHA256SUMS --ignore-missing
```

## Known limitations

- Over-the-air updates are not in this release; update by reflashing.
- No factory reset button yet; delete `/userdata/rustkvm/config.toml` to return to the
setup wizard.
- The TLS certificate is self-signed and regenerated at every start until you install
your own under Settings → TLS.

Full list of changes: [CHANGELOG.md](https://github.com/Rust-KVM/rustkvm/blob/dev/CHANGELOG.md).
Loading
Loading