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;
|
2026-09-20 08:36:13 +00:00
|
|
|
use crate::source::Endpoints;
|
|
|
|
|
use anyhow::{Context, Result, bail};
|
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 regex::Regex;
|
2026-09-20 08:36:13 +00:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
|
|
|
|
/// Resolves the latest release tag for `repo` on whichever service
|
|
|
|
|
/// `endpoints` points at.
|
|
|
|
|
pub fn latest_release(
|
|
|
|
|
client: &reqwest::blocking::Client,
|
|
|
|
|
endpoints: &Endpoints,
|
|
|
|
|
repo: &str,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
match endpoints {
|
|
|
|
|
Endpoints::Github(github) => latest_github_release(client, github, repo),
|
|
|
|
|
Endpoints::Forgejo { api } => latest_forgejo_release(client, api, repo),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolves the latest release tag for `repo` on a Forgejo/Gitea instance.
|
|
|
|
|
///
|
|
|
|
|
/// One call, unlike GitHub's feed-then-confirm dance below: Forgejo's
|
|
|
|
|
/// `releases/latest` already returns only the newest non-draft,
|
|
|
|
|
/// non-prerelease *release object*, so a stray tag with no release behind
|
|
|
|
|
/// it (GitHub's scaleway-cli `-dbg1` problem) can't be returned.
|
|
|
|
|
pub fn latest_forgejo_release(
|
|
|
|
|
client: &reqwest::blocking::Client,
|
|
|
|
|
api: &str,
|
|
|
|
|
repo: &str,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct Latest {
|
|
|
|
|
tag_name: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let url = format!("{api}/repos/{repo}/releases/latest");
|
|
|
|
|
let response = client.get(&url).send()?;
|
|
|
|
|
// Forgejo answers 404 both for an unknown repo and for one with no
|
|
|
|
|
// releases yet — the common state for a project's very first release.
|
|
|
|
|
if response.status() == reqwest::StatusCode::NOT_FOUND {
|
|
|
|
|
bail!("no published release found at {url} (repo missing, or nothing released yet)");
|
|
|
|
|
}
|
|
|
|
|
let latest: Latest = response
|
|
|
|
|
.error_for_status()
|
|
|
|
|
.with_context(|| format!("fetching latest release from {url}"))?
|
|
|
|
|
.json()?;
|
|
|
|
|
Ok(latest.tag_name)
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
/// Resolves the latest release tag for `repo` via its public Atom feed.
|
|
|
|
|
///
|
2026-09-17 07:00:53 +00:00
|
|
|
/// Deliberately not a full XML parse: the feed lists entries newest-first,
|
|
|
|
|
/// and each `<link rel="alternate" .../releases/tag/<tag>"/>` is matched
|
|
|
|
|
/// in document order. Revisit with a real XML parser if GitHub's feed
|
|
|
|
|
/// shape ever changes.
|
|
|
|
|
///
|
|
|
|
|
/// The feed can list a tag newer than any tag with a real Release object
|
|
|
|
|
/// behind it — observed on scaleway/scaleway-cli, which pushes a
|
|
|
|
|
/// `vX.Y.Z-dbg1` tag (no corresponding Release; `releases/tags/<tag>`
|
|
|
|
|
/// 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
|
|
|
|
|
/// order, returning the first that actually resolves.
|
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
|
|
|
pub fn latest_github_release(
|
|
|
|
|
client: &reqwest::blocking::Client,
|
|
|
|
|
endpoints: &GithubEndpoints,
|
|
|
|
|
repo: &str,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
let url = format!("{}/{repo}/releases.atom", endpoints.web);
|
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 body = client.get(&url).send()?.error_for_status()?.text()?;
|
|
|
|
|
|
Set up project tooling to match the rest of ~/dev's Rust fleet
Surveyed sporah/doubleo7/feedsignal/uy-immigration-watcher/notif-picker
for conventions and replicated the current dominant pattern rather than
inventing a new one:
- Forgejo CI (.forgejo/workflows/ci.yml): build/test/audit jobs on the
rust-ci runner label, cargo+sccache caching, cargo fmt --check, cargo
clippy --all-targets -- -D warnings, cargo audit. Matches
sporah/doubleo7/feedsignal/uy-immigration-watcher; notif-picker's
docker-label/manual-toolchain-install variant looks like an earlier
iteration superseded by this one.
- Makefile.toml with format/format-check/lint/test/audit/build tasks and
a `ci` task chaining them — copied from notif-picker's clean version,
the only project that had this pattern. `cargo make ci` now runs the
same checks locally that CI runs.
- Explicit empty [workspace] in Cargo.toml (doubleo7's pattern) so a
nested git-worktree checkout can't accidentally inherit an ancestor
directory's workspace manifest.
- rustfmt: no rustfmt.toml, matching every sibling project — default
style is the established convention here, not an oversight.
New for this fleet, since nothing else in ~/dev has it: a git-native
pre-commit hook (.githooks/pre-commit, activated via `cargo make
install-hooks` / `git config core.hooksPath .githooks`) that runs `cargo
fmt` and re-stages whatever it reformats. Chose git's native hooksPath
over the pre-commit(.com) framework or cargo-husky — no extra runtime
dependency, hook is tracked and shareable, and nothing else here needs
Python. Kept to formatting only; clippy/audit stay in CI, which already
covers them and can run heavier checks than a commit hook should.
Fixed one clippy finding (useless format! in checker.rs) and reformatted
the existing code to match the now-enforced default rustfmt style.
`cargo make ci` passes clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:13:29 +00:00
|
|
|
let re = Regex::new(r#"releases/tag/([^"]+)""#)?;
|
2026-09-17 07:00:53 +00:00
|
|
|
let mut candidates = re
|
|
|
|
|
.captures_iter(&body)
|
|
|
|
|
.map(|caps| caps[1].to_string())
|
|
|
|
|
.peekable();
|
|
|
|
|
if candidates.peek().is_none() {
|
|
|
|
|
bail!("no release tag found in {url}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for tag in candidates {
|
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 release_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
|
2026-09-17 07:00:53 +00:00
|
|
|
if client.get(&release_url).send()?.status().is_success() {
|
|
|
|
|
return Ok(tag);
|
|
|
|
|
}
|
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
|
|
|
}
|
2026-09-17 07:00:53 +00:00
|
|
|
bail!("no release tag in {url} resolved to a real release via the API")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Strips a leading `v` from a release tag, e.g. `v2.62.0` -> `2.62.0`.
|
|
|
|
|
///
|
|
|
|
|
/// Some projects (uv) tag releases with the bare version and use it
|
|
|
|
|
/// verbatim in asset filenames; others (scaleway-cli) tag `vX.Y.Z` but
|
|
|
|
|
/// still use the bare version in filenames. This is the version string
|
|
|
|
|
/// substituted into `{version}` placeholders in `asset_pattern` /
|
|
|
|
|
/// `checksum_asset_pattern`, not the tag used for API/attestation calls.
|
|
|
|
|
pub fn version_from_tag(tag: &str) -> &str {
|
|
|
|
|
tag.strip_prefix('v').unwrap_or(tag)
|
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
|
|
|
}
|
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 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"));
|
|
|
|
|
}
|
2026-09-20 08:36:13 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn latest_forgejo_release_returns_tag_name() {
|
|
|
|
|
let mut server = mockito::Server::new();
|
|
|
|
|
let _latest = server
|
|
|
|
|
.mock("GET", "/repos/o/r/releases/latest")
|
|
|
|
|
.with_status(200)
|
|
|
|
|
.with_body(r#"{"tag_name": "v0.1.0", "assets": []}"#)
|
|
|
|
|
.create();
|
|
|
|
|
|
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
|
let tag = latest_forgejo_release(&client, &server.url(), "o/r").unwrap();
|
|
|
|
|
assert_eq!(tag, "v0.1.0");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn latest_forgejo_release_names_the_no_releases_case() {
|
|
|
|
|
let mut server = mockito::Server::new();
|
|
|
|
|
let _latest = server
|
|
|
|
|
.mock("GET", "/repos/o/r/releases/latest")
|
|
|
|
|
.with_status(404)
|
|
|
|
|
.create();
|
|
|
|
|
|
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
|
let err = latest_forgejo_release(&client, &server.url(), "o/r").unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("no published release"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn latest_forgejo_release_surfaces_server_errors() {
|
|
|
|
|
let mut server = mockito::Server::new();
|
|
|
|
|
let _latest = server
|
|
|
|
|
.mock("GET", "/repos/o/r/releases/latest")
|
|
|
|
|
.with_status(500)
|
|
|
|
|
.create();
|
|
|
|
|
|
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
|
let err = latest_forgejo_release(&client, &server.url(), "o/r").unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("fetching latest release"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn latest_release_dispatches_on_endpoint_kind() {
|
|
|
|
|
let mut server = mockito::Server::new();
|
|
|
|
|
let _forgejo = server
|
|
|
|
|
.mock("GET", "/repos/o/r/releases/latest")
|
|
|
|
|
.with_status(200)
|
|
|
|
|
.with_body(r#"{"tag_name": "v2.0.0"}"#)
|
|
|
|
|
.create();
|
|
|
|
|
let _feed = server
|
|
|
|
|
.mock("GET", "/o/r/releases.atom")
|
|
|
|
|
.with_body(atom_feed(&["v1.0.0"]))
|
|
|
|
|
.create();
|
|
|
|
|
let _release = server
|
|
|
|
|
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
|
|
|
|
|
.with_status(200)
|
|
|
|
|
.with_body("{}")
|
|
|
|
|
.create();
|
|
|
|
|
|
|
|
|
|
let client = reqwest::blocking::Client::new();
|
|
|
|
|
let forgejo = Endpoints::Forgejo { api: server.url() };
|
|
|
|
|
assert_eq!(latest_release(&client, &forgejo, "o/r").unwrap(), "v2.0.0");
|
|
|
|
|
|
|
|
|
|
let github = Endpoints::Github(GithubEndpoints {
|
|
|
|
|
web: server.url(),
|
|
|
|
|
api: server.url(),
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(latest_release(&client, &github, "o/r").unwrap(), "v1.0.0");
|
|
|
|
|
}
|
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
|
|
|
}
|