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 25 additions and 31 deletions
Showing only changes of commit 984c11066f - Show all commits

View file

@ -35,8 +35,8 @@ 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
(`source`, `fetcher`, `verifier`, `builder`, `sanity`, `publisher`,
`state`), not generic buckets. (`source` is a directory module: the
(`release_source`, `fetcher`, `verifier`, `builder`, `sanity`,
`publisher`, `state`), not generic buckets. (`release_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.
@ -109,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**: `source/github.rs`'s doc comment on
**Example already here**: `release_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,12 +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/source/` defines the
*(Implemented for GitHub and Forgejo. `src/release_source/` defines the
`ReleaseSource` trait (latest release + API root); each host implements
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
`crate::release_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,5 +1,5 @@
//! 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
//! comes from a `ReleaseSource` (see `release_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 —
//! `source` only resolves version tags, never downloads.
//! `release_source` only resolves version tags, never downloads.
use anyhow::{Context, Result};
use serde::Deserialize;

View file

@ -11,8 +11,8 @@ mod notifier;
mod paths;
mod pipeline;
mod publisher;
mod release_source;
mod sanity;
mod source;
mod state;
#[cfg(test)]
mod test_support;

View file

@ -12,8 +12,8 @@ use crate::fetcher::{self, DownloadedAsset};
use crate::notifier::{self, Event};
use crate::paths::Paths;
use crate::publisher;
use crate::release_source::{self, ReleaseSource};
use crate::sanity;
use crate::source::{self, ReleaseSource};
use crate::state;
use crate::verifier::{self, VerificationResult};
use anyhow::{Context, Result, bail};
@ -72,15 +72,8 @@ pub fn run_check() -> Result<()> {
let mut any_failed = false;
for (name, pkg) in &packages {
println!("== {name} ({}) ==", pkg.repo);
let result = source::for_package(pkg).and_then(|release_source| {
process_package(
&client,
release_source.as_ref(),
&state_dir,
&work_dir,
name,
pkg,
)
let result = release_source::for_package(pkg).and_then(|host| {
process_package(&client, host.as_ref(), &state_dir, &work_dir, name, pkg)
});
if let Err(err) = result {
eprintln!(" error: {err:#}");
@ -127,13 +120,13 @@ fn decide_tier_action(tier: u8, passed: bool, already_pending_this_version: bool
fn process_package(
client: &reqwest::blocking::Client,
release_source: &dyn ReleaseSource,
host: &dyn ReleaseSource,
state_dir: &Path,
work_dir: &Path,
name: &str,
pkg: &Package,
) -> Result<()> {
let latest = release_source.latest_release(client, &pkg.repo)?;
let latest = host.latest_release(client, &pkg.repo)?;
let last_seen = state::load_last_version(state_dir, name);
if last_seen.as_deref() == Some(latest.as_str()) {
println!(" up to date at {latest}");
@ -141,7 +134,7 @@ fn process_package(
}
println!(" new version detected: {latest} (previously: {last_seen:?})");
let fetched = fetch_and_verify(client, release_source, work_dir, name, pkg, &latest)?;
let fetched = fetch_and_verify(client, host, work_dir, name, pkg, &latest)?;
println!(" fetched {}", fetched.asset.path.display());
println!(
" verification (tier {}): {} — {}",
@ -214,7 +207,7 @@ struct FetchVerifyResult {
/// trusting a possibly-stale flag from an earlier run).
fn fetch_and_verify(
client: &reqwest::blocking::Client,
release_source: &dyn ReleaseSource,
host: &dyn ReleaseSource,
work_dir: &Path,
name: &str,
pkg: &Package,
@ -224,7 +217,7 @@ fn fetch_and_verify(
let asset_name = pkg.asset_pattern.replace("{version}", &version);
let dest_dir = work_dir.join(name).join(tag);
let api = release_source.api();
let api = host.api();
let asset = fetcher::download_asset(client, api, &pkg.repo, tag, &asset_name, &dest_dir)?;
let verification = verifier::verify(
client,
@ -327,11 +320,11 @@ pub fn run_review(args: &[String]) -> Result<()> {
fn approve(state_dir: &Path, work_dir: &Path, name: &str, pkg: &Package, tag: &str) -> Result<()> {
let client = build_client()?;
let release_source = source::for_package(pkg)?;
let host = release_source::for_package(pkg)?;
// Re-verify rather than trusting the earlier flag: the artifact at
// this tag could in principle have changed since it was queued.
let fetched = fetch_and_verify(&client, release_source.as_ref(), work_dir, name, pkg, tag)?;
let fetched = fetch_and_verify(&client, host.as_ref(), work_dir, name, pkg, tag)?;
if !fetched.verification.passed {
bail!(
"re-verification failed on approve: {}",
@ -353,7 +346,7 @@ fn approve(state_dir: &Path, work_dir: &Path, name: &str, pkg: &Package, tag: &s
#[cfg(test)]
mod tests {
use super::*;
use crate::source::{ForgejoEndpoints, GithubEndpoints};
use crate::release_source::{ForgejoEndpoints, GithubEndpoints};
use crate::test_support::same_origin_package;
#[test]
@ -425,14 +418,14 @@ mod tests {
/// Runs `process_package` for a package whose checksum is wrong and
/// asserts it errors with "verification failed" while leaving no trace
/// in state.
fn assert_verification_failure_is_an_error(release_source: &dyn ReleaseSource, pkg: &Package) {
fn assert_verification_failure_is_an_error(host: &dyn ReleaseSource, pkg: &Package) {
let client = reqwest::blocking::Client::new();
let state_dir = tempfile::tempdir().unwrap();
let work_dir = tempfile::tempdir().unwrap();
let err = process_package(
&client,
release_source,
host,
state_dir.path(),
work_dir.path(),
"thing",

View file

@ -2,15 +2,16 @@
//! 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.
//! imports everything from `crate::release_source` and never a host's
//! file.
mod contract;
mod forgejo;
mod github;
mod release_source;
pub use contract::ReleaseSource;
pub use forgejo::ForgejoEndpoints;
pub use github::GithubEndpoints;
pub use release_source::ReleaseSource;
use crate::config::{Package, Source};
use anyhow::{Context, Result};