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
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Cargo.lock
/target/
/.cache/
27 changes: 24 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <rkuester@insymbols.com>"]
Expand Down
21 changes: 21 additions & 0 deletions build.Containerfile
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
Loading