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 determinism: Option<crate::DeterminismState>,
373 pub pending_outcome: Option<crate::PublisherOutcome>,
383 pub pending_evidence: Option<crate::PublishEvidence>,
396 built_crate_names: Option<std::collections::HashSet<String>>,
407 env_source: Arc<dyn EnvSource>,
415 #[cfg(feature = "test-helpers")]
423 pub log_capture: Option<crate::log::LogCapture>,
424 render_strict: std::cell::Cell<bool>,
439}
440
441impl Context {
442 pub fn new(config: Config, options: ContextOptions) -> Self {
443 let mut vars = TemplateVars::new();
444 vars.set("ProjectName", &config.project_name);
445 Self {
446 config,
447 artifacts: ArtifactRegistry::new(),
448 options,
449 stage_outputs: StageOutputs::default(),
450 template_vars: vars,
451 git_info: None,
452 token_type: ScmTokenType::GitHub,
453 skip_memento: crate::pipe_skip::SkipMemento::new(),
454 publish_report: None,
455 determinism: None,
456 pending_outcome: None,
457 pending_evidence: None,
458 built_crate_names: None,
459 env_source: Arc::new(ProcessEnvSource),
460 #[cfg(feature = "test-helpers")]
461 log_capture: None,
462 render_strict: std::cell::Cell::new(false),
463 }
464 }
465
466 pub fn env_var(&self, name: &str) -> Option<String> {
474 self.env_source.var(name)
475 }
476
477 pub fn set_env_source<S: EnvSource + 'static>(&mut self, src: S) {
483 self.env_source = Arc::new(src);
484 }
485
486 pub fn env_source(&self) -> &dyn EnvSource {
491 self.env_source.as_ref()
492 }
493
494 pub fn env_source_arc(&self) -> Arc<dyn EnvSource> {
500 Arc::clone(&self.env_source)
501 }
502
503 #[cfg(feature = "test-helpers")]
509 pub fn with_log_capture(&mut self, capture: crate::log::LogCapture) {
510 self.log_capture = Some(capture);
511 }
512
513 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
521 self.pending_outcome = Some(outcome);
522 }
523
524 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
528 self.pending_outcome.take()
529 }
530
531 pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
536 self.pending_evidence = Some(evidence);
537 }
538
539 pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
543 self.pending_evidence.take()
544 }
545
546 pub fn publish_report(&self) -> Option<&PublishReport> {
549 self.publish_report.as_ref()
550 }
551
552 pub fn set_publish_report(&mut self, r: PublishReport) {
558 self.publish_report = Some(r);
559 }
560
561 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
564 self.built_crate_names.as_ref()
565 }
566
567 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
570 self.built_crate_names = Some(names);
571 }
572
573 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
580 self.skip_memento.remember(stage, label, reason);
581 }
582
583 pub fn template_vars(&self) -> &TemplateVars {
584 &self.template_vars
585 }
586
587 pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
588 &mut self.template_vars
589 }
590
591 pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
592 crate::template::render(template, &self.template_vars)
593 }
594
595 pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
597 template.map(|t| self.render_template(t)).transpose()
598 }
599
600 pub fn skip_with_log(
609 &self,
610 skip: &Option<crate::config::StringOrBool>,
611 log: &StageLogger,
612 label: &str,
613 ) -> anyhow::Result<bool> {
614 let Some(d) = skip else {
615 return Ok(false);
616 };
617 let should_skip = d
618 .try_evaluates_to_true(|s| self.render_template(s))
619 .with_context(|| format!("evaluate skip expression for {label}"))?;
620 if should_skip {
621 log.status(&format!("{} skipped", label));
622 }
623 Ok(should_skip)
624 }
625
626 pub fn should_skip(&self, stage_name: &str) -> bool {
627 self.options.skip_stages.iter().any(|s| s == stage_name)
628 }
629
630 pub fn skip_validate(&self) -> bool {
632 self.should_skip("validate")
633 }
634
635 pub fn is_dry_run(&self) -> bool {
636 self.options.dry_run
637 }
638
639 pub fn is_snapshot(&self) -> bool {
640 self.options.snapshot
641 }
642
643 pub fn is_strict(&self) -> bool {
644 self.options.strict
645 }
646
647 pub fn set_render_strict(&self, on: bool) -> bool {
655 self.render_strict.replace(on)
656 }
657
658 pub fn render_is_strict(&self) -> bool {
665 self.render_strict.get() || self.is_strict()
666 }
667
668 pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
671 if self.options.strict {
672 anyhow::bail!("{} (strict mode)", msg);
673 }
674 log.warn(msg);
675 Ok(())
676 }
677
678 pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
687 if self.is_snapshot() {
688 log.status(&format!("{}: skipped (snapshot mode)", stage));
689 true
690 } else {
691 false
692 }
693 }
694
695 pub fn render_template_strict(
697 &self,
698 template: &str,
699 label: &str,
700 log: &crate::log::StageLogger,
701 ) -> anyhow::Result<String> {
702 match self.render_template(template) {
703 Ok(rendered) => Ok(rendered),
704 Err(e) => {
705 if self.options.strict {
706 anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
707 }
708 log.warn(&format!("{}: failed to render template: {}", label, e));
709 Ok(template.to_string())
710 }
711 }
712 }
713
714 pub fn is_nightly(&self) -> bool {
715 self.options.nightly
716 }
717
718 pub fn set_release_url(&mut self, url: &str) {
723 self.template_vars.set("ReleaseURL", url);
724 }
725
726 pub fn version(&self) -> String {
729 self.template_vars
730 .get("Version")
731 .cloned()
732 .unwrap_or_default()
733 }
734
735 pub fn verbosity(&self) -> Verbosity {
737 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
738 }
739
740 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
746 self.config.retry.unwrap_or_default().to_policy()
747 }
748
749 pub fn logger(&self, stage: &'static str) -> StageLogger {
757 #[allow(unused_mut)]
758 let mut log = StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact());
759 #[cfg(feature = "test-helpers")]
760 if let Some(cap) = &self.log_capture {
761 log = log.with_capture_handle(cap.clone());
762 }
763 log
764 }
765
766 fn env_for_redact(&self) -> Vec<(String, String)> {
772 use std::collections::HashMap;
773 let mut map: HashMap<String, String> = std::env::vars().collect();
774 for (k, v) in self.template_vars.all_env() {
775 map.insert(k.clone(), v.clone());
776 }
777 map.into_iter().collect()
778 }
779
780 pub fn populate_git_vars(&mut self) {
822 if let Some(ref info) = self.git_info {
823 let raw_version = info.semver.raw_version_string();
825
826 let version = info.semver.version_string();
832
833 self.template_vars.set("Tag", &info.tag);
834 self.template_vars.set("Version", &version);
835 self.template_vars.set("RawVersion", &raw_version);
836 self.template_vars.set("Base", &raw_version);
841 self.template_vars
842 .set("Major", &info.semver.major.to_string());
843 self.template_vars
844 .set("Minor", &info.semver.minor.to_string());
845 self.template_vars
846 .set("Patch", &info.semver.patch.to_string());
847 self.template_vars.set(
848 "Prerelease",
849 info.semver.prerelease.as_deref().unwrap_or(""),
850 );
851 self.template_vars.set(
852 "BuildMetadata",
853 info.semver.build_metadata.as_deref().unwrap_or(""),
854 );
855 self.template_vars.set("FullCommit", &info.commit);
856 self.template_vars.set("Commit", &info.commit);
857 self.template_vars.set("ShortCommit", &info.short_commit);
858 self.template_vars.set("Branch", &info.branch);
859 self.template_vars.set("CommitDate", &info.commit_date);
860 self.template_vars
861 .set("CommitTimestamp", &info.commit_timestamp);
862 self.template_vars
863 .set("IsGitDirty", if info.dirty { "true" } else { "false" });
864 self.template_vars
865 .set("IsGitClean", if info.dirty { "false" } else { "true" });
866 self.template_vars
867 .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
868 self.template_vars.set("GitURL", &info.remote_url);
869 self.template_vars.set("Summary", &info.summary);
870 self.template_vars.set("TagSubject", &info.tag_subject);
871 self.template_vars.set("TagContents", &info.tag_contents);
872 self.template_vars.set("TagBody", &info.tag_body);
873 self.template_vars
874 .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
875 self.template_vars
876 .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
877
878 let monorepo_prefix = self.config.monorepo_tag_prefix();
889
890 if let Some(prefix) = monorepo_prefix {
896 self.template_vars.set("PrefixedTag", &info.tag);
899
900 let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
902 self.template_vars.set("Tag", stripped_tag);
903
904 let version = info.semver.version_string();
914 self.template_vars.set("Version", &version);
915
916 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
918 self.template_vars.set("PrefixedPreviousTag", prev_tag);
919
920 let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
922 self.template_vars.set("PreviousTag", stripped_prev);
923
924 self.template_vars.set("PrefixedSummary", &info.summary);
928 let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
930 self.template_vars.set("Summary", stripped_summary);
931 } else {
932 let tag_prefix = self
934 .config
935 .tag
936 .as_ref()
937 .and_then(|t| t.tag_prefix.as_deref())
938 .unwrap_or("");
939 self.template_vars
940 .set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
941 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
942 let prefixed_prev = if prev_tag.is_empty() {
943 String::new()
944 } else {
945 format!("{}{}", tag_prefix, prev_tag)
946 };
947 self.template_vars
948 .set("PrefixedPreviousTag", &prefixed_prev);
949 self.template_vars.set(
950 "PrefixedSummary",
951 &format!("{}{}", tag_prefix, info.summary),
952 );
953 }
954 }
955
956 let nightly_build = if self.git_info.is_some() {
969 let root = self
970 .options
971 .project_root
972 .clone()
973 .unwrap_or_else(|| PathBuf::from("."));
974 let monorepo_prefix = self.config.monorepo_tag_prefix();
975 crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
976 } else {
977 0
978 };
979 self.template_vars
980 .set("NightlyBuild", &nightly_build.to_string());
981
982 self.template_vars.set(
983 "IsSnapshot",
984 if self.options.snapshot {
985 "true"
986 } else {
987 "false"
988 },
989 );
990 self.template_vars.set(
991 "IsNightly",
992 if self.options.nightly {
993 "true"
994 } else {
995 "false"
996 },
997 );
998 self.template_vars.set(
1002 "IsHarness",
1003 if self.env_var("ANODIZER_IN_DETERMINISM_HARNESS").is_some() {
1004 "true"
1005 } else {
1006 "false"
1007 },
1008 );
1009 let is_draft = self
1011 .config
1012 .release
1013 .as_ref()
1014 .and_then(|r| r.draft)
1015 .unwrap_or(false);
1016 self.template_vars
1017 .set("IsDraft", if is_draft { "true" } else { "false" });
1018 self.template_vars.set(
1019 "IsSingleTarget",
1020 if self.options.single_target.is_some() {
1021 "true"
1022 } else {
1023 "false"
1024 },
1025 );
1026
1027 let is_release = !self.options.snapshot && !self.options.nightly;
1029 self.template_vars
1030 .set("IsRelease", if is_release { "true" } else { "false" });
1031
1032 self.template_vars.set(
1034 "IsMerging",
1035 if self.options.merge { "true" } else { "false" },
1036 );
1037 }
1038
1039 pub fn populate_time_vars(&mut self) {
1067 let now = crate::sde::resolve_now_with_env(self.env_source());
1075 self.template_vars.set("Date", &now.to_rfc3339());
1076 self.template_vars
1077 .set("Timestamp", &now.timestamp().to_string());
1078 self.template_vars.set("Now", &now.to_rfc3339());
1079 self.template_vars
1080 .set("Year", &now.format("%Y").to_string());
1081 self.template_vars
1082 .set("Month", &now.format("%m").to_string());
1083 self.template_vars.set("Day", &now.format("%d").to_string());
1084 self.template_vars
1085 .set("Hour", &now.format("%H").to_string());
1086 self.template_vars
1087 .set("Minute", &now.format("%M").to_string());
1088 }
1089
1090 pub fn populate_runtime_vars(&mut self) {
1099 let goos = map_os_to_goos(std::env::consts::OS);
1100 let goarch = map_arch_to_goarch(std::env::consts::ARCH);
1101 self.template_vars.set("RuntimeGoos", goos);
1102 self.template_vars.set("RuntimeGoarch", goarch);
1103 self.template_vars.set("Runtime_Goos", goos);
1106 self.template_vars.set("Runtime_Goarch", goarch);
1107 self.populate_rustc_vars();
1111 }
1112
1113 fn populate_rustc_vars(&mut self) {
1120 let ver = crate::partial::detect_rustc_version().unwrap_or_default();
1121 self.template_vars.set("RustcVersion", &ver);
1122 }
1123
1124 pub fn populate_release_notes_var(&mut self) {
1132 let notes = self
1134 .config
1135 .crates
1136 .iter()
1137 .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
1138 .cloned()
1139 .unwrap_or_default();
1140 self.template_vars.set("ReleaseNotes", ¬es);
1141 }
1142
1143 pub fn refresh_artifacts_var(&mut self) {
1158 const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
1163 const JSON_LIST_KEYS: &[&str] = &["Platforms"];
1169
1170 let artifacts_value: Vec<serde_json::Value> = self
1171 .artifacts
1172 .all()
1173 .iter()
1174 .map(|a| {
1175 let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
1177 for (k, v) in &a.metadata {
1178 if CSV_LIST_KEYS.contains(&k.as_str()) {
1179 let items: Vec<serde_json::Value> = if v.is_empty() {
1180 Vec::new()
1181 } else {
1182 v.split(',')
1183 .map(|s| serde_json::Value::String(s.to_string()))
1184 .collect()
1185 };
1186 metadata_map.insert(k.clone(), serde_json::Value::Array(items));
1187 } else if JSON_LIST_KEYS.contains(&k.as_str()) {
1188 let parsed = serde_json::from_str::<serde_json::Value>(v)
1192 .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
1193 metadata_map.insert(k.clone(), parsed);
1194 } else {
1195 metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
1196 }
1197 }
1198 serde_json::json!({
1199 "name": a.name,
1200 "path": a.path.to_string_lossy(),
1201 "target": a.target.as_deref().unwrap_or(""),
1202 "kind": a.kind.as_str(),
1203 "crate_name": a.crate_name,
1204 "metadata": serde_json::Value::Object(metadata_map),
1205 })
1206 })
1207 .collect();
1208 let tera_value = tera::Value::Array(artifacts_value);
1211 self.template_vars.set_structured("Artifacts", tera_value);
1212 }
1213
1214 pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
1227 let (
1230 description,
1231 homepage,
1232 license,
1233 maintainers,
1234 mod_timestamp,
1235 full_desc_src,
1236 commit_author,
1237 ) = {
1238 let meta = self.config.metadata.as_ref();
1239 let description = meta
1240 .and_then(|m| m.description.as_deref())
1241 .unwrap_or("")
1242 .to_string();
1243 let homepage = meta
1244 .and_then(|m| m.homepage.as_deref())
1245 .unwrap_or("")
1246 .to_string();
1247 let license = meta
1248 .and_then(|m| m.license.as_deref())
1249 .unwrap_or("")
1250 .to_string();
1251 let maintainers: Vec<String> = meta
1252 .and_then(|m| m.maintainers.as_ref())
1253 .cloned()
1254 .unwrap_or_default();
1255 let mod_timestamp = meta
1256 .and_then(|m| m.mod_timestamp.as_deref())
1257 .unwrap_or("")
1258 .to_string();
1259 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1260 let commit_author = meta.and_then(|m| m.commit_author.clone());
1261 (
1262 description,
1263 homepage,
1264 license,
1265 maintainers,
1266 mod_timestamp,
1267 full_desc_src,
1268 commit_author,
1269 )
1270 };
1271
1272 let full_description = match full_desc_src {
1278 None => String::new(),
1279 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1280 };
1281
1282 let commit_author_map = serde_json::json!({
1283 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1284 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1285 });
1286
1287 let meta_map = serde_json::json!({
1288 "Description": description,
1289 "Homepage": homepage,
1290 "License": license,
1291 "Maintainers": maintainers,
1292 "ModTimestamp": mod_timestamp,
1293 "FullDescription": full_description,
1294 "CommitAuthor": commit_author_map,
1295 });
1296 self.template_vars.set_structured("Metadata", meta_map);
1298 Ok(())
1299 }
1300}
1301
1302pub fn map_os_to_goos(os: &str) -> &str {
1305 match os {
1306 "macos" => "darwin",
1307 other => other, }
1309}
1310
1311pub fn map_arch_to_goarch(arch: &str) -> &str {
1314 match arch {
1315 "x86_64" => "amd64",
1316 "x86" => "386",
1317 "aarch64" => "arm64",
1318 "powerpc64" => "ppc64",
1319 "s390x" => "s390x",
1320 "mips" => "mips",
1321 "mips64" => "mips64",
1322 "riscv64" => "riscv64",
1323 other => other,
1324 }
1325}
1326
1327#[cfg(test)]
1328#[allow(clippy::field_reassign_with_default)]
1329mod tests {
1330 use super::*;
1331 use crate::config::Config;
1332 use crate::git::{GitInfo, SemVer};
1333
1334 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1335 let tag = match prerelease {
1336 Some(pre) => format!("v1.2.3-{pre}"),
1337 None => "v1.2.3".to_string(),
1338 };
1339 GitInfo {
1340 tag,
1341 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1342 short_commit: "abc123d".to_string(),
1343 branch: "main".to_string(),
1344 dirty,
1345 semver: SemVer {
1346 major: 1,
1347 minor: 2,
1348 patch: 3,
1349 prerelease: prerelease.map(|s| s.to_string()),
1350 build_metadata: None,
1351 },
1352 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1353 commit_timestamp: "1774463400".to_string(),
1354 previous_tag: Some("v1.2.2".to_string()),
1355 remote_url: "https://github.com/test/repo.git".to_string(),
1356 summary: "v1.2.3-0-gabc123d".to_string(),
1357 tag_subject: "Release v1.2.3".to_string(),
1358 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1359 tag_body: "Full release notes here.".to_string(),
1360 first_commit: None,
1361 }
1362 }
1363
1364 #[test]
1365 fn test_context_template_vars() {
1366 let mut config = Config::default();
1367 config.project_name = "test-project".to_string();
1368 let ctx = Context::new(config, ContextOptions::default());
1369 assert_eq!(
1370 ctx.template_vars().get("ProjectName"),
1371 Some(&"test-project".to_string())
1372 );
1373 }
1374
1375 #[test]
1376 fn test_context_should_skip() {
1377 let config = Config::default();
1378 let opts = ContextOptions {
1379 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1380 ..Default::default()
1381 };
1382 let ctx = Context::new(config, opts);
1383 assert!(ctx.should_skip("publish"));
1384 assert!(ctx.should_skip("announce"));
1385 assert!(!ctx.should_skip("build"));
1386 }
1387
1388 #[test]
1389 fn test_context_render_template() {
1390 let mut config = Config::default();
1391 config.project_name = "myapp".to_string();
1392 let ctx = Context::new(config, ContextOptions::default());
1393 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1394 assert_eq!(result, "myapp-release");
1395 }
1396
1397 #[test]
1398 fn test_populate_git_vars_sets_all_expected_vars() {
1399 let config = Config::default();
1400 let mut ctx = Context::new(config, ContextOptions::default());
1401 ctx.git_info = Some(make_git_info(false, None));
1402 ctx.populate_git_vars();
1403
1404 let v = ctx.template_vars();
1405 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1406 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1407 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1408 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1409 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1410 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1411 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1412 assert_eq!(
1413 v.get("FullCommit"),
1414 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1415 );
1416 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1417 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1418 assert_eq!(
1419 v.get("CommitDate"),
1420 Some(&"2026-03-25T10:30:00+00:00".to_string())
1421 );
1422 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1423 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1424 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1427 }
1428
1429 #[test]
1430 fn test_nightly_build_defaults_to_zero_without_git_info() {
1431 let config = Config::default();
1434 let mut ctx = Context::new(config, ContextOptions::default());
1435 ctx.git_info = None;
1436 ctx.populate_git_vars();
1437 assert_eq!(
1438 ctx.template_vars().get("NightlyBuild"),
1439 Some(&"0".to_string())
1440 );
1441 }
1442
1443 #[test]
1444 fn test_commit_is_alias_for_full_commit() {
1445 let config = Config::default();
1446 let mut ctx = Context::new(config, ContextOptions::default());
1447 ctx.git_info = Some(make_git_info(false, None));
1448 ctx.populate_git_vars();
1449
1450 let v = ctx.template_vars();
1451 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1452 }
1453
1454 #[test]
1455 fn test_populate_git_vars_prerelease() {
1456 let config = Config::default();
1457 let mut ctx = Context::new(config, ContextOptions::default());
1458 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1459 ctx.populate_git_vars();
1460
1461 let v = ctx.template_vars();
1462 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1463 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1464 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1465 }
1466
1467 #[test]
1468 fn test_build_metadata_template_var() {
1469 let config = Config::default();
1470 let mut ctx = Context::new(config, ContextOptions::default());
1471 let mut info = make_git_info(false, None);
1472 info.tag = "v1.2.3+build.42".to_string();
1473 info.semver.build_metadata = Some("build.42".to_string());
1474 ctx.git_info = Some(info);
1475 ctx.populate_git_vars();
1476
1477 let v = ctx.template_vars();
1478 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1479 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1481 }
1482
1483 #[test]
1484 fn test_build_metadata_empty_when_none() {
1485 let config = Config::default();
1486 let mut ctx = Context::new(config, ContextOptions::default());
1487 ctx.git_info = Some(make_git_info(false, None));
1488 ctx.populate_git_vars();
1489
1490 assert_eq!(
1491 ctx.template_vars().get("BuildMetadata"),
1492 Some(&"".to_string())
1493 );
1494 }
1495
1496 #[test]
1497 fn test_populate_git_vars_monorepo_prefixed_tag() {
1498 let config = Config::default();
1501 let mut ctx = Context::new(config, ContextOptions::default());
1502 let mut info = make_git_info(false, None);
1503 info.tag = "core-v0.3.2".to_string();
1504 info.semver = SemVer {
1505 major: 0,
1506 minor: 3,
1507 patch: 2,
1508 prerelease: None,
1509 build_metadata: None,
1510 };
1511 ctx.git_info = Some(info);
1512 ctx.populate_git_vars();
1513
1514 let v = ctx.template_vars();
1515 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1516 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1517 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1518 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1519 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1520 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1521 }
1522
1523 #[test]
1524 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1525 let config = Config::default();
1526 let mut ctx = Context::new(config, ContextOptions::default());
1527 let mut info = make_git_info(false, None);
1528 info.tag = "operator-v1.0.0-rc.1".to_string();
1529 info.semver = SemVer {
1530 major: 1,
1531 minor: 0,
1532 patch: 0,
1533 prerelease: Some("rc.1".to_string()),
1534 build_metadata: None,
1535 };
1536 ctx.git_info = Some(info);
1537 ctx.populate_git_vars();
1538
1539 let v = ctx.template_vars();
1540 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1541 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1542 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1543 }
1544
1545 #[test]
1546 fn test_git_tree_state_clean() {
1547 let config = Config::default();
1548 let mut ctx = Context::new(config, ContextOptions::default());
1549 ctx.git_info = Some(make_git_info(false, None));
1550 ctx.populate_git_vars();
1551
1552 let v = ctx.template_vars();
1553 assert_eq!(v.get("IsGitDirty"), Some(&"false".to_string()));
1554 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1555 }
1556
1557 #[test]
1558 fn test_git_tree_state_dirty() {
1559 let config = Config::default();
1560 let mut ctx = Context::new(config, ContextOptions::default());
1561 ctx.git_info = Some(make_git_info(true, None));
1562 ctx.populate_git_vars();
1563
1564 let v = ctx.template_vars();
1565 assert_eq!(v.get("IsGitDirty"), Some(&"true".to_string()));
1566 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1567 }
1568
1569 #[test]
1570 fn test_is_snapshot_reflects_context_options() {
1571 let config = Config::default();
1572 let opts = ContextOptions {
1573 snapshot: true,
1574 ..Default::default()
1575 };
1576 let mut ctx = Context::new(config, opts);
1577 ctx.git_info = Some(make_git_info(false, None));
1578 ctx.populate_git_vars();
1579
1580 assert_eq!(
1581 ctx.template_vars().get("IsSnapshot"),
1582 Some(&"true".to_string())
1583 );
1584
1585 let config2 = Config::default();
1587 let opts2 = ContextOptions {
1588 snapshot: false,
1589 ..Default::default()
1590 };
1591 let mut ctx2 = Context::new(config2, opts2);
1592 ctx2.git_info = Some(make_git_info(false, None));
1593 ctx2.populate_git_vars();
1594
1595 assert_eq!(
1596 ctx2.template_vars().get("IsSnapshot"),
1597 Some(&"false".to_string())
1598 );
1599 }
1600
1601 #[test]
1602 fn test_is_draft_defaults_to_false() {
1603 let config = Config::default();
1604 let mut ctx = Context::new(config, ContextOptions::default());
1605 ctx.git_info = Some(make_git_info(false, None));
1606 ctx.populate_git_vars();
1607
1608 assert_eq!(
1609 ctx.template_vars().get("IsDraft"),
1610 Some(&"false".to_string())
1611 );
1612 }
1613
1614 #[test]
1615 fn test_previous_tag_empty_when_none() {
1616 let config = Config::default();
1617 let mut ctx = Context::new(config, ContextOptions::default());
1618 let mut info = make_git_info(false, None);
1619 info.previous_tag = None;
1620 ctx.git_info = Some(info);
1621 ctx.populate_git_vars();
1622
1623 assert_eq!(
1624 ctx.template_vars().get("PreviousTag"),
1625 Some(&"".to_string())
1626 );
1627 }
1628
1629 #[test]
1638 fn populate_time_vars_uses_source_date_epoch_when_set() {
1639 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1643 let config = Config::default();
1644 let mut ctx = Context::new(config, ContextOptions::default());
1645 ctx.set_env_source(env);
1646 ctx.populate_time_vars();
1647
1648 let v = ctx.template_vars();
1649 assert_eq!(
1650 v.get("Timestamp"),
1651 Some(&"1715000000".to_string()),
1652 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1653 );
1654 assert_eq!(
1655 v.get("Date"),
1656 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1657 "Date must be RFC 3339 derived from SDE"
1658 );
1659 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1660 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1661 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1662 }
1663
1664 #[test]
1665 fn test_populate_time_vars() {
1666 let env = crate::MapEnvSource::new();
1669 let config = Config::default();
1670 let mut ctx = Context::new(config, ContextOptions::default());
1671 ctx.set_env_source(env);
1672 ctx.populate_time_vars();
1673
1674 let v = ctx.template_vars();
1675
1676 let date = v
1678 .get("Date")
1679 .unwrap_or_else(|| panic!("Date should be set"));
1680 assert!(
1681 date.contains('T') && date.len() > 10,
1682 "Date should be RFC 3339, got: {date}"
1683 );
1684
1685 let ts = v
1687 .get("Timestamp")
1688 .unwrap_or_else(|| panic!("Timestamp should be set"));
1689 assert!(
1690 ts.parse::<i64>().is_ok(),
1691 "Timestamp should be a numeric string, got: {ts}"
1692 );
1693
1694 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1696 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1697 }
1698
1699 #[test]
1700 fn test_env_vars_accessible_in_templates() {
1701 let mut config = Config::default();
1702 config.project_name = "myapp".to_string();
1703 let mut ctx = Context::new(config, ContextOptions::default());
1704 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1705 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1706
1707 let result = ctx
1708 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1709 .unwrap();
1710 assert_eq!(result, "hello-world-staging");
1711 }
1712
1713 #[test]
1714 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1715 let config = Config::default();
1716 let opts = ContextOptions {
1717 snapshot: true,
1718 ..Default::default()
1719 };
1720 let mut ctx = Context::new(config, opts);
1721 ctx.populate_git_vars();
1723
1724 assert_eq!(
1725 ctx.template_vars().get("IsSnapshot"),
1726 Some(&"true".to_string())
1727 );
1728 assert_eq!(
1729 ctx.template_vars().get("IsDraft"),
1730 Some(&"false".to_string())
1731 );
1732 assert_eq!(ctx.template_vars().get("Tag"), None);
1734 }
1735
1736 #[test]
1737 fn test_is_nightly_set_when_nightly_mode_active() {
1738 let config = Config::default();
1739 let opts = ContextOptions {
1740 nightly: true,
1741 ..Default::default()
1742 };
1743 let mut ctx = Context::new(config, opts);
1744 ctx.git_info = Some(make_git_info(false, None));
1745 ctx.populate_git_vars();
1746
1747 assert_eq!(
1748 ctx.template_vars().get("IsNightly"),
1749 Some(&"true".to_string()),
1750 "IsNightly should be 'true' when nightly mode is active"
1751 );
1752 assert!(ctx.is_nightly(), "is_nightly() should return true");
1753 }
1754
1755 #[test]
1756 fn test_is_nightly_false_by_default() {
1757 let config = Config::default();
1758 let mut ctx = Context::new(config, ContextOptions::default());
1759 ctx.git_info = Some(make_git_info(false, None));
1760 ctx.populate_git_vars();
1761
1762 assert_eq!(
1763 ctx.template_vars().get("IsNightly"),
1764 Some(&"false".to_string()),
1765 "IsNightly should default to 'false'"
1766 );
1767 assert!(
1768 !ctx.is_nightly(),
1769 "is_nightly() should return false by default"
1770 );
1771 }
1772
1773 #[test]
1774 fn test_version_returns_populated_value() {
1775 let config = Config::default();
1776 let mut ctx = Context::new(config, ContextOptions::default());
1777 ctx.git_info = Some(make_git_info(false, None));
1778 ctx.populate_git_vars();
1779
1780 assert_eq!(ctx.version(), "1.2.3");
1781 }
1782
1783 #[test]
1784 fn test_version_returns_empty_when_not_set() {
1785 let config = Config::default();
1786 let ctx = Context::new(config, ContextOptions::default());
1787 assert_eq!(ctx.version(), "");
1788 }
1789
1790 #[test]
1791 fn test_is_nightly_without_git_info() {
1792 let config = Config::default();
1793 let opts = ContextOptions {
1794 nightly: true,
1795 ..Default::default()
1796 };
1797 let mut ctx = Context::new(config, opts);
1798 ctx.populate_git_vars();
1800
1801 assert_eq!(
1802 ctx.template_vars().get("IsNightly"),
1803 Some(&"true".to_string()),
1804 "IsNightly should be set even without git info"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_is_git_clean_when_not_dirty() {
1810 let config = Config::default();
1811 let mut ctx = Context::new(config, ContextOptions::default());
1812 ctx.git_info = Some(make_git_info(false, None));
1813 ctx.populate_git_vars();
1814
1815 assert_eq!(
1816 ctx.template_vars().get("IsGitClean"),
1817 Some(&"true".to_string())
1818 );
1819 }
1820
1821 #[test]
1822 fn test_is_git_clean_when_dirty() {
1823 let config = Config::default();
1824 let mut ctx = Context::new(config, ContextOptions::default());
1825 ctx.git_info = Some(make_git_info(true, None));
1826 ctx.populate_git_vars();
1827
1828 assert_eq!(
1829 ctx.template_vars().get("IsGitClean"),
1830 Some(&"false".to_string())
1831 );
1832 }
1833
1834 #[test]
1835 fn test_git_url_set_from_git_info() {
1836 let config = Config::default();
1837 let mut ctx = Context::new(config, ContextOptions::default());
1838 ctx.git_info = Some(make_git_info(false, None));
1839 ctx.populate_git_vars();
1840
1841 assert_eq!(
1842 ctx.template_vars().get("GitURL"),
1843 Some(&"https://github.com/test/repo.git".to_string())
1844 );
1845 }
1846
1847 #[test]
1848 fn test_summary_set_from_git_info() {
1849 let config = Config::default();
1850 let mut ctx = Context::new(config, ContextOptions::default());
1851 ctx.git_info = Some(make_git_info(false, None));
1852 ctx.populate_git_vars();
1853
1854 assert_eq!(
1855 ctx.template_vars().get("Summary"),
1856 Some(&"v1.2.3-0-gabc123d".to_string())
1857 );
1858 }
1859
1860 #[test]
1861 fn test_tag_subject_set_from_git_info() {
1862 let config = Config::default();
1863 let mut ctx = Context::new(config, ContextOptions::default());
1864 ctx.git_info = Some(make_git_info(false, None));
1865 ctx.populate_git_vars();
1866
1867 assert_eq!(
1868 ctx.template_vars().get("TagSubject"),
1869 Some(&"Release v1.2.3".to_string())
1870 );
1871 }
1872
1873 #[test]
1874 fn test_tag_contents_set_from_git_info() {
1875 let config = Config::default();
1876 let mut ctx = Context::new(config, ContextOptions::default());
1877 ctx.git_info = Some(make_git_info(false, None));
1878 ctx.populate_git_vars();
1879
1880 assert_eq!(
1881 ctx.template_vars().get("TagContents"),
1882 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
1883 );
1884 }
1885
1886 #[test]
1887 fn test_tag_body_set_from_git_info() {
1888 let config = Config::default();
1889 let mut ctx = Context::new(config, ContextOptions::default());
1890 ctx.git_info = Some(make_git_info(false, None));
1891 ctx.populate_git_vars();
1892
1893 assert_eq!(
1894 ctx.template_vars().get("TagBody"),
1895 Some(&"Full release notes here.".to_string())
1896 );
1897 }
1898
1899 #[test]
1900 fn test_is_single_target_false_by_default() {
1901 let config = Config::default();
1902 let mut ctx = Context::new(config, ContextOptions::default());
1903 ctx.git_info = Some(make_git_info(false, None));
1904 ctx.populate_git_vars();
1905
1906 assert_eq!(
1907 ctx.template_vars().get("IsSingleTarget"),
1908 Some(&"false".to_string())
1909 );
1910 }
1911
1912 #[test]
1913 fn test_is_single_target_true_when_set() {
1914 let config = Config::default();
1915 let opts = ContextOptions {
1916 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
1917 ..Default::default()
1918 };
1919 let mut ctx = Context::new(config, opts);
1920 ctx.git_info = Some(make_git_info(false, None));
1921 ctx.populate_git_vars();
1922
1923 assert_eq!(
1924 ctx.template_vars().get("IsSingleTarget"),
1925 Some(&"true".to_string())
1926 );
1927 }
1928
1929 #[test]
1930 #[serial_test::serial]
1931 fn test_populate_runtime_vars() {
1932 let config = Config::default();
1933 let mut ctx = Context::new(config, ContextOptions::default());
1934 ctx.populate_runtime_vars();
1935
1936 let v = ctx.template_vars();
1937
1938 let goos = v
1939 .get("RuntimeGoos")
1940 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
1941 assert!(
1942 !goos.is_empty(),
1943 "RuntimeGoos should not be empty, got: {goos}"
1944 );
1945 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
1947
1948 let goarch = v
1949 .get("RuntimeGoarch")
1950 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
1951 assert!(
1952 !goarch.is_empty(),
1953 "RuntimeGoarch should not be empty, got: {goarch}"
1954 );
1955 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
1957 }
1958
1959 #[test]
1960 fn test_populate_release_notes_var_with_changelogs() {
1961 let mut config = Config::default();
1962 config.crates.push(crate::config::CrateConfig {
1963 name: "my-crate".to_string(),
1964 ..Default::default()
1965 });
1966 let mut ctx = Context::new(config, ContextOptions::default());
1967 ctx.stage_outputs
1968 .changelogs
1969 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
1970 ctx.populate_release_notes_var();
1971
1972 assert_eq!(
1973 ctx.template_vars().get("ReleaseNotes"),
1974 Some(&"## Changes\n- fix bug".to_string())
1975 );
1976 }
1977
1978 #[test]
1979 fn test_populate_release_notes_var_empty_when_no_changelogs() {
1980 let config = Config::default();
1981 let mut ctx = Context::new(config, ContextOptions::default());
1982 ctx.populate_release_notes_var();
1983
1984 assert_eq!(
1985 ctx.template_vars().get("ReleaseNotes"),
1986 Some(&"".to_string())
1987 );
1988 }
1989
1990 #[test]
1991 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
1992 let mut config = Config::default();
1993 config.crates.push(crate::config::CrateConfig {
1994 name: "crate-a".to_string(),
1995 ..Default::default()
1996 });
1997 config.crates.push(crate::config::CrateConfig {
1998 name: "crate-b".to_string(),
1999 ..Default::default()
2000 });
2001 let mut ctx = Context::new(config, ContextOptions::default());
2002 ctx.stage_outputs
2003 .changelogs
2004 .insert("crate-a".to_string(), "notes-a".to_string());
2005 ctx.stage_outputs
2006 .changelogs
2007 .insert("crate-b".to_string(), "notes-b".to_string());
2008 ctx.populate_release_notes_var();
2009
2010 assert_eq!(
2012 ctx.template_vars().get("ReleaseNotes"),
2013 Some(&"notes-a".to_string())
2014 );
2015 }
2016
2017 #[test]
2018 fn test_outputs_accessible_in_templates() {
2019 let mut config = Config::default();
2020 config.project_name = "myapp".to_string();
2021 let mut ctx = Context::new(config, ContextOptions::default());
2022 ctx.template_vars_mut().set_output("build_id", "abc123");
2023 ctx.template_vars_mut()
2024 .set_output("deploy_url", "https://example.com");
2025
2026 let result = ctx
2027 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
2028 .unwrap();
2029 assert_eq!(result, "abc123-https://example.com");
2030 }
2031
2032 #[test]
2033 fn test_artifact_ext_and_target_template_vars() {
2034 let mut config = Config::default();
2035 config.project_name = "myapp".to_string();
2036 let mut ctx = Context::new(config, ContextOptions::default());
2037 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
2038 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
2039 ctx.template_vars_mut()
2040 .set("Target", "x86_64-unknown-linux-gnu");
2041
2042 let result = ctx
2043 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
2044 .unwrap();
2045 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
2046 }
2047
2048 #[test]
2049 fn test_checksums_template_var() {
2050 let mut config = Config::default();
2051 config.project_name = "myapp".to_string();
2052 let mut ctx = Context::new(config, ContextOptions::default());
2053 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
2054 ctx.template_vars_mut().set("Checksums", checksum_text);
2055
2056 let result = ctx.render_template("{{ .Checksums }}").unwrap();
2057 assert_eq!(result, checksum_text);
2058 }
2059
2060 #[test]
2063 fn test_prefixed_tag_with_tag_prefix() {
2064 let mut config = Config::default();
2065 config.tag = Some(crate::config::TagConfig {
2066 tag_prefix: Some("api/".to_string()),
2067 ..Default::default()
2068 });
2069 let mut ctx = Context::new(config, ContextOptions::default());
2070 ctx.git_info = Some(make_git_info(false, None));
2071 ctx.populate_git_vars();
2072
2073 assert_eq!(
2074 ctx.template_vars().get("PrefixedTag"),
2075 Some(&"api/v1.2.3".to_string())
2076 );
2077 }
2078
2079 #[test]
2080 fn test_prefixed_tag_without_tag_prefix() {
2081 let config = Config::default();
2082 let mut ctx = Context::new(config, ContextOptions::default());
2083 ctx.git_info = Some(make_git_info(false, None));
2084 ctx.populate_git_vars();
2085
2086 assert_eq!(
2088 ctx.template_vars().get("PrefixedTag"),
2089 Some(&"v1.2.3".to_string())
2090 );
2091 }
2092
2093 #[test]
2094 fn test_prefixed_previous_tag_with_tag_prefix() {
2095 let mut config = Config::default();
2096 config.tag = Some(crate::config::TagConfig {
2097 tag_prefix: Some("api/".to_string()),
2098 ..Default::default()
2099 });
2100 let mut ctx = Context::new(config, ContextOptions::default());
2101 ctx.git_info = Some(make_git_info(false, None));
2102 ctx.populate_git_vars();
2103
2104 assert_eq!(
2105 ctx.template_vars().get("PrefixedPreviousTag"),
2106 Some(&"api/v1.2.2".to_string())
2107 );
2108 }
2109
2110 #[test]
2111 fn test_prefixed_previous_tag_empty_when_no_previous() {
2112 let mut config = Config::default();
2113 config.tag = Some(crate::config::TagConfig {
2114 tag_prefix: Some("api/".to_string()),
2115 ..Default::default()
2116 });
2117 let mut ctx = Context::new(config, ContextOptions::default());
2118 let mut info = make_git_info(false, None);
2119 info.previous_tag = None;
2120 ctx.git_info = Some(info);
2121 ctx.populate_git_vars();
2122
2123 assert_eq!(
2126 ctx.template_vars().get("PrefixedPreviousTag"),
2127 Some(&"".to_string())
2128 );
2129 }
2130
2131 #[test]
2132 fn test_prefixed_summary_with_tag_prefix() {
2133 let mut config = Config::default();
2134 config.tag = Some(crate::config::TagConfig {
2135 tag_prefix: Some("api/".to_string()),
2136 ..Default::default()
2137 });
2138 let mut ctx = Context::new(config, ContextOptions::default());
2139 ctx.git_info = Some(make_git_info(false, None));
2140 ctx.populate_git_vars();
2141
2142 assert_eq!(
2143 ctx.template_vars().get("PrefixedSummary"),
2144 Some(&"api/v1.2.3-0-gabc123d".to_string())
2145 );
2146 }
2147
2148 #[test]
2149 fn test_is_release_true_for_normal_release() {
2150 let config = Config::default();
2151 let opts = ContextOptions {
2152 snapshot: false,
2153 nightly: false,
2154 ..Default::default()
2155 };
2156 let mut ctx = Context::new(config, opts);
2157 ctx.git_info = Some(make_git_info(false, None));
2158 ctx.populate_git_vars();
2159
2160 assert_eq!(
2161 ctx.template_vars().get("IsRelease"),
2162 Some(&"true".to_string())
2163 );
2164 }
2165
2166 #[test]
2167 fn test_is_release_false_for_snapshot() {
2168 let config = Config::default();
2169 let opts = ContextOptions {
2170 snapshot: true,
2171 ..Default::default()
2172 };
2173 let mut ctx = Context::new(config, opts);
2174 ctx.git_info = Some(make_git_info(false, None));
2175 ctx.populate_git_vars();
2176
2177 assert_eq!(
2178 ctx.template_vars().get("IsRelease"),
2179 Some(&"false".to_string())
2180 );
2181 }
2182
2183 #[test]
2184 fn test_is_release_false_for_nightly() {
2185 let config = Config::default();
2186 let opts = ContextOptions {
2187 nightly: true,
2188 ..Default::default()
2189 };
2190 let mut ctx = Context::new(config, opts);
2191 ctx.git_info = Some(make_git_info(false, None));
2192 ctx.populate_git_vars();
2193
2194 assert_eq!(
2195 ctx.template_vars().get("IsRelease"),
2196 Some(&"false".to_string())
2197 );
2198 }
2199
2200 #[test]
2201 fn test_is_merging_true_when_merge_flag_set() {
2202 let config = Config::default();
2203 let opts = ContextOptions {
2204 merge: true,
2205 ..Default::default()
2206 };
2207 let mut ctx = Context::new(config, opts);
2208 ctx.git_info = Some(make_git_info(false, None));
2209 ctx.populate_git_vars();
2210
2211 assert_eq!(
2212 ctx.template_vars().get("IsMerging"),
2213 Some(&"true".to_string())
2214 );
2215 }
2216
2217 #[test]
2218 fn test_is_merging_false_by_default() {
2219 let config = Config::default();
2220 let mut ctx = Context::new(config, ContextOptions::default());
2221 ctx.git_info = Some(make_git_info(false, None));
2222 ctx.populate_git_vars();
2223
2224 assert_eq!(
2225 ctx.template_vars().get("IsMerging"),
2226 Some(&"false".to_string())
2227 );
2228 }
2229
2230 #[test]
2231 fn test_refresh_artifacts_var_empty() {
2232 let config = Config::default();
2233 let mut ctx = Context::new(config, ContextOptions::default());
2234 ctx.refresh_artifacts_var();
2235
2236 let result = ctx
2238 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2239 .unwrap();
2240 assert_eq!(result, "");
2241 }
2242
2243 #[test]
2244 fn test_refresh_artifacts_var_with_artifacts() {
2245 use crate::artifact::{Artifact, ArtifactKind};
2246 use std::collections::HashMap;
2247 use std::path::PathBuf;
2248
2249 let config = Config::default();
2250 let mut ctx = Context::new(config, ContextOptions::default());
2251 ctx.artifacts.add(Artifact {
2255 kind: ArtifactKind::Archive,
2256 name: String::new(),
2257 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2258 target: Some("x86_64-unknown-linux-gnu".to_string()),
2259 crate_name: "myapp".to_string(),
2260 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2261 size: None,
2262 });
2263 ctx.artifacts.add(Artifact {
2264 kind: ArtifactKind::Binary,
2265 name: String::new(),
2266 path: PathBuf::from("dist/myapp"),
2267 target: Some("x86_64-unknown-linux-gnu".to_string()),
2268 crate_name: "myapp".to_string(),
2269 metadata: HashMap::new(),
2270 size: None,
2271 });
2272 ctx.refresh_artifacts_var();
2273
2274 let result = ctx
2276 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2277 .unwrap();
2278 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2279 assert!(result.contains("myapp"));
2280
2281 let result_kinds = ctx
2283 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2284 .unwrap();
2285 assert!(result_kinds.contains("archive"));
2286 assert!(result_kinds.contains("binary"));
2287 }
2288
2289 #[test]
2290 fn test_populate_metadata_var_with_mod_timestamp() {
2291 let mut config = Config::default();
2292 config.metadata = Some(crate::config::MetadataConfig {
2293 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2294 ..Default::default()
2295 });
2296 let mut ctx = Context::new(config, ContextOptions::default());
2297 ctx.populate_metadata_var().unwrap();
2298
2299 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2301 assert_eq!(result, "{{ .CommitTimestamp }}");
2302 }
2303
2304 #[test]
2305 fn test_populate_metadata_var_empty_when_no_config() {
2306 let config = Config::default();
2307 let mut ctx = Context::new(config, ContextOptions::default());
2308 ctx.populate_metadata_var().unwrap();
2309
2310 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2312 assert_eq!(result, "");
2313 }
2314
2315 #[test]
2316 fn test_populate_metadata_var_reads_from_config() {
2317 let mut config = Config::default();
2318 config.metadata = Some(crate::config::MetadataConfig {
2319 description: Some("A test project".to_string()),
2320 homepage: Some("https://example.com".to_string()),
2321 license: Some("MIT".to_string()),
2322 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2323 mod_timestamp: Some("1234567890".to_string()),
2324 ..Default::default()
2325 });
2326 let mut ctx = Context::new(config, ContextOptions::default());
2327 ctx.populate_metadata_var().unwrap();
2328
2329 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2330 assert_eq!(desc, "A test project");
2331
2332 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2333 assert_eq!(home, "https://example.com");
2334
2335 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2336 assert_eq!(lic, "MIT");
2337
2338 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2339 assert_eq!(ts, "1234567890");
2340 }
2341
2342 #[test]
2343 fn test_populate_metadata_var_full_description_inline() {
2344 use crate::config::ContentSource;
2345 let mut config = Config::default();
2346 config.metadata = Some(crate::config::MetadataConfig {
2347 full_description: Some(ContentSource::Inline(
2348 "A long-form description of the project.".to_string(),
2349 )),
2350 ..Default::default()
2351 });
2352 let mut ctx = Context::new(config, ContextOptions::default());
2353 ctx.populate_metadata_var().unwrap();
2354 let rendered = ctx
2355 .render_template("{{ Metadata.FullDescription }}")
2356 .unwrap();
2357 assert_eq!(rendered, "A long-form description of the project.");
2358 }
2359
2360 #[test]
2361 fn test_populate_metadata_var_full_description_from_file() {
2362 use crate::config::ContentSource;
2363 let tmp = tempfile::tempdir().unwrap();
2364 let desc_path = tmp.path().join("DESCRIPTION.md");
2365 std::fs::write(&desc_path, "read from disk").unwrap();
2366 let mut config = Config::default();
2367 config.metadata = Some(crate::config::MetadataConfig {
2368 full_description: Some(ContentSource::FromFile {
2369 from_file: desc_path.to_string_lossy().into_owned(),
2370 }),
2371 ..Default::default()
2372 });
2373 let mut ctx = Context::new(config, ContextOptions::default());
2374 ctx.populate_metadata_var().unwrap();
2375 let rendered = ctx
2376 .render_template("{{ Metadata.FullDescription }}")
2377 .unwrap();
2378 assert_eq!(rendered, "read from disk");
2379 }
2380
2381 #[test]
2382 fn test_populate_metadata_var_full_description_from_url_resolves() {
2383 use crate::config::ContentSource;
2388 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2389
2390 let body = "long form description body";
2391 let body_len = body.len();
2392 let response: &'static str = Box::leak(
2393 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2394 );
2395 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2396
2397 let mut config = Config::default();
2398 config.metadata = Some(crate::config::MetadataConfig {
2399 full_description: Some(ContentSource::FromUrl {
2400 from_url: format!("http://{addr}/description.md"),
2401 headers: None,
2402 }),
2403 ..Default::default()
2404 });
2405 let mut ctx = Context::new(config, ContextOptions::default());
2406 ctx.populate_metadata_var()
2407 .expect("from_url should resolve through content_source");
2408 let rendered = ctx
2409 .render_template("{{ Metadata.FullDescription }}")
2410 .unwrap();
2411 assert_eq!(rendered, body);
2412 }
2413
2414 #[test]
2415 fn test_populate_metadata_var_commit_author() {
2416 use crate::config::CommitAuthorConfig;
2417 let mut config = Config::default();
2418 config.metadata = Some(crate::config::MetadataConfig {
2419 commit_author: Some(CommitAuthorConfig {
2420 name: Some("Alice Developer".to_string()),
2421 email: Some("alice@example.com".to_string()),
2422 signing: None,
2423 use_github_app_token: false,
2424 }),
2425 ..Default::default()
2426 });
2427 let mut ctx = Context::new(config, ContextOptions::default());
2428 ctx.populate_metadata_var().unwrap();
2429 let name = ctx
2430 .render_template("{{ Metadata.CommitAuthor.Name }}")
2431 .unwrap();
2432 assert_eq!(name, "Alice Developer");
2433 let email = ctx
2434 .render_template("{{ Metadata.CommitAuthor.Email }}")
2435 .unwrap();
2436 assert_eq!(email, "alice@example.com");
2437 }
2438
2439 #[test]
2440 fn test_artifact_id_template_var() {
2441 let mut config = Config::default();
2442 config.project_name = "myapp".to_string();
2443 let mut ctx = Context::new(config, ContextOptions::default());
2444 ctx.template_vars_mut().set("ArtifactID", "default");
2445
2446 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2447 assert_eq!(result, "default");
2448 }
2449
2450 #[test]
2451 fn test_artifact_id_empty_when_not_set() {
2452 let mut config = Config::default();
2453 config.project_name = "myapp".to_string();
2454 let mut ctx = Context::new(config, ContextOptions::default());
2455 ctx.template_vars_mut().set("ArtifactID", "");
2456
2457 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2458 assert_eq!(result, "");
2459 }
2460
2461 #[test]
2462 fn test_pro_vars_rendered_in_templates() {
2463 let mut config = Config::default();
2465 config.tag = Some(crate::config::TagConfig {
2466 tag_prefix: Some("api/".to_string()),
2467 ..Default::default()
2468 });
2469 let opts = ContextOptions {
2470 snapshot: false,
2471 nightly: false,
2472 merge: true,
2473 ..Default::default()
2474 };
2475 let mut ctx = Context::new(config, opts);
2476 ctx.git_info = Some(make_git_info(false, None));
2477 ctx.populate_git_vars();
2478
2479 let result = ctx
2480 .render_template(
2481 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2482 )
2483 .unwrap();
2484 assert_eq!(result, "release-merge-api/v1.2.3");
2485 }
2486
2487 #[test]
2488 fn test_is_release_without_git_info() {
2489 let config = Config::default();
2491 let opts = ContextOptions {
2492 snapshot: false,
2493 nightly: false,
2494 ..Default::default()
2495 };
2496 let mut ctx = Context::new(config, opts);
2497 ctx.populate_git_vars();
2498
2499 assert_eq!(
2500 ctx.template_vars().get("IsRelease"),
2501 Some(&"true".to_string())
2502 );
2503 }
2504
2505 #[test]
2506 fn test_is_merging_without_git_info() {
2507 let config = Config::default();
2509 let opts = ContextOptions {
2510 merge: true,
2511 ..Default::default()
2512 };
2513 let mut ctx = Context::new(config, opts);
2514 ctx.populate_git_vars();
2515
2516 assert_eq!(
2517 ctx.template_vars().get("IsMerging"),
2518 Some(&"true".to_string())
2519 );
2520 }
2521
2522 #[test]
2532 fn test_monorepo_version_matches_shared_semver_helper() {
2533 let mut config = Config::default();
2534 config.monorepo = Some(crate::config::MonorepoConfig {
2535 tag_prefix: Some("core/".to_string()),
2536 dir: None,
2537 });
2538 let mut ctx = Context::new(config, ContextOptions::default());
2539
2540 let semver = SemVer {
2541 major: 2,
2542 minor: 1,
2543 patch: 0,
2544 prerelease: Some("rc.1".to_string()),
2545 build_metadata: Some("build.7".to_string()),
2546 };
2547 let mut info = make_git_info(false, None);
2548 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2549 info.semver = semver.clone();
2550 ctx.git_info = Some(info);
2551 ctx.populate_git_vars();
2552
2553 let v = ctx.template_vars();
2554 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2557 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2558 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2559 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2560 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2562 }
2563
2564 #[test]
2565 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2566 let mut config = Config::default();
2567 config.monorepo = Some(crate::config::MonorepoConfig {
2568 tag_prefix: Some("subproject1/".to_string()),
2569 dir: None,
2570 });
2571 let mut ctx = Context::new(config, ContextOptions::default());
2572
2573 let mut info = make_git_info(false, None);
2575 info.tag = "subproject1/v1.2.3".to_string();
2576 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2577 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2578 ctx.git_info = Some(info);
2579 ctx.populate_git_vars();
2580
2581 let v = ctx.template_vars();
2582 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2584 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2586 assert_eq!(
2588 v.get("PrefixedTag"),
2589 Some(&"subproject1/v1.2.3".to_string())
2590 );
2591 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2593 assert_eq!(
2595 v.get("PrefixedPreviousTag"),
2596 Some(&"subproject1/v1.2.2".to_string())
2597 );
2598 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2600 assert_eq!(
2602 v.get("PrefixedSummary"),
2603 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2604 );
2605 }
2606
2607 #[test]
2608 fn test_monorepo_prefixed_previous_tag() {
2609 let mut config = Config::default();
2610 config.monorepo = Some(crate::config::MonorepoConfig {
2611 tag_prefix: Some("svc/".to_string()),
2612 dir: None,
2613 });
2614 let mut ctx = Context::new(config, ContextOptions::default());
2615
2616 let mut info = make_git_info(false, None);
2617 info.tag = "svc/v2.0.0".to_string();
2618 info.previous_tag = Some("svc/v1.9.0".to_string());
2619 ctx.git_info = Some(info);
2620 ctx.populate_git_vars();
2621
2622 let v = ctx.template_vars();
2623 assert_eq!(
2625 v.get("PrefixedPreviousTag"),
2626 Some(&"svc/v1.9.0".to_string())
2627 );
2628 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2630 }
2631
2632 #[test]
2633 fn test_no_monorepo_falls_back_to_tag_prefix() {
2634 let mut config = Config::default();
2636 config.tag = Some(crate::config::TagConfig {
2637 tag_prefix: Some("release/".to_string()),
2638 ..Default::default()
2639 });
2640 let mut ctx = Context::new(config, ContextOptions::default());
2641 ctx.git_info = Some(make_git_info(false, None));
2642 ctx.populate_git_vars();
2643
2644 let v = ctx.template_vars();
2645 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2647 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2649 assert_eq!(
2650 v.get("PrefixedPreviousTag"),
2651 Some(&"release/v1.2.2".to_string())
2652 );
2653 }
2654
2655 #[test]
2656 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2657 let mut config = Config::default();
2660 config.tag = Some(crate::config::TagConfig {
2661 tag_prefix: Some("release/".to_string()),
2662 ..Default::default()
2663 });
2664 config.monorepo = Some(crate::config::MonorepoConfig {
2665 tag_prefix: Some("svc/".to_string()),
2666 dir: None,
2667 });
2668 let mut ctx = Context::new(config, ContextOptions::default());
2669
2670 let mut info = make_git_info(false, None);
2671 info.tag = "svc/v1.2.3".to_string();
2672 info.previous_tag = Some("svc/v1.2.2".to_string());
2673 ctx.git_info = Some(info);
2674 ctx.populate_git_vars();
2675
2676 let v = ctx.template_vars();
2677 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2679 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2681 }
2682
2683 #[test]
2684 fn test_monorepo_prefixed_summary() {
2685 let mut config = Config::default();
2686 config.monorepo = Some(crate::config::MonorepoConfig {
2687 tag_prefix: Some("pkg/".to_string()),
2688 dir: None,
2689 });
2690 let mut ctx = Context::new(config, ContextOptions::default());
2691
2692 let mut info = make_git_info(false, None);
2693 info.tag = "pkg/v1.2.3".to_string();
2694 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
2696 ctx.git_info = Some(info);
2697 ctx.populate_git_vars();
2698
2699 assert_eq!(
2701 ctx.template_vars().get("PrefixedSummary"),
2702 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
2703 );
2704 assert_eq!(
2706 ctx.template_vars().get("Summary"),
2707 Some(&"v1.2.3-0-gabc123d".to_string())
2708 );
2709 }
2710
2711 #[test]
2712 fn test_monorepo_no_previous_tag() {
2713 let mut config = Config::default();
2714 config.monorepo = Some(crate::config::MonorepoConfig {
2715 tag_prefix: Some("svc/".to_string()),
2716 dir: None,
2717 });
2718 let mut ctx = Context::new(config, ContextOptions::default());
2719
2720 let mut info = make_git_info(false, None);
2721 info.tag = "svc/v1.0.0".to_string();
2722 info.previous_tag = None;
2723 ctx.git_info = Some(info);
2724 ctx.populate_git_vars();
2725
2726 let v = ctx.template_vars();
2727 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
2728 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
2730 }
2731
2732 #[test]
2737 fn test_monorepo_full_flow_all_vars() {
2738 let mut config = Config::default();
2741 config.project_name = "mymonorepo".to_string();
2742 config.monorepo = Some(crate::config::MonorepoConfig {
2743 tag_prefix: Some("services/api/".to_string()),
2744 dir: Some("services/api".to_string()),
2745 });
2746
2747 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
2749 assert_eq!(config.monorepo_dir(), Some("services/api"));
2750
2751 let mut ctx = Context::new(config, ContextOptions::default());
2752
2753 let mut info = make_git_info(false, None);
2756 info.tag = "services/api/v2.1.0".to_string();
2757 info.previous_tag = Some("services/api/v2.0.5".to_string());
2758 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
2759 info.semver = crate::git::SemVer {
2760 major: 2,
2761 minor: 1,
2762 patch: 0,
2763 prerelease: None,
2764 build_metadata: None,
2765 };
2766 ctx.git_info = Some(info);
2767 ctx.populate_git_vars();
2768
2769 let v = ctx.template_vars();
2770
2771 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
2773 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
2774 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2775 assert_eq!(v.get("Major"), Some(&"2".to_string()));
2776 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
2777 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
2778 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
2779 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
2780
2781 assert_eq!(
2783 v.get("PrefixedTag"),
2784 Some(&"services/api/v2.1.0".to_string())
2785 );
2786 assert_eq!(
2787 v.get("PrefixedPreviousTag"),
2788 Some(&"services/api/v2.0.5".to_string())
2789 );
2790 assert_eq!(
2791 v.get("PrefixedSummary"),
2792 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
2793 );
2794
2795 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
2797 }
2798
2799 #[test]
2800 fn context_env_var_defaults_to_process_env_source() {
2801 let ctx = Context::new(Config::default(), ContextOptions::default());
2802 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
2804 }
2805
2806 #[test]
2807 fn context_env_var_routes_to_injected_source() {
2808 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2809 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
2810 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
2811 assert_eq!(ctx.env_var("PATH"), None);
2815 }
2816
2817 #[test]
2818 #[serial_test::serial]
2819 fn populate_runtime_vars_sets_rustc_version() {
2820 let config = Config::default();
2821 let mut ctx = Context::new(config, ContextOptions::default());
2822 ctx.populate_runtime_vars();
2825
2826 let ver = ctx
2827 .template_vars()
2828 .get("RustcVersion")
2829 .expect("RustcVersion should be set after populate_runtime_vars");
2830 if !ver.is_empty() {
2834 assert!(
2835 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
2836 "RustcVersion should start with a digit: {ver}"
2837 );
2838 }
2839 }
2840}