diff --git a/.github/workflows/CICD.md b/.github/workflows/CICD.md index ad5deb0acc..f1d8cc2d01 100644 --- a/.github/workflows/CICD.md +++ b/.github/workflows/CICD.md @@ -36,6 +36,7 @@ Trigger: pull_request to develop or master + DCO check (independent, develop PRs only) + Dependabot (weekly: Cargo deps + GitHub Actions) + + coverage.yml (also on push to develop: the tests under cargo-llvm-cov, then Codecov) ``` ## Merge to develop — pre-release (cd.yml) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cd8030780..c141849306 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: permissions: contents: read - pull-requests: read env: CARGO_TERM_COLOR: always @@ -31,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: components: rustfmt - run: cargo fmt --all -- --check @@ -42,10 +41,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 # `proxy/` is not a workspace member (it would break # `cargo install --git`), so it gets its own job below. Here # --workspace covers `rtk` + `engine`; a bare `cargo clippy` would @@ -58,8 +57,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - run: cargo doc --no-deps # ─── Parallel gates (all need code to compile) ─── @@ -74,10 +73,10 @@ jobs: needs: clippy steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - run: cargo fmt --manifest-path proxy/Cargo.toml --all --check - run: cargo clippy --manifest-path proxy/Cargo.toml --all-targets - run: cargo test --manifest-path proxy/Cargo.toml @@ -94,10 +93,10 @@ jobs: - uses: actions/checkout@v7 - name: Install required search tools run: sudo apt-get update && sudo apt-get install -y ripgrep grep - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - run: cargo clippy --no-default-features --all-targets - run: cargo test --no-default-features # Match the crate-name column, not the whole line: the checkout path @@ -156,10 +155,10 @@ jobs: grep --version rg --version rg --pcre2-version - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 # `--all` is `--workspace`: `rtk` + `engine`. `proxy/` is tested in # its own job, since it is not a member. - run: cargo test --all @@ -173,9 +172,9 @@ jobs: with: fetch-depth: 0 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - name: Install cargo-audit run: cargo install cargo-audit @@ -308,18 +307,18 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 - name: Build rtk - run: cargo build --release + run: cargo build --locked --release - name: Install system tools run: sudo apt-get install -y tree ripgrep - name: Install Python tools - run: pip install ruff pytest mypy + run: pip install --only-binary ':all:' ruff==0.17.3 pytest==9.2.0 mypy==2.4.1 - name: Install Go uses: actions/setup-go@v7 @@ -327,7 +326,7 @@ jobs: go-version: "stable" - name: Install Go tools - run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 - name: Run benchmark run: ./scripts/benchmark.sh @@ -338,6 +337,10 @@ jobs: name: doc review if: github.base_ref == 'develop' runs-on: ubuntu-latest + # Only this job reads the pull request, with `gh pr view` and `gh pr diff`. + permissions: + contents: read + pull-requests: read steps: - uses: actions/checkout@v7 with: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000000..f8f114f481 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,80 @@ +name: Coverage + +# ci.yml runs on pull requests only, so the test suite also runs here, under +# cargo-llvm-cov, on every push to develop. Its result is the README's tests +# badge, and its report feeds the Codecov badge. +on: + push: + branches: [develop] + pull_request: + branches: [develop] + workflow_dispatch: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: test with coverage (ubuntu-latest) + # A fork's pull request gets no Codecov upload; ci.yml still tests it. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install required search tools + run: sudo apt-get update && sudo apt-get install -y ripgrep grep + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + with: + components: llvm-tools + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + - name: Install the pinned cargo-llvm-cov + uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2.87.31 + with: + tool: cargo-llvm-cov@0.9.1 + fallback: none + # The same tests as ci.yml's `cargo test --all`. An explicit target keeps + # the cross mode ci.yml's test legs use; cargo-llvm-cov hands it to rustc, + # which has no 'host-tuple', so the host triple is named. cargo-llvm-cov + # skips doctests, and the workspace has none. + - name: Run the tests with line coverage + run: CARGO_BUILD_TARGET="$(rustc --print host-tuple)" cargo llvm-cov --locked --workspace --lcov --output-path coverage-rust.lcov + - name: Keep the Rust coverage report for the Codecov upload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-rust + path: coverage-rust.lcov + retention-days: 1 + + # The upload runs apart from the tests so the OIDC token is never available + # while test code runs. Keep it out of the required checks: a Codecov outage + # must not block merges. + codecov: + name: upload coverage to Codecov + needs: test + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository + # A Codecov outage must not turn the tests badge red. + continue-on-error: true + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write # Authenticate the Codecov upload with OIDC; no stored token. + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: coverage-rust + - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + use_oidc: true + files: coverage-rust.lcov + disable_search: true + fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f4088a1f3..6cd5b7074d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: sudo apt-get install -y musl-tools - name: Build rtk - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --locked --release --target ${{ matrix.target }} env: RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} @@ -116,7 +116,7 @@ jobs: # so only LICENSE travels with it. Same binary name `rtk` inside -- an # installed hook command must keep working across editions. - name: Build rtk (Apache-2.0 edition) - run: cargo build --release --no-default-features --target ${{ matrix.target }} + run: cargo build --locked --release --no-default-features --target ${{ matrix.target }} env: RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} @@ -156,7 +156,7 @@ jobs: - name: Build rtk-proxy id: build_proxy run: > - cargo build --release + cargo build --locked --release --manifest-path proxy/Cargo.toml --target-dir target --target ${{ matrix.target }} @@ -220,7 +220,7 @@ jobs: uses: actions/checkout@v7 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable - name: Install cargo-deb run: cargo install cargo-deb @@ -253,7 +253,7 @@ jobs: run: cargo install cargo-generate-rpm - name: Build release - run: cargo build --release --no-default-features + run: cargo build --locked --release --no-default-features env: RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} @@ -283,7 +283,7 @@ jobs: run: cargo install cargo-generate-rpm - name: Build release - run: cargo build --release + run: cargo build --locked --release env: RTK_TELEMETRY_URL: ${{ vars.RTK_TELEMETRY_URL }} RTK_TELEMETRY_TOKEN: ${{ secrets.RTK_TELEMETRY_TOKEN }} diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 0000000000..b9693b8f3b --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,70 @@ +name: Snyk + +# Snyk runs weekly and when a dependency file changes, which keeps it inside +# the free plan's monthly test allowance. +on: + schedule: + - cron: "23 4 * * 1" + workflow_dispatch: + push: + branches: [develop] + paths: + - "**/Cargo.lock" + - "**/Cargo.toml" + - ".github/workflows/snyk.yml" + pull_request: + paths: + - "**/Cargo.lock" + - "**/Cargo.toml" + - ".github/workflows/snyk.yml" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust: + name: Snyk Rust dependencies (${{ matrix.root }}) + # Pull requests from forks, and Dependabot's (which get Dependabot secrets + # only), receive no SNYK_TOKEN; the weekly run on develop scans their changes. + if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + # proxy/ is its own workspace with its own Cargo.lock. + root: [".", proxy] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0 + # Snyk cannot read Cargo.lock directly, so the graph is described as a + # CycloneDX SBOM and tested with `snyk sbom test`. + - name: Install the SBOM generator + run: | + rustup toolchain install 1.91 --profile minimal + rustup default 1.91 + cargo install cargo-cyclonedx --locked --version 0.5.9 + - name: Test the Cargo graph + working-directory: ${{ matrix.root }} + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + # cargo-cyclonedx writes one SBOM beside each workspace member's + # manifest; each is tested. + cargo cyclonedx --format json --spec-version 1.5 --override-filename snyk-sbom + [ -n "$SNYK_TOKEN" ] || { echo "::error::The SNYK_TOKEN repository secret is not set, so nothing was scanned."; exit 1; } + mapfile -t sboms < <(find . -name snyk-sbom.json -not -path './target/*' | sort) + [ "${#sboms[@]}" -gt 0 ] || { echo "::error::cargo-cyclonedx wrote no SBOM."; exit 1; } + status=0 + for sbom in "${sboms[@]}"; do + snyk sbom test --experimental --file="$sbom" || status=1 + done + exit "$status" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3232421547..b3390fed21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -248,7 +248,7 @@ your branch --> develop (review + CI + integration testing) --> version branch - ## Testing -Every change **must** include tests. We follow **TDD (Red-Green-Refactor)**: write a failing test first, implement the minimum to pass, then refactor. +Every change **must** include tests. New functionality and bug fixes come with automated tests, in `#[cfg(test)]` modules beside the code or in `tests/`, run by `cargo test --all` in CI. We follow **TDD (Red-Green-Refactor)**: write a failing test first, implement the minimum to pass, then refactor. For how to write tests (fixtures, snapshots, token savings verification), see [docs/contributing/TECHNICAL.md — Testing](docs/contributing/TECHNICAL.md#8-testing). diff --git a/Cargo.lock b/Cargo.lock index 3bfa885339..307159cf57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1357,9 +1357,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -1381,9 +1381,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", diff --git a/README.md b/README.md index 8f791821aa..4f7085a8f4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@