# Builds the binaries people actually download. # # `nesdoctor` is handed to strangers and asked to be trusted, so the release # artefacts are built here and nowhere else: a binary someone produced on their # laptop and uploaded is not auditable, however honest they are. # # Tag `nesdoctor-v0.1.0` to cut a release, or run it by hand to check the # matrix still builds. name: release nesdoctor on: push: tags: ["nesdoctor-v*"] workflow_dispatch: permissions: contents: write jobs: build: name: ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: # One broken target must not suppress the others: a Windows failure # should still leave the Linux binary available to look at. fail-fast: false matrix: include: # musl and not glibc, so one Linux binary runs on every distro # regardless of its glibc version. Static linking is the whole reason # this target is here. - os: ubuntu-latest target: x86_64-unknown-linux-musl bin: nesdoctor smoke: true - os: windows-latest target: x86_64-pc-windows-msvc bin: nesdoctor.exe smoke: true - os: macos-latest target: aarch64-apple-darwin bin: nesdoctor smoke: true # Intel Macs are still a large installed base and they are clients, # which is a category we want answers from. Cross-compiled from the # arm64 runner rather than built on `macos-13`: that runner is being # retired and a dispatch on 2026-09-02 sat queued indefinitely # waiting for one, which is not a dependency a release should have. # # The cost is honest and stated: an x86_64 binary cannot be executed # on an arm64 runner without Rosetta, which these images do not # carry, so this is the one target whose smoke test cannot run. It # ships cross-compiled and unexercised, and the release notes say so. - os: macos-latest target: x86_64-apple-darwin bin: nesdoctor smoke: false steps: - uses: actions/checkout@v4 - name: Rust toolchain run: | rustup toolchain install stable --profile minimal --no-self-update rustup target add ${{ matrix.target }} # `ring`, under rustls, compiles C. On musl that needs the musl C # toolchain present or the build fails at link time with an error that # does not mention TLS at all. - name: musl toolchain if: matrix.target == 'x86_64-unknown-linux-musl' run: sudo apt-get update && sudo apt-get install -y musl-tools - name: Build run: cargo build --release -p nesdoctor --target ${{ matrix.target }} # The binary is run here on purpose, and this step is the reason this # workflow is worth having rather than a `cargo build` someone trusts. # # A static musl build resolves root certificates through the host trust # store, so TLS can compile perfectly and then fail on the machine it is # shipped to -- which would break the network test, the one feature # anybody runs this for, silently and only for other people. Running the # real thing here catches that class of failure before a tag exists. - name: Smoke test — the whole run, network included if: matrix.smoke shell: bash run: | set -euo pipefail BIN="target/${{ matrix.target }}/release/${{ matrix.bin }}" test -x "$BIN" # --no-steam because a runner has no Steam and the consent prompt # would block; stdin is closed so any prompt reads as a skip. OUT="$("$BIN" --quiet --no-steam --json "$RUNNER_TEMP/nd.json" < /dev/null)" echo "$OUT" # The summary line must exist and must not have fallen back to # "net=unmeasured", which is what a TLS or upload failure looks like. grep -q "nesdoctor " <<<"$OUT" if grep -q "net=unmeasured" <<<"$OUT"; then echo "::error::network test did not run in the built binary — \ TLS or the upload sink failed at runtime, which is exactly the \ failure this step exists to catch" exit 1 fi test -s "$RUNNER_TEMP/nd.json" # Eyes on the platforms the developer machine is not. # # Every Windows bug this tool has had was found by a person reading the # results channel: a virtual display adapter reported as the GPU, and a # URL truncated at its first `&`. Both were in code that only runs on # Windows, and the development machine is Linux -- so nobody had ever # seen what these probes return on the platform most of the audience # uses. # # CI already runs a real Windows machine and a real macOS one. Printing # the full report from each is nearly free and turns "we are working # blind" into "we are looking at it once per release". - name: Show what the probes actually return here if: matrix.smoke shell: bash run: | set -uo pipefail BIN="target/${{ matrix.target }}/release/${{ matrix.bin }}" echo "::group::${{ matrix.target }} — full report" # Not `set -e`: this step is for looking, and a probe that fails on a # runner must not fail the release. "$BIN" --no-net --no-steam --json "$RUNNER_TEMP/probe.json" < /dev/null || true echo "::endgroup::" echo "::group::${{ matrix.target }} — JSON" cat "$RUNNER_TEMP/probe.json" 2>/dev/null || echo "(no json written)" echo "::endgroup::" - name: Package shell: bash run: | set -euo pipefail mkdir -p dist NAME="nesdoctor-${{ matrix.target }}" cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "dist/$NAME${{ matrix.bin == 'nesdoctor.exe' && '.exe' || '' }}" cd dist # Checksums beside the binary, because "download this and run it" is # only a reasonable request if the file can be verified. if command -v sha256sum >/dev/null; then sha256sum * > "$NAME.sha256" else shasum -a 256 * > "$NAME.sha256" fi cat *.sha256 - uses: actions/upload-artifact@v4 with: name: nesdoctor-${{ matrix.target }} path: dist/* if-no-files-found: error release: # Only on a tag. A manual run builds and smoke-tests the matrix without # publishing anything, which is what you want while iterating. if: startsWith(github.ref, 'refs/tags/nesdoctor-v') needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Collect checksums run: | cd dist cat *.sha256 | sort -k2 > SHA256SUMS rm -f nesdoctor-*.sha256 ls -la cat SHA256SUMS - uses: softprops/action-gh-release@v2 with: # A draft, always. The binaries are attached and the notes are # written, and a person reads both and presses publish -- which is # the only step in this pipeline that is irreversible in public. draft: true files: dist/* generate_release_notes: true body: | **nesdoctor** — checks whether this machine can host a Nestri box, and measures what your connection actually does under load. Nothing is uploaded. There is no server to upload to: the network test talks to Cloudflare's public speed-test sink and to `1.1.1.1`, neither of which is ours. The output is a line on your terminal that you may choose to paste somewhere. Download the file for your platform, verify it against `SHA256SUMS`, and run it. On macOS and Linux you will need `chmod +x` first. Source is in `apps/nesdoctor`. The Intel macOS binary is cross-compiled and is the one target CI cannot execute to test. Every other binary here was run by the workflow that built it. The number worth running it for is **added latency under load**. Everybody knows their download speed; almost nobody has seen this one, and for anything interactive it is the figure that decides it.