1use crate::artifact::ArtifactRegistry;
2use crate::config::Config;
3use crate::env_source::{EnvSource, ProcessEnvSource};
4use crate::git::GitInfo;
5use crate::log::{StageLogger, Verbosity};
6use crate::partial::PartialTarget;
7use crate::publish_report::PublishReport;
8use crate::scm::ScmTokenType;
9use crate::template::TemplateVars;
10use anyhow::Context as _;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum RollbackMode {
24 None,
27 #[default]
34 BestEffort,
35}
36
37pub const VALID_RELEASE_SKIPS: &[&str] = &[
44 "publish",
45 "announce",
46 "sign",
47 "validate",
48 "sbom",
49 "attest",
50 "docker",
51 "docker-sign",
52 "winget",
53 "choco",
54 "snapcraft",
55 "snapcraft-publish",
56 "scoop",
57 "brew",
58 "nix",
59 "aur",
60 "cargo",
61 "krew",
62 "nfpm",
63 "makeself",
64 "appimage",
65 "flatpak",
66 "srpm",
67 "before",
68 "before-publish",
69 "notarize",
70 "archive",
71 "source",
72 "build",
73 "changelog",
74 "release",
75 "checksum",
76 "upx",
77 "blob",
78 "templatefiles",
79 "dmg",
80 "msi",
81 "nsis",
82 "pkg",
83 "appbundle",
84 "verify-release",
85];
86
87pub const VALID_BUILD_SKIPS: &[&str] = &["pre-hooks", "post-hooks", "validate", "before"];
89
90pub fn validate_skip_values(skip: &[String], valid: &[&str]) -> Result<(), String> {
95 let invalid: Vec<&str> = skip
96 .iter()
97 .map(|s| s.as_str())
98 .filter(|s| !valid.contains(s))
99 .collect();
100 if invalid.is_empty() {
101 Ok(())
102 } else {
103 Err(format!(
104 "invalid --skip value(s): {}. Valid options: {}",
105 invalid.join(", "),
106 valid.join(", "),
107 ))
108 }
109}
110
111pub struct ContextOptions {
112 pub snapshot: bool,
113 pub nightly: bool,
114 pub dry_run: bool,
115 pub quiet: bool,
116 pub verbose: bool,
117 pub debug: bool,
118 pub skip_stages: Vec<String>,
119 pub selected_crates: Vec<String>,
120 pub token: Option<String>,
121 pub parallelism: usize,
123 pub single_target: Option<String>,
125 pub release_notes_path: Option<PathBuf>,
127 pub fail_fast: bool,
129 pub partial_target: Option<PartialTarget>,
132 pub merge: bool,
134 pub publish_only: bool,
144 pub project_root: Option<PathBuf>,
147 pub strict: bool,
149 pub resume_release: bool,
154 pub replace_existing_artifacts: bool,
158 pub skip_post_publish_poll: bool,
166 pub gate_submitter: Option<bool>,
174 pub rollback_mode: Option<RollbackMode>,
179 pub simulate_failure_publishers: Vec<String>,
187 pub rollback_only: bool,
193 pub allow_rerun: bool,
208 pub from_run: Option<String>,
212 pub runtime_nondeterministic_allowlist: Vec<(String, String)>,
219 pub summary_json_path: Option<PathBuf>,
223 pub allow_ai_failure: bool,
230 pub changelog_from: Option<String>,
237 pub changelog_full_history: bool,
244 pub changelog_to: Option<String>,
253 pub changelog_preview: bool,
269}
270
271impl Default for ContextOptions {
272 fn default() -> Self {
273 Self {
274 snapshot: false,
275 nightly: false,
276 dry_run: false,
277 quiet: false,
278 verbose: false,
279 debug: false,
280 skip_stages: Vec::new(),
281 selected_crates: Vec::new(),
282 token: None,
283 parallelism: 4,
284 single_target: None,
285 release_notes_path: None,
286 fail_fast: false,
287 partial_target: None,
288 merge: false,
289 publish_only: false,
290 project_root: None,
291 strict: false,
292 resume_release: false,
293 replace_existing_artifacts: false,
294 skip_post_publish_poll: false,
295 gate_submitter: None,
296 rollback_mode: None,
297 simulate_failure_publishers: Vec::new(),
298 rollback_only: false,
299 allow_rerun: false,
300 from_run: None,
301 runtime_nondeterministic_allowlist: Vec::new(),
302 summary_json_path: None,
303 allow_ai_failure: false,
304 changelog_from: None,
305 changelog_full_history: false,
306 changelog_to: None,
307 changelog_preview: false,
308 }
309 }
310}
311
312#[derive(Debug, Default)]
317pub struct StageOutputs {
318 pub github_native_changelog: bool,
322 pub changelogs: HashMap<String, String>,
324 pub changelog_header: Option<String>,
329 pub changelog_footer: Option<String>,
332 pub post_publish_results: Vec<serde_json::Value>,
340}
341
342pub struct Context {
343 pub config: Config,
344 pub artifacts: ArtifactRegistry,
345 pub options: ContextOptions,
346 pub stage_outputs: StageOutputs,
348 template_vars: TemplateVars,
349 pub git_info: Option<GitInfo>,
350 pub token_type: ScmTokenType,
352 pub skip_memento: crate::pipe_skip::SkipMemento,
358 pub publish_report: Option<PublishReport>,
366 pub publish_attempted: bool,
372 pub determinism: Option<crate::DeterminismState>,
379 pub pending_outcome: Option<crate::PublisherOutcome>,
389 pub pending_evidence: Option<crate::PublishEvidence>,
402 built_crate_names: Option<std::collections::HashSet<String>>,
413 env_source: Arc<dyn EnvSource>,
421 #[cfg(feature = "test-helpers")]
429 pub log_capture: Option<crate::log::LogCapture>,
430 render_strict: std::cell::Cell<bool>,
445}
446
447impl Context {
448 pub fn new(config: Config, options: ContextOptions) -> Self {
449 let mut vars = TemplateVars::new();
450 vars.set("ProjectName", &config.project_name);
451 Self {
452 config,
453 artifacts: ArtifactRegistry::new(),
454 options,
455 stage_outputs: StageOutputs::default(),
456 template_vars: vars,
457 git_info: None,
458 token_type: ScmTokenType::GitHub,
459 skip_memento: crate::pipe_skip::SkipMemento::new(),
460 publish_report: None,
461 publish_attempted: false,
462 determinism: None,
463 pending_outcome: None,
464 pending_evidence: None,
465 built_crate_names: None,
466 env_source: Arc::new(ProcessEnvSource),
467 #[cfg(feature = "test-helpers")]
468 log_capture: None,
469 render_strict: std::cell::Cell::new(false),
470 }
471 }
472
473 pub fn env_var(&self, name: &str) -> Option<String> {
481 self.env_source.var(name)
482 }
483
484 pub fn set_env_source<S: EnvSource + 'static>(&mut self, src: S) {
490 self.env_source = Arc::new(src);
491 }
492
493 pub fn env_source(&self) -> &dyn EnvSource {
498 self.env_source.as_ref()
499 }
500
501 pub fn env_source_arc(&self) -> Arc<dyn EnvSource> {
507 Arc::clone(&self.env_source)
508 }
509
510 #[cfg(feature = "test-helpers")]
516 pub fn with_log_capture(&mut self, capture: crate::log::LogCapture) {
517 self.log_capture = Some(capture);
518 }
519
520 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
528 self.pending_outcome = Some(outcome);
529 }
530
531 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
535 self.pending_outcome.take()
536 }
537
538 pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
543 self.pending_evidence = Some(evidence);
544 }
545
546 pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
550 self.pending_evidence.take()
551 }
552
553 pub fn publish_report(&self) -> Option<&PublishReport> {
556 self.publish_report.as_ref()
557 }
558
559 pub fn publish_attempted(&self) -> bool {
562 self.publish_attempted
563 }
564
565 pub fn set_publish_attempted(&mut self) {
569 self.publish_attempted = true;
570 }
571
572 pub fn set_publish_report(&mut self, r: PublishReport) {
578 self.publish_report = Some(r);
579 }
580
581 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
584 self.built_crate_names.as_ref()
585 }
586
587 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
590 self.built_crate_names = Some(names);
591 }
592
593 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
600 self.skip_memento.remember(stage, label, reason);
601 }
602
603 pub fn template_vars(&self) -> &TemplateVars {
604 &self.template_vars
605 }
606
607 pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
608 &mut self.template_vars
609 }
610
611 pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
612 crate::template::render(template, &self.template_vars)
613 }
614
615 pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
617 template.map(|t| self.render_template(t)).transpose()
618 }
619
620 pub fn skip_with_log(
629 &self,
630 skip: &Option<crate::config::StringOrBool>,
631 log: &StageLogger,
632 label: &str,
633 ) -> anyhow::Result<bool> {
634 let Some(d) = skip else {
635 return Ok(false);
636 };
637 let should_skip = d
638 .try_evaluates_to_true(|s| self.render_template(s))
639 .with_context(|| format!("evaluate skip expression for {label}"))?;
640 if should_skip {
641 log.status(&format!("{} skipped", label));
642 }
643 Ok(should_skip)
644 }
645
646 pub fn should_skip(&self, stage_name: &str) -> bool {
647 self.options.skip_stages.iter().any(|s| s == stage_name)
648 }
649
650 pub fn skip_validate(&self) -> bool {
652 self.should_skip("validate")
653 }
654
655 pub fn is_dry_run(&self) -> bool {
656 self.options.dry_run
657 }
658
659 pub fn is_snapshot(&self) -> bool {
660 self.options.snapshot
661 }
662
663 pub fn is_strict(&self) -> bool {
664 self.options.strict
665 }
666
667 pub fn set_render_strict(&self, on: bool) -> bool {
675 self.render_strict.replace(on)
676 }
677
678 pub fn render_is_strict(&self) -> bool {
685 self.render_strict.get() || self.is_strict()
686 }
687
688 pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
691 if self.options.strict {
692 anyhow::bail!("{} (strict mode)", msg);
693 }
694 log.warn(msg);
695 Ok(())
696 }
697
698 pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
707 if self.is_snapshot() {
708 log.status(&format!("skipping {stage} (snapshot mode)"));
712 true
713 } else {
714 false
715 }
716 }
717
718 pub fn render_template_strict(
720 &self,
721 template: &str,
722 label: &str,
723 log: &crate::log::StageLogger,
724 ) -> anyhow::Result<String> {
725 match self.render_template(template) {
726 Ok(rendered) => Ok(rendered),
727 Err(e) => {
728 if self.options.strict {
729 anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
730 }
731 log.warn(&format!("failed to render template for {}: {}", label, e));
732 Ok(template.to_string())
733 }
734 }
735 }
736
737 pub fn is_nightly(&self) -> bool {
738 self.options.nightly
739 }
740
741 pub fn set_release_url(&mut self, url: &str) {
746 self.template_vars.set("ReleaseURL", url);
747 }
748
749 pub fn version(&self) -> String {
752 self.template_vars
753 .get("Version")
754 .cloned()
755 .unwrap_or_default()
756 }
757
758 pub fn verbosity(&self) -> Verbosity {
760 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
761 }
762
763 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
769 self.config.retry.unwrap_or_default().to_policy()
770 }
771
772 pub fn logger(&self, stage: &'static str) -> StageLogger {
780 #[allow(unused_mut)]
781 let mut log = StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact());
782 #[cfg(feature = "test-helpers")]
783 if let Some(cap) = &self.log_capture {
784 log = log.with_capture_handle(cap.clone());
785 }
786 log
787 }
788
789 fn env_for_redact(&self) -> Vec<(String, String)> {
795 use std::collections::HashMap;
796 let mut map: HashMap<String, String> = std::env::vars().collect();
797 for (k, v) in self.template_vars.all_env() {
798 map.insert(k.clone(), v.clone());
799 }
800 map.into_iter().collect()
801 }
802
803 pub fn populate_git_vars(&mut self) {
845 if let Some(ref info) = self.git_info {
846 let raw_version = info.semver.raw_version_string();
848
849 let version = info.semver.version_string();
855
856 self.template_vars.set("Tag", &info.tag);
857 self.template_vars.set("Version", &version);
858 self.template_vars.set("RawVersion", &raw_version);
859 self.template_vars.set("Base", &raw_version);
864 self.template_vars
865 .set("Major", &info.semver.major.to_string());
866 self.template_vars
867 .set("Minor", &info.semver.minor.to_string());
868 self.template_vars
869 .set("Patch", &info.semver.patch.to_string());
870 self.template_vars.set(
871 "Prerelease",
872 info.semver.prerelease.as_deref().unwrap_or(""),
873 );
874 self.template_vars.set(
875 "BuildMetadata",
876 info.semver.build_metadata.as_deref().unwrap_or(""),
877 );
878 self.template_vars.set("FullCommit", &info.commit);
879 self.template_vars.set("Commit", &info.commit);
880 self.template_vars.set("ShortCommit", &info.short_commit);
881 self.template_vars.set("Branch", &info.branch);
882 self.template_vars.set("CommitDate", &info.commit_date);
883 self.template_vars
884 .set("CommitTimestamp", &info.commit_timestamp);
885 self.template_vars.set_bool("IsGitDirty", info.dirty);
886 self.template_vars.set_bool("IsGitClean", !info.dirty);
887 self.template_vars
888 .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
889 self.template_vars.set("GitURL", &info.remote_url);
890 self.template_vars.set("Summary", &info.summary);
891 self.template_vars.set("TagSubject", &info.tag_subject);
892 self.template_vars.set("TagContents", &info.tag_contents);
893 self.template_vars.set("TagBody", &info.tag_body);
894 self.template_vars
895 .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
896 self.template_vars
897 .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
898
899 let monorepo_prefix = self.config.monorepo_tag_prefix();
910
911 if let Some(prefix) = monorepo_prefix {
917 self.template_vars.set("PrefixedTag", &info.tag);
920
921 let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
923 self.template_vars.set("Tag", stripped_tag);
924
925 let version = info.semver.version_string();
935 self.template_vars.set("Version", &version);
936
937 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
939 self.template_vars.set("PrefixedPreviousTag", prev_tag);
940
941 let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
943 self.template_vars.set("PreviousTag", stripped_prev);
944
945 self.template_vars.set("PrefixedSummary", &info.summary);
949 let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
951 self.template_vars.set("Summary", stripped_summary);
952 } else {
953 let tag_prefix = self
955 .config
956 .tag
957 .as_ref()
958 .and_then(|t| t.tag_prefix.as_deref())
959 .unwrap_or("");
960 self.template_vars
961 .set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
962 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
963 let prefixed_prev = if prev_tag.is_empty() {
964 String::new()
965 } else {
966 format!("{}{}", tag_prefix, prev_tag)
967 };
968 self.template_vars
969 .set("PrefixedPreviousTag", &prefixed_prev);
970 self.template_vars.set(
971 "PrefixedSummary",
972 &format!("{}{}", tag_prefix, info.summary),
973 );
974 }
975 }
976
977 let nightly_build = if self.git_info.is_some() {
990 let root = self
991 .options
992 .project_root
993 .clone()
994 .unwrap_or_else(|| PathBuf::from("."));
995 let monorepo_prefix = self.config.monorepo_tag_prefix();
996 crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
997 } else {
998 0
999 };
1000 self.template_vars
1001 .set_structured("NightlyBuild", tera::Value::from(nightly_build));
1002
1003 self.template_vars
1008 .set_bool("IsSnapshot", self.options.snapshot);
1009 self.template_vars
1010 .set_bool("IsNightly", self.options.nightly);
1011 self.template_vars.set_bool(
1015 "IsHarness",
1016 self.env_var("ANODIZER_IN_DETERMINISM_HARNESS").is_some(),
1017 );
1018 let is_draft = self
1020 .config
1021 .release
1022 .as_ref()
1023 .and_then(|r| r.draft)
1024 .unwrap_or(false);
1025 self.template_vars.set_bool("IsDraft", is_draft);
1026 self.template_vars
1027 .set_bool("IsSingleTarget", self.options.single_target.is_some());
1028
1029 let is_release = !self.options.snapshot && !self.options.nightly;
1031 self.template_vars.set_bool("IsRelease", is_release);
1032
1033 self.template_vars.set_bool("IsMerging", self.options.merge);
1035 }
1036
1037 pub fn populate_time_vars(&mut self) {
1065 let now = crate::sde::resolve_now_with_env(self.env_source());
1073 self.template_vars.set("Date", &now.to_rfc3339());
1074 self.template_vars
1075 .set("Timestamp", &now.timestamp().to_string());
1076 self.template_vars.set("Now", &now.to_rfc3339());
1077 self.template_vars
1078 .set("Year", &now.format("%Y").to_string());
1079 self.template_vars
1080 .set("Month", &now.format("%m").to_string());
1081 self.template_vars.set("Day", &now.format("%d").to_string());
1082 self.template_vars
1083 .set("Hour", &now.format("%H").to_string());
1084 self.template_vars
1085 .set("Minute", &now.format("%M").to_string());
1086 }
1087
1088 pub fn populate_runtime_vars(&mut self) {
1097 let goos = map_os_to_goos(std::env::consts::OS);
1098 let goarch = map_arch_to_goarch(std::env::consts::ARCH);
1099 self.template_vars.set("RuntimeGoos", goos);
1100 self.template_vars.set("RuntimeGoarch", goarch);
1101 self.template_vars.set("Runtime_Goos", goos);
1104 self.template_vars.set("Runtime_Goarch", goarch);
1105 self.populate_rustc_vars();
1109 }
1110
1111 fn populate_rustc_vars(&mut self) {
1118 let ver = crate::partial::detect_rustc_version().unwrap_or_default();
1119 self.template_vars.set("RustcVersion", &ver);
1120 }
1121
1122 pub fn populate_release_notes_var(&mut self) {
1130 let notes = self
1132 .config
1133 .crates
1134 .iter()
1135 .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
1136 .cloned()
1137 .unwrap_or_default();
1138 self.template_vars.set("ReleaseNotes", ¬es);
1139 }
1140
1141 pub fn refresh_artifacts_var(&mut self) {
1156 const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
1161 const JSON_LIST_KEYS: &[&str] = &["Platforms"];
1167
1168 let artifacts_value: Vec<serde_json::Value> = self
1169 .artifacts
1170 .all()
1171 .iter()
1172 .map(|a| {
1173 let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
1175 for (k, v) in &a.metadata {
1176 if CSV_LIST_KEYS.contains(&k.as_str()) {
1177 let items: Vec<serde_json::Value> = if v.is_empty() {
1178 Vec::new()
1179 } else {
1180 v.split(',')
1181 .map(|s| serde_json::Value::String(s.to_string()))
1182 .collect()
1183 };
1184 metadata_map.insert(k.clone(), serde_json::Value::Array(items));
1185 } else if JSON_LIST_KEYS.contains(&k.as_str()) {
1186 let parsed = serde_json::from_str::<serde_json::Value>(v)
1190 .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
1191 metadata_map.insert(k.clone(), parsed);
1192 } else {
1193 metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
1194 }
1195 }
1196 serde_json::json!({
1197 "name": a.name,
1198 "path": a.path.to_string_lossy(),
1199 "target": a.target.as_deref().unwrap_or(""),
1200 "kind": a.kind.as_str(),
1201 "crate_name": a.crate_name,
1202 "metadata": serde_json::Value::Object(metadata_map),
1203 })
1204 })
1205 .collect();
1206 let tera_value = tera::Value::Array(artifacts_value);
1209 self.template_vars.set_structured("Artifacts", tera_value);
1210 }
1211
1212 pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
1225 let (
1228 description,
1229 homepage,
1230 license,
1231 maintainers,
1232 mod_timestamp,
1233 full_desc_src,
1234 commit_author,
1235 ) = {
1236 let meta = self.config.metadata.as_ref();
1237 let description = meta
1238 .and_then(|m| m.description.as_deref())
1239 .unwrap_or("")
1240 .to_string();
1241 let homepage = meta
1242 .and_then(|m| m.homepage.as_deref())
1243 .unwrap_or("")
1244 .to_string();
1245 let license = meta
1246 .and_then(|m| m.license.as_deref())
1247 .unwrap_or("")
1248 .to_string();
1249 let maintainers: Vec<String> = meta
1250 .and_then(|m| m.maintainers.as_ref())
1251 .cloned()
1252 .unwrap_or_default();
1253 let mod_timestamp = meta
1254 .and_then(|m| m.mod_timestamp.as_deref())
1255 .unwrap_or("")
1256 .to_string();
1257 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1258 let commit_author = meta.and_then(|m| m.commit_author.clone());
1259 (
1260 description,
1261 homepage,
1262 license,
1263 maintainers,
1264 mod_timestamp,
1265 full_desc_src,
1266 commit_author,
1267 )
1268 };
1269
1270 let full_description = match full_desc_src {
1276 None => String::new(),
1277 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1278 };
1279
1280 let commit_author_map = serde_json::json!({
1281 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1282 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1283 });
1284
1285 let meta_map = serde_json::json!({
1286 "Description": description,
1287 "Homepage": homepage,
1288 "License": license,
1289 "Maintainers": maintainers,
1290 "ModTimestamp": mod_timestamp,
1291 "FullDescription": full_description,
1292 "CommitAuthor": commit_author_map,
1293 });
1294 self.template_vars.set_structured("Metadata", meta_map);
1296 Ok(())
1297 }
1298}
1299
1300pub fn map_os_to_goos(os: &str) -> &str {
1303 match os {
1304 "macos" => "darwin",
1305 other => other, }
1307}
1308
1309pub fn map_arch_to_goarch(arch: &str) -> &str {
1312 match arch {
1313 "x86_64" => "amd64",
1314 "x86" => "386",
1315 "aarch64" => "arm64",
1316 "powerpc64" => "ppc64",
1317 "s390x" => "s390x",
1318 "mips" => "mips",
1319 "mips64" => "mips64",
1320 "riscv64" => "riscv64",
1321 other => other,
1322 }
1323}
1324
1325#[cfg(test)]
1326#[allow(clippy::field_reassign_with_default)]
1327mod tests {
1328 use super::*;
1329 use crate::config::Config;
1330 use crate::git::{GitInfo, SemVer};
1331
1332 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1333 let tag = match prerelease {
1334 Some(pre) => format!("v1.2.3-{pre}"),
1335 None => "v1.2.3".to_string(),
1336 };
1337 GitInfo {
1338 tag,
1339 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1340 short_commit: "abc123d".to_string(),
1341 branch: "main".to_string(),
1342 dirty,
1343 semver: SemVer {
1344 major: 1,
1345 minor: 2,
1346 patch: 3,
1347 prerelease: prerelease.map(|s| s.to_string()),
1348 build_metadata: None,
1349 },
1350 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1351 commit_timestamp: "1774463400".to_string(),
1352 previous_tag: Some("v1.2.2".to_string()),
1353 remote_url: "https://github.com/test/repo.git".to_string(),
1354 summary: "v1.2.3-0-gabc123d".to_string(),
1355 tag_subject: "Release v1.2.3".to_string(),
1356 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1357 tag_body: "Full release notes here.".to_string(),
1358 first_commit: None,
1359 }
1360 }
1361
1362 #[test]
1363 fn test_context_template_vars() {
1364 let mut config = Config::default();
1365 config.project_name = "test-project".to_string();
1366 let ctx = Context::new(config, ContextOptions::default());
1367 assert_eq!(
1368 ctx.template_vars().get("ProjectName"),
1369 Some(&"test-project".to_string())
1370 );
1371 }
1372
1373 #[test]
1374 fn test_context_should_skip() {
1375 let config = Config::default();
1376 let opts = ContextOptions {
1377 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1378 ..Default::default()
1379 };
1380 let ctx = Context::new(config, opts);
1381 assert!(ctx.should_skip("publish"));
1382 assert!(ctx.should_skip("announce"));
1383 assert!(!ctx.should_skip("build"));
1384 }
1385
1386 #[test]
1387 fn test_context_render_template() {
1388 let mut config = Config::default();
1389 config.project_name = "myapp".to_string();
1390 let ctx = Context::new(config, ContextOptions::default());
1391 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1392 assert_eq!(result, "myapp-release");
1393 }
1394
1395 #[test]
1396 fn test_populate_git_vars_sets_all_expected_vars() {
1397 let config = Config::default();
1398 let mut ctx = Context::new(config, ContextOptions::default());
1399 ctx.git_info = Some(make_git_info(false, None));
1400 ctx.populate_git_vars();
1401
1402 let v = ctx.template_vars();
1403 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1404 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1405 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1406 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1407 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1408 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1409 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1410 assert_eq!(
1411 v.get("FullCommit"),
1412 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1413 );
1414 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1415 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1416 assert_eq!(
1417 v.get("CommitDate"),
1418 Some(&"2026-03-25T10:30:00+00:00".to_string())
1419 );
1420 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1421 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1422 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1425 }
1426
1427 #[test]
1428 fn test_nightly_build_defaults_to_zero_without_git_info() {
1429 let config = Config::default();
1432 let mut ctx = Context::new(config, ContextOptions::default());
1433 ctx.git_info = None;
1434 ctx.populate_git_vars();
1435 assert_eq!(
1436 ctx.template_vars().get_structured("NightlyBuild"),
1437 Some(&tera::Value::from(0u64))
1438 );
1439 }
1440
1441 #[test]
1442 fn test_commit_is_alias_for_full_commit() {
1443 let config = Config::default();
1444 let mut ctx = Context::new(config, ContextOptions::default());
1445 ctx.git_info = Some(make_git_info(false, None));
1446 ctx.populate_git_vars();
1447
1448 let v = ctx.template_vars();
1449 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1450 }
1451
1452 #[test]
1453 fn test_populate_git_vars_prerelease() {
1454 let config = Config::default();
1455 let mut ctx = Context::new(config, ContextOptions::default());
1456 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1457 ctx.populate_git_vars();
1458
1459 let v = ctx.template_vars();
1460 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1461 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1462 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1463 }
1464
1465 #[test]
1466 fn test_build_metadata_template_var() {
1467 let config = Config::default();
1468 let mut ctx = Context::new(config, ContextOptions::default());
1469 let mut info = make_git_info(false, None);
1470 info.tag = "v1.2.3+build.42".to_string();
1471 info.semver.build_metadata = Some("build.42".to_string());
1472 ctx.git_info = Some(info);
1473 ctx.populate_git_vars();
1474
1475 let v = ctx.template_vars();
1476 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1477 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1479 }
1480
1481 #[test]
1482 fn test_build_metadata_empty_when_none() {
1483 let config = Config::default();
1484 let mut ctx = Context::new(config, ContextOptions::default());
1485 ctx.git_info = Some(make_git_info(false, None));
1486 ctx.populate_git_vars();
1487
1488 assert_eq!(
1489 ctx.template_vars().get("BuildMetadata"),
1490 Some(&"".to_string())
1491 );
1492 }
1493
1494 #[test]
1495 fn test_populate_git_vars_monorepo_prefixed_tag() {
1496 let config = Config::default();
1499 let mut ctx = Context::new(config, ContextOptions::default());
1500 let mut info = make_git_info(false, None);
1501 info.tag = "core-v0.3.2".to_string();
1502 info.semver = SemVer {
1503 major: 0,
1504 minor: 3,
1505 patch: 2,
1506 prerelease: None,
1507 build_metadata: None,
1508 };
1509 ctx.git_info = Some(info);
1510 ctx.populate_git_vars();
1511
1512 let v = ctx.template_vars();
1513 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1514 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1515 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1516 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1517 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1518 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1519 }
1520
1521 #[test]
1522 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1523 let config = Config::default();
1524 let mut ctx = Context::new(config, ContextOptions::default());
1525 let mut info = make_git_info(false, None);
1526 info.tag = "operator-v1.0.0-rc.1".to_string();
1527 info.semver = SemVer {
1528 major: 1,
1529 minor: 0,
1530 patch: 0,
1531 prerelease: Some("rc.1".to_string()),
1532 build_metadata: None,
1533 };
1534 ctx.git_info = Some(info);
1535 ctx.populate_git_vars();
1536
1537 let v = ctx.template_vars();
1538 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1539 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1540 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1541 }
1542
1543 #[test]
1544 fn test_git_tree_state_clean() {
1545 let config = Config::default();
1546 let mut ctx = Context::new(config, ContextOptions::default());
1547 ctx.git_info = Some(make_git_info(false, None));
1548 ctx.populate_git_vars();
1549
1550 let v = ctx.template_vars();
1551 assert_eq!(
1552 v.get_structured("IsGitDirty"),
1553 Some(&tera::Value::Bool(false))
1554 );
1555 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1556 }
1557
1558 #[test]
1559 fn test_git_tree_state_dirty() {
1560 let config = Config::default();
1561 let mut ctx = Context::new(config, ContextOptions::default());
1562 ctx.git_info = Some(make_git_info(true, None));
1563 ctx.populate_git_vars();
1564
1565 let v = ctx.template_vars();
1566 assert_eq!(
1567 v.get_structured("IsGitDirty"),
1568 Some(&tera::Value::Bool(true))
1569 );
1570 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1571 }
1572
1573 #[test]
1574 fn test_is_snapshot_reflects_context_options() {
1575 let config = Config::default();
1576 let opts = ContextOptions {
1577 snapshot: true,
1578 ..Default::default()
1579 };
1580 let mut ctx = Context::new(config, opts);
1581 ctx.git_info = Some(make_git_info(false, None));
1582 ctx.populate_git_vars();
1583
1584 assert_eq!(
1585 ctx.template_vars().get_structured("IsSnapshot"),
1586 Some(&tera::Value::Bool(true))
1587 );
1588
1589 let config2 = Config::default();
1591 let opts2 = ContextOptions {
1592 snapshot: false,
1593 ..Default::default()
1594 };
1595 let mut ctx2 = Context::new(config2, opts2);
1596 ctx2.git_info = Some(make_git_info(false, None));
1597 ctx2.populate_git_vars();
1598
1599 assert_eq!(
1600 ctx2.template_vars().get_structured("IsSnapshot"),
1601 Some(&tera::Value::Bool(false))
1602 );
1603 }
1604
1605 #[test]
1606 fn test_is_draft_defaults_to_false() {
1607 let config = Config::default();
1608 let mut ctx = Context::new(config, ContextOptions::default());
1609 ctx.git_info = Some(make_git_info(false, None));
1610 ctx.populate_git_vars();
1611
1612 assert_eq!(
1613 ctx.template_vars().get_structured("IsDraft"),
1614 Some(&tera::Value::Bool(false))
1615 );
1616 }
1617
1618 #[test]
1619 fn test_previous_tag_empty_when_none() {
1620 let config = Config::default();
1621 let mut ctx = Context::new(config, ContextOptions::default());
1622 let mut info = make_git_info(false, None);
1623 info.previous_tag = None;
1624 ctx.git_info = Some(info);
1625 ctx.populate_git_vars();
1626
1627 assert_eq!(
1628 ctx.template_vars().get("PreviousTag"),
1629 Some(&"".to_string())
1630 );
1631 }
1632
1633 #[test]
1642 fn populate_time_vars_uses_source_date_epoch_when_set() {
1643 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1647 let config = Config::default();
1648 let mut ctx = Context::new(config, ContextOptions::default());
1649 ctx.set_env_source(env);
1650 ctx.populate_time_vars();
1651
1652 let v = ctx.template_vars();
1653 assert_eq!(
1654 v.get("Timestamp"),
1655 Some(&"1715000000".to_string()),
1656 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1657 );
1658 assert_eq!(
1659 v.get("Date"),
1660 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1661 "Date must be RFC 3339 derived from SDE"
1662 );
1663 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1664 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1665 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1666 }
1667
1668 #[test]
1669 fn test_populate_time_vars() {
1670 let env = crate::MapEnvSource::new();
1673 let config = Config::default();
1674 let mut ctx = Context::new(config, ContextOptions::default());
1675 ctx.set_env_source(env);
1676 ctx.populate_time_vars();
1677
1678 let v = ctx.template_vars();
1679
1680 let date = v
1682 .get("Date")
1683 .unwrap_or_else(|| panic!("Date should be set"));
1684 assert!(
1685 date.contains('T') && date.len() > 10,
1686 "Date should be RFC 3339, got: {date}"
1687 );
1688
1689 let ts = v
1691 .get("Timestamp")
1692 .unwrap_or_else(|| panic!("Timestamp should be set"));
1693 assert!(
1694 ts.parse::<i64>().is_ok(),
1695 "Timestamp should be a numeric string, got: {ts}"
1696 );
1697
1698 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1700 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1701 }
1702
1703 #[test]
1704 fn test_env_vars_accessible_in_templates() {
1705 let mut config = Config::default();
1706 config.project_name = "myapp".to_string();
1707 let mut ctx = Context::new(config, ContextOptions::default());
1708 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1709 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1710
1711 let result = ctx
1712 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1713 .unwrap();
1714 assert_eq!(result, "hello-world-staging");
1715 }
1716
1717 #[test]
1718 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1719 let config = Config::default();
1720 let opts = ContextOptions {
1721 snapshot: true,
1722 ..Default::default()
1723 };
1724 let mut ctx = Context::new(config, opts);
1725 ctx.populate_git_vars();
1727
1728 assert_eq!(
1729 ctx.template_vars().get_structured("IsSnapshot"),
1730 Some(&tera::Value::Bool(true))
1731 );
1732 assert_eq!(
1733 ctx.template_vars().get_structured("IsDraft"),
1734 Some(&tera::Value::Bool(false))
1735 );
1736 assert_eq!(ctx.template_vars().get("Tag"), None);
1738 }
1739
1740 #[test]
1741 fn test_is_nightly_set_when_nightly_mode_active() {
1742 let config = Config::default();
1743 let opts = ContextOptions {
1744 nightly: true,
1745 ..Default::default()
1746 };
1747 let mut ctx = Context::new(config, opts);
1748 ctx.git_info = Some(make_git_info(false, None));
1749 ctx.populate_git_vars();
1750
1751 assert_eq!(
1752 ctx.template_vars().get_structured("IsNightly"),
1753 Some(&tera::Value::Bool(true)),
1754 "IsNightly should be 'true' when nightly mode is active"
1755 );
1756 assert!(ctx.is_nightly(), "is_nightly() should return true");
1757 }
1758
1759 #[test]
1760 fn test_is_nightly_false_by_default() {
1761 let config = Config::default();
1762 let mut ctx = Context::new(config, ContextOptions::default());
1763 ctx.git_info = Some(make_git_info(false, None));
1764 ctx.populate_git_vars();
1765
1766 assert_eq!(
1767 ctx.template_vars().get_structured("IsNightly"),
1768 Some(&tera::Value::Bool(false)),
1769 "IsNightly should default to 'false'"
1770 );
1771 assert!(
1772 !ctx.is_nightly(),
1773 "is_nightly() should return false by default"
1774 );
1775 }
1776
1777 #[test]
1778 fn test_version_returns_populated_value() {
1779 let config = Config::default();
1780 let mut ctx = Context::new(config, ContextOptions::default());
1781 ctx.git_info = Some(make_git_info(false, None));
1782 ctx.populate_git_vars();
1783
1784 assert_eq!(ctx.version(), "1.2.3");
1785 }
1786
1787 #[test]
1788 fn test_version_returns_empty_when_not_set() {
1789 let config = Config::default();
1790 let ctx = Context::new(config, ContextOptions::default());
1791 assert_eq!(ctx.version(), "");
1792 }
1793
1794 #[test]
1795 fn test_is_nightly_without_git_info() {
1796 let config = Config::default();
1797 let opts = ContextOptions {
1798 nightly: true,
1799 ..Default::default()
1800 };
1801 let mut ctx = Context::new(config, opts);
1802 ctx.populate_git_vars();
1804
1805 assert_eq!(
1806 ctx.template_vars().get_structured("IsNightly"),
1807 Some(&tera::Value::Bool(true)),
1808 "IsNightly should be set even without git info"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_is_git_clean_when_not_dirty() {
1814 let config = Config::default();
1815 let mut ctx = Context::new(config, ContextOptions::default());
1816 ctx.git_info = Some(make_git_info(false, None));
1817 ctx.populate_git_vars();
1818
1819 assert_eq!(
1820 ctx.template_vars().get_structured("IsGitClean"),
1821 Some(&tera::Value::Bool(true))
1822 );
1823 }
1824
1825 #[test]
1826 fn test_is_git_clean_when_dirty() {
1827 let config = Config::default();
1828 let mut ctx = Context::new(config, ContextOptions::default());
1829 ctx.git_info = Some(make_git_info(true, None));
1830 ctx.populate_git_vars();
1831
1832 assert_eq!(
1833 ctx.template_vars().get_structured("IsGitClean"),
1834 Some(&tera::Value::Bool(false))
1835 );
1836 }
1837
1838 #[test]
1839 fn test_git_url_set_from_git_info() {
1840 let config = Config::default();
1841 let mut ctx = Context::new(config, ContextOptions::default());
1842 ctx.git_info = Some(make_git_info(false, None));
1843 ctx.populate_git_vars();
1844
1845 assert_eq!(
1846 ctx.template_vars().get("GitURL"),
1847 Some(&"https://github.com/test/repo.git".to_string())
1848 );
1849 }
1850
1851 #[test]
1852 fn test_summary_set_from_git_info() {
1853 let config = Config::default();
1854 let mut ctx = Context::new(config, ContextOptions::default());
1855 ctx.git_info = Some(make_git_info(false, None));
1856 ctx.populate_git_vars();
1857
1858 assert_eq!(
1859 ctx.template_vars().get("Summary"),
1860 Some(&"v1.2.3-0-gabc123d".to_string())
1861 );
1862 }
1863
1864 #[test]
1865 fn test_tag_subject_set_from_git_info() {
1866 let config = Config::default();
1867 let mut ctx = Context::new(config, ContextOptions::default());
1868 ctx.git_info = Some(make_git_info(false, None));
1869 ctx.populate_git_vars();
1870
1871 assert_eq!(
1872 ctx.template_vars().get("TagSubject"),
1873 Some(&"Release v1.2.3".to_string())
1874 );
1875 }
1876
1877 #[test]
1878 fn test_tag_contents_set_from_git_info() {
1879 let config = Config::default();
1880 let mut ctx = Context::new(config, ContextOptions::default());
1881 ctx.git_info = Some(make_git_info(false, None));
1882 ctx.populate_git_vars();
1883
1884 assert_eq!(
1885 ctx.template_vars().get("TagContents"),
1886 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
1887 );
1888 }
1889
1890 #[test]
1891 fn test_tag_body_set_from_git_info() {
1892 let config = Config::default();
1893 let mut ctx = Context::new(config, ContextOptions::default());
1894 ctx.git_info = Some(make_git_info(false, None));
1895 ctx.populate_git_vars();
1896
1897 assert_eq!(
1898 ctx.template_vars().get("TagBody"),
1899 Some(&"Full release notes here.".to_string())
1900 );
1901 }
1902
1903 #[test]
1904 fn test_is_single_target_false_by_default() {
1905 let config = Config::default();
1906 let mut ctx = Context::new(config, ContextOptions::default());
1907 ctx.git_info = Some(make_git_info(false, None));
1908 ctx.populate_git_vars();
1909
1910 assert_eq!(
1911 ctx.template_vars().get_structured("IsSingleTarget"),
1912 Some(&tera::Value::Bool(false))
1913 );
1914 }
1915
1916 #[test]
1917 fn test_is_single_target_true_when_set() {
1918 let config = Config::default();
1919 let opts = ContextOptions {
1920 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
1921 ..Default::default()
1922 };
1923 let mut ctx = Context::new(config, opts);
1924 ctx.git_info = Some(make_git_info(false, None));
1925 ctx.populate_git_vars();
1926
1927 assert_eq!(
1928 ctx.template_vars().get_structured("IsSingleTarget"),
1929 Some(&tera::Value::Bool(true))
1930 );
1931 }
1932
1933 #[test]
1934 #[serial_test::serial]
1935 fn test_populate_runtime_vars() {
1936 let config = Config::default();
1937 let mut ctx = Context::new(config, ContextOptions::default());
1938 ctx.populate_runtime_vars();
1939
1940 let v = ctx.template_vars();
1941
1942 let goos = v
1943 .get("RuntimeGoos")
1944 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
1945 assert!(
1946 !goos.is_empty(),
1947 "RuntimeGoos should not be empty, got: {goos}"
1948 );
1949 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
1951
1952 let goarch = v
1953 .get("RuntimeGoarch")
1954 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
1955 assert!(
1956 !goarch.is_empty(),
1957 "RuntimeGoarch should not be empty, got: {goarch}"
1958 );
1959 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
1961 }
1962
1963 #[test]
1964 fn test_populate_release_notes_var_with_changelogs() {
1965 let mut config = Config::default();
1966 config.crates.push(crate::config::CrateConfig {
1967 name: "my-crate".to_string(),
1968 ..Default::default()
1969 });
1970 let mut ctx = Context::new(config, ContextOptions::default());
1971 ctx.stage_outputs
1972 .changelogs
1973 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
1974 ctx.populate_release_notes_var();
1975
1976 assert_eq!(
1977 ctx.template_vars().get("ReleaseNotes"),
1978 Some(&"## Changes\n- fix bug".to_string())
1979 );
1980 }
1981
1982 #[test]
1983 fn test_populate_release_notes_var_empty_when_no_changelogs() {
1984 let config = Config::default();
1985 let mut ctx = Context::new(config, ContextOptions::default());
1986 ctx.populate_release_notes_var();
1987
1988 assert_eq!(
1989 ctx.template_vars().get("ReleaseNotes"),
1990 Some(&"".to_string())
1991 );
1992 }
1993
1994 #[test]
1995 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
1996 let mut config = Config::default();
1997 config.crates.push(crate::config::CrateConfig {
1998 name: "crate-a".to_string(),
1999 ..Default::default()
2000 });
2001 config.crates.push(crate::config::CrateConfig {
2002 name: "crate-b".to_string(),
2003 ..Default::default()
2004 });
2005 let mut ctx = Context::new(config, ContextOptions::default());
2006 ctx.stage_outputs
2007 .changelogs
2008 .insert("crate-a".to_string(), "notes-a".to_string());
2009 ctx.stage_outputs
2010 .changelogs
2011 .insert("crate-b".to_string(), "notes-b".to_string());
2012 ctx.populate_release_notes_var();
2013
2014 assert_eq!(
2016 ctx.template_vars().get("ReleaseNotes"),
2017 Some(&"notes-a".to_string())
2018 );
2019 }
2020
2021 #[test]
2022 fn test_outputs_accessible_in_templates() {
2023 let mut config = Config::default();
2024 config.project_name = "myapp".to_string();
2025 let mut ctx = Context::new(config, ContextOptions::default());
2026 ctx.template_vars_mut().set_output("build_id", "abc123");
2027 ctx.template_vars_mut()
2028 .set_output("deploy_url", "https://example.com");
2029
2030 let result = ctx
2031 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
2032 .unwrap();
2033 assert_eq!(result, "abc123-https://example.com");
2034 }
2035
2036 #[test]
2037 fn test_artifact_ext_and_target_template_vars() {
2038 let mut config = Config::default();
2039 config.project_name = "myapp".to_string();
2040 let mut ctx = Context::new(config, ContextOptions::default());
2041 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
2042 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
2043 ctx.template_vars_mut()
2044 .set("Target", "x86_64-unknown-linux-gnu");
2045
2046 let result = ctx
2047 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
2048 .unwrap();
2049 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
2050 }
2051
2052 #[test]
2053 fn test_checksums_template_var() {
2054 let mut config = Config::default();
2055 config.project_name = "myapp".to_string();
2056 let mut ctx = Context::new(config, ContextOptions::default());
2057 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
2058 ctx.template_vars_mut().set("Checksums", checksum_text);
2059
2060 let result = ctx.render_template("{{ .Checksums }}").unwrap();
2061 assert_eq!(result, checksum_text);
2062 }
2063
2064 #[test]
2067 fn test_prefixed_tag_with_tag_prefix() {
2068 let mut config = Config::default();
2069 config.tag = Some(crate::config::TagConfig {
2070 tag_prefix: Some("api/".to_string()),
2071 ..Default::default()
2072 });
2073 let mut ctx = Context::new(config, ContextOptions::default());
2074 ctx.git_info = Some(make_git_info(false, None));
2075 ctx.populate_git_vars();
2076
2077 assert_eq!(
2078 ctx.template_vars().get("PrefixedTag"),
2079 Some(&"api/v1.2.3".to_string())
2080 );
2081 }
2082
2083 #[test]
2084 fn test_prefixed_tag_without_tag_prefix() {
2085 let config = Config::default();
2086 let mut ctx = Context::new(config, ContextOptions::default());
2087 ctx.git_info = Some(make_git_info(false, None));
2088 ctx.populate_git_vars();
2089
2090 assert_eq!(
2092 ctx.template_vars().get("PrefixedTag"),
2093 Some(&"v1.2.3".to_string())
2094 );
2095 }
2096
2097 #[test]
2098 fn test_prefixed_previous_tag_with_tag_prefix() {
2099 let mut config = Config::default();
2100 config.tag = Some(crate::config::TagConfig {
2101 tag_prefix: Some("api/".to_string()),
2102 ..Default::default()
2103 });
2104 let mut ctx = Context::new(config, ContextOptions::default());
2105 ctx.git_info = Some(make_git_info(false, None));
2106 ctx.populate_git_vars();
2107
2108 assert_eq!(
2109 ctx.template_vars().get("PrefixedPreviousTag"),
2110 Some(&"api/v1.2.2".to_string())
2111 );
2112 }
2113
2114 #[test]
2115 fn test_prefixed_previous_tag_empty_when_no_previous() {
2116 let mut config = Config::default();
2117 config.tag = Some(crate::config::TagConfig {
2118 tag_prefix: Some("api/".to_string()),
2119 ..Default::default()
2120 });
2121 let mut ctx = Context::new(config, ContextOptions::default());
2122 let mut info = make_git_info(false, None);
2123 info.previous_tag = None;
2124 ctx.git_info = Some(info);
2125 ctx.populate_git_vars();
2126
2127 assert_eq!(
2130 ctx.template_vars().get("PrefixedPreviousTag"),
2131 Some(&"".to_string())
2132 );
2133 }
2134
2135 #[test]
2136 fn test_prefixed_summary_with_tag_prefix() {
2137 let mut config = Config::default();
2138 config.tag = Some(crate::config::TagConfig {
2139 tag_prefix: Some("api/".to_string()),
2140 ..Default::default()
2141 });
2142 let mut ctx = Context::new(config, ContextOptions::default());
2143 ctx.git_info = Some(make_git_info(false, None));
2144 ctx.populate_git_vars();
2145
2146 assert_eq!(
2147 ctx.template_vars().get("PrefixedSummary"),
2148 Some(&"api/v1.2.3-0-gabc123d".to_string())
2149 );
2150 }
2151
2152 #[test]
2153 fn test_is_release_true_for_normal_release() {
2154 let config = Config::default();
2155 let opts = ContextOptions {
2156 snapshot: false,
2157 nightly: false,
2158 ..Default::default()
2159 };
2160 let mut ctx = Context::new(config, opts);
2161 ctx.git_info = Some(make_git_info(false, None));
2162 ctx.populate_git_vars();
2163
2164 assert_eq!(
2165 ctx.template_vars().get_structured("IsRelease"),
2166 Some(&tera::Value::Bool(true))
2167 );
2168 }
2169
2170 #[test]
2171 fn test_is_release_false_for_snapshot() {
2172 let config = Config::default();
2173 let opts = ContextOptions {
2174 snapshot: true,
2175 ..Default::default()
2176 };
2177 let mut ctx = Context::new(config, opts);
2178 ctx.git_info = Some(make_git_info(false, None));
2179 ctx.populate_git_vars();
2180
2181 assert_eq!(
2182 ctx.template_vars().get_structured("IsRelease"),
2183 Some(&tera::Value::Bool(false))
2184 );
2185 }
2186
2187 #[test]
2188 fn test_is_release_false_for_nightly() {
2189 let config = Config::default();
2190 let opts = ContextOptions {
2191 nightly: true,
2192 ..Default::default()
2193 };
2194 let mut ctx = Context::new(config, opts);
2195 ctx.git_info = Some(make_git_info(false, None));
2196 ctx.populate_git_vars();
2197
2198 assert_eq!(
2199 ctx.template_vars().get_structured("IsRelease"),
2200 Some(&tera::Value::Bool(false))
2201 );
2202 }
2203
2204 #[test]
2205 fn test_is_merging_true_when_merge_flag_set() {
2206 let config = Config::default();
2207 let opts = ContextOptions {
2208 merge: true,
2209 ..Default::default()
2210 };
2211 let mut ctx = Context::new(config, opts);
2212 ctx.git_info = Some(make_git_info(false, None));
2213 ctx.populate_git_vars();
2214
2215 assert_eq!(
2216 ctx.template_vars().get_structured("IsMerging"),
2217 Some(&tera::Value::Bool(true))
2218 );
2219 }
2220
2221 #[test]
2222 fn test_is_merging_false_by_default() {
2223 let config = Config::default();
2224 let mut ctx = Context::new(config, ContextOptions::default());
2225 ctx.git_info = Some(make_git_info(false, None));
2226 ctx.populate_git_vars();
2227
2228 assert_eq!(
2229 ctx.template_vars().get_structured("IsMerging"),
2230 Some(&tera::Value::Bool(false))
2231 );
2232 }
2233
2234 #[test]
2235 fn test_refresh_artifacts_var_empty() {
2236 let config = Config::default();
2237 let mut ctx = Context::new(config, ContextOptions::default());
2238 ctx.refresh_artifacts_var();
2239
2240 let result = ctx
2242 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2243 .unwrap();
2244 assert_eq!(result, "");
2245 }
2246
2247 #[test]
2248 fn test_refresh_artifacts_var_with_artifacts() {
2249 use crate::artifact::{Artifact, ArtifactKind};
2250 use std::collections::HashMap;
2251 use std::path::PathBuf;
2252
2253 let config = Config::default();
2254 let mut ctx = Context::new(config, ContextOptions::default());
2255 ctx.artifacts.add(Artifact {
2259 kind: ArtifactKind::Archive,
2260 name: String::new(),
2261 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2262 target: Some("x86_64-unknown-linux-gnu".to_string()),
2263 crate_name: "myapp".to_string(),
2264 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2265 size: None,
2266 });
2267 ctx.artifacts.add(Artifact {
2268 kind: ArtifactKind::Binary,
2269 name: String::new(),
2270 path: PathBuf::from("dist/myapp"),
2271 target: Some("x86_64-unknown-linux-gnu".to_string()),
2272 crate_name: "myapp".to_string(),
2273 metadata: HashMap::new(),
2274 size: None,
2275 });
2276 ctx.refresh_artifacts_var();
2277
2278 let result = ctx
2280 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2281 .unwrap();
2282 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2283 assert!(result.contains("myapp"));
2284
2285 let result_kinds = ctx
2287 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2288 .unwrap();
2289 assert!(result_kinds.contains("archive"));
2290 assert!(result_kinds.contains("binary"));
2291 }
2292
2293 #[test]
2294 fn test_populate_metadata_var_with_mod_timestamp() {
2295 let mut config = Config::default();
2296 config.metadata = Some(crate::config::MetadataConfig {
2297 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2298 ..Default::default()
2299 });
2300 let mut ctx = Context::new(config, ContextOptions::default());
2301 ctx.populate_metadata_var().unwrap();
2302
2303 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2305 assert_eq!(result, "{{ .CommitTimestamp }}");
2306 }
2307
2308 #[test]
2309 fn test_populate_metadata_var_empty_when_no_config() {
2310 let config = Config::default();
2311 let mut ctx = Context::new(config, ContextOptions::default());
2312 ctx.populate_metadata_var().unwrap();
2313
2314 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2316 assert_eq!(result, "");
2317 }
2318
2319 #[test]
2320 fn test_populate_metadata_var_reads_from_config() {
2321 let mut config = Config::default();
2322 config.metadata = Some(crate::config::MetadataConfig {
2323 description: Some("A test project".to_string()),
2324 homepage: Some("https://example.com".to_string()),
2325 license: Some("MIT".to_string()),
2326 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2327 mod_timestamp: Some("1234567890".to_string()),
2328 ..Default::default()
2329 });
2330 let mut ctx = Context::new(config, ContextOptions::default());
2331 ctx.populate_metadata_var().unwrap();
2332
2333 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2334 assert_eq!(desc, "A test project");
2335
2336 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2337 assert_eq!(home, "https://example.com");
2338
2339 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2340 assert_eq!(lic, "MIT");
2341
2342 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2343 assert_eq!(ts, "1234567890");
2344 }
2345
2346 #[test]
2347 fn test_populate_metadata_var_full_description_inline() {
2348 use crate::config::ContentSource;
2349 let mut config = Config::default();
2350 config.metadata = Some(crate::config::MetadataConfig {
2351 full_description: Some(ContentSource::Inline(
2352 "A long-form description of the project.".to_string(),
2353 )),
2354 ..Default::default()
2355 });
2356 let mut ctx = Context::new(config, ContextOptions::default());
2357 ctx.populate_metadata_var().unwrap();
2358 let rendered = ctx
2359 .render_template("{{ Metadata.FullDescription }}")
2360 .unwrap();
2361 assert_eq!(rendered, "A long-form description of the project.");
2362 }
2363
2364 #[test]
2365 fn test_populate_metadata_var_full_description_from_file() {
2366 use crate::config::ContentSource;
2367 let tmp = tempfile::tempdir().unwrap();
2368 let desc_path = tmp.path().join("DESCRIPTION.md");
2369 std::fs::write(&desc_path, "read from disk").unwrap();
2370 let mut config = Config::default();
2371 config.metadata = Some(crate::config::MetadataConfig {
2372 full_description: Some(ContentSource::FromFile {
2373 from_file: desc_path.to_string_lossy().into_owned(),
2374 }),
2375 ..Default::default()
2376 });
2377 let mut ctx = Context::new(config, ContextOptions::default());
2378 ctx.populate_metadata_var().unwrap();
2379 let rendered = ctx
2380 .render_template("{{ Metadata.FullDescription }}")
2381 .unwrap();
2382 assert_eq!(rendered, "read from disk");
2383 }
2384
2385 #[test]
2386 fn test_populate_metadata_var_full_description_from_url_resolves() {
2387 use crate::config::ContentSource;
2392 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2393
2394 let body = "long form description body";
2395 let body_len = body.len();
2396 let response: &'static str = Box::leak(
2397 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2398 );
2399 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2400
2401 let mut config = Config::default();
2402 config.metadata = Some(crate::config::MetadataConfig {
2403 full_description: Some(ContentSource::FromUrl {
2404 from_url: format!("http://{addr}/description.md"),
2405 headers: None,
2406 }),
2407 ..Default::default()
2408 });
2409 let mut ctx = Context::new(config, ContextOptions::default());
2410 ctx.populate_metadata_var()
2411 .expect("from_url should resolve through content_source");
2412 let rendered = ctx
2413 .render_template("{{ Metadata.FullDescription }}")
2414 .unwrap();
2415 assert_eq!(rendered, body);
2416 }
2417
2418 #[test]
2419 fn test_populate_metadata_var_commit_author() {
2420 use crate::config::CommitAuthorConfig;
2421 let mut config = Config::default();
2422 config.metadata = Some(crate::config::MetadataConfig {
2423 commit_author: Some(CommitAuthorConfig {
2424 name: Some("Alice Developer".to_string()),
2425 email: Some("alice@example.com".to_string()),
2426 signing: None,
2427 use_github_app_token: false,
2428 }),
2429 ..Default::default()
2430 });
2431 let mut ctx = Context::new(config, ContextOptions::default());
2432 ctx.populate_metadata_var().unwrap();
2433 let name = ctx
2434 .render_template("{{ Metadata.CommitAuthor.Name }}")
2435 .unwrap();
2436 assert_eq!(name, "Alice Developer");
2437 let email = ctx
2438 .render_template("{{ Metadata.CommitAuthor.Email }}")
2439 .unwrap();
2440 assert_eq!(email, "alice@example.com");
2441 }
2442
2443 #[test]
2444 fn test_artifact_id_template_var() {
2445 let mut config = Config::default();
2446 config.project_name = "myapp".to_string();
2447 let mut ctx = Context::new(config, ContextOptions::default());
2448 ctx.template_vars_mut().set("ArtifactID", "default");
2449
2450 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2451 assert_eq!(result, "default");
2452 }
2453
2454 #[test]
2455 fn test_artifact_id_empty_when_not_set() {
2456 let mut config = Config::default();
2457 config.project_name = "myapp".to_string();
2458 let mut ctx = Context::new(config, ContextOptions::default());
2459 ctx.template_vars_mut().set("ArtifactID", "");
2460
2461 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2462 assert_eq!(result, "");
2463 }
2464
2465 #[test]
2466 fn test_pro_vars_rendered_in_templates() {
2467 let mut config = Config::default();
2469 config.tag = Some(crate::config::TagConfig {
2470 tag_prefix: Some("api/".to_string()),
2471 ..Default::default()
2472 });
2473 let opts = ContextOptions {
2474 snapshot: false,
2475 nightly: false,
2476 merge: true,
2477 ..Default::default()
2478 };
2479 let mut ctx = Context::new(config, opts);
2480 ctx.git_info = Some(make_git_info(false, None));
2481 ctx.populate_git_vars();
2482
2483 let result = ctx
2484 .render_template(
2485 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2486 )
2487 .unwrap();
2488 assert_eq!(result, "release-merge-api/v1.2.3");
2489 }
2490
2491 #[test]
2492 fn test_is_release_without_git_info() {
2493 let config = Config::default();
2495 let opts = ContextOptions {
2496 snapshot: false,
2497 nightly: false,
2498 ..Default::default()
2499 };
2500 let mut ctx = Context::new(config, opts);
2501 ctx.populate_git_vars();
2502
2503 assert_eq!(
2504 ctx.template_vars().get_structured("IsRelease"),
2505 Some(&tera::Value::Bool(true))
2506 );
2507 }
2508
2509 #[test]
2510 fn test_is_merging_without_git_info() {
2511 let config = Config::default();
2513 let opts = ContextOptions {
2514 merge: true,
2515 ..Default::default()
2516 };
2517 let mut ctx = Context::new(config, opts);
2518 ctx.populate_git_vars();
2519
2520 assert_eq!(
2521 ctx.template_vars().get_structured("IsMerging"),
2522 Some(&tera::Value::Bool(true))
2523 );
2524 }
2525
2526 #[test]
2536 fn test_monorepo_version_matches_shared_semver_helper() {
2537 let mut config = Config::default();
2538 config.monorepo = Some(crate::config::MonorepoConfig {
2539 tag_prefix: Some("core/".to_string()),
2540 dir: None,
2541 });
2542 let mut ctx = Context::new(config, ContextOptions::default());
2543
2544 let semver = SemVer {
2545 major: 2,
2546 minor: 1,
2547 patch: 0,
2548 prerelease: Some("rc.1".to_string()),
2549 build_metadata: Some("build.7".to_string()),
2550 };
2551 let mut info = make_git_info(false, None);
2552 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2553 info.semver = semver.clone();
2554 ctx.git_info = Some(info);
2555 ctx.populate_git_vars();
2556
2557 let v = ctx.template_vars();
2558 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2561 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2562 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2563 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2564 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2566 }
2567
2568 #[test]
2569 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2570 let mut config = Config::default();
2571 config.monorepo = Some(crate::config::MonorepoConfig {
2572 tag_prefix: Some("subproject1/".to_string()),
2573 dir: None,
2574 });
2575 let mut ctx = Context::new(config, ContextOptions::default());
2576
2577 let mut info = make_git_info(false, None);
2579 info.tag = "subproject1/v1.2.3".to_string();
2580 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2581 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2582 ctx.git_info = Some(info);
2583 ctx.populate_git_vars();
2584
2585 let v = ctx.template_vars();
2586 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2588 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2590 assert_eq!(
2592 v.get("PrefixedTag"),
2593 Some(&"subproject1/v1.2.3".to_string())
2594 );
2595 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2597 assert_eq!(
2599 v.get("PrefixedPreviousTag"),
2600 Some(&"subproject1/v1.2.2".to_string())
2601 );
2602 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2604 assert_eq!(
2606 v.get("PrefixedSummary"),
2607 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2608 );
2609 }
2610
2611 #[test]
2612 fn test_monorepo_prefixed_previous_tag() {
2613 let mut config = Config::default();
2614 config.monorepo = Some(crate::config::MonorepoConfig {
2615 tag_prefix: Some("svc/".to_string()),
2616 dir: None,
2617 });
2618 let mut ctx = Context::new(config, ContextOptions::default());
2619
2620 let mut info = make_git_info(false, None);
2621 info.tag = "svc/v2.0.0".to_string();
2622 info.previous_tag = Some("svc/v1.9.0".to_string());
2623 ctx.git_info = Some(info);
2624 ctx.populate_git_vars();
2625
2626 let v = ctx.template_vars();
2627 assert_eq!(
2629 v.get("PrefixedPreviousTag"),
2630 Some(&"svc/v1.9.0".to_string())
2631 );
2632 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2634 }
2635
2636 #[test]
2637 fn test_no_monorepo_falls_back_to_tag_prefix() {
2638 let mut config = Config::default();
2640 config.tag = Some(crate::config::TagConfig {
2641 tag_prefix: Some("release/".to_string()),
2642 ..Default::default()
2643 });
2644 let mut ctx = Context::new(config, ContextOptions::default());
2645 ctx.git_info = Some(make_git_info(false, None));
2646 ctx.populate_git_vars();
2647
2648 let v = ctx.template_vars();
2649 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2651 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2653 assert_eq!(
2654 v.get("PrefixedPreviousTag"),
2655 Some(&"release/v1.2.2".to_string())
2656 );
2657 }
2658
2659 #[test]
2660 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2661 let mut config = Config::default();
2664 config.tag = Some(crate::config::TagConfig {
2665 tag_prefix: Some("release/".to_string()),
2666 ..Default::default()
2667 });
2668 config.monorepo = Some(crate::config::MonorepoConfig {
2669 tag_prefix: Some("svc/".to_string()),
2670 dir: None,
2671 });
2672 let mut ctx = Context::new(config, ContextOptions::default());
2673
2674 let mut info = make_git_info(false, None);
2675 info.tag = "svc/v1.2.3".to_string();
2676 info.previous_tag = Some("svc/v1.2.2".to_string());
2677 ctx.git_info = Some(info);
2678 ctx.populate_git_vars();
2679
2680 let v = ctx.template_vars();
2681 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2683 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2685 }
2686
2687 #[test]
2688 fn test_monorepo_prefixed_summary() {
2689 let mut config = Config::default();
2690 config.monorepo = Some(crate::config::MonorepoConfig {
2691 tag_prefix: Some("pkg/".to_string()),
2692 dir: None,
2693 });
2694 let mut ctx = Context::new(config, ContextOptions::default());
2695
2696 let mut info = make_git_info(false, None);
2697 info.tag = "pkg/v1.2.3".to_string();
2698 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
2700 ctx.git_info = Some(info);
2701 ctx.populate_git_vars();
2702
2703 assert_eq!(
2705 ctx.template_vars().get("PrefixedSummary"),
2706 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
2707 );
2708 assert_eq!(
2710 ctx.template_vars().get("Summary"),
2711 Some(&"v1.2.3-0-gabc123d".to_string())
2712 );
2713 }
2714
2715 #[test]
2716 fn test_monorepo_no_previous_tag() {
2717 let mut config = Config::default();
2718 config.monorepo = Some(crate::config::MonorepoConfig {
2719 tag_prefix: Some("svc/".to_string()),
2720 dir: None,
2721 });
2722 let mut ctx = Context::new(config, ContextOptions::default());
2723
2724 let mut info = make_git_info(false, None);
2725 info.tag = "svc/v1.0.0".to_string();
2726 info.previous_tag = None;
2727 ctx.git_info = Some(info);
2728 ctx.populate_git_vars();
2729
2730 let v = ctx.template_vars();
2731 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
2732 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
2734 }
2735
2736 #[test]
2741 fn test_monorepo_full_flow_all_vars() {
2742 let mut config = Config::default();
2745 config.project_name = "mymonorepo".to_string();
2746 config.monorepo = Some(crate::config::MonorepoConfig {
2747 tag_prefix: Some("services/api/".to_string()),
2748 dir: Some("services/api".to_string()),
2749 });
2750
2751 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
2753 assert_eq!(config.monorepo_dir(), Some("services/api"));
2754
2755 let mut ctx = Context::new(config, ContextOptions::default());
2756
2757 let mut info = make_git_info(false, None);
2760 info.tag = "services/api/v2.1.0".to_string();
2761 info.previous_tag = Some("services/api/v2.0.5".to_string());
2762 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
2763 info.semver = crate::git::SemVer {
2764 major: 2,
2765 minor: 1,
2766 patch: 0,
2767 prerelease: None,
2768 build_metadata: None,
2769 };
2770 ctx.git_info = Some(info);
2771 ctx.populate_git_vars();
2772
2773 let v = ctx.template_vars();
2774
2775 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
2777 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
2778 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2779 assert_eq!(v.get("Major"), Some(&"2".to_string()));
2780 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
2781 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
2782 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
2783 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
2784
2785 assert_eq!(
2787 v.get("PrefixedTag"),
2788 Some(&"services/api/v2.1.0".to_string())
2789 );
2790 assert_eq!(
2791 v.get("PrefixedPreviousTag"),
2792 Some(&"services/api/v2.0.5".to_string())
2793 );
2794 assert_eq!(
2795 v.get("PrefixedSummary"),
2796 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
2797 );
2798
2799 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
2801 }
2802
2803 #[test]
2804 fn context_env_var_defaults_to_process_env_source() {
2805 let ctx = Context::new(Config::default(), ContextOptions::default());
2806 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
2808 }
2809
2810 #[test]
2811 fn context_env_var_routes_to_injected_source() {
2812 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2813 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
2814 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
2815 assert_eq!(ctx.env_var("PATH"), None);
2819 }
2820
2821 #[test]
2822 #[serial_test::serial]
2823 fn populate_runtime_vars_sets_rustc_version() {
2824 let config = Config::default();
2825 let mut ctx = Context::new(config, ContextOptions::default());
2826 ctx.populate_runtime_vars();
2829
2830 let ver = ctx
2831 .template_vars()
2832 .get("RustcVersion")
2833 .expect("RustcVersion should be set after populate_runtime_vars");
2834 if !ver.is_empty() {
2838 assert!(
2839 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
2840 "RustcVersion should start with a digit: {ver}"
2841 );
2842 }
2843 }
2844}