Add a Forgejo release source #5

Merged
schaefera merged 5 commits from worktree-forgejo-source into master 2026-09-20 09:20:00 +00:00
10 changed files with 52 additions and 43 deletions
Showing only changes of commit 6459856aab - Show all commits

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
up touching files scattered across every layer.
**Rule**: modules are named after what they do in the pipeline
(`checker`, `fetcher`, `verifier`, `builder`, `sanity`, `publisher`,
`state`), not generic buckets. A new pipeline stage gets a new module
(`source`, `fetcher`, `verifier`, `builder`, `sanity`, `publisher`,
`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.
**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
@ -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
alternative — needs to live somewhere a future reader will actually see
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
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

View file

@ -373,10 +373,12 @@ Open questions on the schema:
likely reuses `nvchecker`'s logic/sources conceptually for non-GitHub
sources eventually. For GitHub sources, prefers the `github-atom` feed
(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
it in its own module (`github.rs`, `forgejo.rs`), and `source.rs` picks
one per package, so adding a host doesn't touch existing ones. A trait
it in its own file (`github.rs`, `forgejo.rs`), and the module's
`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
genuinely different logic. GitHub regex-matches the first
`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
//! latest release is and where its releases API lives, and
//! `version_from_tag` turns the resulting tag into a version string. Each
//! 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;
}
//! Turns a release tag into a version string. What the latest tag *is*
//! comes from a `ReleaseSource` (see `source`); this is the one piece of
//! the check stage that isn't host-specific.
/// 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
//! 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 serde::Deserialize;

View file

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

View file

@ -6,14 +6,14 @@
//! `main.rs`.
use crate::builder;
use crate::checker::{self, ReleaseSource};
use crate::checker;
use crate::config::{self, Package};
use crate::fetcher::{self, DownloadedAsset};
use crate::notifier::{self, Event};
use crate::paths::Paths;
use crate::publisher;
use crate::sanity;
use crate::source;
use crate::source::{self, ReleaseSource};
use crate::state;
use crate::verifier::{self, VerificationResult};
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)]
mod tests {
use super::*;
use crate::forgejo::ForgejoEndpoints;
use crate::github::GithubEndpoints;
use crate::source::{ForgejoEndpoints, GithubEndpoints};
use crate::test_support::same_origin_package;
#[test]

View file

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

View file

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

View file

@ -1,11 +1,18 @@
//! Picks the `ReleaseSource` implementation for a package from its
//! configured `source`. The one place that knows which hosts exist; the
//! pipeline and the stages it calls only ever see the trait.
//! Where a package's releases are published: the `ReleaseSource` trait,
//! one file per host implementing it, and `for_package`, which picks the
//! 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::forgejo::ForgejoEndpoints;
use crate::github::GithubEndpoints;
use anyhow::{Context, Result};
/// 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;
}