From ebbc241154b5c8e89d7cc7a0eec8571469184a30 Mon Sep 17 00:00:00 2001 From: Ryan Kuester Date: Wed, 11 Mar 2026 10:07:46 -0500 Subject: [PATCH 1/4] build: ignore .cache directory Separate from the CI commit so reverting CI doesn't accidentally unignore .cache/, which may also be used by other tooling. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 80faede..3e07533 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Cargo.lock /target/ +/.cache/ From f602de77545839d389e6a06ccc1985c982b6d2df Mon Sep 17 00:00:00 2001 From: Ryan Kuester Date: Wed, 11 Mar 2026 10:06:42 -0500 Subject: [PATCH 2/4] build: add build toolchain container image Add a Podman-based build toolchain image (build.Containerfile) with pinned Rust compiler, rustfmt, clippy, and just. The base image is pinned by digest for reproducibility. The justfile gains two new recipes: - build-image: builds the toolchain image, skipping if unchanged - in-container: runs any just recipe inside the toolchain image The image is tagged with a content hash of the Containerfile so build-image can detect staleness without rebuilding. This is necessary because podman save/load (used for CI caching) doesn't preserve layer cache metadata, so podman build would rebuild from scratch even with a loaded image. The content-hash tag lets `podman image exists` skip the build entirely. Downloaded crates are cached in .cache/ via bind mounts so they persist between container runs and are accessible to CI caching. --- Cargo.toml | 1 + build.Containerfile | 21 +++++++++++++++++++++ justfile | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 build.Containerfile diff --git a/Cargo.toml b/Cargo.toml index 813481a..6985a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = ["mujina-miner", "tools/mujina-dissect"] resolver = "2" [workspace.package] +# When bumping the compiler version, also update build.Containerfile. edition = "2024" license = "GPL-3.0-or-later" authors = ["Ryan Kuester "] diff --git a/build.Containerfile b/build.Containerfile new file mode 100644 index 0000000..62e0f5d --- /dev/null +++ b/build.Containerfile @@ -0,0 +1,21 @@ +# Build toolchain image for running checks in a reproducible environment. +# +# The Rust compiler and just are pinned to exact versions. The base +# image is pinned by digest. Apt packages come from the Debian +# bookworm repos bundled in the base image; their exact versions can +# drift across builds but are stable system libraries that don't +# affect build output. + +FROM docker.io/library/rust:1.94-bookworm@sha256:b2fe2c0f26e0e1759752b6b2eb93b119d30a80b91304f5b18069b31ea73eaee8 + +# These are pinned to the exact toolchain release by the base image digest. +RUN rustup component add rustfmt clippy + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libudev-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN cargo install just --version 1.40.0 + +WORKDIR /workspace diff --git a/justfile b/justfile index 63ce5e0..021a83a 100644 --- a/justfile +++ b/justfile @@ -21,6 +21,44 @@ test: run: cargo run --bin mujina-minerd +BUILD_IMAGE := "mujina-build" +# Tag with a content hash of the Containerfile so we can detect +# staleness without rebuilding. This matters in CI where podman +# save/load doesn't preserve layer cache---podman build would +# rebuild from scratch even with a loaded image. The content-hash +# tag lets `podman image exists` skip the build entirely. +BUILD_TAG := `sha256sum build.Containerfile | cut -c1-12` + +# Build the build toolchain image (skips if unchanged) +[group('container')] +build-image: + podman image exists {{BUILD_IMAGE}}:{{BUILD_TAG}} || \ + podman build -t {{BUILD_IMAGE}}:{{BUILD_TAG}} -f build.Containerfile . + +# Remove stale build toolchain images +[group('container')] +build-image-clean: + podman images --format '{{{{.Repository}}:{{{{.Tag}}' \ + | grep '^{{BUILD_IMAGE}}:' \ + | grep -v ':{{BUILD_TAG}}$' \ + | xargs -r podman rmi + +# Run a just recipe inside the build toolchain image +[group('container')] +in-container *args: build-image + mkdir -p .cache/cargo-registry .cache/cargo-git + podman run --rm \ + -v "$(pwd)":/workspace:Z \ + -v "$(pwd)/.cache/cargo-registry":/usr/local/cargo/registry \ + -v "$(pwd)/.cache/cargo-git":/usr/local/cargo/git \ + -w /workspace \ + {{BUILD_IMAGE}}:{{BUILD_TAG}} \ + just {{args}} + +# The CI pipeline. This is what GitHub Actions runs. +[group('ci')] +ci: (in-container "checks") + [group('container')] container-build tag=`git rev-parse --abbrev-ref HEAD`: podman build -t mujina-minerd:{{tag}} -f Containerfile . From 6bb2977548026f396fe247b1574a7fce4054d3b3 Mon Sep 17 00:00:00 2001 From: Ryan Kuester Date: Wed, 11 Mar 2026 10:08:05 -0500 Subject: [PATCH 3/4] ci: add GitHub Actions workflow Run `just ci` on pushes to main and pull requests. The justfile's ci recipe delegates to `in-container "checks"`, keeping the workflow minimal and the justfile as the single source of truth for what CI does. The build toolchain image and cargo caches (compiled dependencies and downloaded crates) are cached between runs via actions/cache. Project crate artifacts are pruned before the target cache saves so only dependency objects are stored. --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4613d1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +# CI Design Principles +# +# 1. The justfile is the source of truth. This workflow is plumbing +# (checkout, cache, `just ci`). Test logic lives in the justfile +# where contributors can run it locally. +# +# 2. Build the toolchain container from source, don't pull it. The +# image is defined by build.Containerfile in the same commit being +# tested---full traceability and repeatability, no external +# registry to trust or keep in sync. +# +# 3. Use `pull_request`, not `pull_request_target`. PR workflows run +# the code from the PR (including any Containerfile changes), with +# read-only repo access and no secrets. A PR author can modify the +# container and workflow, but that's visible in the diff and is the +# reviewer's job to catch. + +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: just ci + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 + + # Cache the build toolchain image as a portable tar archive. + # Podman's overlay storage can't be cached directly (tar fails + # on overlay whiteout files), so we round-trip through podman + # save/load. The cache key is the hash of build.Containerfile. + # The image is tagged with a short hash of the Containerfile so + # that build-image in the justfile recognizes it and skips the + # build. + - uses: actions/cache@v5 + id: image-cache + with: + path: /tmp/mujina-build.tar + key: build-image-${{ hashFiles('build.Containerfile') }} + + - name: Load cached build image + if: steps.image-cache.outputs.cache-hit == 'true' + run: podman load -i /tmp/mujina-build.tar + + # Cache compiled dependencies and downloaded crates to speed + # up builds. The key includes the toolchain (Containerfile) + # so a compiler bump invalidates stale artifacts. + - uses: actions/cache@v5 + with: + path: | + target + .cache + key: cargo-${{ hashFiles('build.Containerfile') }}-${{ hashFiles('Cargo.lock') }} + + - run: just ci + + # Prune project crate artifacts before the cache saves, + # keeping only compiled dependencies. + - name: Prune project artifacts from target cache + run: | + find target -name 'mujina*' -delete 2>/dev/null || true + find target -name 'libmujina*' -delete 2>/dev/null || true + + - name: Save build image for cache + if: steps.image-cache.outputs.cache-hit != 'true' + run: | + TAG=$(sha256sum build.Containerfile | cut -c1-12) + podman save -o /tmp/mujina-build.tar "mujina-build:$TAG" From a709438b83f55ef77ad02a93e83b11ffc9496ca4 Mon Sep 17 00:00:00 2001 From: Ryan Kuester Date: Wed, 11 Mar 2026 10:08:16 -0500 Subject: [PATCH 4/4] docs(contributing): document just checks and just ci Update prerequisites to list just and optionally Podman. Replace the raw cargo commands in "Making Changes" with `just checks`, and add a "Reproducing CI locally" section explaining `just ci`. --- CONTRIBUTING.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3acd3db..64bbac4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,8 +105,10 @@ will be accepted with a high degree of certainty. ### Prerequisites - Rust toolchain (stable) +- [just](https://just.systems/) command runner - Linux development environment - Git +- Optional: [Podman](https://podman.io/) for reproducing CI locally - Optional: Hardware for testing (Bitaxe boards, etc.) ### Setting Up Your Development Environment @@ -147,9 +149,28 @@ will be accepted with a high degree of certainty. 2. Follow the project's module structure 3. Add tests for new functionality 4. Update documentation as needed -5. Ensure all tests pass: `cargo test` -6. Run clippy: `cargo clippy -- -D warnings` -7. Format your code: `cargo fmt` +5. Run all checks before committing: + ```bash + just checks + ``` + This runs `cargo fmt --check`, `cargo clippy`, and `cargo test` + using your local Rust toolchain. + +#### Reproducing CI locally + +Pull requests are gated on CI that runs inside a Podman container +with a pinned Rust toolchain. If `just checks` passes locally but +CI fails (or vice versa), you can run the same checks in the same +container CI uses: + +```bash +just ci +``` + +This builds a toolchain container from `build.Containerfile` and +runs `just checks` inside it. The container image is cached +locally and only rebuilds when the Containerfile changes. Podman +is required for this step but not for regular development. ### Testing