Add your own release backend¶
When your artefacts live somewhere none of the six built-in backends
understands — an internal Artifactory, a package registry, a signed manifest
service — you implement ReleaseProvider in your own crate. No fork, no patch
to rtb-forge.
The snippets below accumulate into one module and compile as written against
rtb-forge 0.7.
Implement the trait¶
Four methods, all read-only:
use async_trait::async_trait;
use rtb_forge::{ProviderError, Release, ReleaseAsset, ReleaseProvider};
use tokio::io::AsyncRead;
pub struct MirrorProvider {
endpoint: String,
channel: String,
}
#[async_trait]
impl ReleaseProvider for MirrorProvider {
async fn latest_release(&self) -> Result<Release, ProviderError> {
// … fetch, then build the value type:
let mut release = Release::new("1.4.2", "v1.4.2", time::OffsetDateTime::now_utc());
release.assets = vec![ReleaseAsset::new(
"tool-1.4.2.tar.gz",
"tool-1.4.2.tar.gz",
format!("{}/{}/tool-1.4.2.tar.gz", self.endpoint, self.channel),
)];
Ok(release)
}
async fn release_by_tag(&self, tag: &str) -> Result<Release, ProviderError> {
Err(ProviderError::NotFound { what: tag.to_string() })
}
async fn list_releases(&self, _limit: usize) -> Result<Vec<Release>, ProviderError> {
Err(ProviderError::Unsupported)
}
async fn download_asset(
&self,
_asset: &ReleaseAsset,
) -> Result<(Box<dyn AsyncRead + Send + Unpin>, u64), ProviderError> {
todo!()
}
}
Release and ReleaseAsset are #[non_exhaustive], so build them with
Release::new(name, tag, created_at) and ReleaseAsset::new(id, name,
download_url) and assign the optional fields afterwards. Struct literals will
not compile from outside the crate.
Returning ProviderError::Unsupported for a capability you genuinely do not
have is correct and expected — the Bitbucket backend does exactly that for
list_releases.
Write a factory¶
A ProviderFactory is a plain fn pointer that turns config plus an optional
token into an Arc<dyn ReleaseProvider>:
use std::sync::Arc;
use rtb_forge::{ProviderFactory, ReleaseSourceConfig};
use secrecy::SecretString;
pub fn factory(
cfg: &ReleaseSourceConfig,
_token: Option<SecretString>,
) -> Result<Arc<dyn ReleaseProvider>, ProviderError> {
let ReleaseSourceConfig::Custom { params, .. } = cfg else {
return Err(ProviderError::InvalidConfig(format!(
"internal-mirror factory called with source_type={}",
cfg.source_type()
)));
};
let endpoint = params.get("endpoint").ok_or_else(|| {
ProviderError::InvalidConfig("internal-mirror requires `endpoint`".into())
})?;
let channel = params.get("channel").map_or("stable", String::as_str);
Ok(Arc::new(MirrorProvider {
endpoint: endpoint.clone(),
channel: channel.to_string(),
}))
}
Validate everything here. The factory is the only place a configuration error
can be raised before the network is touched, and InvalidConfig is the variant
for it.
Register it at link time¶
Write a small type implementing ProviderRegistration and a function returning
it, annotated with linkme:
use linkme::distributed_slice;
use rtb_forge::{ProviderRegistration, RELEASE_PROVIDERS};
struct MirrorRegistration;
impl ProviderRegistration for MirrorRegistration {
fn source_type(&self) -> &'static str {
"internal-mirror"
}
fn factory(&self) -> ProviderFactory {
factory as ProviderFactory
}
}
#[allow(unsafe_code)] // linkme emits #[link_section]; no hand-written unsafe here.
#[distributed_slice(RELEASE_PROVIDERS)]
fn register_mirror() -> Box<dyn ProviderRegistration> {
Box::new(MirrorRegistration)
}
Add linkme to your own Cargo.toml at a version compatible with the one
rtb-forge uses:
The #[allow(unsafe_code)] is only needed if your crate denies or forbids
unsafe_code. linkme expands to a #[link_section] attribute, which Rust
1.95 and later attribute to that lint; there is no actual unsafe block.
Make sure the registration is linked in¶
This is the one failure mode worth planning for. If your backend lives in a
library crate that the final binary never otherwise references, the linker may
drop the object file and your registration disappears — lookup returns
None with no error anywhere.
The fix is to make the binary reference the crate:
Then assert it at startup, so a regression is loud:
debug_assert!(
rtb_forge::registered_types().contains(&"internal-mirror"),
"mirror backend was not linked in"
);
Configure it¶
The Custom variant carries your discriminator in a nested type key, and
everything else in a string-to-string map:
release:
source_type: custom
type: internal-mirror
params:
endpoint: https://artifacts.corp.internal/deploy-tool
channel: stable
ReleaseSourceConfig::source_type() returns internal-mirror for that config
— not custom — so the ordinary resolution path finds your factory with no
special case:
let factory = rtb_forge::lookup(cfg.source_type()).ok_or(/* … */)?;
let provider = factory(&cfg, token)?;
Two constraints on params: values are all strings, so numbers and booleans
must be quoted in YAML, and there is no nesting. If you need structured
configuration, parse it out of a string value yourself.
Do not collide with a built-in¶
Your source_type must not be github, gitlab, gitea, codeberg,
bitbucket or direct. lookup returns the first match in slice order,
which is link order — not something you should rely on. Pick a name that
cannot collide, and check registered_types() in a test.