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::Result;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
/// Last-known-published version per package, so re-runs don't re-flag a
|
|
|
|
|
/// version already handled. Deliberately just one file per package for
|
|
|
|
|
/// now — this is where a real review-queue persistence layer plugs in
|
|
|
|
|
/// later (see SPEC.md > Architecture > Reviewer queue).
|
|
|
|
|
pub fn load_last_version(state_dir: &Path, name: &str) -> Option<String> {
|
|
|
|
|
std::fs::read_to_string(state_dir.join(format!("{name}.version")))
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save_last_version(state_dir: &Path, name: &str, version: &str) -> Result<()> {
|
|
|
|
|
std::fs::create_dir_all(state_dir)?;
|
|
|
|
|
std::fs::write(state_dir.join(format!("{name}.version")), version)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
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 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())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|