Group release sources into a source module with re-exports
All checks were successful
CI / build (pull_request) Successful in 35s
CI / test (pull_request) Successful in 2m26s
CI / audit (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 6m11s

Move the ReleaseSource trait, GithubEndpoints and ForgejoEndpoints under
src/source/, each in its own file (release_source.rs, github.rs,
forgejo.rs). The submodules are private; mod.rs re-exports their types and
holds for_package, so the rest of the crate imports from crate::source and
never names a host's file. checker.rs keeps only version_from_tag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Austin Schaefer 2026-09-20 11:08:25 +02:00
parent ba2c5c2d02
commit 6459856aab
10 changed files with 52 additions and 43 deletions

View file

@ -35,8 +35,10 @@ practice rather than asserted from habit — see Further reading.
`helpers/`) tends toward the opposite, and a single feature change ends `helpers/`) tends toward the opposite, and a single feature change ends
up touching files scattered across every layer. up touching files scattered across every layer.
**Rule**: modules are named after what they do in the pipeline **Rule**: modules are named after what they do in the pipeline
(`checker`, `fetcher`, `verifier`, `builder`, `sanity`, `publisher`, (`source`, `fetcher`, `verifier`, `builder`, `sanity`, `publisher`,
`state`), not generic buckets. A new pipeline stage gets a new module `state`), not generic buckets. (`source` is a directory module: the
`ReleaseSource` trait and one file per host, re-exported from its
`mod.rs` so the rest of the crate never names a host's file.) A new pipeline stage gets a new module
named after the stage, not a method bolted onto an existing one. named after the stage, not a method bolted onto an existing one.
**Anti-example to keep watching for**: a `utils.rs` grab-bag. `hash.rs` **Anti-example to keep watching for**: a `utils.rs` grab-bag. `hash.rs`
could look like one but isn't — it exists for exactly one piece of could look like one but isn't — it exists for exactly one piece of
@ -107,7 +109,7 @@ practice rather than asserted from habit — see Further reading.
directory, but the same information — why this way and not the obvious directory, but the same information — why this way and not the obvious
alternative — needs to live somewhere a future reader will actually see alternative — needs to live somewhere a future reader will actually see
it: the doc comment on the thing itself. it: the doc comment on the thing itself.
**Example already here**: `github.rs`'s doc comment on **Example already here**: `source/github.rs`'s doc comment on
`GithubEndpoints::latest_release` explains why the newest Atom-feed entry isn't `GithubEndpoints::latest_release` explains why the newest Atom-feed entry isn't
trusted outright (scaleway-cli's `-dbg1` tag has no real Release behind trusted outright (scaleway-cli's `-dbg1` tag has no real Release behind
it) — the reasoning lives right next to the code it justifies, not in a it) — the reasoning lives right next to the code it justifies, not in a

View file

@ -373,10 +373,12 @@ Open questions on the schema:
likely reuses `nvchecker`'s logic/sources conceptually for non-GitHub likely reuses `nvchecker`'s logic/sources conceptually for non-GitHub
sources eventually. For GitHub sources, prefers the `github-atom` feed sources eventually. For GitHub sources, prefers the `github-atom` feed
(see Scaling > Check method) over unconditional REST polling. (see Scaling > Check method) over unconditional REST polling.
*(Implemented for GitHub and Forgejo. `src/checker.rs` defines the *(Implemented for GitHub and Forgejo. `src/source/` defines the
`ReleaseSource` trait (latest release + API root); each host implements `ReleaseSource` trait (latest release + API root); each host implements
it in its own module (`github.rs`, `forgejo.rs`), and `source.rs` picks it in its own file (`github.rs`, `forgejo.rs`), and the module's
one per package, so adding a host doesn't touch existing ones. A trait `for_package` picks one per package, so adding a host doesn't touch
existing ones. The rest of the crate imports the trait and hosts from
`crate::source`, which re-exports them. A trait
rather than an enum match because there are now two real hosts with rather than an enum match because there are now two real hosts with
genuinely different logic. GitHub regex-matches the first genuinely different logic. GitHub regex-matches the first
`releases/tag/<tag>` link in the feed rather than doing a full XML `releases/tag/<tag>` link in the feed rather than doing a full XML

View file

@ -1,25 +1,6 @@
//! The check stage's contract: a `ReleaseSource` says what a package's //! Turns a release tag into a version string. What the latest tag *is*
//! latest release is and where its releases API lives, and //! comes from a `ReleaseSource` (see `source`); this is the one piece of
//! `version_from_tag` turns the resulting tag into a version string. Each //! the check stage that isn't host-specific.
//! hosting service implements the trait in its own module (`github`,
//! `forgejo`) so per-host logic doesn't accumulate here;
//! `source::for_package` picks the implementation for a package.
use anyhow::Result;
/// A hosting service a package's releases are published on. `Debug` so a
/// `Box<dyn ReleaseSource>` can sit in a `Result` that tests unwrap.
pub trait ReleaseSource: std::fmt::Debug {
/// The latest release tag for `repo`.
fn latest_release(&self, client: &reqwest::blocking::Client, repo: &str) -> Result<String>;
/// The releases API root, which `fetcher` and `verifier` build their
/// own paths under. GitHub and Forgejo both serve
/// `/repos/{owner}/{repo}/releases/tags/{tag}` there with the same
/// `assets[].{name, browser_download_url}` shape, which is why those
/// stages need only this and not the source itself.
fn api(&self) -> &str;
}
/// Strips a leading `v` from a release tag, e.g. `v2.62.0` -> `2.62.0`. /// Strips a leading `v` from a release tag, e.g. `v2.62.0` -> `2.62.0`.
/// ///

View file

@ -1,6 +1,6 @@
//! Downloads a named release asset (from GitHub or Forgejo) to a local //! Downloads a named release asset (from GitHub or Forgejo) to a local
//! path. The only module that talks to the releases API for asset bytes — //! path. The only module that talks to the releases API for asset bytes —
//! `checker` only resolves version tags, never downloads. //! `source` only resolves version tags, never downloads.
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;

View file

@ -6,8 +6,6 @@ mod builder;
mod checker; mod checker;
mod config; mod config;
mod fetcher; mod fetcher;
mod forgejo;
mod github;
mod hash; mod hash;
mod notifier; mod notifier;
mod paths; mod paths;

View file

@ -6,14 +6,14 @@
//! `main.rs`. //! `main.rs`.
use crate::builder; use crate::builder;
use crate::checker::{self, ReleaseSource}; use crate::checker;
use crate::config::{self, Package}; use crate::config::{self, Package};
use crate::fetcher::{self, DownloadedAsset}; use crate::fetcher::{self, DownloadedAsset};
use crate::notifier::{self, Event}; use crate::notifier::{self, Event};
use crate::paths::Paths; use crate::paths::Paths;
use crate::publisher; use crate::publisher;
use crate::sanity; use crate::sanity;
use crate::source; use crate::source::{self, ReleaseSource};
use crate::state; use crate::state;
use crate::verifier::{self, VerificationResult}; use crate::verifier::{self, VerificationResult};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
@ -353,8 +353,7 @@ fn approve(state_dir: &Path, work_dir: &Path, name: &str, pkg: &Package, tag: &s
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::forgejo::ForgejoEndpoints; use crate::source::{ForgejoEndpoints, GithubEndpoints};
use crate::github::GithubEndpoints;
use crate::test_support::same_origin_package; use crate::test_support::same_origin_package;
#[test] #[test]

View file

@ -1,7 +1,7 @@
//! A Forgejo (or Gitea) instance as a release source: its API root, and //! A Forgejo (or Gitea) instance as a release source: its API root, and
//! how to find a repo's latest release there. //! how to find a repo's latest release there.
use crate::checker::ReleaseSource; use super::ReleaseSource;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::Deserialize; use serde::Deserialize;

View file

@ -1,7 +1,7 @@
//! GitHub as a release source: its endpoints, and how to find a repo's //! GitHub as a release source: its endpoints, and how to find a repo's
//! latest release there. //! latest release there.
use crate::checker::ReleaseSource; use super::ReleaseSource;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use regex::Regex; use regex::Regex;

View file

@ -1,11 +1,18 @@
//! Picks the `ReleaseSource` implementation for a package from its //! Where a package's releases are published: the `ReleaseSource` trait,
//! configured `source`. The one place that knows which hosts exist; the //! one file per host implementing it, and `for_package`, which picks the
//! pipeline and the stages it calls only ever see the trait. //! implementation for a package from its configured `source`. The
//! submodules are private and re-exported here, so the rest of the crate
//! imports everything from `crate::source` and never a host's file.
mod forgejo;
mod github;
mod release_source;
pub use forgejo::ForgejoEndpoints;
pub use github::GithubEndpoints;
pub use release_source::ReleaseSource;
use crate::checker::ReleaseSource;
use crate::config::{Package, Source}; use crate::config::{Package, Source};
use crate::forgejo::ForgejoEndpoints;
use crate::github::GithubEndpoints;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
/// Defensive: errors only if a `forgejo-release` package has no /// Defensive: errors only if a `forgejo-release` package has no

View file

@ -0,0 +1,20 @@
//! The `ReleaseSource` trait: what the pipeline needs from a hosting
//! service a package's releases are published on. Each host implements it
//! in its own file next to this one, so per-host logic never accumulates
//! here.
use anyhow::Result;
/// `Debug` so a `Box<dyn ReleaseSource>` can sit in a `Result` that tests
/// unwrap.
pub trait ReleaseSource: std::fmt::Debug {
/// The latest release tag for `repo`.
fn latest_release(&self, client: &reqwest::blocking::Client, repo: &str) -> Result<String>;
/// The releases API root, which `fetcher` and `verifier` build their
/// own paths under. GitHub and Forgejo both serve
/// `/repos/{owner}/{repo}/releases/tags/{tag}` there with the same
/// `assets[].{name, browser_download_url}` shape, which is why those
/// stages need only this and not the source itself.
fn api(&self) -> &str;
}