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 crate::verify_release_summary::VerifyReleaseSummary;
11use anyhow::Context as _;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum RollbackMode {
25 None,
28 #[default]
35 BestEffort,
36}
37
38pub const VALID_RELEASE_SKIPS: &[&str] = &[
45 "publish",
46 "announce",
47 "sign",
48 "validate",
49 "sbom",
50 "attest",
51 "docker",
52 "docker-sign",
53 "winget",
54 "choco",
55 "snapcraft",
56 "snapcraft-publish",
57 "scoop",
58 "brew",
59 "nix",
60 "aur",
61 "cargo",
62 "krew",
63 "nfpm",
64 "makeself",
65 "appimage",
66 "flatpak",
67 "srpm",
68 "before",
69 "before-publish",
70 "notarize",
71 "archive",
72 "source",
73 "build",
74 "changelog",
75 "release",
76 "checksum",
77 "upx",
78 "blob",
79 "templatefiles",
80 "dmg",
81 "msi",
82 "nsis",
83 "pkg",
84 "appbundle",
85 "verify-release",
86];
87
88pub const VALID_BUILD_SKIPS: &[&str] = &["pre-hooks", "post-hooks", "validate", "before"];
90
91pub fn validate_skip_values(skip: &[String], valid: &[&str]) -> Result<(), String> {
96 let invalid: Vec<&str> = skip
97 .iter()
98 .map(|s| s.as_str())
99 .filter(|s| !valid.contains(s))
100 .collect();
101 if invalid.is_empty() {
102 Ok(())
103 } else {
104 Err(format!(
105 "invalid --skip value(s): {}. Valid options: {}",
106 invalid.join(", "),
107 valid.join(", "),
108 ))
109 }
110}
111
112pub struct ContextOptions {
113 pub snapshot: bool,
114 pub nightly: bool,
115 pub dry_run: bool,
116 pub quiet: bool,
117 pub verbose: bool,
118 pub debug: bool,
119 pub skip_stages: Vec<String>,
120 pub selected_crates: Vec<String>,
121 pub token: Option<String>,
122 pub parallelism: usize,
124 pub single_target: Option<String>,
126 pub release_notes_path: Option<PathBuf>,
128 pub fail_fast: bool,
130 pub partial_target: Option<PartialTarget>,
133 pub merge: bool,
135 pub publish_only: bool,
145 pub project_root: Option<PathBuf>,
148 pub strict: bool,
150 pub resume_release: bool,
155 pub replace_existing_artifacts: bool,
159 pub skip_post_publish_poll: bool,
167 pub gate_submitter: Option<bool>,
175 pub rollback_mode: Option<RollbackMode>,
180 pub simulate_failure_publishers: Vec<String>,
188 pub rollback_only: bool,
194 pub allow_rerun: bool,
209 pub show_skipped: bool,
218 pub from_run: Option<String>,
222 pub runtime_nondeterministic_allowlist: Vec<(String, String)>,
229 pub summary_json_path: Option<PathBuf>,
233 pub allow_ai_failure: bool,
240 pub changelog_from: Option<String>,
247 pub changelog_full_history: bool,
254 pub changelog_to: Option<String>,
263 pub changelog_preview: bool,
279}
280
281impl Default for ContextOptions {
282 fn default() -> Self {
283 Self {
284 snapshot: false,
285 nightly: false,
286 dry_run: false,
287 quiet: false,
288 verbose: false,
289 debug: false,
290 skip_stages: Vec::new(),
291 selected_crates: Vec::new(),
292 token: None,
293 parallelism: 4,
294 single_target: None,
295 release_notes_path: None,
296 fail_fast: false,
297 partial_target: None,
298 merge: false,
299 publish_only: false,
300 project_root: None,
301 strict: false,
302 resume_release: false,
303 replace_existing_artifacts: false,
304 skip_post_publish_poll: false,
305 gate_submitter: None,
306 rollback_mode: None,
307 simulate_failure_publishers: Vec::new(),
308 rollback_only: false,
309 allow_rerun: false,
310 show_skipped: false,
311 from_run: None,
312 runtime_nondeterministic_allowlist: Vec::new(),
313 summary_json_path: None,
314 allow_ai_failure: false,
315 changelog_from: None,
316 changelog_full_history: false,
317 changelog_to: None,
318 changelog_preview: false,
319 }
320 }
321}
322
323#[derive(Debug, Default)]
328pub struct StageOutputs {
329 pub github_native_changelog: bool,
333 pub changelogs: HashMap<String, String>,
335 pub changelog_header: Option<String>,
340 pub changelog_footer: Option<String>,
343 pub post_publish_results: Vec<serde_json::Value>,
351}
352
353pub struct Context {
354 pub config: Config,
355 pub artifacts: ArtifactRegistry,
356 pub options: ContextOptions,
357 pub stage_outputs: StageOutputs,
359 template_vars: TemplateVars,
360 pub git_info: Option<GitInfo>,
361 pub token_type: ScmTokenType,
363 pub skip_memento: crate::pipe_skip::SkipMemento,
369 pub publish_report: Option<PublishReport>,
377 pub publish_attempted: bool,
383 pub verify_release: Option<VerifyReleaseSummary>,
394 pub determinism: Option<crate::DeterminismState>,
401 pub pending_outcome: Option<crate::PublisherOutcome>,
411 pub pending_evidence: Option<crate::PublishEvidence>,
424 built_crate_names: Option<std::collections::HashSet<String>>,
435 env_source: Arc<dyn EnvSource>,
443 #[cfg(feature = "test-helpers")]
451 pub log_capture: Option<crate::log::LogCapture>,
452 render_strict: std::cell::Cell<bool>,
467}
468
469impl Context {
470 pub fn new(config: Config, options: ContextOptions) -> Self {
471 let mut vars = TemplateVars::new();
472 vars.set("ProjectName", &config.project_name);
473 Self {
474 config,
475 artifacts: ArtifactRegistry::new(),
476 options,
477 stage_outputs: StageOutputs::default(),
478 template_vars: vars,
479 git_info: None,
480 token_type: ScmTokenType::GitHub,
481 skip_memento: crate::pipe_skip::SkipMemento::new(),
482 publish_report: None,
483 publish_attempted: false,
484 verify_release: None,
485 determinism: None,
486 pending_outcome: None,
487 pending_evidence: None,
488 built_crate_names: None,
489 env_source: Arc::new(ProcessEnvSource),
490 #[cfg(feature = "test-helpers")]
491 log_capture: None,
492 render_strict: std::cell::Cell::new(false),
493 }
494 }
495
496 pub fn env_var(&self, name: &str) -> Option<String> {
504 self.env_source.var(name)
505 }
506
507 pub fn set_env_source<S: EnvSource + 'static>(&mut self, src: S) {
513 self.env_source = Arc::new(src);
514 }
515
516 pub fn env_source(&self) -> &dyn EnvSource {
521 self.env_source.as_ref()
522 }
523
524 pub fn env_source_arc(&self) -> Arc<dyn EnvSource> {
530 Arc::clone(&self.env_source)
531 }
532
533 #[cfg(feature = "test-helpers")]
539 pub fn with_log_capture(&mut self, capture: crate::log::LogCapture) {
540 self.log_capture = Some(capture);
541 }
542
543 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
551 self.pending_outcome = Some(outcome);
552 }
553
554 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
558 self.pending_outcome.take()
559 }
560
561 pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
566 self.pending_evidence = Some(evidence);
567 }
568
569 pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
573 self.pending_evidence.take()
574 }
575
576 pub fn publish_report(&self) -> Option<&PublishReport> {
579 self.publish_report.as_ref()
580 }
581
582 pub fn publish_attempted(&self) -> bool {
585 self.publish_attempted
586 }
587
588 pub fn set_publish_attempted(&mut self) {
592 self.publish_attempted = true;
593 }
594
595 pub fn set_publish_report(&mut self, r: PublishReport) {
601 self.publish_report = Some(r);
602 }
603
604 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
607 self.built_crate_names.as_ref()
608 }
609
610 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
613 self.built_crate_names = Some(names);
614 }
615
616 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
623 self.skip_memento.remember(stage, label, reason);
624 }
625
626 pub fn template_vars(&self) -> &TemplateVars {
627 &self.template_vars
628 }
629
630 pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
631 &mut self.template_vars
632 }
633
634 pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
635 crate::template::render(template, &self.template_vars)
636 }
637
638 pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
640 template.map(|t| self.render_template(t)).transpose()
641 }
642
643 pub fn skip_with_log(
652 &self,
653 skip: &Option<crate::config::StringOrBool>,
654 log: &StageLogger,
655 label: &str,
656 ) -> anyhow::Result<bool> {
657 let Some(d) = skip else {
658 return Ok(false);
659 };
660 let should_skip = d
661 .try_evaluates_to_true(|s| self.render_template(s))
662 .with_context(|| format!("evaluate skip expression for {label}"))?;
663 if should_skip {
664 log.status(&format!("{} skipped", label));
665 }
666 Ok(should_skip)
667 }
668
669 pub fn should_skip(&self, stage_name: &str) -> bool {
670 self.options.skip_stages.iter().any(|s| s == stage_name)
671 }
672
673 pub fn skip_validate(&self) -> bool {
675 self.should_skip("validate")
676 }
677
678 pub fn is_dry_run(&self) -> bool {
679 self.options.dry_run
680 }
681
682 pub fn is_snapshot(&self) -> bool {
683 self.options.snapshot
684 }
685
686 pub fn is_strict(&self) -> bool {
687 self.options.strict
688 }
689
690 pub fn set_render_strict(&self, on: bool) -> bool {
698 self.render_strict.replace(on)
699 }
700
701 pub fn render_is_strict(&self) -> bool {
708 self.render_strict.get() || self.is_strict()
709 }
710
711 pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
714 if self.options.strict {
715 anyhow::bail!("{} (strict mode)", msg);
716 }
717 log.warn(msg);
718 Ok(())
719 }
720
721 pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
730 if self.is_snapshot() {
731 log.status(&format!("skipping {stage} (snapshot mode)"));
735 true
736 } else {
737 false
738 }
739 }
740
741 pub fn render_template_strict(
743 &self,
744 template: &str,
745 label: &str,
746 log: &crate::log::StageLogger,
747 ) -> anyhow::Result<String> {
748 match self.render_template(template) {
749 Ok(rendered) => Ok(rendered),
750 Err(e) => {
751 if self.options.strict {
752 anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
753 }
754 log.warn(&format!("failed to render template for {}: {}", label, e));
755 Ok(template.to_string())
756 }
757 }
758 }
759
760 pub fn is_nightly(&self) -> bool {
761 self.options.nightly
762 }
763
764 pub fn set_release_url(&mut self, url: &str) {
769 self.template_vars.set("ReleaseURL", url);
770 }
771
772 pub fn version(&self) -> String {
775 self.template_vars
776 .get("Version")
777 .cloned()
778 .unwrap_or_default()
779 }
780
781 pub fn verbosity(&self) -> Verbosity {
783 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
784 }
785
786 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
792 self.config.retry.unwrap_or_default().to_policy()
793 }
794
795 pub fn logger(&self, stage: &'static str) -> StageLogger {
803 #[allow(unused_mut)]
804 let mut log = StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact());
805 #[cfg(feature = "test-helpers")]
806 if let Some(cap) = &self.log_capture {
807 log = log.with_capture_handle(cap.clone());
808 }
809 log
810 }
811
812 fn env_for_redact(&self) -> Vec<(String, String)> {
818 use std::collections::HashMap;
819 let mut map: HashMap<String, String> = std::env::vars().collect();
820 for (k, v) in self.template_vars.all_env() {
821 map.insert(k.clone(), v.clone());
822 }
823 map.into_iter().collect()
824 }
825
826 pub fn populate_git_vars(&mut self) {
868 if let Some(ref info) = self.git_info {
869 let raw_version = info.semver.raw_version_string();
871
872 let version = info.semver.version_string();
878
879 self.template_vars.set("Tag", &info.tag);
880 self.template_vars.set("Version", &version);
881 self.template_vars.set("RawVersion", &raw_version);
882 self.template_vars.set("Base", &raw_version);
887 self.template_vars
888 .set("Major", &info.semver.major.to_string());
889 self.template_vars
890 .set("Minor", &info.semver.minor.to_string());
891 self.template_vars
892 .set("Patch", &info.semver.patch.to_string());
893 self.template_vars.set(
894 "Prerelease",
895 info.semver.prerelease.as_deref().unwrap_or(""),
896 );
897 self.template_vars.set(
898 "BuildMetadata",
899 info.semver.build_metadata.as_deref().unwrap_or(""),
900 );
901 self.template_vars.set("FullCommit", &info.commit);
902 self.template_vars.set("Commit", &info.commit);
903 self.template_vars.set("ShortCommit", &info.short_commit);
904 self.template_vars.set("Branch", &info.branch);
905 self.template_vars.set("CommitDate", &info.commit_date);
906 self.template_vars
907 .set("CommitTimestamp", &info.commit_timestamp);
908 self.template_vars.set_bool("IsGitDirty", info.dirty);
909 self.template_vars.set_bool("IsGitClean", !info.dirty);
910 self.template_vars
911 .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
912 self.template_vars.set("GitURL", &info.remote_url);
913 self.template_vars.set("Summary", &info.summary);
914 self.template_vars.set("TagSubject", &info.tag_subject);
915 self.template_vars.set("TagContents", &info.tag_contents);
916 self.template_vars.set("TagBody", &info.tag_body);
917 self.template_vars
918 .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
919 self.template_vars
920 .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
921
922 let monorepo_prefix = self.config.monorepo_tag_prefix();
933
934 if let Some(prefix) = monorepo_prefix {
940 self.template_vars.set("PrefixedTag", &info.tag);
943
944 let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
946 self.template_vars.set("Tag", stripped_tag);
947
948 let version = info.semver.version_string();
958 self.template_vars.set("Version", &version);
959
960 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
962 self.template_vars.set("PrefixedPreviousTag", prev_tag);
963
964 let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
966 self.template_vars.set("PreviousTag", stripped_prev);
967
968 self.template_vars.set("PrefixedSummary", &info.summary);
972 let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
974 self.template_vars.set("Summary", stripped_summary);
975 } else {
976 let tag_prefix = self
978 .config
979 .tag
980 .as_ref()
981 .and_then(|t| t.tag_prefix.as_deref())
982 .unwrap_or("");
983 self.template_vars
984 .set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
985 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
986 let prefixed_prev = if prev_tag.is_empty() {
987 String::new()
988 } else {
989 format!("{}{}", tag_prefix, prev_tag)
990 };
991 self.template_vars
992 .set("PrefixedPreviousTag", &prefixed_prev);
993 self.template_vars.set(
994 "PrefixedSummary",
995 &format!("{}{}", tag_prefix, info.summary),
996 );
997 }
998 }
999
1000 let nightly_build = if self.git_info.is_some() {
1013 let root = self
1014 .options
1015 .project_root
1016 .clone()
1017 .unwrap_or_else(|| PathBuf::from("."));
1018 let monorepo_prefix = self.config.monorepo_tag_prefix();
1019 crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
1020 } else {
1021 0
1022 };
1023 self.template_vars
1024 .set_structured("NightlyBuild", tera::Value::from(nightly_build));
1025
1026 self.template_vars
1031 .set_bool("IsSnapshot", self.options.snapshot);
1032 self.template_vars
1033 .set_bool("IsNightly", self.options.nightly);
1034 self.template_vars.set_bool(
1038 "IsHarness",
1039 self.env_var("ANODIZER_IN_DETERMINISM_HARNESS").is_some(),
1040 );
1041 let is_draft = self
1043 .config
1044 .release
1045 .as_ref()
1046 .and_then(|r| r.draft)
1047 .unwrap_or(false);
1048 self.template_vars.set_bool("IsDraft", is_draft);
1049 self.template_vars
1050 .set_bool("IsSingleTarget", self.options.single_target.is_some());
1051
1052 let is_release = !self.options.snapshot && !self.options.nightly;
1054 self.template_vars.set_bool("IsRelease", is_release);
1055
1056 self.template_vars.set_bool("IsMerging", self.options.merge);
1058 }
1059
1060 pub fn populate_time_vars(&mut self) {
1088 let now = crate::sde::resolve_now_with_env(self.env_source());
1096 self.template_vars.set("Date", &now.to_rfc3339());
1097 self.template_vars
1098 .set("Timestamp", &now.timestamp().to_string());
1099 self.template_vars.set("Now", &now.to_rfc3339());
1100 self.template_vars
1101 .set("Year", &now.format("%Y").to_string());
1102 self.template_vars
1103 .set("Month", &now.format("%m").to_string());
1104 self.template_vars.set("Day", &now.format("%d").to_string());
1105 self.template_vars
1106 .set("Hour", &now.format("%H").to_string());
1107 self.template_vars
1108 .set("Minute", &now.format("%M").to_string());
1109 }
1110
1111 pub fn populate_runtime_vars(&mut self) {
1120 let goos = map_os_to_goos(std::env::consts::OS);
1121 let goarch = map_arch_to_goarch(std::env::consts::ARCH);
1122 self.template_vars.set("RuntimeGoos", goos);
1123 self.template_vars.set("RuntimeGoarch", goarch);
1124 self.template_vars.set("Runtime_Goos", goos);
1127 self.template_vars.set("Runtime_Goarch", goarch);
1128 self.populate_rustc_vars();
1132 }
1133
1134 fn populate_rustc_vars(&mut self) {
1141 let ver = crate::partial::detect_rustc_version().unwrap_or_default();
1142 self.template_vars.set("RustcVersion", &ver);
1143 }
1144
1145 pub fn populate_release_notes_var(&mut self) {
1153 let notes = self
1155 .config
1156 .crates
1157 .iter()
1158 .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
1159 .cloned()
1160 .unwrap_or_default();
1161 self.template_vars.set("ReleaseNotes", ¬es);
1162 }
1163
1164 pub fn refresh_artifacts_var(&mut self) {
1179 const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
1184 const JSON_LIST_KEYS: &[&str] = &["Platforms"];
1190
1191 let artifacts_value: Vec<serde_json::Value> = self
1192 .artifacts
1193 .all()
1194 .iter()
1195 .map(|a| {
1196 let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
1198 for (k, v) in &a.metadata {
1199 if CSV_LIST_KEYS.contains(&k.as_str()) {
1200 let items: Vec<serde_json::Value> = if v.is_empty() {
1201 Vec::new()
1202 } else {
1203 v.split(',')
1204 .map(|s| serde_json::Value::String(s.to_string()))
1205 .collect()
1206 };
1207 metadata_map.insert(k.clone(), serde_json::Value::Array(items));
1208 } else if JSON_LIST_KEYS.contains(&k.as_str()) {
1209 let parsed = serde_json::from_str::<serde_json::Value>(v)
1213 .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
1214 metadata_map.insert(k.clone(), parsed);
1215 } else {
1216 metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
1217 }
1218 }
1219 serde_json::json!({
1220 "name": a.name,
1221 "path": a.path.to_string_lossy(),
1222 "target": a.target.as_deref().unwrap_or(""),
1223 "kind": a.kind.as_str(),
1224 "crate_name": a.crate_name,
1225 "metadata": serde_json::Value::Object(metadata_map),
1226 })
1227 })
1228 .collect();
1229 let tera_value = tera::Value::Array(artifacts_value);
1232 self.template_vars.set_structured("Artifacts", tera_value);
1233 }
1234
1235 pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
1248 let (
1251 description,
1252 homepage,
1253 license,
1254 maintainers,
1255 mod_timestamp,
1256 full_desc_src,
1257 commit_author,
1258 ) = {
1259 let meta = self.config.metadata.as_ref();
1260 let description = meta
1261 .and_then(|m| m.description.as_deref())
1262 .unwrap_or("")
1263 .to_string();
1264 let homepage = meta
1265 .and_then(|m| m.homepage.as_deref())
1266 .unwrap_or("")
1267 .to_string();
1268 let license = meta
1269 .and_then(|m| m.license.as_deref())
1270 .unwrap_or("")
1271 .to_string();
1272 let maintainers: Vec<String> = meta
1273 .and_then(|m| m.maintainers.as_ref())
1274 .cloned()
1275 .unwrap_or_default();
1276 let mod_timestamp = meta
1277 .and_then(|m| m.mod_timestamp.as_deref())
1278 .unwrap_or("")
1279 .to_string();
1280 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1281 let commit_author = meta.and_then(|m| m.commit_author.clone());
1282 (
1283 description,
1284 homepage,
1285 license,
1286 maintainers,
1287 mod_timestamp,
1288 full_desc_src,
1289 commit_author,
1290 )
1291 };
1292
1293 let full_description = match full_desc_src {
1299 None => String::new(),
1300 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1301 };
1302
1303 let commit_author_map = serde_json::json!({
1304 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1305 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1306 });
1307
1308 let meta_map = serde_json::json!({
1309 "Description": description,
1310 "Homepage": homepage,
1311 "License": license,
1312 "Maintainers": maintainers,
1313 "ModTimestamp": mod_timestamp,
1314 "FullDescription": full_description,
1315 "CommitAuthor": commit_author_map,
1316 });
1317 self.template_vars.set_structured("Metadata", meta_map);
1319 Ok(())
1320 }
1321}
1322
1323pub fn map_os_to_goos(os: &str) -> &str {
1326 match os {
1327 "macos" => "darwin",
1328 other => other, }
1330}
1331
1332pub fn map_arch_to_goarch(arch: &str) -> &str {
1335 match arch {
1336 "x86_64" => "amd64",
1337 "x86" => "386",
1338 "aarch64" => "arm64",
1339 "powerpc64" => "ppc64",
1340 "s390x" => "s390x",
1341 "mips" => "mips",
1342 "mips64" => "mips64",
1343 "riscv64" => "riscv64",
1344 other => other,
1345 }
1346}
1347
1348#[cfg(test)]
1349#[allow(clippy::field_reassign_with_default)]
1350mod tests {
1351 use super::*;
1352 use crate::config::Config;
1353 use crate::git::{GitInfo, SemVer};
1354
1355 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1356 let tag = match prerelease {
1357 Some(pre) => format!("v1.2.3-{pre}"),
1358 None => "v1.2.3".to_string(),
1359 };
1360 GitInfo {
1361 tag,
1362 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1363 short_commit: "abc123d".to_string(),
1364 branch: "main".to_string(),
1365 dirty,
1366 semver: SemVer {
1367 major: 1,
1368 minor: 2,
1369 patch: 3,
1370 prerelease: prerelease.map(|s| s.to_string()),
1371 build_metadata: None,
1372 },
1373 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1374 commit_timestamp: "1774463400".to_string(),
1375 previous_tag: Some("v1.2.2".to_string()),
1376 remote_url: "https://github.com/test/repo.git".to_string(),
1377 summary: "v1.2.3-0-gabc123d".to_string(),
1378 tag_subject: "Release v1.2.3".to_string(),
1379 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1380 tag_body: "Full release notes here.".to_string(),
1381 first_commit: None,
1382 }
1383 }
1384
1385 #[test]
1386 fn test_context_template_vars() {
1387 let mut config = Config::default();
1388 config.project_name = "test-project".to_string();
1389 let ctx = Context::new(config, ContextOptions::default());
1390 assert_eq!(
1391 ctx.template_vars().get("ProjectName"),
1392 Some(&"test-project".to_string())
1393 );
1394 }
1395
1396 #[test]
1397 fn test_context_should_skip() {
1398 let config = Config::default();
1399 let opts = ContextOptions {
1400 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1401 ..Default::default()
1402 };
1403 let ctx = Context::new(config, opts);
1404 assert!(ctx.should_skip("publish"));
1405 assert!(ctx.should_skip("announce"));
1406 assert!(!ctx.should_skip("build"));
1407 }
1408
1409 #[test]
1410 fn test_context_render_template() {
1411 let mut config = Config::default();
1412 config.project_name = "myapp".to_string();
1413 let ctx = Context::new(config, ContextOptions::default());
1414 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1415 assert_eq!(result, "myapp-release");
1416 }
1417
1418 #[test]
1419 fn test_populate_git_vars_sets_all_expected_vars() {
1420 let config = Config::default();
1421 let mut ctx = Context::new(config, ContextOptions::default());
1422 ctx.git_info = Some(make_git_info(false, None));
1423 ctx.populate_git_vars();
1424
1425 let v = ctx.template_vars();
1426 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1427 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1428 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1429 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1430 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1431 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1432 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1433 assert_eq!(
1434 v.get("FullCommit"),
1435 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1436 );
1437 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1438 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1439 assert_eq!(
1440 v.get("CommitDate"),
1441 Some(&"2026-03-25T10:30:00+00:00".to_string())
1442 );
1443 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1444 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1445 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1448 }
1449
1450 #[test]
1451 fn test_nightly_build_defaults_to_zero_without_git_info() {
1452 let config = Config::default();
1455 let mut ctx = Context::new(config, ContextOptions::default());
1456 ctx.git_info = None;
1457 ctx.populate_git_vars();
1458 assert_eq!(
1459 ctx.template_vars().get_structured("NightlyBuild"),
1460 Some(&tera::Value::from(0u64))
1461 );
1462 }
1463
1464 #[test]
1465 fn test_commit_is_alias_for_full_commit() {
1466 let config = Config::default();
1467 let mut ctx = Context::new(config, ContextOptions::default());
1468 ctx.git_info = Some(make_git_info(false, None));
1469 ctx.populate_git_vars();
1470
1471 let v = ctx.template_vars();
1472 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1473 }
1474
1475 #[test]
1476 fn test_populate_git_vars_prerelease() {
1477 let config = Config::default();
1478 let mut ctx = Context::new(config, ContextOptions::default());
1479 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1480 ctx.populate_git_vars();
1481
1482 let v = ctx.template_vars();
1483 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1484 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1485 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1486 }
1487
1488 #[test]
1489 fn test_build_metadata_template_var() {
1490 let config = Config::default();
1491 let mut ctx = Context::new(config, ContextOptions::default());
1492 let mut info = make_git_info(false, None);
1493 info.tag = "v1.2.3+build.42".to_string();
1494 info.semver.build_metadata = Some("build.42".to_string());
1495 ctx.git_info = Some(info);
1496 ctx.populate_git_vars();
1497
1498 let v = ctx.template_vars();
1499 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1500 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1502 }
1503
1504 #[test]
1505 fn test_build_metadata_empty_when_none() {
1506 let config = Config::default();
1507 let mut ctx = Context::new(config, ContextOptions::default());
1508 ctx.git_info = Some(make_git_info(false, None));
1509 ctx.populate_git_vars();
1510
1511 assert_eq!(
1512 ctx.template_vars().get("BuildMetadata"),
1513 Some(&"".to_string())
1514 );
1515 }
1516
1517 #[test]
1518 fn test_populate_git_vars_monorepo_prefixed_tag() {
1519 let config = Config::default();
1522 let mut ctx = Context::new(config, ContextOptions::default());
1523 let mut info = make_git_info(false, None);
1524 info.tag = "core-v0.3.2".to_string();
1525 info.semver = SemVer {
1526 major: 0,
1527 minor: 3,
1528 patch: 2,
1529 prerelease: None,
1530 build_metadata: None,
1531 };
1532 ctx.git_info = Some(info);
1533 ctx.populate_git_vars();
1534
1535 let v = ctx.template_vars();
1536 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1537 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1538 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1539 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1540 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1541 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1542 }
1543
1544 #[test]
1545 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1546 let config = Config::default();
1547 let mut ctx = Context::new(config, ContextOptions::default());
1548 let mut info = make_git_info(false, None);
1549 info.tag = "operator-v1.0.0-rc.1".to_string();
1550 info.semver = SemVer {
1551 major: 1,
1552 minor: 0,
1553 patch: 0,
1554 prerelease: Some("rc.1".to_string()),
1555 build_metadata: None,
1556 };
1557 ctx.git_info = Some(info);
1558 ctx.populate_git_vars();
1559
1560 let v = ctx.template_vars();
1561 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1562 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1563 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1564 }
1565
1566 #[test]
1567 fn test_git_tree_state_clean() {
1568 let config = Config::default();
1569 let mut ctx = Context::new(config, ContextOptions::default());
1570 ctx.git_info = Some(make_git_info(false, None));
1571 ctx.populate_git_vars();
1572
1573 let v = ctx.template_vars();
1574 assert_eq!(
1575 v.get_structured("IsGitDirty"),
1576 Some(&tera::Value::Bool(false))
1577 );
1578 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1579 }
1580
1581 #[test]
1582 fn test_git_tree_state_dirty() {
1583 let config = Config::default();
1584 let mut ctx = Context::new(config, ContextOptions::default());
1585 ctx.git_info = Some(make_git_info(true, None));
1586 ctx.populate_git_vars();
1587
1588 let v = ctx.template_vars();
1589 assert_eq!(
1590 v.get_structured("IsGitDirty"),
1591 Some(&tera::Value::Bool(true))
1592 );
1593 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1594 }
1595
1596 #[test]
1597 fn test_is_snapshot_reflects_context_options() {
1598 let config = Config::default();
1599 let opts = ContextOptions {
1600 snapshot: true,
1601 ..Default::default()
1602 };
1603 let mut ctx = Context::new(config, opts);
1604 ctx.git_info = Some(make_git_info(false, None));
1605 ctx.populate_git_vars();
1606
1607 assert_eq!(
1608 ctx.template_vars().get_structured("IsSnapshot"),
1609 Some(&tera::Value::Bool(true))
1610 );
1611
1612 let config2 = Config::default();
1614 let opts2 = ContextOptions {
1615 snapshot: false,
1616 ..Default::default()
1617 };
1618 let mut ctx2 = Context::new(config2, opts2);
1619 ctx2.git_info = Some(make_git_info(false, None));
1620 ctx2.populate_git_vars();
1621
1622 assert_eq!(
1623 ctx2.template_vars().get_structured("IsSnapshot"),
1624 Some(&tera::Value::Bool(false))
1625 );
1626 }
1627
1628 #[test]
1629 fn test_is_draft_defaults_to_false() {
1630 let config = Config::default();
1631 let mut ctx = Context::new(config, ContextOptions::default());
1632 ctx.git_info = Some(make_git_info(false, None));
1633 ctx.populate_git_vars();
1634
1635 assert_eq!(
1636 ctx.template_vars().get_structured("IsDraft"),
1637 Some(&tera::Value::Bool(false))
1638 );
1639 }
1640
1641 #[test]
1642 fn test_previous_tag_empty_when_none() {
1643 let config = Config::default();
1644 let mut ctx = Context::new(config, ContextOptions::default());
1645 let mut info = make_git_info(false, None);
1646 info.previous_tag = None;
1647 ctx.git_info = Some(info);
1648 ctx.populate_git_vars();
1649
1650 assert_eq!(
1651 ctx.template_vars().get("PreviousTag"),
1652 Some(&"".to_string())
1653 );
1654 }
1655
1656 #[test]
1665 fn populate_time_vars_uses_source_date_epoch_when_set() {
1666 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1670 let config = Config::default();
1671 let mut ctx = Context::new(config, ContextOptions::default());
1672 ctx.set_env_source(env);
1673 ctx.populate_time_vars();
1674
1675 let v = ctx.template_vars();
1676 assert_eq!(
1677 v.get("Timestamp"),
1678 Some(&"1715000000".to_string()),
1679 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1680 );
1681 assert_eq!(
1682 v.get("Date"),
1683 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1684 "Date must be RFC 3339 derived from SDE"
1685 );
1686 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1687 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1688 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1689 }
1690
1691 #[test]
1692 fn test_populate_time_vars() {
1693 let env = crate::MapEnvSource::new();
1696 let config = Config::default();
1697 let mut ctx = Context::new(config, ContextOptions::default());
1698 ctx.set_env_source(env);
1699 ctx.populate_time_vars();
1700
1701 let v = ctx.template_vars();
1702
1703 let date = v
1705 .get("Date")
1706 .unwrap_or_else(|| panic!("Date should be set"));
1707 assert!(
1708 date.contains('T') && date.len() > 10,
1709 "Date should be RFC 3339, got: {date}"
1710 );
1711
1712 let ts = v
1714 .get("Timestamp")
1715 .unwrap_or_else(|| panic!("Timestamp should be set"));
1716 assert!(
1717 ts.parse::<i64>().is_ok(),
1718 "Timestamp should be a numeric string, got: {ts}"
1719 );
1720
1721 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1723 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1724 }
1725
1726 #[test]
1727 fn test_env_vars_accessible_in_templates() {
1728 let mut config = Config::default();
1729 config.project_name = "myapp".to_string();
1730 let mut ctx = Context::new(config, ContextOptions::default());
1731 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1732 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1733
1734 let result = ctx
1735 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1736 .unwrap();
1737 assert_eq!(result, "hello-world-staging");
1738 }
1739
1740 #[test]
1741 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1742 let config = Config::default();
1743 let opts = ContextOptions {
1744 snapshot: true,
1745 ..Default::default()
1746 };
1747 let mut ctx = Context::new(config, opts);
1748 ctx.populate_git_vars();
1750
1751 assert_eq!(
1752 ctx.template_vars().get_structured("IsSnapshot"),
1753 Some(&tera::Value::Bool(true))
1754 );
1755 assert_eq!(
1756 ctx.template_vars().get_structured("IsDraft"),
1757 Some(&tera::Value::Bool(false))
1758 );
1759 assert_eq!(ctx.template_vars().get("Tag"), None);
1761 }
1762
1763 #[test]
1764 fn test_is_nightly_set_when_nightly_mode_active() {
1765 let config = Config::default();
1766 let opts = ContextOptions {
1767 nightly: true,
1768 ..Default::default()
1769 };
1770 let mut ctx = Context::new(config, opts);
1771 ctx.git_info = Some(make_git_info(false, None));
1772 ctx.populate_git_vars();
1773
1774 assert_eq!(
1775 ctx.template_vars().get_structured("IsNightly"),
1776 Some(&tera::Value::Bool(true)),
1777 "IsNightly should be 'true' when nightly mode is active"
1778 );
1779 assert!(ctx.is_nightly(), "is_nightly() should return true");
1780 }
1781
1782 #[test]
1783 fn test_is_nightly_false_by_default() {
1784 let config = Config::default();
1785 let mut ctx = Context::new(config, ContextOptions::default());
1786 ctx.git_info = Some(make_git_info(false, None));
1787 ctx.populate_git_vars();
1788
1789 assert_eq!(
1790 ctx.template_vars().get_structured("IsNightly"),
1791 Some(&tera::Value::Bool(false)),
1792 "IsNightly should default to 'false'"
1793 );
1794 assert!(
1795 !ctx.is_nightly(),
1796 "is_nightly() should return false by default"
1797 );
1798 }
1799
1800 #[test]
1801 fn test_version_returns_populated_value() {
1802 let config = Config::default();
1803 let mut ctx = Context::new(config, ContextOptions::default());
1804 ctx.git_info = Some(make_git_info(false, None));
1805 ctx.populate_git_vars();
1806
1807 assert_eq!(ctx.version(), "1.2.3");
1808 }
1809
1810 #[test]
1811 fn test_version_returns_empty_when_not_set() {
1812 let config = Config::default();
1813 let ctx = Context::new(config, ContextOptions::default());
1814 assert_eq!(ctx.version(), "");
1815 }
1816
1817 #[test]
1818 fn test_is_nightly_without_git_info() {
1819 let config = Config::default();
1820 let opts = ContextOptions {
1821 nightly: true,
1822 ..Default::default()
1823 };
1824 let mut ctx = Context::new(config, opts);
1825 ctx.populate_git_vars();
1827
1828 assert_eq!(
1829 ctx.template_vars().get_structured("IsNightly"),
1830 Some(&tera::Value::Bool(true)),
1831 "IsNightly should be set even without git info"
1832 );
1833 }
1834
1835 #[test]
1836 fn test_is_git_clean_when_not_dirty() {
1837 let config = Config::default();
1838 let mut ctx = Context::new(config, ContextOptions::default());
1839 ctx.git_info = Some(make_git_info(false, None));
1840 ctx.populate_git_vars();
1841
1842 assert_eq!(
1843 ctx.template_vars().get_structured("IsGitClean"),
1844 Some(&tera::Value::Bool(true))
1845 );
1846 }
1847
1848 #[test]
1849 fn test_is_git_clean_when_dirty() {
1850 let config = Config::default();
1851 let mut ctx = Context::new(config, ContextOptions::default());
1852 ctx.git_info = Some(make_git_info(true, None));
1853 ctx.populate_git_vars();
1854
1855 assert_eq!(
1856 ctx.template_vars().get_structured("IsGitClean"),
1857 Some(&tera::Value::Bool(false))
1858 );
1859 }
1860
1861 #[test]
1862 fn test_git_url_set_from_git_info() {
1863 let config = Config::default();
1864 let mut ctx = Context::new(config, ContextOptions::default());
1865 ctx.git_info = Some(make_git_info(false, None));
1866 ctx.populate_git_vars();
1867
1868 assert_eq!(
1869 ctx.template_vars().get("GitURL"),
1870 Some(&"https://github.com/test/repo.git".to_string())
1871 );
1872 }
1873
1874 #[test]
1875 fn test_summary_set_from_git_info() {
1876 let config = Config::default();
1877 let mut ctx = Context::new(config, ContextOptions::default());
1878 ctx.git_info = Some(make_git_info(false, None));
1879 ctx.populate_git_vars();
1880
1881 assert_eq!(
1882 ctx.template_vars().get("Summary"),
1883 Some(&"v1.2.3-0-gabc123d".to_string())
1884 );
1885 }
1886
1887 #[test]
1888 fn test_tag_subject_set_from_git_info() {
1889 let config = Config::default();
1890 let mut ctx = Context::new(config, ContextOptions::default());
1891 ctx.git_info = Some(make_git_info(false, None));
1892 ctx.populate_git_vars();
1893
1894 assert_eq!(
1895 ctx.template_vars().get("TagSubject"),
1896 Some(&"Release v1.2.3".to_string())
1897 );
1898 }
1899
1900 #[test]
1901 fn test_tag_contents_set_from_git_info() {
1902 let config = Config::default();
1903 let mut ctx = Context::new(config, ContextOptions::default());
1904 ctx.git_info = Some(make_git_info(false, None));
1905 ctx.populate_git_vars();
1906
1907 assert_eq!(
1908 ctx.template_vars().get("TagContents"),
1909 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
1910 );
1911 }
1912
1913 #[test]
1914 fn test_tag_body_set_from_git_info() {
1915 let config = Config::default();
1916 let mut ctx = Context::new(config, ContextOptions::default());
1917 ctx.git_info = Some(make_git_info(false, None));
1918 ctx.populate_git_vars();
1919
1920 assert_eq!(
1921 ctx.template_vars().get("TagBody"),
1922 Some(&"Full release notes here.".to_string())
1923 );
1924 }
1925
1926 #[test]
1927 fn test_is_single_target_false_by_default() {
1928 let config = Config::default();
1929 let mut ctx = Context::new(config, ContextOptions::default());
1930 ctx.git_info = Some(make_git_info(false, None));
1931 ctx.populate_git_vars();
1932
1933 assert_eq!(
1934 ctx.template_vars().get_structured("IsSingleTarget"),
1935 Some(&tera::Value::Bool(false))
1936 );
1937 }
1938
1939 #[test]
1940 fn test_is_single_target_true_when_set() {
1941 let config = Config::default();
1942 let opts = ContextOptions {
1943 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
1944 ..Default::default()
1945 };
1946 let mut ctx = Context::new(config, opts);
1947 ctx.git_info = Some(make_git_info(false, None));
1948 ctx.populate_git_vars();
1949
1950 assert_eq!(
1951 ctx.template_vars().get_structured("IsSingleTarget"),
1952 Some(&tera::Value::Bool(true))
1953 );
1954 }
1955
1956 #[test]
1957 #[serial_test::serial]
1958 fn test_populate_runtime_vars() {
1959 let config = Config::default();
1960 let mut ctx = Context::new(config, ContextOptions::default());
1961 ctx.populate_runtime_vars();
1962
1963 let v = ctx.template_vars();
1964
1965 let goos = v
1966 .get("RuntimeGoos")
1967 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
1968 assert!(
1969 !goos.is_empty(),
1970 "RuntimeGoos should not be empty, got: {goos}"
1971 );
1972 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
1974
1975 let goarch = v
1976 .get("RuntimeGoarch")
1977 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
1978 assert!(
1979 !goarch.is_empty(),
1980 "RuntimeGoarch should not be empty, got: {goarch}"
1981 );
1982 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
1984 }
1985
1986 #[test]
1987 fn test_populate_release_notes_var_with_changelogs() {
1988 let mut config = Config::default();
1989 config.crates.push(crate::config::CrateConfig {
1990 name: "my-crate".to_string(),
1991 ..Default::default()
1992 });
1993 let mut ctx = Context::new(config, ContextOptions::default());
1994 ctx.stage_outputs
1995 .changelogs
1996 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
1997 ctx.populate_release_notes_var();
1998
1999 assert_eq!(
2000 ctx.template_vars().get("ReleaseNotes"),
2001 Some(&"## Changes\n- fix bug".to_string())
2002 );
2003 }
2004
2005 #[test]
2006 fn test_populate_release_notes_var_empty_when_no_changelogs() {
2007 let config = Config::default();
2008 let mut ctx = Context::new(config, ContextOptions::default());
2009 ctx.populate_release_notes_var();
2010
2011 assert_eq!(
2012 ctx.template_vars().get("ReleaseNotes"),
2013 Some(&"".to_string())
2014 );
2015 }
2016
2017 #[test]
2018 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
2019 let mut config = Config::default();
2020 config.crates.push(crate::config::CrateConfig {
2021 name: "crate-a".to_string(),
2022 ..Default::default()
2023 });
2024 config.crates.push(crate::config::CrateConfig {
2025 name: "crate-b".to_string(),
2026 ..Default::default()
2027 });
2028 let mut ctx = Context::new(config, ContextOptions::default());
2029 ctx.stage_outputs
2030 .changelogs
2031 .insert("crate-a".to_string(), "notes-a".to_string());
2032 ctx.stage_outputs
2033 .changelogs
2034 .insert("crate-b".to_string(), "notes-b".to_string());
2035 ctx.populate_release_notes_var();
2036
2037 assert_eq!(
2039 ctx.template_vars().get("ReleaseNotes"),
2040 Some(&"notes-a".to_string())
2041 );
2042 }
2043
2044 #[test]
2045 fn test_outputs_accessible_in_templates() {
2046 let mut config = Config::default();
2047 config.project_name = "myapp".to_string();
2048 let mut ctx = Context::new(config, ContextOptions::default());
2049 ctx.template_vars_mut().set_output("build_id", "abc123");
2050 ctx.template_vars_mut()
2051 .set_output("deploy_url", "https://example.com");
2052
2053 let result = ctx
2054 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
2055 .unwrap();
2056 assert_eq!(result, "abc123-https://example.com");
2057 }
2058
2059 #[test]
2060 fn test_artifact_ext_and_target_template_vars() {
2061 let mut config = Config::default();
2062 config.project_name = "myapp".to_string();
2063 let mut ctx = Context::new(config, ContextOptions::default());
2064 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
2065 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
2066 ctx.template_vars_mut()
2067 .set("Target", "x86_64-unknown-linux-gnu");
2068
2069 let result = ctx
2070 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
2071 .unwrap();
2072 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
2073 }
2074
2075 #[test]
2076 fn test_checksums_template_var() {
2077 let mut config = Config::default();
2078 config.project_name = "myapp".to_string();
2079 let mut ctx = Context::new(config, ContextOptions::default());
2080 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
2081 ctx.template_vars_mut().set("Checksums", checksum_text);
2082
2083 let result = ctx.render_template("{{ .Checksums }}").unwrap();
2084 assert_eq!(result, checksum_text);
2085 }
2086
2087 #[test]
2090 fn test_prefixed_tag_with_tag_prefix() {
2091 let mut config = Config::default();
2092 config.tag = Some(crate::config::TagConfig {
2093 tag_prefix: Some("api/".to_string()),
2094 ..Default::default()
2095 });
2096 let mut ctx = Context::new(config, ContextOptions::default());
2097 ctx.git_info = Some(make_git_info(false, None));
2098 ctx.populate_git_vars();
2099
2100 assert_eq!(
2101 ctx.template_vars().get("PrefixedTag"),
2102 Some(&"api/v1.2.3".to_string())
2103 );
2104 }
2105
2106 #[test]
2107 fn test_prefixed_tag_without_tag_prefix() {
2108 let config = Config::default();
2109 let mut ctx = Context::new(config, ContextOptions::default());
2110 ctx.git_info = Some(make_git_info(false, None));
2111 ctx.populate_git_vars();
2112
2113 assert_eq!(
2115 ctx.template_vars().get("PrefixedTag"),
2116 Some(&"v1.2.3".to_string())
2117 );
2118 }
2119
2120 #[test]
2121 fn test_prefixed_previous_tag_with_tag_prefix() {
2122 let mut config = Config::default();
2123 config.tag = Some(crate::config::TagConfig {
2124 tag_prefix: Some("api/".to_string()),
2125 ..Default::default()
2126 });
2127 let mut ctx = Context::new(config, ContextOptions::default());
2128 ctx.git_info = Some(make_git_info(false, None));
2129 ctx.populate_git_vars();
2130
2131 assert_eq!(
2132 ctx.template_vars().get("PrefixedPreviousTag"),
2133 Some(&"api/v1.2.2".to_string())
2134 );
2135 }
2136
2137 #[test]
2138 fn test_prefixed_previous_tag_empty_when_no_previous() {
2139 let mut config = Config::default();
2140 config.tag = Some(crate::config::TagConfig {
2141 tag_prefix: Some("api/".to_string()),
2142 ..Default::default()
2143 });
2144 let mut ctx = Context::new(config, ContextOptions::default());
2145 let mut info = make_git_info(false, None);
2146 info.previous_tag = None;
2147 ctx.git_info = Some(info);
2148 ctx.populate_git_vars();
2149
2150 assert_eq!(
2153 ctx.template_vars().get("PrefixedPreviousTag"),
2154 Some(&"".to_string())
2155 );
2156 }
2157
2158 #[test]
2159 fn test_prefixed_summary_with_tag_prefix() {
2160 let mut config = Config::default();
2161 config.tag = Some(crate::config::TagConfig {
2162 tag_prefix: Some("api/".to_string()),
2163 ..Default::default()
2164 });
2165 let mut ctx = Context::new(config, ContextOptions::default());
2166 ctx.git_info = Some(make_git_info(false, None));
2167 ctx.populate_git_vars();
2168
2169 assert_eq!(
2170 ctx.template_vars().get("PrefixedSummary"),
2171 Some(&"api/v1.2.3-0-gabc123d".to_string())
2172 );
2173 }
2174
2175 #[test]
2176 fn test_is_release_true_for_normal_release() {
2177 let config = Config::default();
2178 let opts = ContextOptions {
2179 snapshot: false,
2180 nightly: false,
2181 ..Default::default()
2182 };
2183 let mut ctx = Context::new(config, opts);
2184 ctx.git_info = Some(make_git_info(false, None));
2185 ctx.populate_git_vars();
2186
2187 assert_eq!(
2188 ctx.template_vars().get_structured("IsRelease"),
2189 Some(&tera::Value::Bool(true))
2190 );
2191 }
2192
2193 #[test]
2194 fn test_is_release_false_for_snapshot() {
2195 let config = Config::default();
2196 let opts = ContextOptions {
2197 snapshot: true,
2198 ..Default::default()
2199 };
2200 let mut ctx = Context::new(config, opts);
2201 ctx.git_info = Some(make_git_info(false, None));
2202 ctx.populate_git_vars();
2203
2204 assert_eq!(
2205 ctx.template_vars().get_structured("IsRelease"),
2206 Some(&tera::Value::Bool(false))
2207 );
2208 }
2209
2210 #[test]
2211 fn test_is_release_false_for_nightly() {
2212 let config = Config::default();
2213 let opts = ContextOptions {
2214 nightly: true,
2215 ..Default::default()
2216 };
2217 let mut ctx = Context::new(config, opts);
2218 ctx.git_info = Some(make_git_info(false, None));
2219 ctx.populate_git_vars();
2220
2221 assert_eq!(
2222 ctx.template_vars().get_structured("IsRelease"),
2223 Some(&tera::Value::Bool(false))
2224 );
2225 }
2226
2227 #[test]
2228 fn test_is_merging_true_when_merge_flag_set() {
2229 let config = Config::default();
2230 let opts = ContextOptions {
2231 merge: true,
2232 ..Default::default()
2233 };
2234 let mut ctx = Context::new(config, opts);
2235 ctx.git_info = Some(make_git_info(false, None));
2236 ctx.populate_git_vars();
2237
2238 assert_eq!(
2239 ctx.template_vars().get_structured("IsMerging"),
2240 Some(&tera::Value::Bool(true))
2241 );
2242 }
2243
2244 #[test]
2245 fn test_is_merging_false_by_default() {
2246 let config = Config::default();
2247 let mut ctx = Context::new(config, ContextOptions::default());
2248 ctx.git_info = Some(make_git_info(false, None));
2249 ctx.populate_git_vars();
2250
2251 assert_eq!(
2252 ctx.template_vars().get_structured("IsMerging"),
2253 Some(&tera::Value::Bool(false))
2254 );
2255 }
2256
2257 #[test]
2258 fn test_refresh_artifacts_var_empty() {
2259 let config = Config::default();
2260 let mut ctx = Context::new(config, ContextOptions::default());
2261 ctx.refresh_artifacts_var();
2262
2263 let result = ctx
2265 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2266 .unwrap();
2267 assert_eq!(result, "");
2268 }
2269
2270 #[test]
2271 fn test_refresh_artifacts_var_with_artifacts() {
2272 use crate::artifact::{Artifact, ArtifactKind};
2273 use std::collections::HashMap;
2274 use std::path::PathBuf;
2275
2276 let config = Config::default();
2277 let mut ctx = Context::new(config, ContextOptions::default());
2278 ctx.artifacts.add(Artifact {
2282 kind: ArtifactKind::Archive,
2283 name: String::new(),
2284 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2285 target: Some("x86_64-unknown-linux-gnu".to_string()),
2286 crate_name: "myapp".to_string(),
2287 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2288 size: None,
2289 });
2290 ctx.artifacts.add(Artifact {
2291 kind: ArtifactKind::Binary,
2292 name: String::new(),
2293 path: PathBuf::from("dist/myapp"),
2294 target: Some("x86_64-unknown-linux-gnu".to_string()),
2295 crate_name: "myapp".to_string(),
2296 metadata: HashMap::new(),
2297 size: None,
2298 });
2299 ctx.refresh_artifacts_var();
2300
2301 let result = ctx
2303 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2304 .unwrap();
2305 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2306 assert!(result.contains("myapp"));
2307
2308 let result_kinds = ctx
2310 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2311 .unwrap();
2312 assert!(result_kinds.contains("archive"));
2313 assert!(result_kinds.contains("binary"));
2314 }
2315
2316 #[test]
2317 fn test_populate_metadata_var_with_mod_timestamp() {
2318 let mut config = Config::default();
2319 config.metadata = Some(crate::config::MetadataConfig {
2320 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2321 ..Default::default()
2322 });
2323 let mut ctx = Context::new(config, ContextOptions::default());
2324 ctx.populate_metadata_var().unwrap();
2325
2326 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2328 assert_eq!(result, "{{ .CommitTimestamp }}");
2329 }
2330
2331 #[test]
2332 fn test_populate_metadata_var_empty_when_no_config() {
2333 let config = Config::default();
2334 let mut ctx = Context::new(config, ContextOptions::default());
2335 ctx.populate_metadata_var().unwrap();
2336
2337 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2339 assert_eq!(result, "");
2340 }
2341
2342 #[test]
2343 fn test_populate_metadata_var_reads_from_config() {
2344 let mut config = Config::default();
2345 config.metadata = Some(crate::config::MetadataConfig {
2346 description: Some("A test project".to_string()),
2347 homepage: Some("https://example.com".to_string()),
2348 license: Some("MIT".to_string()),
2349 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2350 mod_timestamp: Some("1234567890".to_string()),
2351 ..Default::default()
2352 });
2353 let mut ctx = Context::new(config, ContextOptions::default());
2354 ctx.populate_metadata_var().unwrap();
2355
2356 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2357 assert_eq!(desc, "A test project");
2358
2359 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2360 assert_eq!(home, "https://example.com");
2361
2362 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2363 assert_eq!(lic, "MIT");
2364
2365 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2366 assert_eq!(ts, "1234567890");
2367 }
2368
2369 #[test]
2370 fn test_populate_metadata_var_full_description_inline() {
2371 use crate::config::ContentSource;
2372 let mut config = Config::default();
2373 config.metadata = Some(crate::config::MetadataConfig {
2374 full_description: Some(ContentSource::Inline(
2375 "A long-form description of the project.".to_string(),
2376 )),
2377 ..Default::default()
2378 });
2379 let mut ctx = Context::new(config, ContextOptions::default());
2380 ctx.populate_metadata_var().unwrap();
2381 let rendered = ctx
2382 .render_template("{{ Metadata.FullDescription }}")
2383 .unwrap();
2384 assert_eq!(rendered, "A long-form description of the project.");
2385 }
2386
2387 #[test]
2388 fn test_populate_metadata_var_full_description_from_file() {
2389 use crate::config::ContentSource;
2390 let tmp = tempfile::tempdir().unwrap();
2391 let desc_path = tmp.path().join("DESCRIPTION.md");
2392 std::fs::write(&desc_path, "read from disk").unwrap();
2393 let mut config = Config::default();
2394 config.metadata = Some(crate::config::MetadataConfig {
2395 full_description: Some(ContentSource::FromFile {
2396 from_file: desc_path.to_string_lossy().into_owned(),
2397 }),
2398 ..Default::default()
2399 });
2400 let mut ctx = Context::new(config, ContextOptions::default());
2401 ctx.populate_metadata_var().unwrap();
2402 let rendered = ctx
2403 .render_template("{{ Metadata.FullDescription }}")
2404 .unwrap();
2405 assert_eq!(rendered, "read from disk");
2406 }
2407
2408 #[test]
2409 fn test_populate_metadata_var_full_description_from_url_resolves() {
2410 use crate::config::ContentSource;
2415 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2416
2417 let body = "long form description body";
2418 let body_len = body.len();
2419 let response: &'static str = Box::leak(
2420 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2421 );
2422 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2423
2424 let mut config = Config::default();
2425 config.metadata = Some(crate::config::MetadataConfig {
2426 full_description: Some(ContentSource::FromUrl {
2427 from_url: format!("http://{addr}/description.md"),
2428 headers: None,
2429 }),
2430 ..Default::default()
2431 });
2432 let mut ctx = Context::new(config, ContextOptions::default());
2433 ctx.populate_metadata_var()
2434 .expect("from_url should resolve through content_source");
2435 let rendered = ctx
2436 .render_template("{{ Metadata.FullDescription }}")
2437 .unwrap();
2438 assert_eq!(rendered, body);
2439 }
2440
2441 #[test]
2442 fn test_populate_metadata_var_commit_author() {
2443 use crate::config::CommitAuthorConfig;
2444 let mut config = Config::default();
2445 config.metadata = Some(crate::config::MetadataConfig {
2446 commit_author: Some(CommitAuthorConfig {
2447 name: Some("Alice Developer".to_string()),
2448 email: Some("alice@example.com".to_string()),
2449 signing: None,
2450 use_github_app_token: false,
2451 }),
2452 ..Default::default()
2453 });
2454 let mut ctx = Context::new(config, ContextOptions::default());
2455 ctx.populate_metadata_var().unwrap();
2456 let name = ctx
2457 .render_template("{{ Metadata.CommitAuthor.Name }}")
2458 .unwrap();
2459 assert_eq!(name, "Alice Developer");
2460 let email = ctx
2461 .render_template("{{ Metadata.CommitAuthor.Email }}")
2462 .unwrap();
2463 assert_eq!(email, "alice@example.com");
2464 }
2465
2466 #[test]
2467 fn test_artifact_id_template_var() {
2468 let mut config = Config::default();
2469 config.project_name = "myapp".to_string();
2470 let mut ctx = Context::new(config, ContextOptions::default());
2471 ctx.template_vars_mut().set("ArtifactID", "default");
2472
2473 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2474 assert_eq!(result, "default");
2475 }
2476
2477 #[test]
2478 fn test_artifact_id_empty_when_not_set() {
2479 let mut config = Config::default();
2480 config.project_name = "myapp".to_string();
2481 let mut ctx = Context::new(config, ContextOptions::default());
2482 ctx.template_vars_mut().set("ArtifactID", "");
2483
2484 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2485 assert_eq!(result, "");
2486 }
2487
2488 #[test]
2489 fn test_pro_vars_rendered_in_templates() {
2490 let mut config = Config::default();
2492 config.tag = Some(crate::config::TagConfig {
2493 tag_prefix: Some("api/".to_string()),
2494 ..Default::default()
2495 });
2496 let opts = ContextOptions {
2497 snapshot: false,
2498 nightly: false,
2499 merge: true,
2500 ..Default::default()
2501 };
2502 let mut ctx = Context::new(config, opts);
2503 ctx.git_info = Some(make_git_info(false, None));
2504 ctx.populate_git_vars();
2505
2506 let result = ctx
2507 .render_template(
2508 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2509 )
2510 .unwrap();
2511 assert_eq!(result, "release-merge-api/v1.2.3");
2512 }
2513
2514 #[test]
2515 fn test_is_release_without_git_info() {
2516 let config = Config::default();
2518 let opts = ContextOptions {
2519 snapshot: false,
2520 nightly: false,
2521 ..Default::default()
2522 };
2523 let mut ctx = Context::new(config, opts);
2524 ctx.populate_git_vars();
2525
2526 assert_eq!(
2527 ctx.template_vars().get_structured("IsRelease"),
2528 Some(&tera::Value::Bool(true))
2529 );
2530 }
2531
2532 #[test]
2533 fn test_is_merging_without_git_info() {
2534 let config = Config::default();
2536 let opts = ContextOptions {
2537 merge: true,
2538 ..Default::default()
2539 };
2540 let mut ctx = Context::new(config, opts);
2541 ctx.populate_git_vars();
2542
2543 assert_eq!(
2544 ctx.template_vars().get_structured("IsMerging"),
2545 Some(&tera::Value::Bool(true))
2546 );
2547 }
2548
2549 #[test]
2559 fn test_monorepo_version_matches_shared_semver_helper() {
2560 let mut config = Config::default();
2561 config.monorepo = Some(crate::config::MonorepoConfig {
2562 tag_prefix: Some("core/".to_string()),
2563 dir: None,
2564 });
2565 let mut ctx = Context::new(config, ContextOptions::default());
2566
2567 let semver = SemVer {
2568 major: 2,
2569 minor: 1,
2570 patch: 0,
2571 prerelease: Some("rc.1".to_string()),
2572 build_metadata: Some("build.7".to_string()),
2573 };
2574 let mut info = make_git_info(false, None);
2575 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2576 info.semver = semver.clone();
2577 ctx.git_info = Some(info);
2578 ctx.populate_git_vars();
2579
2580 let v = ctx.template_vars();
2581 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2584 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2585 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2586 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2587 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2589 }
2590
2591 #[test]
2592 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2593 let mut config = Config::default();
2594 config.monorepo = Some(crate::config::MonorepoConfig {
2595 tag_prefix: Some("subproject1/".to_string()),
2596 dir: None,
2597 });
2598 let mut ctx = Context::new(config, ContextOptions::default());
2599
2600 let mut info = make_git_info(false, None);
2602 info.tag = "subproject1/v1.2.3".to_string();
2603 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2604 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2605 ctx.git_info = Some(info);
2606 ctx.populate_git_vars();
2607
2608 let v = ctx.template_vars();
2609 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2611 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2613 assert_eq!(
2615 v.get("PrefixedTag"),
2616 Some(&"subproject1/v1.2.3".to_string())
2617 );
2618 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2620 assert_eq!(
2622 v.get("PrefixedPreviousTag"),
2623 Some(&"subproject1/v1.2.2".to_string())
2624 );
2625 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2627 assert_eq!(
2629 v.get("PrefixedSummary"),
2630 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2631 );
2632 }
2633
2634 #[test]
2635 fn test_monorepo_prefixed_previous_tag() {
2636 let mut config = Config::default();
2637 config.monorepo = Some(crate::config::MonorepoConfig {
2638 tag_prefix: Some("svc/".to_string()),
2639 dir: None,
2640 });
2641 let mut ctx = Context::new(config, ContextOptions::default());
2642
2643 let mut info = make_git_info(false, None);
2644 info.tag = "svc/v2.0.0".to_string();
2645 info.previous_tag = Some("svc/v1.9.0".to_string());
2646 ctx.git_info = Some(info);
2647 ctx.populate_git_vars();
2648
2649 let v = ctx.template_vars();
2650 assert_eq!(
2652 v.get("PrefixedPreviousTag"),
2653 Some(&"svc/v1.9.0".to_string())
2654 );
2655 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2657 }
2658
2659 #[test]
2660 fn test_no_monorepo_falls_back_to_tag_prefix() {
2661 let mut config = Config::default();
2663 config.tag = Some(crate::config::TagConfig {
2664 tag_prefix: Some("release/".to_string()),
2665 ..Default::default()
2666 });
2667 let mut ctx = Context::new(config, ContextOptions::default());
2668 ctx.git_info = Some(make_git_info(false, None));
2669 ctx.populate_git_vars();
2670
2671 let v = ctx.template_vars();
2672 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2674 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2676 assert_eq!(
2677 v.get("PrefixedPreviousTag"),
2678 Some(&"release/v1.2.2".to_string())
2679 );
2680 }
2681
2682 #[test]
2683 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2684 let mut config = Config::default();
2687 config.tag = Some(crate::config::TagConfig {
2688 tag_prefix: Some("release/".to_string()),
2689 ..Default::default()
2690 });
2691 config.monorepo = Some(crate::config::MonorepoConfig {
2692 tag_prefix: Some("svc/".to_string()),
2693 dir: None,
2694 });
2695 let mut ctx = Context::new(config, ContextOptions::default());
2696
2697 let mut info = make_git_info(false, None);
2698 info.tag = "svc/v1.2.3".to_string();
2699 info.previous_tag = Some("svc/v1.2.2".to_string());
2700 ctx.git_info = Some(info);
2701 ctx.populate_git_vars();
2702
2703 let v = ctx.template_vars();
2704 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2706 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2708 }
2709
2710 #[test]
2711 fn test_monorepo_prefixed_summary() {
2712 let mut config = Config::default();
2713 config.monorepo = Some(crate::config::MonorepoConfig {
2714 tag_prefix: Some("pkg/".to_string()),
2715 dir: None,
2716 });
2717 let mut ctx = Context::new(config, ContextOptions::default());
2718
2719 let mut info = make_git_info(false, None);
2720 info.tag = "pkg/v1.2.3".to_string();
2721 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
2723 ctx.git_info = Some(info);
2724 ctx.populate_git_vars();
2725
2726 assert_eq!(
2728 ctx.template_vars().get("PrefixedSummary"),
2729 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
2730 );
2731 assert_eq!(
2733 ctx.template_vars().get("Summary"),
2734 Some(&"v1.2.3-0-gabc123d".to_string())
2735 );
2736 }
2737
2738 #[test]
2739 fn test_monorepo_no_previous_tag() {
2740 let mut config = Config::default();
2741 config.monorepo = Some(crate::config::MonorepoConfig {
2742 tag_prefix: Some("svc/".to_string()),
2743 dir: None,
2744 });
2745 let mut ctx = Context::new(config, ContextOptions::default());
2746
2747 let mut info = make_git_info(false, None);
2748 info.tag = "svc/v1.0.0".to_string();
2749 info.previous_tag = None;
2750 ctx.git_info = Some(info);
2751 ctx.populate_git_vars();
2752
2753 let v = ctx.template_vars();
2754 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
2755 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
2757 }
2758
2759 #[test]
2764 fn test_monorepo_full_flow_all_vars() {
2765 let mut config = Config::default();
2768 config.project_name = "mymonorepo".to_string();
2769 config.monorepo = Some(crate::config::MonorepoConfig {
2770 tag_prefix: Some("services/api/".to_string()),
2771 dir: Some("services/api".to_string()),
2772 });
2773
2774 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
2776 assert_eq!(config.monorepo_dir(), Some("services/api"));
2777
2778 let mut ctx = Context::new(config, ContextOptions::default());
2779
2780 let mut info = make_git_info(false, None);
2783 info.tag = "services/api/v2.1.0".to_string();
2784 info.previous_tag = Some("services/api/v2.0.5".to_string());
2785 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
2786 info.semver = crate::git::SemVer {
2787 major: 2,
2788 minor: 1,
2789 patch: 0,
2790 prerelease: None,
2791 build_metadata: None,
2792 };
2793 ctx.git_info = Some(info);
2794 ctx.populate_git_vars();
2795
2796 let v = ctx.template_vars();
2797
2798 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
2800 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
2801 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2802 assert_eq!(v.get("Major"), Some(&"2".to_string()));
2803 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
2804 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
2805 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
2806 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
2807
2808 assert_eq!(
2810 v.get("PrefixedTag"),
2811 Some(&"services/api/v2.1.0".to_string())
2812 );
2813 assert_eq!(
2814 v.get("PrefixedPreviousTag"),
2815 Some(&"services/api/v2.0.5".to_string())
2816 );
2817 assert_eq!(
2818 v.get("PrefixedSummary"),
2819 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
2820 );
2821
2822 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
2824 }
2825
2826 #[test]
2827 fn context_env_var_defaults_to_process_env_source() {
2828 let ctx = Context::new(Config::default(), ContextOptions::default());
2829 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
2831 }
2832
2833 #[test]
2834 fn context_env_var_routes_to_injected_source() {
2835 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2836 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
2837 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
2838 assert_eq!(ctx.env_var("PATH"), None);
2842 }
2843
2844 #[test]
2845 #[serial_test::serial]
2846 fn populate_runtime_vars_sets_rustc_version() {
2847 let config = Config::default();
2848 let mut ctx = Context::new(config, ContextOptions::default());
2849 ctx.populate_runtime_vars();
2852
2853 let ver = ctx
2854 .template_vars()
2855 .get("RustcVersion")
2856 .expect("RustcVersion should be set after populate_runtime_vars");
2857 if !ver.is_empty() {
2861 assert!(
2862 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
2863 "RustcVersion should start with a digit: {ver}"
2864 );
2865 }
2866 }
2867}