diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e698697 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +**/target +**/node_modules +**/.next +**/coverage +**/.turbo +**/.cache +**/*.pdb +**/*.exe +Dockerfile.ci +.dockerignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9899a37..1263dfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -53,4 +83,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: wasm-artifacts - path: target/wasm32-unknown-unknown/release/*.wasm \ No newline at end of file + path: target/wasm32-unknown-unknown/release/*.wasm diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 0000000..75a2af9 --- /dev/null +++ b/Dockerfile.ci @@ -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 diff --git a/docs/CI_DOCKER_LAYER_CACHING.md b/docs/CI_DOCKER_LAYER_CACHING.md new file mode 100644 index 0000000..da3d82e --- /dev/null +++ b/docs/CI_DOCKER_LAYER_CACHING.md @@ -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.