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
72 changes: 50 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ jobs:
steps:
- uses: actions/checkout@v7

# The firmware pins its own toolchain in firmware/rust-toolchain.toml
# -- stable, plus the two bare-metal targets and the components these
# recipes invoke -- and rustup reads that from the directory it runs
# in. The repository root deliberately has no toolchain file.
- name: Install the firmware's pinned toolchain
working-directory: firmware
# Two packages pin a toolchain, each for its own reasons: the firmware
# needs both bare-metal targets and the llvm-tools behind `cargo
# objcopy`, and the OTA package needs one bare-metal target for the
# `no_std` half of `make clippy-ota`. rustup reads a
# `rust-toolchain.toml` from the directory a command runs in, so
# materialising them means running `rustup show` in each. The
# repository root deliberately has no toolchain file.
- name: Install the pinned toolchains
run: |
rustup show
(cd firmware && rustup show)
(cd ota && rustup show)
cargo --version

# Unlike rpi-hal, this repository's build does not stop at `cargo
Expand All @@ -48,27 +51,31 @@ jobs:
workspaces: |
firmware
cli
ota

- run: make fmt-check
- run: make clippy
- run: make clippy64
- run: make clippy-cli
- run: make clippy-ota
- run: make build-bcm2837
- run: make build64-bcm2837
- run: make build-bcm2711
- run: make build64-bcm2711
- run: make build-cli
- run: make test-cli
- run: make test-ota
- run: make doc

# Proof the release job's artifacts are actually produced, caught
# here rather than halfway through an irreversible publish.
- name: Both images exist
run: test -s firmware/target/kernel7.img && test -s firmware/target/kernel8.img

# Guards the `rust-version` claim in both manifests. Without this an MSRV
# Guards the `rust-version` claim in every manifest. Without this an MSRV
# is a comment that rots the first time a dependency or a `core` API
# moves. Both packages have one now that the firmware builds on stable.
# moves. All three packages have one now that the firmware builds on
# stable.
#
# Each package is checked against its own declared floor, and the
# `+toolchain` argument overrides firmware/rust-toolchain.toml, which is
Expand All @@ -83,11 +90,25 @@ jobs:
strategy:
fail-fast: false
matrix:
# `targets` is a plain space-separated list, read by both the
# install and the build step below. It used to carry the `--target`
# flags for the install while the build step named the same targets
# again in a hardcoded loop, which meant a package added here built
# for the firmware's targets whatever this column said.
#
# `features` exists because an MSRV claim covers every configuration
# a package offers, and the OTA package's floor comes from a feature
# that a default build does not enable.
include:
- package: cli
targets: ""
features: ""
- package: firmware
targets: "--target armv7a-none-eabi --target aarch64-unknown-none-softfloat"
targets: "armv7a-none-eabi aarch64-unknown-none-softfloat"
features: ""
- package: ota
targets: "armv7a-none-eabi"
features: "--all-features"
steps:
- uses: actions/checkout@v7

Expand All @@ -103,39 +124,46 @@ jobs:

- name: Install that toolchain, with any targets the package needs
run: |
flags=""
for target in ${{ matrix.targets }}; do
flags="$flags --target $target"
done
rustup toolchain install ${{ steps.msrv.outputs.version }} \
--profile minimal ${{ matrix.targets }}
--profile minimal $flags

# Build only, not test: the tests pull in dev-dependencies whose own
# floors are their business, and it is the shipped artifact this
# claim is about. The firmware builds for both bare-metal targets;
# the CLI builds for the host.
# claim is about. A package with no targets listed builds for the
# host; otherwise it builds for each of the ones it named.
- name: Build
working-directory: ${{ matrix.package }}
run: |
if [ -n "${{ matrix.targets }}" ]; then
for target in armv7a-none-eabi aarch64-unknown-none-softfloat; do
cargo +${{ steps.msrv.outputs.version }} build --release --target "$target"
done
if [ -z "${{ matrix.targets }}" ]; then
cargo +${{ steps.msrv.outputs.version }} build --release ${{ matrix.features }}
else
cargo +${{ steps.msrv.outputs.version }} build --release
for target in ${{ matrix.targets }}; do
cargo +${{ steps.msrv.outputs.version }} build --release \
${{ matrix.features }} --target "$target"
done
fi

# `cargo package` finishes by building the packaged tarball, which catches
# a whole class of "works here, broken on crates.io" problems: a file the
# manifest excludes but the build needs, or a path that only resolves in
# this working copy. Only the CLI is publishable -- the firmware sets
# `publish = false`.
# this working copy. Two of the three packages are publishable -- the
# firmware sets `publish = false` -- and each is verified separately,
# because they are released independently of one another.
#
# Deliberately uncached. `make package` sends the verification build to
# Deliberately uncached. Both recipes send their verification build to
# its own CARGO_TARGET_DIR, and there is nothing to gain by keeping a
# throwaway target directory for one small crate.
# throwaway target directory for two small crates.
package:
name: package verifies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: make package
- run: make package-ota

# Hardware-in-the-loop testing is deliberately absent: every check above
# runs against a fake device on a pty, which pins the wire format but
Expand Down
124 changes: 120 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ name: Release

# Publishing to crates.io is permanent -- a version can be yanked but never
# replaced or deleted -- so this never triggers on a branch push. Pushing a
# `v*` tag is the deliberate act that starts a release, and
# `workflow_dispatch` covers re-running it after a failure without moving
# the tag.
# tag is the deliberate act that starts a release, and `workflow_dispatch`
# covers re-running one after a failure without moving the tag.
#
# Two independent releases share this file, told apart by their tag:
#
# v<version> the CLI and the loader firmware, which ship together
# ota-v<version> the `rpi-loader-ota` library, which does not
#
# The prefixes do not overlap -- `refs/tags/ota-v0.1.0` does not start with
# `refs/tags/v` -- so each job below runs for exactly one of them. A
# dispatch has no tag to read, so it says which package it means.
on:
push:
tags: ["v*"]
tags: ["v*", "ota-v*"]
workflow_dispatch:
inputs:
package:
description: Which package to release
type: choice
options: [cli, ota]
required: true

env:
CARGO_TERM_COLOR: always
Expand All @@ -21,6 +35,7 @@ permissions:
jobs:
release:
name: publish the CLI and upload the images
if: startsWith(github.ref, 'refs/tags/v') || inputs.package == 'cli'
runs-on: ubuntu-latest
environment: crates-io
steps:
Expand Down Expand Up @@ -147,3 +162,104 @@ jobs:
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish

# The library. Same guards in the same order as the job above, and
# deliberately not factored together with it: the two release different
# things on different schedules, and a shared job with conditionals in it
# would be harder to read than the repetition, on the one workflow where
# being able to read what will happen matters most.
#
# No images and no firmware toolchain -- this package builds for the host
# like any other library, and `cargo package` is the only artifact.
ota:
name: publish rpi-loader-ota
if: startsWith(github.ref, 'refs/tags/ota-v') || inputs.package == 'ota'
runs-on: ubuntu-latest
environment: crates-io
steps:
- uses: actions/checkout@v7

- name: The tag matches the manifest
id: version
run: |
version="$(sed -n 's/^version = "\(.*\)"/\1/p' ota/Cargo.toml | head -1)"
test -n "$version" || {
echo "ota/Cargo.toml has no version" >&2
exit 1
}
# A dispatch run has no tag in its context, so the manifest is
# the authority there and there is nothing to cross-check.
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "ota-v$version" != "$GITHUB_REF_NAME" ]; then
echo "tag $GITHUB_REF_NAME does not match the manifest version $version" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=ota-v$version" >> "$GITHUB_OUTPUT"

# Its own changelog, because its versions are its own. Dated, for the
# same reason as the CLI's: an undated entry claims the version was
# never released.
- name: The changelog has a dated section for this version
run: |
version="${{ steps.version.outputs.version }}"
if grep -q ReleaseDate ota/CHANGELOG.md; then
echo "ota/CHANGELOG.md still has the ReleaseDate placeholder" >&2
exit 1
fi
if ! grep -qE "^## \[$version\] - [0-9]{4}-[0-9]{2}-[0-9]{2}" ota/CHANGELOG.md; then
echo "ota/CHANGELOG.md has no dated '## [$version] - YYYY-MM-DD' section" >&2
exit 1
fi

- name: The packaged tarball builds
run: make package-ota

- name: Extract this version's changelog section
run: |
version="${{ steps.version.outputs.version }}"
awk -v v="## [$version]" '
index($0, v) == 1 { inside = 1; next }
inside && /^## \[/ { exit }
inside { print }
' ota/CHANGELOG.md > release-notes.md
cat >> release-notes.md <<'NOTE'

Add it with `cargo add rpi-loader-ota`. The `rpi-loader` CLI
builds bundles in this format; a device installs one.
NOTE

# Before the irreversible step, and idempotent, so a re-run after a
# failed publish does not stall here.
- name: Create or update the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${{ steps.version.outputs.tag }}"
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" --notes-file release-notes.md
else
gh release create "$tag" \
--title "rpi-loader-ota ${{ steps.version.outputs.version }}" \
--notes-file release-notes.md
fi

# As above: crates.io refuses a version that exists, and that refusal
# would mask whatever a re-run was actually for.
- name: Is this version already on crates.io?
id: published
run: |
version="${{ steps.version.outputs.version }}"
index="$(curl -sS https://index.crates.io/rp/i-/rpi-loader-ota || true)"
if printf '%s' "$index" | grep -q "\"vers\":\"$version\""; then
echo "already=true" >> "$GITHUB_OUTPUT"
echo "rpi-loader-ota $version is already published; skipping"
else
echo "already=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish to crates.io
if: steps.published.outputs.already == 'false'
working-directory: ota
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ The firmware and the host CLI share one version and ship as one release:
they are two halves of a wire protocol, and a version that identifies
only one of them says nothing useful about compatibility.

The `rpi-loader-ota` library in `ota/` is not part of that pair and keeps
its own history in [`ota/CHANGELOG.md`](ota/CHANGELOG.md). Its consumers
are firmware projects in other repositories, and a renamed command-line
flag here is no reason to bump their dependency.

## [0.2.0] - 2026-08-30

### Added
Expand Down
Loading