use crate::artifact::ArtifactRegistry;
use crate::config::Config;
use crate::git::GitInfo;
use crate::log::{StageLogger, Verbosity};
use crate::partial::PartialTarget;
use crate::scm::ScmTokenType;
use crate::template::TemplateVars;
use anyhow::Context as _;
use chrono::Utc;
use std::collections::HashMap;
use std::path::PathBuf;
pub const VALID_RELEASE_SKIPS: &[&str] = &[
"publish",
"announce",
"sign",
"validate",
"sbom",
"docker",
"winget",
"choco",
"snapcraft",
"snapcraft-publish",
"scoop",
"brew",
"nix",
"aur",
"cargo",
"krew",
"nfpm",
"makeself",
"flatpak",
"srpm",
"before",
"notarize",
"archive",
"source",
"build",
"changelog",
"release",
"checksum",
"upx",
"blob",
"templatefiles",
"dmg",
"msi",
"nsis",
"pkg",
"appbundle",
];
pub const VALID_BUILD_SKIPS: &[&str] = &["pre-hooks", "post-hooks", "validate", "before"];
pub fn validate_skip_values(skip: &[String], valid: &[&str]) -> Result<(), String> {
let invalid: Vec<&str> = skip
.iter()
.map(|s| s.as_str())
.filter(|s| !valid.contains(s))
.collect();
if invalid.is_empty() {
Ok(())
} else {
Err(format!(
"invalid --skip value(s): {}. Valid options: {}",
invalid.join(", "),
valid.join(", "),
))
}
}
pub struct ContextOptions {
pub snapshot: bool,
pub nightly: bool,
pub dry_run: bool,
pub quiet: bool,
pub verbose: bool,
pub debug: bool,
pub skip_stages: Vec<String>,
pub selected_crates: Vec<String>,
pub token: Option<String>,
pub parallelism: usize,
pub single_target: Option<String>,
pub release_notes_path: Option<PathBuf>,
pub fail_fast: bool,
pub partial_target: Option<PartialTarget>,
pub merge: bool,
pub project_root: Option<PathBuf>,
pub strict: bool,
}
impl Default for ContextOptions {
fn default() -> Self {
Self {
snapshot: false,
nightly: false,
dry_run: false,
quiet: false,
verbose: false,
debug: false,
skip_stages: Vec::new(),
selected_crates: Vec::new(),
token: None,
parallelism: 4,
single_target: None,
release_notes_path: None,
fail_fast: false,
partial_target: None,
merge: false,
project_root: None,
strict: false,
}
}
}
#[derive(Debug, Default)]
pub struct StageOutputs {
pub github_native_changelog: bool,
pub changelogs: HashMap<String, String>,
pub changelog_header: Option<String>,
pub changelog_footer: Option<String>,
}
pub struct Context {
pub config: Config,
pub artifacts: ArtifactRegistry,
pub options: ContextOptions,
pub stage_outputs: StageOutputs,
template_vars: TemplateVars,
pub git_info: Option<GitInfo>,
pub token_type: ScmTokenType,
pub skip_memento: crate::pipe_skip::SkipMemento,
}
impl Context {
pub fn new(config: Config, options: ContextOptions) -> Self {
let mut vars = TemplateVars::new();
vars.set("ProjectName", &config.project_name);
Self {
config,
artifacts: ArtifactRegistry::new(),
options,
stage_outputs: StageOutputs::default(),
template_vars: vars,
git_info: None,
token_type: ScmTokenType::GitHub,
skip_memento: crate::pipe_skip::SkipMemento::new(),
}
}
pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
self.skip_memento.remember(stage, label, reason);
}
pub fn template_vars(&self) -> &TemplateVars {
&self.template_vars
}
pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
&mut self.template_vars
}
pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
crate::template::render(template, &self.template_vars)
}
pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
template.map(|t| self.render_template(t)).transpose()
}
pub fn skip_with_log(
&self,
skip: &Option<crate::config::StringOrBool>,
log: &StageLogger,
label: &str,
) -> anyhow::Result<bool> {
let Some(d) = skip else {
return Ok(false);
};
let should_skip = d
.try_evaluates_to_true(|s| self.render_template(s))
.with_context(|| format!("evaluate skip expression for {label}"))?;
if should_skip {
log.status(&format!("{} skipped", label));
}
Ok(should_skip)
}
pub fn should_skip(&self, stage_name: &str) -> bool {
self.options.skip_stages.iter().any(|s| s == stage_name)
}
pub fn skip_validate(&self) -> bool {
self.should_skip("validate")
}
pub fn is_dry_run(&self) -> bool {
self.options.dry_run
}
pub fn is_snapshot(&self) -> bool {
self.options.snapshot
}
pub fn is_strict(&self) -> bool {
self.options.strict
}
pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
if self.options.strict {
anyhow::bail!("{} (strict mode)", msg);
}
log.warn(msg);
Ok(())
}
pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
if self.is_snapshot() {
log.status(&format!("{}: skipped (snapshot mode)", stage));
true
} else {
false
}
}
pub fn render_template_strict(
&self,
template: &str,
label: &str,
log: &crate::log::StageLogger,
) -> anyhow::Result<String> {
match self.render_template(template) {
Ok(rendered) => Ok(rendered),
Err(e) => {
if self.options.strict {
anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
}
log.warn(&format!("{}: failed to render template: {}", label, e));
Ok(template.to_string())
}
}
}
pub fn is_nightly(&self) -> bool {
self.options.nightly
}
pub fn set_release_url(&mut self, url: &str) {
self.template_vars.set("ReleaseURL", url);
}
pub fn version(&self) -> String {
self.template_vars
.get("Version")
.cloned()
.unwrap_or_default()
}
pub fn verbosity(&self) -> Verbosity {
Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
}
pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
self.config.retry.unwrap_or_default().to_policy()
}
pub fn logger(&self, stage: &'static str) -> StageLogger {
StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact())
}
fn env_for_redact(&self) -> Vec<(String, String)> {
use std::collections::HashMap;
let mut map: HashMap<String, String> = std::env::vars().collect();
for (k, v) in self.template_vars.all_env() {
map.insert(k.clone(), v.clone());
}
map.into_iter().collect()
}
pub fn populate_git_vars(&mut self) {
if let Some(ref info) = self.git_info {
let raw_version = format!(
"{}.{}.{}",
info.semver.major, info.semver.minor, info.semver.patch
);
let mut version = raw_version.clone();
if let Some(ref pre) = info.semver.prerelease {
version.push('-');
version.push_str(pre);
}
if let Some(ref meta) = info.semver.build_metadata {
version.push('+');
version.push_str(meta);
}
self.template_vars.set("Tag", &info.tag);
self.template_vars.set("Version", &version);
self.template_vars.set("RawVersion", &raw_version);
self.template_vars
.set("Major", &info.semver.major.to_string());
self.template_vars
.set("Minor", &info.semver.minor.to_string());
self.template_vars
.set("Patch", &info.semver.patch.to_string());
self.template_vars.set(
"Prerelease",
info.semver.prerelease.as_deref().unwrap_or(""),
);
self.template_vars.set(
"BuildMetadata",
info.semver.build_metadata.as_deref().unwrap_or(""),
);
self.template_vars.set("FullCommit", &info.commit);
self.template_vars.set("Commit", &info.commit);
self.template_vars.set("ShortCommit", &info.short_commit);
self.template_vars.set("Branch", &info.branch);
self.template_vars.set("CommitDate", &info.commit_date);
self.template_vars
.set("CommitTimestamp", &info.commit_timestamp);
self.template_vars
.set("IsGitDirty", if info.dirty { "true" } else { "false" });
self.template_vars
.set("IsGitClean", if info.dirty { "false" } else { "true" });
self.template_vars
.set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
self.template_vars.set("GitURL", &info.remote_url);
self.template_vars.set("Summary", &info.summary);
self.template_vars.set("TagSubject", &info.tag_subject);
self.template_vars.set("TagContents", &info.tag_contents);
self.template_vars.set("TagBody", &info.tag_body);
self.template_vars
.set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
self.template_vars
.set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
let monorepo_prefix = self.config.monorepo_tag_prefix();
if let Some(prefix) = monorepo_prefix {
self.template_vars.set("PrefixedTag", &info.tag);
let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
self.template_vars.set("Tag", stripped_tag);
let version = stripped_tag
.strip_prefix('v')
.unwrap_or(stripped_tag)
.to_string();
self.template_vars.set("Version", &version);
let prev_tag = info.previous_tag.as_deref().unwrap_or("");
self.template_vars.set("PrefixedPreviousTag", prev_tag);
let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
self.template_vars.set("PreviousTag", stripped_prev);
self.template_vars.set("PrefixedSummary", &info.summary);
let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
self.template_vars.set("Summary", stripped_summary);
} else {
let tag_prefix = self
.config
.tag
.as_ref()
.and_then(|t| t.tag_prefix.as_deref())
.unwrap_or("");
self.template_vars
.set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
let prev_tag = info.previous_tag.as_deref().unwrap_or("");
let prefixed_prev = if prev_tag.is_empty() {
String::new()
} else {
format!("{}{}", tag_prefix, prev_tag)
};
self.template_vars
.set("PrefixedPreviousTag", &prefixed_prev);
self.template_vars.set(
"PrefixedSummary",
&format!("{}{}", tag_prefix, info.summary),
);
}
}
self.template_vars.set(
"IsSnapshot",
if self.options.snapshot {
"true"
} else {
"false"
},
);
self.template_vars.set(
"IsNightly",
if self.options.nightly {
"true"
} else {
"false"
},
);
let is_draft = self
.config
.release
.as_ref()
.and_then(|r| r.draft)
.unwrap_or(false);
self.template_vars
.set("IsDraft", if is_draft { "true" } else { "false" });
self.template_vars.set(
"IsSingleTarget",
if self.options.single_target.is_some() {
"true"
} else {
"false"
},
);
let is_release = !self.options.snapshot && !self.options.nightly;
self.template_vars
.set("IsRelease", if is_release { "true" } else { "false" });
self.template_vars.set(
"IsMerging",
if self.options.merge { "true" } else { "false" },
);
}
pub fn populate_time_vars(&mut self) {
let now = Utc::now();
self.template_vars.set("Date", &now.to_rfc3339());
self.template_vars
.set("Timestamp", &now.timestamp().to_string());
self.template_vars.set("Now", &now.to_rfc3339());
self.template_vars
.set("Year", &now.format("%Y").to_string());
self.template_vars
.set("Month", &now.format("%m").to_string());
self.template_vars.set("Day", &now.format("%d").to_string());
self.template_vars
.set("Hour", &now.format("%H").to_string());
self.template_vars
.set("Minute", &now.format("%M").to_string());
}
pub fn populate_runtime_vars(&mut self) {
let goos = map_os_to_goos(std::env::consts::OS);
let goarch = map_arch_to_goarch(std::env::consts::ARCH);
self.template_vars.set("RuntimeGoos", goos);
self.template_vars.set("RuntimeGoarch", goarch);
self.template_vars.set("Runtime_Goos", goos);
self.template_vars.set("Runtime_Goarch", goarch);
}
pub fn populate_release_notes_var(&mut self) {
let notes = self
.config
.crates
.iter()
.find_map(|c| self.stage_outputs.changelogs.get(&c.name))
.cloned()
.unwrap_or_default();
self.template_vars.set("ReleaseNotes", ¬es);
}
pub fn refresh_artifacts_var(&mut self) {
const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
let artifacts_value: Vec<serde_json::Value> = self
.artifacts
.all()
.iter()
.map(|a| {
let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
for (k, v) in &a.metadata {
if CSV_LIST_KEYS.contains(&k.as_str()) {
let items: Vec<serde_json::Value> = if v.is_empty() {
Vec::new()
} else {
v.split(',')
.map(|s| serde_json::Value::String(s.to_string()))
.collect()
};
metadata_map.insert(k.clone(), serde_json::Value::Array(items));
} else {
metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
}
}
serde_json::json!({
"name": a.name,
"path": a.path.to_string_lossy(),
"target": a.target.as_deref().unwrap_or(""),
"kind": a.kind.as_str(),
"crate_name": a.crate_name,
"metadata": serde_json::Value::Object(metadata_map),
})
})
.collect();
let tera_value = tera::Value::Array(artifacts_value);
self.template_vars.set_structured("Artifacts", tera_value);
}
pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
use crate::config::ContentSource;
let (
description,
homepage,
license,
maintainers,
mod_timestamp,
full_desc_src,
commit_author,
) = {
let meta = self.config.metadata.as_ref();
let description = meta
.and_then(|m| m.description.as_deref())
.unwrap_or("")
.to_string();
let homepage = meta
.and_then(|m| m.homepage.as_deref())
.unwrap_or("")
.to_string();
let license = meta
.and_then(|m| m.license.as_deref())
.unwrap_or("")
.to_string();
let maintainers: Vec<String> = meta
.and_then(|m| m.maintainers.as_ref())
.cloned()
.unwrap_or_default();
let mod_timestamp = meta
.and_then(|m| m.mod_timestamp.as_deref())
.unwrap_or("")
.to_string();
let full_desc_src = meta.and_then(|m| m.full_description.clone());
let commit_author = meta.and_then(|m| m.commit_author.clone());
(
description,
homepage,
license,
maintainers,
mod_timestamp,
full_desc_src,
commit_author,
)
};
let full_description = match full_desc_src {
None => String::new(),
Some(ContentSource::Inline(s)) => s,
Some(ContentSource::FromFile { from_file }) => {
let rendered_path = self.render_template(&from_file).with_context(|| {
format!("metadata.full_description: render path '{}'", from_file)
})?;
std::fs::read_to_string(&rendered_path).with_context(|| {
format!(
"metadata.full_description: read from_file '{}'",
rendered_path
)
})?
}
Some(ContentSource::FromUrl { .. }) => {
anyhow::bail!(
"metadata.full_description: `from_url` is not yet supported at metadata \
population time (core has no HTTP client). Use `from_file` with a \
pre-fetched file, or inline the content. Tracked for future: move \
URL resolution into a late-pipeline stage or add reqwest to core."
);
}
};
let commit_author_map = serde_json::json!({
"Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
"Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
});
let meta_map = serde_json::json!({
"Description": description,
"Homepage": homepage,
"License": license,
"Maintainers": maintainers,
"ModTimestamp": mod_timestamp,
"FullDescription": full_description,
"CommitAuthor": commit_author_map,
});
self.template_vars.set_structured("Metadata", meta_map);
Ok(())
}
}
pub fn map_os_to_goos(os: &str) -> &str {
match os {
"macos" => "darwin",
other => other, }
}
pub fn map_arch_to_goarch(arch: &str) -> &str {
match arch {
"x86_64" => "amd64",
"x86" => "386",
"aarch64" => "arm64",
"powerpc64" => "ppc64",
"s390x" => "s390x",
"mips" => "mips",
"mips64" => "mips64",
"riscv64" => "riscv64",
other => other,
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::config::Config;
use crate::git::{GitInfo, SemVer};
fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
let tag = match prerelease {
Some(pre) => format!("v1.2.3-{pre}"),
None => "v1.2.3".to_string(),
};
GitInfo {
tag,
commit: "abc123def456abc123def456abc123def456abc1".to_string(),
short_commit: "abc123d".to_string(),
branch: "main".to_string(),
dirty,
semver: SemVer {
major: 1,
minor: 2,
patch: 3,
prerelease: prerelease.map(|s| s.to_string()),
build_metadata: None,
},
commit_date: "2026-03-25T10:30:00+00:00".to_string(),
commit_timestamp: "1774463400".to_string(),
previous_tag: Some("v1.2.2".to_string()),
remote_url: "https://github.com/test/repo.git".to_string(),
summary: "v1.2.3-0-gabc123d".to_string(),
tag_subject: "Release v1.2.3".to_string(),
tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
tag_body: "Full release notes here.".to_string(),
first_commit: None,
}
}
#[test]
fn test_context_template_vars() {
let mut config = Config::default();
config.project_name = "test-project".to_string();
let ctx = Context::new(config, ContextOptions::default());
assert_eq!(
ctx.template_vars().get("ProjectName"),
Some(&"test-project".to_string())
);
}
#[test]
fn test_context_should_skip() {
let config = Config::default();
let opts = ContextOptions {
skip_stages: vec!["publish".to_string(), "announce".to_string()],
..Default::default()
};
let ctx = Context::new(config, opts);
assert!(ctx.should_skip("publish"));
assert!(ctx.should_skip("announce"));
assert!(!ctx.should_skip("build"));
}
#[test]
fn test_context_render_template() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let ctx = Context::new(config, ContextOptions::default());
let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
assert_eq!(result, "myapp-release");
}
#[test]
fn test_populate_git_vars_sets_all_expected_vars() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
assert_eq!(v.get("Major"), Some(&"1".to_string()));
assert_eq!(v.get("Minor"), Some(&"2".to_string()));
assert_eq!(v.get("Patch"), Some(&"3".to_string()));
assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
assert_eq!(
v.get("FullCommit"),
Some(&"abc123def456abc123def456abc123def456abc1".to_string())
);
assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
assert_eq!(v.get("Branch"), Some(&"main".to_string()));
assert_eq!(
v.get("CommitDate"),
Some(&"2026-03-25T10:30:00+00:00".to_string())
);
assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
}
#[test]
fn test_commit_is_alias_for_full_commit() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Commit"), v.get("FullCommit"));
}
#[test]
fn test_populate_git_vars_prerelease() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, Some("rc.1")));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
}
#[test]
fn test_build_metadata_template_var() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "v1.2.3+build.42".to_string();
info.semver.build_metadata = Some("build.42".to_string());
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
}
#[test]
fn test_build_metadata_empty_when_none() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("BuildMetadata"),
Some(&"".to_string())
);
}
#[test]
fn test_populate_git_vars_monorepo_prefixed_tag() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "core-v0.3.2".to_string();
info.semver = SemVer {
major: 0,
minor: 3,
patch: 2,
prerelease: None,
build_metadata: None,
};
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
assert_eq!(v.get("Major"), Some(&"0".to_string()));
assert_eq!(v.get("Minor"), Some(&"3".to_string()));
assert_eq!(v.get("Patch"), Some(&"2".to_string()));
}
#[test]
fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "operator-v1.0.0-rc.1".to_string();
info.semver = SemVer {
major: 1,
minor: 0,
patch: 0,
prerelease: Some("rc.1".to_string()),
build_metadata: None,
};
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
}
#[test]
fn test_git_tree_state_clean() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("IsGitDirty"), Some(&"false".to_string()));
assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
}
#[test]
fn test_git_tree_state_dirty() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(true, None));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("IsGitDirty"), Some(&"true".to_string()));
assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
}
#[test]
fn test_is_snapshot_reflects_context_options() {
let config = Config::default();
let opts = ContextOptions {
snapshot: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsSnapshot"),
Some(&"true".to_string())
);
let config2 = Config::default();
let opts2 = ContextOptions {
snapshot: false,
..Default::default()
};
let mut ctx2 = Context::new(config2, opts2);
ctx2.git_info = Some(make_git_info(false, None));
ctx2.populate_git_vars();
assert_eq!(
ctx2.template_vars().get("IsSnapshot"),
Some(&"false".to_string())
);
}
#[test]
fn test_is_draft_defaults_to_false() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsDraft"),
Some(&"false".to_string())
);
}
#[test]
fn test_previous_tag_empty_when_none() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.previous_tag = None;
ctx.git_info = Some(info);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PreviousTag"),
Some(&"".to_string())
);
}
#[test]
fn test_populate_time_vars() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_time_vars();
let v = ctx.template_vars();
let date = v
.get("Date")
.unwrap_or_else(|| panic!("Date should be set"));
assert!(
date.contains('T') && date.len() > 10,
"Date should be RFC 3339, got: {date}"
);
let ts = v
.get("Timestamp")
.unwrap_or_else(|| panic!("Timestamp should be set"));
assert!(
ts.parse::<i64>().is_ok(),
"Timestamp should be a numeric string, got: {ts}"
);
let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
}
#[test]
fn test_env_vars_accessible_in_templates() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
let result = ctx
.render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
.unwrap();
assert_eq!(result, "hello-world-staging");
}
#[test]
fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
let config = Config::default();
let opts = ContextOptions {
snapshot: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsSnapshot"),
Some(&"true".to_string())
);
assert_eq!(
ctx.template_vars().get("IsDraft"),
Some(&"false".to_string())
);
assert_eq!(ctx.template_vars().get("Tag"), None);
}
#[test]
fn test_is_nightly_set_when_nightly_mode_active() {
let config = Config::default();
let opts = ContextOptions {
nightly: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsNightly"),
Some(&"true".to_string()),
"IsNightly should be 'true' when nightly mode is active"
);
assert!(ctx.is_nightly(), "is_nightly() should return true");
}
#[test]
fn test_is_nightly_false_by_default() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsNightly"),
Some(&"false".to_string()),
"IsNightly should default to 'false'"
);
assert!(
!ctx.is_nightly(),
"is_nightly() should return false by default"
);
}
#[test]
fn test_version_returns_populated_value() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(ctx.version(), "1.2.3");
}
#[test]
fn test_version_returns_empty_when_not_set() {
let config = Config::default();
let ctx = Context::new(config, ContextOptions::default());
assert_eq!(ctx.version(), "");
}
#[test]
fn test_is_nightly_without_git_info() {
let config = Config::default();
let opts = ContextOptions {
nightly: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsNightly"),
Some(&"true".to_string()),
"IsNightly should be set even without git info"
);
}
#[test]
fn test_is_git_clean_when_not_dirty() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsGitClean"),
Some(&"true".to_string())
);
}
#[test]
fn test_is_git_clean_when_dirty() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(true, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsGitClean"),
Some(&"false".to_string())
);
}
#[test]
fn test_git_url_set_from_git_info() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("GitURL"),
Some(&"https://github.com/test/repo.git".to_string())
);
}
#[test]
fn test_summary_set_from_git_info() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("Summary"),
Some(&"v1.2.3-0-gabc123d".to_string())
);
}
#[test]
fn test_tag_subject_set_from_git_info() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("TagSubject"),
Some(&"Release v1.2.3".to_string())
);
}
#[test]
fn test_tag_contents_set_from_git_info() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("TagContents"),
Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
);
}
#[test]
fn test_tag_body_set_from_git_info() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("TagBody"),
Some(&"Full release notes here.".to_string())
);
}
#[test]
fn test_is_single_target_false_by_default() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsSingleTarget"),
Some(&"false".to_string())
);
}
#[test]
fn test_is_single_target_true_when_set() {
let config = Config::default();
let opts = ContextOptions {
single_target: Some("x86_64-unknown-linux-gnu".to_string()),
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsSingleTarget"),
Some(&"true".to_string())
);
}
#[test]
fn test_populate_runtime_vars() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_runtime_vars();
let v = ctx.template_vars();
let goos = v
.get("RuntimeGoos")
.unwrap_or_else(|| panic!("RuntimeGoos should be set"));
assert!(
!goos.is_empty(),
"RuntimeGoos should not be empty, got: {goos}"
);
assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
let goarch = v
.get("RuntimeGoarch")
.unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
assert!(
!goarch.is_empty(),
"RuntimeGoarch should not be empty, got: {goarch}"
);
assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
}
#[test]
fn test_populate_release_notes_var_with_changelogs() {
let mut config = Config::default();
config.crates.push(crate::config::CrateConfig {
name: "my-crate".to_string(),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.stage_outputs
.changelogs
.insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
ctx.populate_release_notes_var();
assert_eq!(
ctx.template_vars().get("ReleaseNotes"),
Some(&"## Changes\n- fix bug".to_string())
);
}
#[test]
fn test_populate_release_notes_var_empty_when_no_changelogs() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_release_notes_var();
assert_eq!(
ctx.template_vars().get("ReleaseNotes"),
Some(&"".to_string())
);
}
#[test]
fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
let mut config = Config::default();
config.crates.push(crate::config::CrateConfig {
name: "crate-a".to_string(),
..Default::default()
});
config.crates.push(crate::config::CrateConfig {
name: "crate-b".to_string(),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.stage_outputs
.changelogs
.insert("crate-a".to_string(), "notes-a".to_string());
ctx.stage_outputs
.changelogs
.insert("crate-b".to_string(), "notes-b".to_string());
ctx.populate_release_notes_var();
assert_eq!(
ctx.template_vars().get("ReleaseNotes"),
Some(&"notes-a".to_string())
);
}
#[test]
fn test_outputs_accessible_in_templates() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set_output("build_id", "abc123");
ctx.template_vars_mut()
.set_output("deploy_url", "https://example.com");
let result = ctx
.render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
.unwrap();
assert_eq!(result, "abc123-https://example.com");
}
#[test]
fn test_artifact_ext_and_target_template_vars() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
ctx.template_vars_mut()
.set("Target", "x86_64-unknown-linux-gnu");
let result = ctx
.render_template("{{ .ArtifactExt }}_{{ .Target }}")
.unwrap();
assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
}
#[test]
fn test_checksums_template_var() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
ctx.template_vars_mut().set("Checksums", checksum_text);
let result = ctx.render_template("{{ .Checksums }}").unwrap();
assert_eq!(result, checksum_text);
}
#[test]
fn test_prefixed_tag_with_tag_prefix() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("api/".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedTag"),
Some(&"api/v1.2.3".to_string())
);
}
#[test]
fn test_prefixed_tag_without_tag_prefix() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedTag"),
Some(&"v1.2.3".to_string())
);
}
#[test]
fn test_prefixed_previous_tag_with_tag_prefix() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("api/".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedPreviousTag"),
Some(&"api/v1.2.2".to_string())
);
}
#[test]
fn test_prefixed_previous_tag_empty_when_no_previous() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("api/".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.previous_tag = None;
ctx.git_info = Some(info);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedPreviousTag"),
Some(&"".to_string())
);
}
#[test]
fn test_prefixed_summary_with_tag_prefix() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("api/".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedSummary"),
Some(&"api/v1.2.3-0-gabc123d".to_string())
);
}
#[test]
fn test_is_release_true_for_normal_release() {
let config = Config::default();
let opts = ContextOptions {
snapshot: false,
nightly: false,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsRelease"),
Some(&"true".to_string())
);
}
#[test]
fn test_is_release_false_for_snapshot() {
let config = Config::default();
let opts = ContextOptions {
snapshot: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsRelease"),
Some(&"false".to_string())
);
}
#[test]
fn test_is_release_false_for_nightly() {
let config = Config::default();
let opts = ContextOptions {
nightly: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsRelease"),
Some(&"false".to_string())
);
}
#[test]
fn test_is_merging_true_when_merge_flag_set() {
let config = Config::default();
let opts = ContextOptions {
merge: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsMerging"),
Some(&"true".to_string())
);
}
#[test]
fn test_is_merging_false_by_default() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsMerging"),
Some(&"false".to_string())
);
}
#[test]
fn test_refresh_artifacts_var_empty() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.refresh_artifacts_var();
let result = ctx
.render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_refresh_artifacts_var_with_artifacts() {
use crate::artifact::{Artifact, ArtifactKind};
use std::collections::HashMap;
use std::path::PathBuf;
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.artifacts.add(Artifact {
kind: ArtifactKind::Archive,
name: String::new(),
path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
target: Some("x86_64-unknown-linux-gnu".to_string()),
crate_name: "myapp".to_string(),
metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
size: None,
});
ctx.artifacts.add(Artifact {
kind: ArtifactKind::Binary,
name: String::new(),
path: PathBuf::from("dist/myapp"),
target: Some("x86_64-unknown-linux-gnu".to_string()),
crate_name: "myapp".to_string(),
metadata: HashMap::new(),
size: None,
});
ctx.refresh_artifacts_var();
let result = ctx
.render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
.unwrap();
assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
assert!(result.contains("myapp"));
let result_kinds = ctx
.render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
.unwrap();
assert!(result_kinds.contains("archive"));
assert!(result_kinds.contains("binary"));
}
#[test]
fn test_populate_metadata_var_with_mod_timestamp() {
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
assert_eq!(result, "{{ .CommitTimestamp }}");
}
#[test]
fn test_populate_metadata_var_empty_when_no_config() {
let config = Config::default();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
assert_eq!(result, "");
}
#[test]
fn test_populate_metadata_var_reads_from_config() {
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
description: Some("A test project".to_string()),
homepage: Some("https://example.com".to_string()),
license: Some("MIT".to_string()),
maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
mod_timestamp: Some("1234567890".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
assert_eq!(desc, "A test project");
let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
assert_eq!(home, "https://example.com");
let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
assert_eq!(lic, "MIT");
let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
assert_eq!(ts, "1234567890");
}
#[test]
fn test_populate_metadata_var_full_description_inline() {
use crate::config::ContentSource;
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
full_description: Some(ContentSource::Inline(
"A long-form description of the project.".to_string(),
)),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let rendered = ctx
.render_template("{{ Metadata.FullDescription }}")
.unwrap();
assert_eq!(rendered, "A long-form description of the project.");
}
#[test]
fn test_populate_metadata_var_full_description_from_file() {
use crate::config::ContentSource;
let tmp = tempfile::tempdir().unwrap();
let desc_path = tmp.path().join("DESCRIPTION.md");
std::fs::write(&desc_path, "read from disk").unwrap();
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
full_description: Some(ContentSource::FromFile {
from_file: desc_path.to_string_lossy().into_owned(),
}),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let rendered = ctx
.render_template("{{ Metadata.FullDescription }}")
.unwrap();
assert_eq!(rendered, "read from disk");
}
#[test]
fn test_populate_metadata_var_full_description_from_url_errors() {
use crate::config::ContentSource;
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
full_description: Some(ContentSource::FromUrl {
from_url: "https://example.com/description.md".to_string(),
headers: None,
}),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
let err = ctx
.populate_metadata_var()
.expect_err("from_url must error");
let msg = format!("{:#}", err);
assert!(
msg.contains("metadata.full_description") && msg.contains("from_url"),
"error should mention the feature + limitation, got: {msg}"
);
}
#[test]
fn test_populate_metadata_var_commit_author() {
use crate::config::CommitAuthorConfig;
let mut config = Config::default();
config.metadata = Some(crate::config::MetadataConfig {
commit_author: Some(CommitAuthorConfig {
name: Some("Alice Developer".to_string()),
email: Some("alice@example.com".to_string()),
signing: None,
use_github_app_token: false,
}),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.populate_metadata_var().unwrap();
let name = ctx
.render_template("{{ Metadata.CommitAuthor.Name }}")
.unwrap();
assert_eq!(name, "Alice Developer");
let email = ctx
.render_template("{{ Metadata.CommitAuthor.Email }}")
.unwrap();
assert_eq!(email, "alice@example.com");
}
#[test]
fn test_artifact_id_template_var() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set("ArtifactID", "default");
let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
assert_eq!(result, "default");
}
#[test]
fn test_artifact_id_empty_when_not_set() {
let mut config = Config::default();
config.project_name = "myapp".to_string();
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set("ArtifactID", "");
let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
assert_eq!(result, "");
}
#[test]
fn test_pro_vars_rendered_in_templates() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("api/".to_string()),
..Default::default()
});
let opts = ContextOptions {
snapshot: false,
nightly: false,
merge: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
let result = ctx
.render_template(
"{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
)
.unwrap();
assert_eq!(result, "release-merge-api/v1.2.3");
}
#[test]
fn test_is_release_without_git_info() {
let config = Config::default();
let opts = ContextOptions {
snapshot: false,
nightly: false,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsRelease"),
Some(&"true".to_string())
);
}
#[test]
fn test_is_merging_without_git_info() {
let config = Config::default();
let opts = ContextOptions {
merge: true,
..Default::default()
};
let mut ctx = Context::new(config, opts);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("IsMerging"),
Some(&"true".to_string())
);
}
#[test]
fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
let mut config = Config::default();
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("subproject1/".to_string()),
dir: None,
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "subproject1/v1.2.3".to_string();
info.previous_tag = Some("subproject1/v1.2.2".to_string());
info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
assert_eq!(
v.get("PrefixedTag"),
Some(&"subproject1/v1.2.3".to_string())
);
assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
assert_eq!(
v.get("PrefixedPreviousTag"),
Some(&"subproject1/v1.2.2".to_string())
);
assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
assert_eq!(
v.get("PrefixedSummary"),
Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
);
}
#[test]
fn test_monorepo_prefixed_previous_tag() {
let mut config = Config::default();
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("svc/".to_string()),
dir: None,
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "svc/v2.0.0".to_string();
info.previous_tag = Some("svc/v1.9.0".to_string());
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(
v.get("PrefixedPreviousTag"),
Some(&"svc/v1.9.0".to_string())
);
assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
}
#[test]
fn test_no_monorepo_falls_back_to_tag_prefix() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("release/".to_string()),
..Default::default()
});
let mut ctx = Context::new(config, ContextOptions::default());
ctx.git_info = Some(make_git_info(false, None));
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
assert_eq!(
v.get("PrefixedPreviousTag"),
Some(&"release/v1.2.2".to_string())
);
}
#[test]
fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
let mut config = Config::default();
config.tag = Some(crate::config::TagConfig {
tag_prefix: Some("release/".to_string()),
..Default::default()
});
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("svc/".to_string()),
dir: None,
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "svc/v1.2.3".to_string();
info.previous_tag = Some("svc/v1.2.2".to_string());
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
}
#[test]
fn test_monorepo_prefixed_summary() {
let mut config = Config::default();
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("pkg/".to_string()),
dir: None,
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "pkg/v1.2.3".to_string();
info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
ctx.git_info = Some(info);
ctx.populate_git_vars();
assert_eq!(
ctx.template_vars().get("PrefixedSummary"),
Some(&"pkg/v1.2.3-0-gabc123d".to_string())
);
assert_eq!(
ctx.template_vars().get("Summary"),
Some(&"v1.2.3-0-gabc123d".to_string())
);
}
#[test]
fn test_monorepo_no_previous_tag() {
let mut config = Config::default();
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("svc/".to_string()),
dir: None,
});
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "svc/v1.0.0".to_string();
info.previous_tag = None;
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
}
#[test]
fn test_monorepo_full_flow_all_vars() {
let mut config = Config::default();
config.project_name = "mymonorepo".to_string();
config.monorepo = Some(crate::config::MonorepoConfig {
tag_prefix: Some("services/api/".to_string()),
dir: Some("services/api".to_string()),
});
assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
assert_eq!(config.monorepo_dir(), Some("services/api"));
let mut ctx = Context::new(config, ContextOptions::default());
let mut info = make_git_info(false, None);
info.tag = "services/api/v2.1.0".to_string();
info.previous_tag = Some("services/api/v2.0.5".to_string());
info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
info.semver = crate::git::SemVer {
major: 2,
minor: 1,
patch: 0,
prerelease: None,
build_metadata: None,
};
ctx.git_info = Some(info);
ctx.populate_git_vars();
let v = ctx.template_vars();
assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
assert_eq!(v.get("Major"), Some(&"2".to_string()));
assert_eq!(v.get("Minor"), Some(&"1".to_string()));
assert_eq!(v.get("Patch"), Some(&"0".to_string()));
assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
assert_eq!(
v.get("PrefixedTag"),
Some(&"services/api/v2.1.0".to_string())
);
assert_eq!(
v.get("PrefixedPreviousTag"),
Some(&"services/api/v2.0.5".to_string())
);
assert_eq!(
v.get("PrefixedSummary"),
Some(&"services/api/v2.1.0-0-gabc123d".to_string())
);
assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
}
}