diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..444ccb0e --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,103 @@ +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json + +name: Docker + +on: + push: + branches: + - main + tags: + - v[0-9]+.* + +jobs: + lint: + name: Lint Dockerfile + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.3.0 + + build: + name: Build Docker image + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + env: + REGISTRY: ghcr.io + IMAGE_NAME: ghcr.io/${{ github.repository }} + # Only build one base variant at a time so we can take advantage of the + # cache. I.e. only build one Alpine-based image and one Debian-based image + # at a time. + concurrency: + group: ${{ matrix.build_rust_tag }} + queue: max + steps: + - id: docker-metadata + name: Generate image names and tags + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE_NAME }} + flavor: suffix=${{ matrix.tag_suffix }},onlatest=true + # - `type=semver,pattern={{version}}`, + # `type=semver,pattern={{major}}.{{minor}}` and + # `type=semver,pattern={{major}}`: Jobs that run on a semver-looking + # git tag will be tagged as `major.minor.patch`, `major.minor`, + # `major` and "latest". Eg. a job running on the git tag "v1.2.3" + # will be tagged as "1", "1.2", "1.2.3" and "latest". Semantic + # versions that indicate a pre-releases (eg. "v1.2.3-rc.4") will be + # tagged as the full version only (i.e. "1.2.3-rc.4"). + # - `type=edge,branch=main`: Jobs that run on the "main" branch will + # be tagged with "edge". + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=edge,branch=main + - name: Login to GitHub container registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Build and push image + uses: docker/build-push-action@v7 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.docker-metadata.outputs.tags }} + labels: ${{ steps.docker-metadata.outputs.labels }} + build-args: | + BUILD_RUST_TAG=${{ matrix.build_rust_tag }} + FINAL_RUST_TAG=${{ matrix.final_rust_tag }} + # Cache to and from the registry. Use image name like + # "ghcr.io/sourcefrog/cargo-mutants-cache". + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}-cache + cache-to: type=registry,ref=${{ env.IMAGE_NAME }}-cache,mode=max + # - `build_rust_tag`: The Rust image tag to use for the build stage. + # - `final_rust_tag`: The Rust image tag to use for the final stage. + # - `tag_suffix`: The suffix to append to to our image tags. Eg. images that + # use "rust:latest" as the final stage will be tagged like ":1.2.3" and + # images that use "rust:slim" as the final stage will be tagged like + # ":1.2.3-slim". + # TODO: Don't tag as "latest-alpine", just tag as "alpine". I believe this + # is currently not possible using docker/metadata-action. + strategy: + matrix: + include: + - build_rust_tag: slim + final_rust_tag: latest + tag_suffix: "" + - build_rust_tag: slim + final_rust_tag: slim + tag_suffix: -slim + - build_rust_tag: alpine + final_rust_tag: alpine + tag_suffix: -alpine diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..a92627e1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +ARG BUILD_RUST_TAG=latest +ARG FINAL_RUST_TAG=latest + +# This stage sets up everything we need to cross-compile Rust programs in other +# stages. +FROM --platform=$BUILDPLATFORM "docker.io/library/rust:$BUILD_RUST_TAG" AS build + +# Install platform-agnostic dependencies. +# +# Check for the existence of APK/APT to determine how to install dependencies. +# hadolint ignore=DL3008,DL3018 # We don't want to pin package versions. +RUN --mount=type=cache,target=/var/cache/apk \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + ( which apk && apk add clang ) || \ + ( \ + which apt-get && \ + apt-get update && \ + apt-get --no-install-recommends --assume-yes install clang \ + ) + +# Install cross-compilation helper scripts. See +# https://github.com/tonistiigi/xx#rust. +# hadolint ignore=DL3067 # We really do want to copy everything from this image. +COPY --from=docker.io/tonistiigi/xx:latest / / + +# Everything from this point onwards is specific to the target platform. +ARG TARGETPLATFORM + +# Install platform-specific dependencies. +# +# Check for the existence of APK/APT to determine how to install dependencies. +# hadolint ignore=DL3008,DL3018 # We don't want to pin package versions. +RUN --mount=type=cache,target=/var/cache/apk \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + ( which apk && xx-apk add xx-c-essentials ) || \ + ( \ + which apt-get && \ + xx-apt-get update && \ + xx-apt-get --no-install-recommends --assume-yes install \ + xx-c-essentials \ + ) + +# Use the prepared build stage to build `cargo-mutants`. +FROM build AS build-mutants + +# Build cargo-mutants. +# +# Cache `/var/cache/cargo` (dependencies are downloaded here) and `/tmp/target` +# (compilation artifacts are generated here). These downloads/artifacts can be +# cached between invocations of `docker build`. +# +# Use `xx-verify` to confirm that the installed binary was correctly +# cross-compiled for the target architecture. +RUN --mount=type=bind,source=src,target=src \ + --mount=type=bind,source=mutants_attrs,target=mutants_attrs \ + --mount=type=bind,source=Cargo.toml,target=Cargo.toml \ + --mount=type=bind,source=Cargo.lock,target=Cargo.lock \ + --mount=type=cache,target=/tmp/target \ + --mount=type=cache,target=/var/cache/cargo \ + CARGO_HOME=/var/cache/cargo \ + xx-cargo install \ + --path . \ + --root / \ + --target-dir /tmp/target \ + --locked && \ + xx-verify /bin/cargo-mutants + +# Use the prepared build stage to build `cargo-nextest`. +FROM build AS build-nextest + +# Build `cargo-nextest` in a similar fashion to `cargo-mutants` above. +RUN --mount=type=cache,target=/tmp/target \ + --mount=type=cache,target=/var/cache/cargo \ + CARGO_HOME=/var/cache/cargo \ + xx-cargo install \ + --root / \ + --target-dir /tmp/target \ + --locked \ + cargo-nextest && \ + xx-verify /bin/cargo-nextest + +# Create the final image by adding `cargo-mutants` and `cargo-nextest` to the +# Rust image. +FROM "docker.io/library/rust:$FINAL_RUST_TAG" AS final + +COPY --from=build-mutants /bin/cargo-mutants /usr/local/cargo/bin/ +COPY --from=build-nextest /bin/cargo-nextest /usr/local/cargo/bin/ + +WORKDIR /app + +ENTRYPOINT [ "cargo", "mutants" ] diff --git a/NEWS.md b/NEWS.md index 646847c4..d1868c31 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,8 @@ - New: `#[mutants::exclude_re("pattern")]` attribute to exclude specific mutations by regex, without disabling all mutations on the function. The attribute can be placed on functions, `impl` blocks, `trait` blocks, modules, files, and on expressions that can carry an attribute (such as `match`, struct literals, call expressions, method calls, and unary expressions). Multiple patterns can be applied. Also supported within `cfg_attr`. Requires the [mutants](https://crates.io/crates/mutants) crate version `0.0.5` or later. - Fixed: `#[mutants::skip]` (and `#[cfg_attr(..., mutants::skip)]`) is now honoured when placed on `const` and `static` items, including associated constants in `impl` and `trait` blocks. Previously the attribute was silently ignored on these items and operator mutants inside the initializer expression were still generated ([#508](https://github.com/sourcefrog/cargo-mutants/issues/508)). +- New: Build a cargo-mutants Docker image (`ghcr.io/sourcefrog/cargo-mutants`). +- Docs: Add GitLab CI/CD section, which uses the new cargo-mutants Docker image. ## 27.1.0 diff --git a/README.md b/README.md index 75f8e27f..52d88504 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,28 @@ To generate mutants in only one file: cargo mutants -f src/something.rs ``` +## Docker + +The cargo-mutants Docker image is just the official Rust Docker image with cargo-mutants and cargo-nextest preinstalled. + +From within a Rust source directory, just run + +```sh +docker run --rm -v .:/app ghcr.io/sourcefrog/cargo-mutants:latest +``` + +Available Docker tags are + +* `..`, `.`, ``: Tracks a specific cargo-mutants release. + `latest`: Tracks the latest cargo-mutants release. +* `edge`: Tracks the main branch in the cargo-mutants repository. + +A suffix can be added to each tag to specify which Rust Docker image the cargo-mutants Docker image is based on. + +* No suffix: Built on the `rust:latest` Docker image. Eg. `0.27.1`, `latest`, `edge`. +* `-slim` suffix: Built on the `rust:slim` Docker image. Eg. `0.27.1-slim`, `latest-slim`, `edge-slim`. +* `-alpine` suffix: Built on the `rust:alpine` Docker image. Eg. `0.27.1-alpine`, `latest-alpine`, `edge-alpine`. + ## Integration with CI The [manual includes instructions and examples for automatically testing mutants in CI](https://mutants.rs/ci.html), including incremental testing of pull requests and full testing of the development branch. diff --git a/book/src/ci.md b/book/src/ci.md index 48d983a4..6ea9a8a6 100644 --- a/book/src/ci.md +++ b/book/src/ci.md @@ -12,11 +12,13 @@ There are at least two complementary ways to use cargo-mutants in CI: * Use the [`--in-place`](in-place.md) option to avoid copying the tree. -## Installing into CI +## GitHub Actions + +### Installing into CI The recommended way to install cargo-mutants is using [install-action](https://github.com/taiki-e/install-action), which will fetch a binary from cargo-mutants most recent GitHub release, which is faster than building from source. You could alternatively use [baptiste0928/cargo-install](https://github.com/baptiste0928/cargo-install) which will build it from source in your worker and cache the result. -## Example workflow +### Example workflow Here is an example of a GitHub Actions workflow that runs mutation tests and uploads the results as an artifact. This will fail if it finds any uncaught mutants. @@ -29,8 +31,31 @@ The recommended way to install cargo-mutants is using [install-action](https://g The workflow used by cargo-mutants on itself can be seen at , but this is different from what you will typically want to use, because it runs cargo-mutants from HEAD. -## Annotations +### Annotations cargo-mutants will emit GitHub Actions structured annotations when it detects that it's running within an action. (Specifically, when `$GITHUB_ACTION` is set.) This behavior can be forced on with the `--annotations=github` option, or off with `--annotations=none`. + +## GitLab CI/CD + +In GitLab CI/CD, the cargo-mutants Docker image may be used. Here is an example job. + +```yaml +cargo-mutants: + image: + # Use the cargo-mutants Docker image. + name: ghcr.io/sourcefrog/cargo-mutants:latest + # The default entrypoint of this image is ["cargo", "mutants"]. However, the + # GitLab runner needs a shell so it can execute commands, so we must + # override the entrypoint. See + # https://docs.gitlab.com/ci/docker/using_docker_images/#override-the-entrypoint-of-an-image + entrypoint: [""] + script: + # Run cargo-mutants. + # + # - Specify "--in-place" to avoid copying the source tree. + # - Specify "--caught" and "--unviable" so we can see every mutant in the CI + # log, not just the mutants we failed to catch. + - cargo mutants --in-place --caught --unviable +```