From 6dce78dd8bde9166fca8da6b30f89f53d98b9098 Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Tue, 14 Jul 2026 21:40:03 +0300 Subject: [PATCH] feat(ci): use docker image for ci --- .../actions/install-system-deps/action.yml | 10 -- .github/docker/ci.Dockerfile | 73 +++++++++ .github/workflows/bench-regression.yml | 20 ++- .github/workflows/ci-image.yml | 71 +++++++++ .github/workflows/ci.yml | 144 +++++++++--------- 5 files changed, 230 insertions(+), 88 deletions(-) delete mode 100644 .github/actions/install-system-deps/action.yml create mode 100644 .github/docker/ci.Dockerfile create mode 100644 .github/workflows/ci-image.yml diff --git a/.github/actions/install-system-deps/action.yml b/.github/actions/install-system-deps/action.yml deleted file mode 100644 index 28c4b41c..00000000 --- a/.github/actions/install-system-deps/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: Install system dependencies -description: Installs system dependencies in the environment -runs: - using: "composite" - steps: - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential clang libclang-dev libssl-dev pkg-config - shell: bash diff --git a/.github/docker/ci.Dockerfile b/.github/docker/ci.Dockerfile new file mode 100644 index 00000000..303c0328 --- /dev/null +++ b/.github/docker/ci.Dockerfile @@ -0,0 +1,73 @@ +# Image behind the `container:` of the CI jobs. Everything the jobs used to +# install per-run is baked in here instead, so a CI run no longer asks the risc0 +# install servers for anything. +# +# The image tag is a hash of this file plus rust-toolchain.toml, so editing +# either rebuilds the image on the PR that changes it. See ci-image.yml. +# +# Keep the base tag in sync with rust-toolchain.toml. +FROM rust:1.94.0-trixie + +# GitHub sets HOME=/github/home inside container jobs, which hides anything we +# bake into the image's ~. rzup defaults to $HOME/.risc0, so pin it to a fixed +# path; RUSTUP_HOME and CARGO_HOME already point at /usr/local from the base +# image, and rzup honours all three. +ENV RISC0_HOME=/usr/local/risc0 +ENV PATH=/usr/local/risc0/bin:$PATH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + clang \ + libclang-dev \ + libssl-dev \ + pkg-config \ + curl \ + git \ + jq \ + python3-dev \ + && rm -rf /var/lib/apt/lists/* + +# The base image already has this toolchain at the minimal profile, so the +# `profile = "default"` in rust-toolchain.toml is a no-op here: rustup sees the +# toolchain as installed and never applies it. Add what the jobs need by hand. +COPY rust-toolchain.toml /tmp/toolchain/rust-toolchain.toml +RUN cd /tmp/toolchain \ + && rustup toolchain install \ + && rustup component add clippy rustfmt \ + && rm -rf /tmp/toolchain + +# For `cargo +nightly fmt` in the fmt-rs job. +RUN rustup toolchain install nightly --profile minimal --component rustfmt + +# The bootstrap script only ever drops rzup in $HOME/.risc0/bin, so run it +# against the build-time HOME and keep just the binary. rzup itself then honours +# RISC0_HOME and installs the toolchain, r0vm and cargo-risczero under /usr/local. +RUN curl -L https://risczero.com/install | bash \ + && mv /root/.risc0/bin/rzup /usr/local/bin/rzup \ + && rm -rf /root/.risc0 \ + && rzup install + +# Prebuilt binaries; compiling these from source would dominate the image build. +RUN curl -L --proto '=https' --tlsv1.2 -sSf \ + https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \ + && cargo binstall --no-confirm \ + cargo-nextest \ + taplo-cli \ + cargo-machete \ + cargo-deny \ + just + +# Smoke-test every tool the jobs use. A bad install should fail here, not later +# in some CI job that reuses this cached image. pyo3's `auto-initialize` links +# libpython, and the base image ships python3 without it, so check the .so too. +RUN cargo --version \ + && cargo +nightly fmt --version \ + && cargo clippy --version \ + && ls /usr/lib/*/libpython3*.so \ + && r0vm --version \ + && cargo risczero --version \ + && cargo nextest --version \ + && taplo --version \ + && cargo machete --version \ + && cargo deny --version \ + && just --version diff --git a/.github/workflows/bench-regression.yml b/.github/workflows/bench-regression.yml index d82c0f11..89c18e32 100644 --- a/.github/workflows/bench-regression.yml +++ b/.github/workflows/bench-regression.yml @@ -9,12 +9,25 @@ on: permissions: contents: read pull-requests: write + packages: read name: bench-regression jobs: + ci-image: + permissions: + contents: read + packages: write + uses: ./.github/workflows/ci-image.yml + crypto-primitives: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 steps: - uses: actions/checkout@v5 @@ -24,13 +37,6 @@ jobs: # working tree, so we need the full history. fetch-depth: 0 - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Run criterion-compare against base branch uses: boa-dev/criterion-compare-action@v3 with: diff --git a/.github/workflows/ci-image.yml b/.github/workflows/ci-image.yml new file mode 100644 index 00000000..c61908a8 --- /dev/null +++ b/.github/workflows/ci-image.yml @@ -0,0 +1,71 @@ +name: CI image + +# Resolves the CI image the calling workflow's jobs run in, building and pushing +# it only when it isn't published yet. +on: + workflow_call: + outputs: + image: + description: Image reference to pass to a job's `container:`. + value: ${{ jobs.ci-image.outputs.image }} + +jobs: + ci-image: + runs-on: ubuntu-latest + timeout-minutes: 60 + outputs: + image: ${{ steps.image-ref.outputs.image }} + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} + + # Tagging by content hash means a PR that touches the Dockerfile or the + # toolchain builds and tests against its own image, while every other PR + # reuses the one already in the registry. + - name: Compute image reference + id: image-ref + run: | + repo="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + tag="$(cat .github/docker/ci.Dockerfile rust-toolchain.toml | sha256sum | cut -c1-16)" + echo "image=ghcr.io/${repo}/ci:${tag}" >> "$GITHUB_OUTPUT" + echo "CI image: ghcr.io/${repo}/ci:${tag}" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check whether the image is already published + id: probe + run: | + if docker manifest inspect "${{ steps.image-ref.outputs.image }}" > /dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Image already published, skipping build." + else + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "Image not published yet, building it." + fi + + - name: Set up Docker Buildx + if: steps.probe.outputs.exists == 'false' + uses: docker/setup-buildx-action@v3 + + # A pull_request from a fork gets a read-only GITHUB_TOKEN, so this step + # fails there. It only runs when the fork changed the image, which is the + # case that needs a maintainer's eyes anyway. + - name: Build and push CI image + if: steps.probe.outputs.exists == 'false' + uses: docker/build-push-action@v5 + with: + context: . + file: ./.github/docker/ci.Dockerfile + push: true + tags: ${{ steps.image-ref.outputs.image }} + # Links the package to the repo, so it shows up there and inherits its + # access rules. + labels: org.opencontainers.image.source=https://github.com/${{ github.repository }} + cache-from: type=gha,scope=ci-image + cache-to: type=gha,mode=max,scope=ci-image diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 194646f3..5dba7722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,67 +15,91 @@ on: permissions: contents: read pull-requests: read + packages: read name: General jobs: + ci-image: + permissions: + contents: read + packages: write + uses: ./.github/workflows/ci-image.yml + fmt-rs: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - name: Install nightly toolchain for rustfmt - run: rustup install nightly --profile minimal --component rustfmt - - name: Check Rust files are formatted run: cargo +nightly fmt --check fmt-toml: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - name: Install taplo-cli - run: cargo install --locked taplo-cli - + # No path argument: taplo reads a `.` as a literal file, excludes it for + # not being a .toml, and checks nothing. - name: Check TOML files are formatted - run: taplo fmt --check . + run: taplo fmt --check machete: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - name: Install active toolchain - run: rustup install - - - name: Install cargo-machete - run: cargo install cargo-machete - - name: Check for unused dependencies run: cargo machete deny: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - name: Install cargo-deny - run: cargo install --locked cargo-deny - - name: Check licenses and advisories run: cargo deny check lint: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 name: lint @@ -84,13 +108,6 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Restore Rust cache uses: Swatinem/rust-cache@v2 with: @@ -108,29 +125,25 @@ jobs: run: cargo clippy -p "*program" -- -D warnings unit-tests: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Restore Rust cache uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - - name: Install nextest - run: cargo install --locked cargo-nextest - - name: Run tests env: RISC0_DEV_MODE: "1" @@ -138,29 +151,25 @@ jobs: run: cargo nextest run --workspace --exclude integration_tests --exclude test_fixtures --all-features test-fixtures-tests: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Restore Rust cache uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - - name: Install nextest - run: cargo install --locked cargo-nextest - - name: Run test_fixtures tests env: RISC0_DEV_MODE: "1" @@ -168,7 +177,13 @@ jobs: run: cargo nextest run -p test_fixtures integration-tests-prebuild: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} outputs: targets: ${{ steps.discover-targets.outputs.targets }} steps: @@ -176,22 +191,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Restore Rust cache uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - - name: Install nextest - run: cargo install --locked cargo-nextest - - name: Build integration test archive env: RISC0_DEV_MODE: "1" @@ -223,8 +228,13 @@ jobs: echo "Discovered integration targets: $targets_json" integration-tests: - needs: [test-fixtures-tests, integration-tests-prebuild] + needs: [ci-image, test-fixtures-tests, integration-tests-prebuild] runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 90 strategy: fail-fast: false @@ -236,21 +246,11 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Download integration test archive uses: actions/download-artifact@v4 with: name: integration-tests-archive - - name: Install nextest - run: cargo install --locked cargo-nextest - - name: Run tests env: RISC0_DEV_MODE: "1" @@ -258,20 +258,19 @@ jobs: run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})" valid-proof-test: + needs: ci-image runs-on: ubuntu-latest + container: + image: ${{ needs.ci-image.outputs.image }} + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 90 steps: - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - - uses: ./.github/actions/install-system-deps - - - uses: ./.github/actions/install-risc0 - - - name: Install active toolchain - run: rustup install - - name: Restore Rust cache uses: Swatinem/rust-cache@v2 with: @@ -283,6 +282,9 @@ jobs: RUST_LOG: "info" run: cargo test -p integration_tests -- --exact private::private_transfer_to_owned_account + # Not containerised: `just build-artifacts` shells out to `cargo risczero + # build`, which runs the guest build in Docker and bind-mounts the workspace. + # From inside a container job the workspace path wouldn't match the host's. artifacts: runs-on: ubuntu-latest timeout-minutes: 60