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, and not something you should rely on. Pick a name that
cannot collide, and check registered_types() in a test.