diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed77f4e..41dbfc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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/* diff --git a/AGENTS.md b/AGENTS.md index 8620e3f..d828426 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//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. @@ -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// # every local SDK change, applied with git apply after a reset os/rk3588/rootfs-overlay/ # S99rustkvm, dhcpcd/network config, dropbear devmode wrapper diff --git a/docs/README.md b/docs/README.md index bfdb8ac..f5649d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/docs/firmware-release.md b/docs/firmware-release.md index 4575dc6..5ac166a 100644 --- a/docs/firmware-release.md +++ b/docs/firmware-release.md @@ -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 @@ -11,24 +11,58 @@ in [building.md](building.md). | Firmware image | `X.Y.Z-YYYYMMDD-` | `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--sd.img.gz`, `-eflasher.img.gz`, `.release` | `scripts/release-firmware.sh` from a local firmware build | +| `rustkvm-rk3588-.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 diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..1e6d7d5 --- /dev/null +++ b/docs/quickstart.md @@ -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--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--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. diff --git a/docs/release-notes/v0.1.0.md b/docs/release-notes/v0.1.0.md new file mode 100644 index 0000000..4a10748 --- /dev/null +++ b/docs/release-notes/v0.1.0.md @@ -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). diff --git a/scripts/gpl-source-bundle.sh b/scripts/gpl-source-bundle.sh new file mode 100755 index 0000000..a35c51f --- /dev/null +++ b/scripts/gpl-source-bundle.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Package the corresponding source for a RustKVM release (GPL-2.0 compliance). +# +# scripts/gpl-source-bundle.sh → out/release/rustkvm--source.tar.gz +# OUT=dir scripts/gpl-source-bundle.sh +# LEGAL_INFO=1 scripts/gpl-source-bundle.sh also run Buildroot `make legal-info` in the +# firmware SDK and ship it as a second archive +# +# The first archive holds the RustKVM tree at this commit (application + firmware +# layer: pinned SDK manifest, patches, rootfs overlay) and SOURCES.md, which lists +# every upstream SDK project at its exact commit. The legal-info archive adds the +# license texts and source tarballs of every package in the root filesystem. +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT=${OUT:-$ROOT/out/release} +SDK=${RUSTKVM_SDK_DIR:-$HOME/rustkvm-sdk/rk3588} +version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' "$ROOT/Cargo.toml" | head -n1) +rev=$(git -C "$ROOT" rev-parse HEAD) +name=rustkvm-$version-source +mkdir -p "$OUT" + +stage=$(mktemp -d) +trap 'rm -rf "$stage"' EXIT + +git -C "$ROOT" archive --format=tar --prefix="$name/" HEAD | tar -x -C "$stage" + +{ + echo "# RustKVM $version: corresponding source" + echo + echo "RustKVM tree: https://github.com/Rust-KVM/rustkvm/tree/$rev" + echo + echo "The firmware is the FriendlyElec RK3588 Buildroot SDK at the commits below," + echo "with os/rk3588/patches applied and os/rk3588/rootfs-overlay installed." + echo "scripts/build-firmware.sh reproduces it." + echo + echo "| Path | Repository | Commit |" + echo "| --- | --- | --- |" + sed -n 's|.*path="\([^"]*\)" name="\([^"]*\)" remote="friendlyarm" revision="\([0-9a-f]*\)".*|\| \1 \| https://github.com/friendlyarm/\2 \| \3 \||p' \ + "$ROOT/os/rk3588/manifest.xml" + echo + echo "Source for any GPL component is also available on request for three years" + echo "from the date of the release, via https://github.com/Rust-KVM/rustkvm/issues." +} >"$stage/$name/SOURCES.md" + +tar -C "$stage" -czf "$OUT/$name.tar.gz" "$name" +echo "==> $OUT/$name.tar.gz" + +if [ "${LEGAL_INFO:-0}" = 1 ]; then + out_dir=$SDK/buildroot/output/rockchip_rk3588 + [ -f "$out_dir/.config" ] || { echo "error: no built Buildroot tree in $out_dir" >&2; exit 1; } + make -C "$SDK/buildroot" O="$out_dir" legal-info + tar -C "$out_dir" -czf "$OUT/rustkvm-$version-legal-info.tar.gz" legal-info + echo "==> $OUT/rustkvm-$version-legal-info.tar.gz" +fi diff --git a/scripts/release-firmware.sh b/scripts/release-firmware.sh new file mode 100755 index 0000000..566f84d --- /dev/null +++ b/scripts/release-firmware.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Attach a locally built firmware to the DRAFT GitHub release of the same version. +# +# scripts/release-firmware.sh [VERSION] default: workspace version +# +# Expects out/firmware/ from `scripts/devkit.sh firmware` (FW_VERSION=-...). +# Signs the firmware checksum file with GPG (RELEASE_GPG_KEY selects the key, +# default gpg's default key), optionally builds the Buildroot legal-info bundle +# (LEGAL_INFO=1), and uploads everything with gh. It refuses to touch a release +# that is already published. +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +version=${1:-$(sed -n 's/^version *= *"\(.*\)"/\1/p' "$ROOT/Cargo.toml" | head -n1)} +tag=v$version +FW=$ROOT/out/firmware + +draft=$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null) || { + echo "error: no release $tag; run the Release workflow first" >&2 + exit 1 +} +[ "$draft" = true ] || { echo "error: $tag is already published" >&2; exit 1; } + +sums=$(ls -t "$FW"/rustkvm-rk3588-"$version"-*.SHA256SUMS 2>/dev/null | head -n1) || true +[ -n "$sums" ] || { echo "error: no firmware for $version in $FW" >&2; exit 1; } +base=${sums%.SHA256SUMS} + +key=() +[ -n "${RELEASE_GPG_KEY:-}" ] && key=(--local-user "$RELEASE_GPG_KEY") +gpg --batch --yes "${key[@]}" --armor --detach-sign --output "$sums.asc" "$sums" +gpg --batch --verify "$sums.asc" "$sums" + +files=("$base"-*.img.gz "$base.release" "$sums" "$sums.asc") +if [ "${LEGAL_INFO:-0}" = 1 ]; then + OUT=$FW LEGAL_INFO=1 "$ROOT/scripts/gpl-source-bundle.sh" + files+=("$FW/rustkvm-$version-legal-info.tar.gz") +fi + +ls -lh "${files[@]}" +gh release upload "$tag" --clobber "${files[@]}" +echo "==> attached to draft $tag; review it on GitHub before publishing"