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
6 changed files with 45 additions and 60 deletions
Showing only changes of commit e25ff8f6da - Show all commits

View file

@ -61,8 +61,8 @@ practice rather than asserted from habit — see Further reading.
Same testability goal as #3, applied to the specific ways this program
reaches outside itself. A consistent shape beats ad hoc mocking invented
per call site.
**Already in force**: `GithubEndpoints` (checker/fetcher/verifier),
`repo_add_bin` and the pacman.conf path (publisher), `PKGWATCH_REPO_DIR`
**Already in force**: `source::Endpoints` (checker), the plain API root
it hands to fetcher/verifier, `repo_add_bin` and the pacman.conf path (publisher), `PKGWATCH_REPO_DIR`
(main, for manual dry runs against a scratch repo instead of the real
one). A new external call follows the same shape: production code calls
a thin wrapper with the real default; tests call the parameterized

View file

@ -382,18 +382,14 @@ Open questions on the schema:
GitHub's confirm-each-tag step. `check_interval`/per-package cadence not
wired up yet — checks are a fixed hourly tick.)*
Both hosts' HTTP is hand-rolled on the `reqwest` already in the tree,
not an API-client crate: what pkgwatch needs is two `GET`s
(latest-release, release-by-tag), GitHub's check deliberately uses the
Atom feed that no API crate covers (to stay off the rate-limited REST
API), and the release-by-tag call is shared verbatim between the two
hosts, which two per-host crates would split in two. `octocrab` is
async/tokio/hyper against this project's blocking `reqwest`, and its
default tree alone (217 crates) is larger than all of pkgwatch's today
(143); `forgejo-api` has a `sync` feature but is a generated binding of
the whole Forgejo API for one endpoint. Revisit if pkgwatch ever needs
authenticated or write API calls (e.g. publishing its own releases from
code rather than CI).
The HTTP is hand-rolled on the `reqwest` already in the tree, not an
API-client crate: pkgwatch needs two `GET`s, GitHub's check deliberately
uses an Atom feed no API crate covers (to stay off the rate-limited REST
API), and the release-by-tag call is shared verbatim by both hosts.
`octocrab` is async against our blocking `reqwest` with a default tree
larger than pkgwatch's whole current one; `forgejo-api` is a generated
binding of the entire API for one endpoint. Revisit if pkgwatch needs
authenticated or write API calls.
- **Fetcher**: downloads the artifact (and any checksum/signature/
attestation companion) for a resolved version. *(Implemented —
`src/fetcher.rs`, via the GitHub or Forgejo releases API — same

View file

@ -34,7 +34,7 @@ pub struct Package {
/// (the API lives under `/api/v1`). Required for, and only meaningful
/// with, `source = "forgejo-release"`.
pub base_url: Option<String>,
/// Exact GitHub release asset name (still not a glob — see
/// Exact release asset name (still not a glob — see
/// docs/SPEC.md > Architecture > Fetcher), optionally containing a
/// `{version}` placeholder for projects whose asset names embed the
/// version (e.g. `scaleway-cli_{version}_linux_amd64`). Substituted via

View file

@ -347,6 +347,7 @@ fn approve(state_dir: &Path, work_dir: &Path, name: &str, pkg: &Package, tag: &s
mod tests {
use super::*;
use crate::github::GithubEndpoints;
use crate::test_support::same_origin_package;
#[test]
fn decide_tier_action_failed_verification_overrides_everything() {
@ -414,22 +415,6 @@ mod tests {
]
}
/// `pkg_toml_extra` is spliced in before the `[verification]` table, so
/// it can carry top-level keys like `source`.
fn same_origin_package(pkg_toml_extra: &str) -> Package {
toml::from_str(&format!(
r#"
repo = "o/r"
asset_pattern = "thing.tar.gz"
{pkg_toml_extra}
[verification]
method = "same-origin-sha256"
checksum_asset_pattern = "SHA256SUMS"
"#
))
.unwrap()
}
/// Runs `process_package` for a package whose checksum is wrong and
/// asserts it errors with "verification failed" while leaving no trace
/// in state.
@ -497,9 +482,8 @@ mod tests {
.create();
let _release_mocks = mock_release_with_bad_checksum(&mut server);
let pkg = same_origin_package(
"source = \"forgejo-release\"\nbase_url = \"https://forge.example.com\"",
);
assert_verification_failure_is_an_error(&endpoints, &pkg);
// The package's own `source` is irrelevant here: `endpoints` is
// hand-built to point at the mock server, bypassing `for_package`.
assert_verification_failure_is_an_error(&endpoints, &same_origin_package(""));
}
}

View file

@ -1,6 +1,6 @@
//! Maps a package's configured `source` to the endpoints the pipeline
//! stages talk to. The only module that knows how each hosting service
//! lays out its URLs; `checker` and `fetcher` take what it hands them.
//! stages talk to. `checker` and `fetcher` take what it hands them and
//! build their own paths under it.
use crate::config::{Package, Source};
use crate::github::GithubEndpoints;
@ -16,10 +16,8 @@ pub enum Endpoints {
}
impl Endpoints {
/// Errors only if a `forgejo-release` package has no `base_url`, which
/// `config::load_packages_dir` already rejects — this is the same check
/// again for a `Package` built some other way, not a second source of
/// truth.
/// Defensive: errors only if a `forgejo-release` package has no
/// `base_url`, which `config::load_packages_dir` already guarantees.
pub fn for_package(pkg: &Package) -> Result<Self> {
match pkg.source {
Source::GithubRelease => Ok(Endpoints::Github(GithubEndpoints::default())),
@ -50,20 +48,7 @@ impl Endpoints {
#[cfg(test)]
mod tests {
use super::*;
fn package(extra: &str) -> Package {
toml::from_str(&format!(
r#"
repo = "o/r"
asset_pattern = "x"
{extra}
[verification]
method = "same-origin-sha256"
checksum_asset_pattern = "SUMS"
"#
))
.unwrap()
}
use crate::test_support::same_origin_package as package;
#[test]
fn github_source_uses_real_github() {

View file

@ -1,10 +1,11 @@
//! Test-only fixture helpers shared across modules' `#[cfg(test)]` code
//! (`publisher`, `sanity`, `notifier`) — not production code, and not built outside
//! `cargo test`. See docs/ARCHITECTURE.md > "organize by pipeline stage, not
//! by layer": this exists to remove one specific piece of duplication
//! (two near-identical copies of "write an executable shell script"), not
//! as a general test-utils dump.
//! — not production code, and not built outside `cargo test`. See
//! docs/ARCHITECTURE.md > "organize by pipeline stage, not by layer": this
//! exists to remove specific pieces of duplication (near-identical copies
//! of "write an executable shell script" and of "build a same-origin
//! `Package` from TOML"), not as a general test-utils dump.
use crate::config::Package;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
@ -52,3 +53,22 @@ fn wait_until_executable(path: &Path) {
}
}
}
/// A `same-origin-sha256` `Package` for repo `o/r`, asset `thing.tar.gz`,
/// checksum asset `SHA256SUMS`. `extra` is spliced in as top-level keys
/// before the `[verification]` table (e.g. `source`/`base_url`), and is
/// parsed directly rather than through `config::load_packages_dir`, so it
/// skips load-time validation.
pub(crate) fn same_origin_package(extra: &str) -> Package {
toml::from_str(&format!(
r#"
repo = "o/r"
asset_pattern = "thing.tar.gz"
{extra}
[verification]
method = "same-origin-sha256"
checksum_asset_pattern = "SHA256SUMS"
"#
))
.unwrap()
}