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>
2026-09-17 07:16:14 +00:00
|
|
|
use crate::github::GithubEndpoints;
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct Release {
|
|
|
|
|
assets: Vec<Asset>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct Asset {
|
|
|
|
|
name: String,
|
|
|
|
|
browser_download_url: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Downloads the release asset named exactly `asset_name` for `repo`@`tag`
|
|
|
|
|
/// into `dest_dir`, returning the local path.
|
|
|
|
|
pub fn download_asset(
|
|
|
|
|
client: &reqwest::blocking::Client,
|
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>
2026-09-17 07:16:14 +00:00
|
|
|
endpoints: &GithubEndpoints,
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
repo: &str,
|
|
|
|
|
tag: &str,
|
|
|
|
|
asset_name: &str,
|
|
|
|
|
dest_dir: &Path,
|
|
|
|
|
) -> Result<PathBuf> {
|
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>
2026-09-17 07:16:14 +00:00
|
|
|
let api_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
let release: Release = client
|
|
|
|
|
.get(&api_url)
|
|
|
|
|
.send()?
|
|
|
|
|
.error_for_status()
|
|
|
|
|
.with_context(|| format!("fetching release metadata from {api_url}"))?
|
|
|
|
|
.json()?;
|
|
|
|
|
|
|
|
|
|
let asset = release
|
|
|
|
|
.assets
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.name == asset_name)
|
|
|
|
|
.with_context(|| format!("no asset named '{asset_name}' in {repo}@{tag}"))?;
|
|
|
|
|
|
|
|
|
|
std::fs::create_dir_all(dest_dir)?;
|
|
|
|
|
let dest_path = dest_dir.join(&asset.name);
|
|
|
|
|
let bytes = client
|
|
|
|
|
.get(&asset.browser_download_url)
|
|
|
|
|
.send()?
|
|
|
|
|
.error_for_status()?
|
|
|
|
|
.bytes()?;
|
|
|
|
|
std::fs::write(&dest_path, &bytes)?;
|
|
|
|
|
Ok(dest_path)
|
|
|
|
|
}
|
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>
2026-09-17 07:16:14 +00:00
|
|
|
|
|
|
|
|
#[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"));
|
|
|
|
|
}
|
|
|
|
|
}
|