use anodizer_core::config::PrereleaseConfig;
use anodizer_core::context::Context;
use anodizer_core::git;
use anodizer_core::log::{StageLogger, Verbosity};
use anodizer_core::scm::ScmTokenType;
use anyhow::{Context as _, Result};
pub(crate) fn release_log() -> StageLogger {
StageLogger::new("release", Verbosity::Normal)
}
mod gitea;
mod github;
mod gitlab;
mod release_body;
mod run;
#[cfg(test)]
mod tests;
pub(crate) async fn retry_upload<F, Fut>(operation_name: &str, mut f: F) -> Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
use anodizer_core::retry::{RetryPolicy, is_retriable, retry_async};
use std::ops::ControlFlow;
retry_async(&RetryPolicy::UPLOAD, |_attempt| {
let fut = f();
async move {
match fut.await {
Ok(()) => Ok(()),
Err(e) if is_retriable(e.as_ref()) => Err(ControlFlow::Continue(e)),
Err(e) => Err(ControlFlow::Break(e)),
}
}
})
.await
.with_context(|| format!("{operation_name}: retry exhausted"))
}
pub(crate) fn populate_artifact_download_urls(
ctx: &mut Context,
crate_name: &str,
token_type: ScmTokenType,
download_base: &str,
owner: &str,
repo: &str,
tag: &str,
) {
let dl_base = download_base.trim_end_matches('/');
let url_tag = anodizer_core::url::percent_encode_path_segment(tag);
let url_prefix = match token_type {
ScmTokenType::GitLab => {
if owner.is_empty() {
format!("{dl_base}/{repo}/-/releases/{url_tag}/downloads")
} else {
format!("{dl_base}/{owner}/{repo}/-/releases/{url_tag}/downloads")
}
}
ScmTokenType::GitHub | ScmTokenType::Gitea => {
format!("{dl_base}/{owner}/{repo}/releases/download/{url_tag}")
}
};
for artifact in ctx.artifacts.all_mut() {
if artifact.crate_name == crate_name && !artifact.name.is_empty() {
let encoded_name = anodizer_core::url::percent_encode_path_segment(&artifact.name);
artifact
.metadata
.insert("url".to_string(), format!("{url_prefix}/{encoded_name}"));
}
}
}
pub(crate) fn resolve_release_repo(
release_cfg: &anodizer_core::config::ReleaseConfig,
token_type: ScmTokenType,
ctx: &anodizer_core::context::Context,
) -> Result<Option<anodizer_core::config::ScmRepoConfig>> {
let raw = match token_type {
ScmTokenType::GitLab => release_cfg.gitlab.as_ref().or(release_cfg.github.as_ref()),
ScmTokenType::Gitea => release_cfg.gitea.as_ref().or(release_cfg.github.as_ref()),
ScmTokenType::GitHub => release_cfg.github.as_ref(),
};
let Some(repo) = raw else {
return Ok(None);
};
let owner = ctx
.render_template(&repo.owner)
.with_context(|| format!("release: render repo.owner '{}'", repo.owner))?;
let name = ctx
.render_template(&repo.name)
.with_context(|| format!("release: render repo.name '{}'", repo.name))?;
Ok(Some(anodizer_core::config::ScmRepoConfig { owner, name }))
}
pub(crate) fn compose_release_url(
token_type: ScmTokenType,
download_base: &str,
owner: &str,
repo: &str,
tag: &str,
) -> String {
let base = download_base.trim_end_matches('/');
match token_type {
ScmTokenType::GitHub | ScmTokenType::Gitea => {
format!("{}/{}/{}/releases/tag/{}", base, owner, repo, tag)
}
ScmTokenType::GitLab => {
format!("{}/{}/{}/-/releases/{}", base, owner, repo, tag)
}
}
}
pub(crate) fn should_mark_prerelease(config: &Option<PrereleaseConfig>, tag: &str) -> bool {
match config {
Some(PrereleaseConfig::Auto) => git::parse_semver_tag(tag)
.map(|sv| sv.is_prerelease())
.unwrap_or(false),
Some(PrereleaseConfig::Bool(b)) => *b,
None => false,
}
}
pub(crate) fn populate_checksums_var(ctx: &mut Context) {
use anodizer_core::artifact::ArtifactKind;
let checksum_artifacts = ctx.artifacts.by_kind(ArtifactKind::Checksum);
if checksum_artifacts.is_empty() {
ctx.template_vars_mut().set("Checksums", "");
return;
}
let is_combined = |a: &&anodizer_core::artifact::Artifact| {
a.metadata.get("combined").map(|s| s.as_str()) == Some("true")
};
let all_combined = checksum_artifacts.iter().all(is_combined);
let any_split = checksum_artifacts
.iter()
.any(|a| a.metadata.contains_key("ChecksumOf"));
if all_combined && !any_split {
let mut lines: Vec<String> = Vec::new();
for artifact in &checksum_artifacts {
let content = std::fs::read_to_string(&artifact.path).unwrap_or_default();
for line in content.lines() {
if !line.is_empty() {
lines.push(line.to_string());
}
}
}
lines.sort_by(|a, b| {
let name_a = a.split_once(" ").map(|(_, n)| n).unwrap_or(a);
let name_b = b.split_once(" ").map(|(_, n)| n).unwrap_or(b);
name_a.cmp(name_b)
});
lines.dedup();
ctx.template_vars_mut().set("Checksums", &lines.join("\n"));
return;
}
let mut map = serde_json::Map::new();
for artifact in &checksum_artifacts {
let key = artifact
.metadata
.get("ChecksumOf")
.cloned()
.unwrap_or_else(|| artifact.name.clone());
let content = std::fs::read_to_string(&artifact.path).unwrap_or_default();
map.insert(key, serde_json::Value::String(content));
}
ctx.template_vars_mut()
.set_structured("Checksums", serde_json::Value::Object(map));
}
pub struct ReleaseStage;