Add shift-left quality gates: cognitive complexity, coverage, dependency fix
Adds two new gates to the existing format/lint/test/audit pipeline (Makefile.toml `cargo make ci`, .forgejo/workflows/ci.yml): - Cognitive complexity via clippy's nursery cognitive_complexity lint (clippy.toml, threshold 15), scoped to --bins so test code's naturally higher branch count doesn't get gated. Went with this over the closest real cyclomatic-complexity tool (rust-code-analysis-cli) because that crate hasn't shipped a release since Jan 2023. - Test coverage via cargo-llvm-cov, chosen over cargo-tarpaulin for friendlier behavior in containerized/dind CI (no ptrace). Report-only for now (no --fail-under-lines) since a real threshold needs real usage data first — see below. main.rs is excluded: it's orchestration glue exercised by the real end-to-end `cargo run`, not unit tests. Getting both gates running required writing pkgwatch's first tests (previously zero). To make the GitHub-facing modules unit-testable without hitting real github.com/api.github.com, added `GithubEndpoints` (src/github.rs) so checker/fetcher/verifier take injectable base URLs, and added mockito + tempfile as dev-dependencies. Result: 27 tests, 94% region / 96% line coverage excluding main.rs. Also: cargo audit (now wired into `cargo make ci`) immediately caught a real, currently-open advisory (RUSTSEC-2026-0285, published days ago) in the transitive rustls dependency — bumped 0.23.44 -> 0.23.45 to clear it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f4bb4cccf
commit
13a1381bdf
12 changed files with 946 additions and 19 deletions
|
|
@ -39,6 +39,12 @@ jobs:
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy --all-targets -- -D warnings
|
run: cargo clippy --all-targets -- -D warnings
|
||||||
|
|
||||||
|
# Cognitive complexity (clippy nursery lint — see clippy.toml for why
|
||||||
|
# not literal cyclomatic complexity), bin target only so test code's
|
||||||
|
# naturally higher complexity isn't gated on this.
|
||||||
|
- name: Complexity
|
||||||
|
run: cargo clippy --bins -- -D warnings -W clippy::cognitive_complexity
|
||||||
|
|
||||||
- name: Check release profile compiles
|
- name: Check release profile compiles
|
||||||
run: cargo check --release
|
run: cargo check --release
|
||||||
|
|
||||||
|
|
@ -70,6 +76,46 @@ jobs:
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test
|
run: cargo test
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
needs: build
|
||||||
|
runs-on: rust-ci
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo registry and build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
cargo-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Cache sccache compilation objects
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /root/.cache/sccache
|
||||||
|
key: sccache-${{ runner.os }}-${{ github.run_id }}
|
||||||
|
restore-keys: |
|
||||||
|
sccache-${{ runner.os }}-
|
||||||
|
|
||||||
|
# Not baked into the rust-ci image (see docker/rust-ci in
|
||||||
|
# infrastructure) — install fresh each run rather than bumping the
|
||||||
|
# shared image just for this one project.
|
||||||
|
- name: Install cargo-llvm-cov
|
||||||
|
run: |
|
||||||
|
rustup component add llvm-tools-preview
|
||||||
|
command -v cargo-llvm-cov >/dev/null 2>&1 || cargo install cargo-llvm-cov --locked
|
||||||
|
|
||||||
|
# Reports coverage only — no --fail-under-lines yet. main.rs is
|
||||||
|
# excluded: thin orchestration glue exercised by the real end-to-end
|
||||||
|
# `cargo run` against live GitHub, not unit tests, so it's not a
|
||||||
|
# meaningful signal here. See Makefile.toml > coverage-report.
|
||||||
|
- name: Coverage
|
||||||
|
run: cargo llvm-cov --ignore-filename-regex 'main\.rs' --summary-only
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: rust-ci
|
runs-on: rust-ci
|
||||||
|
|
|
||||||
322
Cargo.lock
generated
322
Cargo.lock
generated
|
|
@ -17,6 +17,16 @@ version = "1.0.104"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-json-diff"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -117,7 +127,7 @@ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -129,6 +139,15 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.8"
|
version = "4.6.8"
|
||||||
|
|
@ -213,12 +232,34 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|
@ -296,6 +337,18 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi 5.3.0",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -305,11 +358,30 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi 6.0.0",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
@ -361,6 +433,12 @@ version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
|
|
@ -380,9 +458,11 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
@ -630,12 +710,27 @@ version = "0.2.189"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.34"
|
version = "0.4.34"
|
||||||
|
|
@ -665,6 +760,31 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockito"
|
||||||
|
version = "1.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
|
||||||
|
dependencies = [
|
||||||
|
"assert-json-diff",
|
||||||
|
"bytes",
|
||||||
|
"colored",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"regex",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"similar",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -677,6 +797,29 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
|
|
@ -701,10 +844,12 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hex",
|
"hex",
|
||||||
|
"mockito",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -717,6 +862,15 @@ dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
|
|
@ -756,7 +910,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
"rand",
|
"rand 0.10.2",
|
||||||
"rand_pcg",
|
"rand_pcg",
|
||||||
"ring",
|
"ring",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
|
@ -792,12 +946,28 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
|
@ -806,7 +976,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -821,7 +1010,16 @@ version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core",
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -922,10 +1120,23 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustix"
|
||||||
version = "0.23.44"
|
version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -1002,6 +1213,12 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -1020,6 +1237,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
|
@ -1101,6 +1324,18 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -1134,6 +1369,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "similar"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -1210,6 +1451,19 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.20"
|
version = "2.0.20"
|
||||||
|
|
@ -1264,6 +1518,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
|
|
@ -1279,6 +1534,20 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.6+spec-1.1.0"
|
version = "1.1.6+spec-1.1.0"
|
||||||
|
|
@ -1449,6 +1718,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.4+wasi-0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.128"
|
version = "0.2.128"
|
||||||
|
|
@ -1636,6 +1914,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
|
|
@ -1665,6 +1949,26 @@ dependencies = [
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,7 @@ reqwest = { version = "0.13.5", default-features = false, features = ["blocking"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
sha2 = "0.11.0"
|
sha2 = "0.11.0"
|
||||||
toml = "1.1.6"
|
toml = "1.1.6"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockito = "1.7.2"
|
||||||
|
tempfile = "3.27.0"
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,30 @@ args = ["fmt", "--check"]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
args = ["clippy", "--all-targets", "--", "-D", "warnings"]
|
args = ["clippy", "--all-targets", "--", "-D", "warnings"]
|
||||||
|
|
||||||
|
# Cognitive complexity (clippy's nursery lint, not literal cyclomatic
|
||||||
|
# complexity — see clippy.toml for why) on the bin target only: run without
|
||||||
|
# --all-targets so #[cfg(test)] code, which naturally reads as more
|
||||||
|
# "complex" without being a maintainability signal, isn't gated on this.
|
||||||
|
[tasks.complexity]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["clippy", "--bins", "--", "-D", "warnings", "-W", "clippy::cognitive_complexity"]
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
args = ["test"]
|
args = ["test"]
|
||||||
|
|
||||||
|
# Named coverage-report, not coverage: cargo-make reserves "coverage" for
|
||||||
|
# its own built-in tarpaulin/kcov-routing composite task, which clobbers a
|
||||||
|
# same-named custom one.
|
||||||
|
#
|
||||||
|
# Reports coverage only; not gated on a threshold yet — main.rs is thin
|
||||||
|
# orchestration glue exercised by the real end-to-end `cargo run`, not unit
|
||||||
|
# tests, so it's excluded here rather than dragging the number down for
|
||||||
|
# reasons unrelated to test quality.
|
||||||
|
[tasks.coverage-report]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["llvm-cov", "--ignore-filename-regex", "main\\.rs", "--summary-only"]
|
||||||
|
|
||||||
[tasks.audit]
|
[tasks.audit]
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
args = ["audit"]
|
args = ["audit"]
|
||||||
|
|
@ -31,7 +51,7 @@ command = "git"
|
||||||
args = ["config", "core.hooksPath", ".githooks"]
|
args = ["config", "core.hooksPath", ".githooks"]
|
||||||
|
|
||||||
[tasks.ci]
|
[tasks.ci]
|
||||||
dependencies = ["format-check", "lint", "test", "audit"]
|
dependencies = ["format-check", "lint", "complexity", "test", "coverage-report", "audit"]
|
||||||
|
|
||||||
[tasks.default]
|
[tasks.default]
|
||||||
alias = "ci"
|
alias = "ci"
|
||||||
|
|
|
||||||
9
clippy.toml
Normal file
9
clippy.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Shift-left complexity gate (see `cargo make complexity`). Not literal
|
||||||
|
# McCabe cyclomatic complexity — clippy's nursery-tier cognitive_complexity
|
||||||
|
# lint, chosen over the closest real cyclomatic-complexity tool
|
||||||
|
# (rust-code-analysis-cli) because that crate hasn't been released since
|
||||||
|
# early 2023. Threshold set well above the current codebase's max (5, in
|
||||||
|
# `main`) so it flags genuinely tangled control flow without nagging on
|
||||||
|
# today's code; revisit if legitimate future complexity (e.g. the
|
||||||
|
# build/publish pipeline) needs more room.
|
||||||
|
cognitive-complexity-threshold = 15
|
||||||
106
src/checker.rs
106
src/checker.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::github::GithubEndpoints;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
|
@ -14,8 +15,12 @@ use regex::Regex;
|
||||||
/// 404s) right after each real release, and that tag sorts newest in the
|
/// 404s) right after each real release, and that tag sorts newest in the
|
||||||
/// feed. So each candidate is confirmed against the releases API in feed
|
/// feed. So each candidate is confirmed against the releases API in feed
|
||||||
/// order, returning the first that actually resolves.
|
/// order, returning the first that actually resolves.
|
||||||
pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) -> Result<String> {
|
pub fn latest_github_release(
|
||||||
let url = format!("https://github.com/{repo}/releases.atom");
|
client: &reqwest::blocking::Client,
|
||||||
|
endpoints: &GithubEndpoints,
|
||||||
|
repo: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let url = format!("{}/{repo}/releases.atom", endpoints.web);
|
||||||
let body = client.get(&url).send()?.error_for_status()?.text()?;
|
let body = client.get(&url).send()?.error_for_status()?.text()?;
|
||||||
|
|
||||||
let re = Regex::new(r#"releases/tag/([^"]+)""#)?;
|
let re = Regex::new(r#"releases/tag/([^"]+)""#)?;
|
||||||
|
|
@ -28,7 +33,7 @@ pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in candidates {
|
for tag in candidates {
|
||||||
let release_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}");
|
let release_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
|
||||||
if client.get(&release_url).send()?.status().is_success() {
|
if client.get(&release_url).send()?.status().is_success() {
|
||||||
return Ok(tag);
|
return Ok(tag);
|
||||||
}
|
}
|
||||||
|
|
@ -46,3 +51,98 @@ pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) ->
|
||||||
pub fn version_from_tag(tag: &str) -> &str {
|
pub fn version_from_tag(tag: &str) -> &str {
|
||||||
tag.strip_prefix('v').unwrap_or(tag)
|
tag.strip_prefix('v').unwrap_or(tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_from_tag_strips_leading_v() {
|
||||||
|
assert_eq!(version_from_tag("v2.62.0"), "2.62.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_from_tag_leaves_bare_version_unchanged() {
|
||||||
|
assert_eq!(version_from_tag("0.12.15"), "0.12.15");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atom_feed(tags: &[&str]) -> String {
|
||||||
|
let entries: String = tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
format!(r#"<link rel="alternate" href="https://github.com/o/r/releases/tag/{t}"/>"#)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
format!("<feed>{entries}</feed>")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_github_release_skips_tags_with_no_real_release() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mirrors the real scaleway-cli case: newest feed entry (a -dbg1
|
||||||
|
// tag) has no Release object behind it and 404s.
|
||||||
|
let _feed = server
|
||||||
|
.mock("GET", "/o/r/releases.atom")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(atom_feed(&["v2.62.0-dbg1", "v2.62.0"]))
|
||||||
|
.create();
|
||||||
|
let _missing = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v2.62.0-dbg1")
|
||||||
|
.with_status(404)
|
||||||
|
.create();
|
||||||
|
let _real = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v2.62.0")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body("{}")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let tag = latest_github_release(&client, &endpoints, "o/r").unwrap();
|
||||||
|
assert_eq!(tag, "v2.62.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_github_release_errors_when_feed_has_no_tags() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let _feed = server
|
||||||
|
.mock("GET", "/o/r/releases.atom")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body("<feed></feed>")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let err = latest_github_release(&client, &endpoints, "o/r").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("no release tag found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn latest_github_release_errors_when_no_candidate_resolves() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let _feed = server
|
||||||
|
.mock("GET", "/o/r/releases.atom")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(atom_feed(&["v1.0.0-dbg1"]))
|
||||||
|
.create();
|
||||||
|
let _missing = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0-dbg1")
|
||||||
|
.with_status(404)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let err = latest_github_release(&client, &endpoints, "o/r").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("resolved to a real release"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
116
src/config.rs
116
src/config.rs
|
|
@ -58,3 +58,119 @@ pub fn load_packages_dir(dir: &Path) -> Result<Vec<(String, Package)>> {
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn write(dir: &Path, name: &str, contents: &str) {
|
||||||
|
std::fs::write(dir.join(name), contents).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tier_matches_verification_method() {
|
||||||
|
assert_eq!(
|
||||||
|
Verification::SameOriginSha256 {
|
||||||
|
checksum_asset_pattern: "x".to_string()
|
||||||
|
}
|
||||||
|
.tier(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
assert_eq!(Verification::GithubAttestation.tier(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_same_origin_sha256_package() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
dir.path(),
|
||||||
|
"pkg.toml",
|
||||||
|
r#"
|
||||||
|
[package.pkg]
|
||||||
|
repo = "o/r"
|
||||||
|
asset_pattern = "pkg_{version}_linux_amd64"
|
||||||
|
|
||||||
|
[package.pkg.verification]
|
||||||
|
method = "same-origin-sha256"
|
||||||
|
checksum_asset_pattern = "SHA256SUMS"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let packages = load_packages_dir(dir.path()).unwrap();
|
||||||
|
assert_eq!(packages.len(), 1);
|
||||||
|
let (name, pkg) = &packages[0];
|
||||||
|
assert_eq!(name, "pkg");
|
||||||
|
assert_eq!(pkg.repo, "o/r");
|
||||||
|
assert_eq!(pkg.asset_pattern, "pkg_{version}_linux_amd64");
|
||||||
|
assert_eq!(pkg.verification.tier(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_github_attestation_package() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
dir.path(),
|
||||||
|
"pkg.toml",
|
||||||
|
r#"
|
||||||
|
[package.pkg]
|
||||||
|
repo = "o/r"
|
||||||
|
asset_pattern = "pkg-x86_64.tar.gz"
|
||||||
|
|
||||||
|
[package.pkg.verification]
|
||||||
|
method = "github-attestation"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let packages = load_packages_dir(dir.path()).unwrap();
|
||||||
|
assert_eq!(packages[0].1.verification.tier(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_multiple_files_and_ignores_non_toml() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
write(
|
||||||
|
dir.path(),
|
||||||
|
"a.toml",
|
||||||
|
r#"
|
||||||
|
[package.a]
|
||||||
|
repo = "o/a"
|
||||||
|
asset_pattern = "a.tar.gz"
|
||||||
|
[package.a.verification]
|
||||||
|
method = "github-attestation"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
write(
|
||||||
|
dir.path(),
|
||||||
|
"b.toml",
|
||||||
|
r#"
|
||||||
|
[package.b]
|
||||||
|
repo = "o/b"
|
||||||
|
asset_pattern = "b.tar.gz"
|
||||||
|
[package.b.verification]
|
||||||
|
method = "github-attestation"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
write(dir.path(), "README.md", "not a package file");
|
||||||
|
|
||||||
|
let mut names: Vec<String> = load_packages_dir(dir.path())
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _)| name)
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
assert_eq!(names, vec!["a", "b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn errors_on_invalid_toml() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
write(dir.path(), "bad.toml", "not valid toml [[[");
|
||||||
|
assert!(load_packages_dir(dir.path()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_dir_yields_empty_list() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
assert!(load_packages_dir(dir.path()).unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
100
src/fetcher.rs
100
src/fetcher.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::github::GithubEndpoints;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -17,12 +18,13 @@ struct Asset {
|
||||||
/// into `dest_dir`, returning the local path.
|
/// into `dest_dir`, returning the local path.
|
||||||
pub fn download_asset(
|
pub fn download_asset(
|
||||||
client: &reqwest::blocking::Client,
|
client: &reqwest::blocking::Client,
|
||||||
|
endpoints: &GithubEndpoints,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
asset_name: &str,
|
asset_name: &str,
|
||||||
dest_dir: &Path,
|
dest_dir: &Path,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
let api_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}");
|
let api_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
|
||||||
let release: Release = client
|
let release: Release = client
|
||||||
.get(&api_url)
|
.get(&api_url)
|
||||||
.send()?
|
.send()?
|
||||||
|
|
@ -46,3 +48,99 @@ pub fn download_asset(
|
||||||
std::fs::write(&dest_path, &bytes)?;
|
std::fs::write(&dest_path, &bytes)?;
|
||||||
Ok(dest_path)
|
Ok(dest_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn download_asset_writes_matching_asset_to_dest_dir() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let asset_url = format!("{}/download/thing.tar.gz", server.url());
|
||||||
|
let release_body = format!(
|
||||||
|
r#"{{"assets": [{{"name": "thing.tar.gz", "browser_download_url": "{asset_url}"}}]}}"#
|
||||||
|
);
|
||||||
|
let _release = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(release_body)
|
||||||
|
.create();
|
||||||
|
let _download = server
|
||||||
|
.mock("GET", "/download/thing.tar.gz")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(b"artifact-bytes".as_slice())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let dest_dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = download_asset(
|
||||||
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
"o/r",
|
||||||
|
"v1.0.0",
|
||||||
|
"thing.tar.gz",
|
||||||
|
dest_dir.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(path, dest_dir.path().join("thing.tar.gz"));
|
||||||
|
assert_eq!(std::fs::read(&path).unwrap(), b"artifact-bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn download_asset_errors_when_no_asset_matches() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let _release = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(r#"{"assets": [{"name": "other", "browser_download_url": "https://example.invalid/other"}]}"#)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let dest_dir = tempfile::tempdir().unwrap();
|
||||||
|
let err = download_asset(
|
||||||
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
"o/r",
|
||||||
|
"v1.0.0",
|
||||||
|
"thing.tar.gz",
|
||||||
|
dest_dir.path(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("no asset named"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn download_asset_errors_when_release_not_found() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let _release = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
||||||
|
.with_status(404)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let dest_dir = tempfile::tempdir().unwrap();
|
||||||
|
let err = download_asset(
|
||||||
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
"o/r",
|
||||||
|
"v1.0.0",
|
||||||
|
"thing.tar.gz",
|
||||||
|
dest_dir.path(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("fetching release metadata"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
29
src/github.rs
Normal file
29
src/github.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/// Base URLs for GitHub's public web host (Atom feeds, release pages) and
|
||||||
|
/// its REST API, factored out so tests can point both at a local mock
|
||||||
|
/// server instead of the real github.com/api.github.com.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GithubEndpoints {
|
||||||
|
pub web: String,
|
||||||
|
pub api: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GithubEndpoints {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
web: "https://github.com".to_string(),
|
||||||
|
api: "https://api.github.com".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_points_at_real_github() {
|
||||||
|
let endpoints = GithubEndpoints::default();
|
||||||
|
assert_eq!(endpoints.web, "https://github.com");
|
||||||
|
assert_eq!(endpoints.api, "https://api.github.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main.rs
16
src/main.rs
|
|
@ -1,10 +1,12 @@
|
||||||
mod checker;
|
mod checker;
|
||||||
mod config;
|
mod config;
|
||||||
mod fetcher;
|
mod fetcher;
|
||||||
|
mod github;
|
||||||
mod state;
|
mod state;
|
||||||
mod verifier;
|
mod verifier;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use github::GithubEndpoints;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// First iteration: check -> fetch -> verify -> report, for whatever is
|
/// First iteration: check -> fetch -> verify -> report, for whatever is
|
||||||
|
|
@ -13,6 +15,7 @@ fn main() -> Result<()> {
|
||||||
let client = reqwest::blocking::Client::builder()
|
let client = reqwest::blocking::Client::builder()
|
||||||
.user_agent("pkgwatch/0.1 (PoC; https://code.austinschaefer.com)")
|
.user_agent("pkgwatch/0.1 (PoC; https://code.austinschaefer.com)")
|
||||||
.build()?;
|
.build()?;
|
||||||
|
let endpoints = GithubEndpoints::default();
|
||||||
|
|
||||||
let packages_dir = Path::new("packages.d");
|
let packages_dir = Path::new("packages.d");
|
||||||
let state_dir = Path::new("state");
|
let state_dir = Path::new("state");
|
||||||
|
|
@ -27,7 +30,7 @@ fn main() -> Result<()> {
|
||||||
for (name, pkg) in packages {
|
for (name, pkg) in packages {
|
||||||
println!("== {name} ({}) ==", pkg.repo);
|
println!("== {name} ({}) ==", pkg.repo);
|
||||||
|
|
||||||
let latest = checker::latest_github_release(&client, &pkg.repo)?;
|
let latest = checker::latest_github_release(&client, &endpoints, &pkg.repo)?;
|
||||||
let last_seen = state::load_last_version(state_dir, &name);
|
let last_seen = state::load_last_version(state_dir, &name);
|
||||||
|
|
||||||
if last_seen.as_deref() == Some(latest.as_str()) {
|
if last_seen.as_deref() == Some(latest.as_str()) {
|
||||||
|
|
@ -41,12 +44,19 @@ fn main() -> Result<()> {
|
||||||
let asset_name = pkg
|
let asset_name = pkg
|
||||||
.asset_pattern
|
.asset_pattern
|
||||||
.replace("{version}", checker::version_from_tag(&latest));
|
.replace("{version}", checker::version_from_tag(&latest));
|
||||||
let artifact_path =
|
let artifact_path = fetcher::download_asset(
|
||||||
fetcher::download_asset(&client, &pkg.repo, &latest, &asset_name, &dest_dir)?;
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
&pkg.repo,
|
||||||
|
&latest,
|
||||||
|
&asset_name,
|
||||||
|
&dest_dir,
|
||||||
|
)?;
|
||||||
println!(" fetched {}", artifact_path.display());
|
println!(" fetched {}", artifact_path.display());
|
||||||
|
|
||||||
let result = verifier::verify(
|
let result = verifier::verify(
|
||||||
&client,
|
&client,
|
||||||
|
&endpoints,
|
||||||
&pkg.verification,
|
&pkg.verification,
|
||||||
&pkg.repo,
|
&pkg.repo,
|
||||||
&latest,
|
&latest,
|
||||||
|
|
|
||||||
42
src/state.rs
42
src/state.rs
|
|
@ -16,3 +16,45 @@ pub fn save_last_version(state_dir: &Path, name: &str, version: &str) -> Result<
|
||||||
std::fs::write(state_dir.join(format!("{name}.version")), version)?;
|
std::fs::write(state_dir.join(format!("{name}.version")), version)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_last_version_missing_file_returns_none() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
assert_eq!(load_last_version(dir.path(), "nonexistent"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_then_load_roundtrips() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
save_last_version(dir.path(), "uv", "0.12.15").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
load_last_version(dir.path(), "uv"),
|
||||||
|
Some("0.12.15".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_last_version_trims_trailing_whitespace() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::write(dir.path().join("uv.version"), "0.12.15\n").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
load_last_version(dir.path(), "uv"),
|
||||||
|
Some("0.12.15".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_last_version_creates_state_dir_if_missing() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let nested = dir.path().join("nested/state");
|
||||||
|
save_last_version(&nested, "uv", "0.12.15").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
load_last_version(&nested, "uv"),
|
||||||
|
Some("0.12.15".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
153
src/verifier.rs
153
src/verifier.rs
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::checker::version_from_tag;
|
use crate::checker::version_from_tag;
|
||||||
use crate::config::Verification;
|
use crate::config::Verification;
|
||||||
use crate::fetcher;
|
use crate::fetcher;
|
||||||
|
use crate::github::GithubEndpoints;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -17,6 +18,7 @@ pub struct VerificationResult {
|
||||||
/// each tier does and does not prove.
|
/// each tier does and does not prove.
|
||||||
pub fn verify(
|
pub fn verify(
|
||||||
client: &reqwest::blocking::Client,
|
client: &reqwest::blocking::Client,
|
||||||
|
endpoints: &GithubEndpoints,
|
||||||
verification: &Verification,
|
verification: &Verification,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
|
|
@ -29,8 +31,14 @@ pub fn verify(
|
||||||
} => {
|
} => {
|
||||||
let checksum_asset_name =
|
let checksum_asset_name =
|
||||||
checksum_asset_pattern.replace("{version}", version_from_tag(tag));
|
checksum_asset_pattern.replace("{version}", version_from_tag(tag));
|
||||||
let checksum_path =
|
let checksum_path = fetcher::download_asset(
|
||||||
fetcher::download_asset(client, repo, tag, &checksum_asset_name, dest_dir)?;
|
client,
|
||||||
|
endpoints,
|
||||||
|
repo,
|
||||||
|
tag,
|
||||||
|
&checksum_asset_name,
|
||||||
|
dest_dir,
|
||||||
|
)?;
|
||||||
let checksum_text = std::fs::read_to_string(&checksum_path)?;
|
let checksum_text = std::fs::read_to_string(&checksum_path)?;
|
||||||
let artifact_name = artifact_path
|
let artifact_name = artifact_path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
|
@ -127,3 +135,144 @@ fn expected_checksum(checksum_text: &str, artifact_name: &str) -> Result<String>
|
||||||
lines.len()
|
lines.len()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::Verification;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_bare_single_hash_file() {
|
||||||
|
let text = "f97935763c04be3e692460a7aaeaaab8fc3b78fcf8b389da820b38ae7423a638\n";
|
||||||
|
assert_eq!(
|
||||||
|
expected_checksum(text, "anything.tar.gz").unwrap(),
|
||||||
|
"f97935763c04be3e692460a7aaeaaab8fc3b78fcf8b389da820b38ae7423a638"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_matches_gnu_style_line_by_filename() {
|
||||||
|
let text = "aaaa other-asset\nbbbb scaleway-cli_2.62.0_linux_amd64\ncccc more\n";
|
||||||
|
assert_eq!(
|
||||||
|
expected_checksum(text, "scaleway-cli_2.62.0_linux_amd64").unwrap(),
|
||||||
|
"bbbb"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_matches_bsd_style_line_strips_star() {
|
||||||
|
let text = "aaaa *source.tar.gz\nbbbb *thing.tar.gz\n";
|
||||||
|
assert_eq!(expected_checksum(text, "thing.tar.gz").unwrap(), "bbbb");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_is_case_insensitive() {
|
||||||
|
let text = "ABCDEF thing.tar.gz\n";
|
||||||
|
assert_eq!(expected_checksum(text, "thing.tar.gz").unwrap(), "abcdef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_errors_when_multi_line_file_has_no_match() {
|
||||||
|
let err = expected_checksum("aaaa other\nbbbb another\n", "thing.tar.gz").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("no checksum entry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_checksum_errors_on_empty_file() {
|
||||||
|
let err = expected_checksum("", "thing.tar.gz").unwrap_err();
|
||||||
|
assert!(err.to_string().contains("no checksum entry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_same_origin_sha256_passes_on_matching_checksum() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let dest_dir = tempfile::tempdir().unwrap();
|
||||||
|
let artifact_path = dest_dir.path().join("thing.tar.gz");
|
||||||
|
std::fs::write(&artifact_path, b"hello world").unwrap();
|
||||||
|
let expected_hash = sha256_hex(b"hello world");
|
||||||
|
|
||||||
|
let release_url = format!("{}/download/SHA256SUMS", server.url());
|
||||||
|
let release_body = format!(
|
||||||
|
r#"{{"assets": [{{"name": "SHA256SUMS", "browser_download_url": "{release_url}"}}]}}"#
|
||||||
|
);
|
||||||
|
let _release = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(release_body)
|
||||||
|
.create();
|
||||||
|
let _sums = server
|
||||||
|
.mock("GET", "/download/SHA256SUMS")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(format!("{expected_hash} thing.tar.gz\n"))
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let verification = Verification::SameOriginSha256 {
|
||||||
|
checksum_asset_pattern: "SHA256SUMS".to_string(),
|
||||||
|
};
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let result = verify(
|
||||||
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
&verification,
|
||||||
|
"o/r",
|
||||||
|
"v1.0.0",
|
||||||
|
&artifact_path,
|
||||||
|
dest_dir.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.passed);
|
||||||
|
assert_eq!(result.tier, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_same_origin_sha256_fails_on_mismatched_checksum() {
|
||||||
|
let mut server = mockito::Server::new();
|
||||||
|
let endpoints = GithubEndpoints {
|
||||||
|
web: server.url(),
|
||||||
|
api: server.url(),
|
||||||
|
};
|
||||||
|
let dest_dir = tempfile::tempdir().unwrap();
|
||||||
|
let artifact_path = dest_dir.path().join("thing.tar.gz");
|
||||||
|
std::fs::write(&artifact_path, b"hello world").unwrap();
|
||||||
|
|
||||||
|
let release_url = format!("{}/download/SHA256SUMS", server.url());
|
||||||
|
let release_body = format!(
|
||||||
|
r#"{{"assets": [{{"name": "SHA256SUMS", "browser_download_url": "{release_url}"}}]}}"#
|
||||||
|
);
|
||||||
|
let _release = server
|
||||||
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(release_body)
|
||||||
|
.create();
|
||||||
|
let _sums = server
|
||||||
|
.mock("GET", "/download/SHA256SUMS")
|
||||||
|
.with_status(200)
|
||||||
|
.with_body(
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000 thing.tar.gz\n",
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let verification = Verification::SameOriginSha256 {
|
||||||
|
checksum_asset_pattern: "SHA256SUMS".to_string(),
|
||||||
|
};
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let result = verify(
|
||||||
|
&client,
|
||||||
|
&endpoints,
|
||||||
|
&verification,
|
||||||
|
"o/r",
|
||||||
|
"v1.0.0",
|
||||||
|
&artifact_path,
|
||||||
|
dest_dir.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!result.passed);
|
||||||
|
assert!(result.justification.contains("mismatch"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue