Skip to content
Open
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.github
**/target
**/node_modules
**/.next
**/coverage
**/.turbo
**/.cache
**/*.pdb
**/*.exe
Dockerfile.ci
.dockerignore
32 changes: 31 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ env:
CARGO_TERM_COLOR: always

jobs:
docker-layer-cache:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: rust-root
target: rust-root-test
- name: contracts
target: contracts-test
- name: wasm
target: wasm-build
- name: meter-simulator
target: meter-simulator-test
- name: usage-dashboard
target: usage-dashboard-build
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build ${{ matrix.name }} with GitHub Actions cache
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.ci
target: ${{ matrix.target }}
push: false
cache-from: type=gha,scope=${{ matrix.name }}
cache-to: type=gha,scope=${{ matrix.name }},mode=max

test-and-lint:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -53,4 +83,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: wasm-artifacts
path: target/wasm32-unknown-unknown/release/*.wasm
path: target/wasm32-unknown-unknown/release/*.wasm
85 changes: 85 additions & 0 deletions Dockerfile.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# syntax=docker/dockerfile:1.7

ARG RUST_VERSION=stable
ARG NODE_VERSION=20

FROM rust:${RUST_VERSION}-bookworm AS rust-base
WORKDIR /workspace
RUN rustup target add wasm32-unknown-unknown \
&& rustup component add rustfmt clippy

FROM rust-base AS rust-root-deps
COPY Cargo.toml Cargo.lock ./
COPY main.rs ./main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/workspace/target \
cargo fetch --locked

FROM rust-root-deps AS rust-root-test
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/workspace/target \
cargo fmt --all -- --check \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& cargo test --locked

FROM rust-base AS contracts-deps
WORKDIR /workspace/contracts
COPY contracts/Cargo.toml contracts/Cargo.lock ./
COPY contracts/common/Cargo.toml ./common/Cargo.toml
COPY contracts/meter-aggregator/Cargo.toml ./meter-aggregator/Cargo.toml
COPY contracts/price_oracle/Cargo.toml ./price_oracle/Cargo.toml
COPY contracts/resource-token/Cargo.toml ./resource-token/Cargo.toml
COPY contracts/settlement/Cargo.toml ./settlement/Cargo.toml
COPY contracts/utility_contracts/Cargo.toml ./utility_contracts/Cargo.toml
RUN for crate in common meter-aggregator price_oracle resource-token settlement utility_contracts; do \
mkdir -p "${crate}/src"; \
printf 'pub fn placeholder() {}\n' > "${crate}/src/lib.rs"; \
done
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/workspace/contracts/target \
cargo fetch --locked

FROM contracts-deps AS contracts-test
WORKDIR /workspace/contracts
COPY contracts .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/workspace/contracts/target \
cargo fmt --all -- --check \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& cargo test --locked

FROM contracts-test AS wasm-build
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/workspace/contracts/target \
cargo build --target wasm32-unknown-unknown --release --locked

FROM node:${NODE_VERSION}-bookworm-slim AS meter-simulator-deps
WORKDIR /workspace/meter-simulator
COPY meter-simulator/package.json ./
RUN --mount=type=cache,target=/root/.npm npm install

FROM meter-simulator-deps AS meter-simulator-test
COPY meter-simulator ./
RUN npm test

FROM node:${NODE_VERSION}-bookworm-slim AS usage-dashboard-deps
WORKDIR /workspace/usage-dashboard
COPY usage-dashboard/package.json ./
RUN --mount=type=cache,target=/root/.npm npm install

FROM usage-dashboard-deps AS usage-dashboard-build
COPY usage-dashboard ./
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM scratch AS ci-summary
COPY --from=rust-root-test /workspace/Cargo.toml /rust-root/Cargo.toml
COPY --from=contracts-test /workspace/contracts/Cargo.toml /contracts/Cargo.toml
COPY --from=meter-simulator-test /workspace/meter-simulator/package.json /meter-simulator/package.json
COPY --from=usage-dashboard-build /workspace/usage-dashboard/package.json /usage-dashboard/package.json
34 changes: 34 additions & 0 deletions docs/CI_DOCKER_LAYER_CACHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CI Docker Layer Caching Runbook

## Architecture

CI uses `Dockerfile.ci` as a BuildKit cache boundary for the repository's runnable services:

- root Rust payload generator (`rust-root-test` target),
- Soroban utility contracts (`contracts-test` and `wasm-build` targets),
- meter simulator (`meter-simulator-test` target), and
- usage dashboard (`usage-dashboard-build` target).

Each target copies dependency manifests before source files so dependency layers are reused when application code changes. BuildKit cache mounts persist Cargo registries, Cargo git checkouts, Rust `target` directories, and npm package downloads within the layer graph. GitHub Actions stores and restores those layers with `type=gha` caches scoped per service target.

## CI behavior

The `docker-layer-cache` job builds every service target with `docker/build-push-action` and `push: false`. The job is intentionally non-publishing: it validates image buildability and warms cache metadata only. Existing native Rust jobs continue to produce test output and WASM artifacts.

## Monitoring and alerting

Use the GitHub Actions run summary for these operational checks:

1. The `docker-layer-cache` job should stay green on all pull requests.
2. Repeated cache misses are visible as unusually long BuildKit dependency steps such as `cargo fetch` or `npm install`.
3. A failed `docker-layer-cache` job blocks merges through required status checks when branch protection is enabled.

## Blue-green and canary rollout

Roll out cache changes by first opening a pull request and validating the new cache scopes on that PR. Treat the pull request branch as the canary. After merge, `main` becomes the green environment while previous workflow runs remain the blue fallback reference. If build times regress or cache keys poison, revert the workflow and `Dockerfile.ci` changes; GitHub's `type=gha` cache scopes are isolated by target name, so removing a target stops consuming the affected cache.

## Security review notes

- The Docker workflow does not push images or export secrets.
- `.dockerignore` excludes VCS metadata, local dependency folders, build outputs, coverage data, and binary artifacts from Docker build contexts.
- BuildKit cache mounts are used only for package manager caches and compiler outputs.