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] = &[
48 "publish",
49 "announce",
50 "sign",
51 "validate",
52 "sbom",
53 "attest",
54 "docker",
55 "docker-sign",
56 "winget",
57 "chocolatey",
58 "snapcraft",
59 "snapcraft-publish",
60 "scoop",
61 "homebrew",
62 "nix",
63 "aur",
64 "cargo",
65 "krew",
66 "nfpm",
67 "makeself",
68 "appimage",
69 "flatpak",
70 "srpm",
71 "before",
72 "before-publish",
73 "notarize",
74 "archive",
75 "source",
76 "build",
77 "changelog",
78 "release",
79 "checksum",
80 "upx",
81 "blob",
82 "templatefiles",
83 "dmg",
84 "msi",
85 "nsis",
86 "pkg",
87 "appbundle",
88 "verify-release",
89];
90
91pub const VALID_BUILD_SKIPS: &[&str] = &["pre-hooks", "post-hooks", "validate", "before"];
93
94pub fn validate_skip_values(skip: &[String], valid: &[&str]) -> Result<(), String> {
99 let invalid: Vec<&str> = skip
100 .iter()
101 .map(|s| s.as_str())
102 .filter(|s| !valid.contains(s))
103 .collect();
104 if invalid.is_empty() {
105 Ok(())
106 } else {
107 Err(format!(
108 "invalid --skip value(s): {}. Valid options: {}",
109 invalid.join(", "),
110 valid.join(", "),
111 ))
112 }
113}
114
115pub struct ContextOptions {
116 pub snapshot: bool,
117 pub nightly: bool,
118 pub dry_run: bool,
119 pub quiet: bool,
120 pub verbose: bool,
121 pub debug: bool,
122 pub skip_stages: Vec<String>,
123 pub publisher_allowlist: Vec<String>,
131 pub selected_crates: Vec<String>,
132 pub token: Option<String>,
133 pub parallelism: usize,
135 pub single_target: Option<String>,
137 pub release_notes_path: Option<PathBuf>,
139 pub fail_fast: bool,
141 pub partial_target: Option<PartialTarget>,
144 pub merge: bool,
146 pub publish_only: bool,
156 pub project_root: Option<PathBuf>,
159 pub strict: bool,
161 pub resume_release: bool,
166 pub replace_existing_artifacts: bool,
170 pub skip_post_publish_poll: bool,
178 pub gate_submitter: Option<bool>,
186 pub rollback_mode: Option<RollbackMode>,
191 pub simulate_failure_publishers: Vec<String>,
199 pub rollback_only: bool,
205 pub allow_rerun: bool,
220 pub show_skipped: bool,
229 pub from_run: Option<String>,
233 pub runtime_nondeterministic_allowlist: Vec<(String, String)>,
240 pub summary_json_path: Option<PathBuf>,
244 pub allow_ai_failure: bool,
251 pub changelog_from: Option<String>,
258 pub changelog_full_history: bool,
265 pub changelog_to: Option<String>,
274 pub changelog_preview: bool,
290 pub allow_snapshot_publish: bool,
304}
305
306impl Default for ContextOptions {
307 fn default() -> Self {
308 Self {
309 snapshot: false,
310 nightly: false,
311 dry_run: false,
312 quiet: false,
313 verbose: false,
314 debug: false,
315 skip_stages: Vec::new(),
316 publisher_allowlist: Vec::new(),
317 selected_crates: Vec::new(),
318 token: None,
319 parallelism: 4,
320 single_target: None,
321 release_notes_path: None,
322 fail_fast: false,
323 partial_target: None,
324 merge: false,
325 publish_only: false,
326 project_root: None,
327 strict: false,
328 resume_release: false,
329 replace_existing_artifacts: false,
330 skip_post_publish_poll: false,
331 gate_submitter: None,
332 rollback_mode: None,
333 simulate_failure_publishers: Vec::new(),
334 rollback_only: false,
335 allow_rerun: false,
336 show_skipped: false,
337 from_run: None,
338 runtime_nondeterministic_allowlist: Vec::new(),
339 summary_json_path: None,
340 allow_ai_failure: false,
341 changelog_from: None,
342 changelog_full_history: false,
343 changelog_to: None,
344 changelog_preview: false,
345 allow_snapshot_publish: false,
346 }
347 }
348}
349
350#[derive(Debug, Default)]
355pub struct StageOutputs {
356 pub github_native_changelog: bool,
360 pub changelogs: HashMap<String, String>,
362 pub changelog_header: Option<String>,
367 pub changelog_footer: Option<String>,
370 pub post_publish_results: Vec<serde_json::Value>,
378}
379
380pub struct Context {
381 pub config: Config,
382 pub artifacts: ArtifactRegistry,
383 pub options: ContextOptions,
384 pub stage_outputs: StageOutputs,
386 template_vars: TemplateVars,
387 pub git_info: Option<GitInfo>,
388 pub token_type: ScmTokenType,
390 pub skip_memento: crate::pipe_skip::SkipMemento,
396 pub publish_report: Option<PublishReport>,
404 pub publish_attempted: bool,
410 pub verify_release: Option<VerifyReleaseSummary>,
421 pub determinism: Option<crate::DeterminismState>,
428 pub pending_outcome: Option<crate::PublisherOutcome>,
438 pub pending_evidence: Option<crate::PublishEvidence>,
451 built_crate_names: Option<std::collections::HashSet<String>>,
462 env_source: Arc<dyn EnvSource>,
470 #[cfg(feature = "test-helpers")]
478 pub log_capture: Option<crate::log::LogCapture>,
479 render_strict: std::cell::Cell<bool>,
494 pub literal_message: bool,
501 pub redact_body: bool,
508 process_env_cache: std::cell::OnceCell<std::collections::HashMap<String, String>>,
517}
518
519impl Context {
520 pub fn new(config: Config, options: ContextOptions) -> Self {
521 let mut vars = TemplateVars::new();
522 vars.set("ProjectName", &config.project_name);
523 Self {
524 config,
525 artifacts: ArtifactRegistry::new(),
526 options,
527 stage_outputs: StageOutputs::default(),
528 template_vars: vars,
529 git_info: None,
530 token_type: ScmTokenType::GitHub,
531 skip_memento: crate::pipe_skip::SkipMemento::new(),
532 publish_report: None,
533 publish_attempted: false,
534 verify_release: None,
535 determinism: None,
536 pending_outcome: None,
537 pending_evidence: None,
538 built_crate_names: None,
539 env_source: Arc::new(ProcessEnvSource),
540 #[cfg(feature = "test-helpers")]
541 log_capture: None,
542 render_strict: std::cell::Cell::new(false),
543 literal_message: false,
544 redact_body: true,
545 process_env_cache: std::cell::OnceCell::new(),
546 }
547 }
548
549 pub fn redact(&self, s: &str) -> String {
554 crate::redact::with_env(s, &self.env_for_redact())
555 }
556
557 pub fn env_var(&self, name: &str) -> Option<String> {
565 self.env_source.var(name)
566 }
567
568 pub fn set_env_source<S: EnvSource + 'static>(&mut self, src: S) {
574 self.env_source = Arc::new(src);
575 }
576
577 pub fn env_source(&self) -> &dyn EnvSource {
582 self.env_source.as_ref()
583 }
584
585 pub fn env_source_arc(&self) -> Arc<dyn EnvSource> {
591 Arc::clone(&self.env_source)
592 }
593
594 #[cfg(feature = "test-helpers")]
600 pub fn with_log_capture(&mut self, capture: crate::log::LogCapture) {
601 self.log_capture = Some(capture);
602 }
603
604 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
612 self.pending_outcome = Some(outcome);
613 }
614
615 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
619 self.pending_outcome.take()
620 }
621
622 pub fn record_pending_evidence(&mut self, evidence: crate::PublishEvidence) {
627 self.pending_evidence = Some(evidence);
628 }
629
630 pub fn take_pending_evidence(&mut self) -> Option<crate::PublishEvidence> {
634 self.pending_evidence.take()
635 }
636
637 pub fn publish_report(&self) -> Option<&PublishReport> {
640 self.publish_report.as_ref()
641 }
642
643 pub fn publish_attempted(&self) -> bool {
646 self.publish_attempted
647 }
648
649 pub fn set_publish_attempted(&mut self) {
653 self.publish_attempted = true;
654 }
655
656 pub fn set_publish_report(&mut self, r: PublishReport) {
662 self.publish_report = Some(r);
663 }
664
665 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
668 self.built_crate_names.as_ref()
669 }
670
671 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
674 self.built_crate_names = Some(names);
675 }
676
677 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
684 self.skip_memento.remember(stage, label, reason);
685 }
686
687 pub fn template_vars(&self) -> &TemplateVars {
688 &self.template_vars
689 }
690
691 pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
692 &mut self.template_vars
693 }
694
695 pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
696 crate::template::render(template, &self.template_vars)
697 }
698
699 pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
701 template.map(|t| self.render_template(t)).transpose()
702 }
703
704 pub fn skip_with_log(
713 &self,
714 skip: &Option<crate::config::StringOrBool>,
715 log: &StageLogger,
716 label: &str,
717 ) -> anyhow::Result<bool> {
718 let Some(d) = skip else {
719 return Ok(false);
720 };
721 let should_skip = d
722 .try_evaluates_to_true(|s| self.render_template(s))
723 .with_context(|| format!("evaluate skip expression for {label}"))?;
724 if should_skip {
725 log.status(&format!("{} skipped", label));
726 }
727 Ok(should_skip)
728 }
729
730 pub fn should_skip(&self, stage_name: &str) -> bool {
733 self.options.skip_stages.iter().any(|s| s == stage_name)
734 }
735
736 pub fn publisher_deselected(&self, name: &str) -> bool {
750 self.should_skip(name)
751 || (!self.options.publisher_allowlist.is_empty()
752 && !self.options.publisher_allowlist.iter().any(|s| s == name))
753 }
754
755 pub fn deselected_reason(&self, name: &str) -> String {
766 let reason = if self.should_skip(name) {
767 "excluded via --skip"
768 } else {
769 "not in --publishers allowlist"
770 };
771 format!("skipped {name} — {reason}")
772 }
773
774 pub fn skip_validate(&self) -> bool {
776 self.should_skip("validate")
777 }
778
779 pub fn is_dry_run(&self) -> bool {
780 self.options.dry_run
781 }
782
783 pub fn is_snapshot(&self) -> bool {
784 self.options.snapshot
785 }
786
787 pub fn is_publish_only(&self) -> bool {
795 self.options.publish_only
796 }
797
798 pub fn is_strict(&self) -> bool {
799 self.options.strict
800 }
801
802 pub fn set_render_strict(&self, on: bool) -> bool {
810 self.render_strict.replace(on)
811 }
812
813 pub fn render_is_strict(&self) -> bool {
820 self.render_strict.get() || self.is_strict()
821 }
822
823 pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
826 if self.options.strict {
827 anyhow::bail!("{} (strict mode)", msg);
828 }
829 log.warn(msg);
830 Ok(())
831 }
832
833 pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
842 if self.is_snapshot() {
843 log.status(&format!("skipped {stage} — snapshot mode"));
847 true
848 } else {
849 false
850 }
851 }
852
853 pub fn render_template_strict(
855 &self,
856 template: &str,
857 label: &str,
858 log: &crate::log::StageLogger,
859 ) -> anyhow::Result<String> {
860 match self.render_template(template) {
861 Ok(rendered) => Ok(rendered),
862 Err(e) => {
863 if self.options.strict {
864 anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
865 }
866 log.warn(&format!("failed to render template for {}: {}", label, e));
867 Ok(template.to_string())
868 }
869 }
870 }
871
872 pub fn is_nightly(&self) -> bool {
873 self.options.nightly
874 }
875
876 pub fn set_release_url(&mut self, url: &str) {
881 self.template_vars.set("ReleaseURL", url);
882 }
883
884 pub fn version(&self) -> String {
887 self.template_vars
888 .get("Version")
889 .cloned()
890 .unwrap_or_default()
891 }
892
893 pub fn verbosity(&self) -> Verbosity {
895 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
896 }
897
898 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
904 self.config.retry.unwrap_or_default().to_policy()
905 }
906
907 pub fn logger(&self, stage: &'static str) -> StageLogger {
915 #[allow(unused_mut)]
916 let mut log = StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact());
917 #[cfg(feature = "test-helpers")]
918 if let Some(cap) = &self.log_capture {
919 log = log.with_capture_handle(cap.clone());
920 }
921 log
922 }
923
924 fn env_for_redact(&self) -> Vec<(String, String)> {
935 use std::collections::HashMap;
936 let mut map: HashMap<String, String> = self
937 .process_env_cache
938 .get_or_init(|| std::env::vars().collect())
939 .clone();
940 for (k, v) in self.template_vars.all_env() {
941 map.insert(k.clone(), v.clone());
942 }
943 map.into_iter().collect()
944 }
945
946 pub fn populate_git_vars(&mut self) {
988 if let Some(ref info) = self.git_info {
989 let raw_version = info.semver.raw_version_string();
991
992 let version = info.semver.version_string();
998
999 self.template_vars.set("Tag", &info.tag);
1000 self.template_vars.set("Version", &version);
1001 self.template_vars.set("RawVersion", &raw_version);
1002 self.template_vars.set("Base", &raw_version);
1007 self.template_vars
1008 .set("Major", &info.semver.major.to_string());
1009 self.template_vars
1010 .set("Minor", &info.semver.minor.to_string());
1011 self.template_vars
1012 .set("Patch", &info.semver.patch.to_string());
1013 self.template_vars.set(
1014 "Prerelease",
1015 info.semver.prerelease.as_deref().unwrap_or(""),
1016 );
1017 self.template_vars.set(
1018 "BuildMetadata",
1019 info.semver.build_metadata.as_deref().unwrap_or(""),
1020 );
1021 self.template_vars.set("FullCommit", &info.commit);
1022 self.template_vars.set("Commit", &info.commit);
1023 self.template_vars.set("ShortCommit", &info.short_commit);
1024 self.template_vars.set("Branch", &info.branch);
1025 self.template_vars.set("CommitDate", &info.commit_date);
1026 self.template_vars
1027 .set("CommitTimestamp", &info.commit_timestamp);
1028 self.template_vars.set_bool("IsGitDirty", info.dirty);
1029 self.template_vars.set_bool("IsGitClean", !info.dirty);
1030 self.template_vars
1031 .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
1032 self.template_vars.set("GitURL", &info.remote_url);
1033 self.template_vars.set("Summary", &info.summary);
1034 self.template_vars.set("TagSubject", &info.tag_subject);
1035 self.template_vars.set("TagContents", &info.tag_contents);
1036 self.template_vars.set("TagBody", &info.tag_body);
1037 self.template_vars
1038 .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
1039 self.template_vars
1040 .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
1041
1042 let monorepo_prefix = self.config.monorepo_tag_prefix();
1053
1054 if let Some(prefix) = monorepo_prefix {
1060 self.template_vars.set("PrefixedTag", &info.tag);
1063
1064 let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
1066 self.template_vars.set("Tag", stripped_tag);
1067
1068 let version = info.semver.version_string();
1078 self.template_vars.set("Version", &version);
1079
1080 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
1082 self.template_vars.set("PrefixedPreviousTag", prev_tag);
1083
1084 let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
1086 self.template_vars.set("PreviousTag", stripped_prev);
1087
1088 self.template_vars.set("PrefixedSummary", &info.summary);
1092 let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
1094 self.template_vars.set("Summary", stripped_summary);
1095 } else {
1096 let tag_prefix = self
1098 .config
1099 .tag
1100 .as_ref()
1101 .and_then(|t| t.tag_prefix.as_deref())
1102 .unwrap_or("");
1103 self.template_vars
1104 .set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
1105 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
1106 let prefixed_prev = if prev_tag.is_empty() {
1107 String::new()
1108 } else {
1109 format!("{}{}", tag_prefix, prev_tag)
1110 };
1111 self.template_vars
1112 .set("PrefixedPreviousTag", &prefixed_prev);
1113 self.template_vars.set(
1114 "PrefixedSummary",
1115 &format!("{}{}", tag_prefix, info.summary),
1116 );
1117 }
1118 }
1119
1120 let nightly_build = if self.git_info.is_some() {
1133 let root = self
1134 .options
1135 .project_root
1136 .clone()
1137 .unwrap_or_else(|| PathBuf::from("."));
1138 let monorepo_prefix = self.config.monorepo_tag_prefix();
1139 crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
1140 } else {
1141 0
1142 };
1143 self.template_vars
1144 .set_structured("NightlyBuild", tera::Value::from(nightly_build));
1145
1146 self.template_vars
1151 .set_bool("IsSnapshot", self.options.snapshot);
1152 self.template_vars
1153 .set_bool("IsNightly", self.options.nightly);
1154 self.template_vars.set_bool(
1158 "IsHarness",
1159 self.env_var("ANODIZER_IN_DETERMINISM_HARNESS").is_some(),
1160 );
1161 let is_draft = self
1163 .config
1164 .release
1165 .as_ref()
1166 .and_then(|r| r.draft)
1167 .unwrap_or(false);
1168 self.template_vars.set_bool("IsDraft", is_draft);
1169 self.template_vars
1170 .set_bool("IsSingleTarget", self.options.single_target.is_some());
1171
1172 let is_release = !self.options.snapshot && !self.options.nightly;
1174 self.template_vars.set_bool("IsRelease", is_release);
1175
1176 self.template_vars.set_bool("IsMerging", self.options.merge);
1178 }
1179
1180 pub fn populate_time_vars(&mut self) {
1208 let now = crate::sde::resolve_now_with_env(self.env_source());
1216 self.template_vars.set("Date", &now.to_rfc3339());
1217 self.template_vars
1218 .set("Timestamp", &now.timestamp().to_string());
1219 self.template_vars.set("Now", &now.to_rfc3339());
1220 self.template_vars
1221 .set("Year", &now.format("%Y").to_string());
1222 self.template_vars
1223 .set("Month", &now.format("%m").to_string());
1224 self.template_vars.set("Day", &now.format("%d").to_string());
1225 self.template_vars
1226 .set("Hour", &now.format("%H").to_string());
1227 self.template_vars
1228 .set("Minute", &now.format("%M").to_string());
1229 }
1230
1231 pub fn populate_runtime_vars(&mut self) {
1240 let goos = map_os_to_goos(std::env::consts::OS);
1241 let goarch = map_arch_to_goarch(std::env::consts::ARCH);
1242 self.template_vars.set("RuntimeGoos", goos);
1243 self.template_vars.set("RuntimeGoarch", goarch);
1244 self.template_vars.set("Runtime_Goos", goos);
1247 self.template_vars.set("Runtime_Goarch", goarch);
1248 self.populate_rustc_vars();
1252 }
1253
1254 fn populate_rustc_vars(&mut self) {
1261 let ver = crate::partial::detect_rustc_version().unwrap_or_default();
1262 self.template_vars.set("RustcVersion", &ver);
1263 }
1264
1265 pub fn populate_release_notes_var(&mut self) {
1273 let notes = self
1275 .config
1276 .crates
1277 .iter()
1278 .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
1279 .cloned()
1280 .unwrap_or_default();
1281 self.template_vars.set("ReleaseNotes", ¬es);
1282 }
1283
1284 pub fn refresh_artifacts_var(&mut self) {
1299 const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
1304 const JSON_LIST_KEYS: &[&str] = &["Platforms"];
1310
1311 let artifacts_value: Vec<serde_json::Value> = self
1312 .artifacts
1313 .all()
1314 .iter()
1315 .map(|a| {
1316 let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
1318 for (k, v) in &a.metadata {
1319 if CSV_LIST_KEYS.contains(&k.as_str()) {
1320 let items: Vec<serde_json::Value> = if v.is_empty() {
1321 Vec::new()
1322 } else {
1323 v.split(',')
1324 .map(|s| serde_json::Value::String(s.to_string()))
1325 .collect()
1326 };
1327 metadata_map.insert(k.clone(), serde_json::Value::Array(items));
1328 } else if JSON_LIST_KEYS.contains(&k.as_str()) {
1329 let parsed = serde_json::from_str::<serde_json::Value>(v)
1333 .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
1334 metadata_map.insert(k.clone(), parsed);
1335 } else {
1336 metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
1337 }
1338 }
1339 serde_json::json!({
1340 "name": a.name,
1341 "path": a.path.to_string_lossy(),
1342 "target": a.target.as_deref().unwrap_or(""),
1343 "kind": a.kind.as_str(),
1344 "crate_name": a.crate_name,
1345 "metadata": serde_json::Value::Object(metadata_map),
1346 })
1347 })
1348 .collect();
1349 let tera_value = tera::Value::Array(artifacts_value);
1352 self.template_vars.set_structured("Artifacts", tera_value);
1353 }
1354
1355 pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
1369 let (
1372 description,
1373 homepage,
1374 documentation,
1375 license,
1376 maintainers,
1377 mod_timestamp,
1378 full_desc_src,
1379 commit_author,
1380 ) = {
1381 let meta = self.config.metadata.as_ref();
1382 let description = self
1389 .config
1390 .meta_description_project()
1391 .unwrap_or("")
1392 .to_string();
1393 let homepage = self
1394 .config
1395 .meta_homepage_project()
1396 .unwrap_or("")
1397 .to_string();
1398 let documentation = self
1399 .config
1400 .meta_documentation_project()
1401 .unwrap_or("")
1402 .to_string();
1403 let license = self.config.meta_license_project().unwrap_or("").to_string();
1404 let maintainers: Vec<String> = meta
1405 .and_then(|m| m.maintainers.as_ref())
1406 .cloned()
1407 .unwrap_or_default();
1408 let mod_timestamp = meta
1409 .and_then(|m| m.mod_timestamp.as_deref())
1410 .unwrap_or("")
1411 .to_string();
1412 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1413 let commit_author = meta.and_then(|m| m.commit_author.clone());
1414 (
1415 description,
1416 homepage,
1417 documentation,
1418 license,
1419 maintainers,
1420 mod_timestamp,
1421 full_desc_src,
1422 commit_author,
1423 )
1424 };
1425
1426 let full_description = match full_desc_src {
1432 None => String::new(),
1433 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1434 };
1435
1436 let commit_author_map = serde_json::json!({
1437 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1438 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1439 });
1440
1441 let meta_map = serde_json::json!({
1442 "Description": description,
1443 "Homepage": homepage,
1444 "Documentation": documentation,
1445 "License": license,
1446 "Maintainers": maintainers,
1447 "ModTimestamp": mod_timestamp,
1448 "FullDescription": full_description,
1449 "CommitAuthor": commit_author_map,
1450 });
1451 self.template_vars.set_structured("Metadata", meta_map);
1453 Ok(())
1454 }
1455}
1456
1457pub fn map_os_to_goos(os: &str) -> &str {
1460 match os {
1461 "macos" => "darwin",
1462 other => other, }
1464}
1465
1466pub fn map_arch_to_goarch(arch: &str) -> &str {
1469 match arch {
1470 "x86_64" => "amd64",
1471 "x86" => "386",
1472 "aarch64" => "arm64",
1473 "powerpc64" => "ppc64",
1474 "s390x" => "s390x",
1475 "mips" => "mips",
1476 "mips64" => "mips64",
1477 "riscv64" => "riscv64",
1478 other => other,
1479 }
1480}
1481
1482#[cfg(test)]
1483#[allow(clippy::field_reassign_with_default)]
1484mod tests {
1485 use super::*;
1486 use crate::config::Config;
1487 use crate::git::{GitInfo, SemVer};
1488
1489 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1490 let tag = match prerelease {
1491 Some(pre) => format!("v1.2.3-{pre}"),
1492 None => "v1.2.3".to_string(),
1493 };
1494 GitInfo {
1495 tag,
1496 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1497 short_commit: "abc123d".to_string(),
1498 branch: "main".to_string(),
1499 dirty,
1500 semver: SemVer {
1501 major: 1,
1502 minor: 2,
1503 patch: 3,
1504 prerelease: prerelease.map(|s| s.to_string()),
1505 build_metadata: None,
1506 },
1507 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1508 commit_timestamp: "1774463400".to_string(),
1509 previous_tag: Some("v1.2.2".to_string()),
1510 remote_url: "https://github.com/test/repo.git".to_string(),
1511 summary: "v1.2.3-0-gabc123d".to_string(),
1512 tag_subject: "Release v1.2.3".to_string(),
1513 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1514 tag_body: "Full release notes here.".to_string(),
1515 first_commit: None,
1516 }
1517 }
1518
1519 #[test]
1520 fn test_context_template_vars() {
1521 let mut config = Config::default();
1522 config.project_name = "test-project".to_string();
1523 let ctx = Context::new(config, ContextOptions::default());
1524 assert_eq!(
1525 ctx.template_vars().get("ProjectName"),
1526 Some(&"test-project".to_string())
1527 );
1528 }
1529
1530 #[test]
1531 fn test_context_should_skip() {
1532 let config = Config::default();
1533 let opts = ContextOptions {
1534 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1535 ..Default::default()
1536 };
1537 let ctx = Context::new(config, opts);
1538 assert!(ctx.should_skip("publish"));
1539 assert!(ctx.should_skip("announce"));
1540 assert!(!ctx.should_skip("build"));
1541 }
1542
1543 #[test]
1544 fn publisher_deselected_empty_selectors_runs_everything() {
1545 let ctx = Context::new(Config::default(), ContextOptions::default());
1546 assert!(!ctx.publisher_deselected("npm"));
1547 assert!(!ctx.publisher_deselected("cargo"));
1548 assert!(!ctx.publisher_deselected("anything"));
1549 }
1550
1551 #[test]
1552 fn publisher_deselected_skip_denylists() {
1553 let opts = ContextOptions {
1554 skip_stages: vec!["npm".to_string()],
1555 ..Default::default()
1556 };
1557 let ctx = Context::new(Config::default(), opts);
1558 assert!(ctx.publisher_deselected("npm"));
1559 assert!(!ctx.publisher_deselected("cargo"));
1560 }
1561
1562 #[test]
1563 fn publisher_deselected_allowlist_excludes_unlisted() {
1564 let opts = ContextOptions {
1565 publisher_allowlist: vec!["cargo".to_string()],
1566 ..Default::default()
1567 };
1568 let ctx = Context::new(Config::default(), opts);
1569 assert!(!ctx.publisher_deselected("cargo"));
1570 assert!(ctx.publisher_deselected("npm"));
1571 }
1572
1573 #[test]
1574 fn publisher_deselected_skip_wins_over_allowlist() {
1575 let opts = ContextOptions {
1576 skip_stages: vec!["cargo".to_string()],
1577 publisher_allowlist: vec!["cargo".to_string()],
1578 ..Default::default()
1579 };
1580 let ctx = Context::new(Config::default(), opts);
1581 assert!(ctx.publisher_deselected("cargo"));
1582 }
1583
1584 #[test]
1585 fn test_context_render_template() {
1586 let mut config = Config::default();
1587 config.project_name = "myapp".to_string();
1588 let ctx = Context::new(config, ContextOptions::default());
1589 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1590 assert_eq!(result, "myapp-release");
1591 }
1592
1593 #[test]
1594 fn test_populate_git_vars_sets_all_expected_vars() {
1595 let config = Config::default();
1596 let mut ctx = Context::new(config, ContextOptions::default());
1597 ctx.git_info = Some(make_git_info(false, None));
1598 ctx.populate_git_vars();
1599
1600 let v = ctx.template_vars();
1601 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1602 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1603 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1604 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1605 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1606 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1607 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1608 assert_eq!(
1609 v.get("FullCommit"),
1610 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1611 );
1612 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1613 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1614 assert_eq!(
1615 v.get("CommitDate"),
1616 Some(&"2026-03-25T10:30:00+00:00".to_string())
1617 );
1618 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1619 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1620 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1623 }
1624
1625 #[test]
1626 fn test_nightly_build_defaults_to_zero_without_git_info() {
1627 let config = Config::default();
1630 let mut ctx = Context::new(config, ContextOptions::default());
1631 ctx.git_info = None;
1632 ctx.populate_git_vars();
1633 assert_eq!(
1634 ctx.template_vars().get_structured("NightlyBuild"),
1635 Some(&tera::Value::from(0u64))
1636 );
1637 }
1638
1639 #[test]
1640 fn test_commit_is_alias_for_full_commit() {
1641 let config = Config::default();
1642 let mut ctx = Context::new(config, ContextOptions::default());
1643 ctx.git_info = Some(make_git_info(false, None));
1644 ctx.populate_git_vars();
1645
1646 let v = ctx.template_vars();
1647 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1648 }
1649
1650 #[test]
1651 fn test_populate_git_vars_prerelease() {
1652 let config = Config::default();
1653 let mut ctx = Context::new(config, ContextOptions::default());
1654 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1655 ctx.populate_git_vars();
1656
1657 let v = ctx.template_vars();
1658 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1659 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1660 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1661 }
1662
1663 #[test]
1664 fn test_build_metadata_template_var() {
1665 let config = Config::default();
1666 let mut ctx = Context::new(config, ContextOptions::default());
1667 let mut info = make_git_info(false, None);
1668 info.tag = "v1.2.3+build.42".to_string();
1669 info.semver.build_metadata = Some("build.42".to_string());
1670 ctx.git_info = Some(info);
1671 ctx.populate_git_vars();
1672
1673 let v = ctx.template_vars();
1674 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1675 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1677 }
1678
1679 #[test]
1680 fn test_build_metadata_empty_when_none() {
1681 let config = Config::default();
1682 let mut ctx = Context::new(config, ContextOptions::default());
1683 ctx.git_info = Some(make_git_info(false, None));
1684 ctx.populate_git_vars();
1685
1686 assert_eq!(
1687 ctx.template_vars().get("BuildMetadata"),
1688 Some(&"".to_string())
1689 );
1690 }
1691
1692 #[test]
1693 fn test_populate_git_vars_monorepo_prefixed_tag() {
1694 let config = Config::default();
1697 let mut ctx = Context::new(config, ContextOptions::default());
1698 let mut info = make_git_info(false, None);
1699 info.tag = "core-v0.3.2".to_string();
1700 info.semver = SemVer {
1701 major: 0,
1702 minor: 3,
1703 patch: 2,
1704 prerelease: None,
1705 build_metadata: None,
1706 };
1707 ctx.git_info = Some(info);
1708 ctx.populate_git_vars();
1709
1710 let v = ctx.template_vars();
1711 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1712 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1713 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1714 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1715 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1716 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1717 }
1718
1719 #[test]
1720 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1721 let config = Config::default();
1722 let mut ctx = Context::new(config, ContextOptions::default());
1723 let mut info = make_git_info(false, None);
1724 info.tag = "operator-v1.0.0-rc.1".to_string();
1725 info.semver = SemVer {
1726 major: 1,
1727 minor: 0,
1728 patch: 0,
1729 prerelease: Some("rc.1".to_string()),
1730 build_metadata: None,
1731 };
1732 ctx.git_info = Some(info);
1733 ctx.populate_git_vars();
1734
1735 let v = ctx.template_vars();
1736 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1737 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1738 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1739 }
1740
1741 #[test]
1742 fn test_git_tree_state_clean() {
1743 let config = Config::default();
1744 let mut ctx = Context::new(config, ContextOptions::default());
1745 ctx.git_info = Some(make_git_info(false, None));
1746 ctx.populate_git_vars();
1747
1748 let v = ctx.template_vars();
1749 assert_eq!(
1750 v.get_structured("IsGitDirty"),
1751 Some(&tera::Value::Bool(false))
1752 );
1753 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1754 }
1755
1756 #[test]
1757 fn test_git_tree_state_dirty() {
1758 let config = Config::default();
1759 let mut ctx = Context::new(config, ContextOptions::default());
1760 ctx.git_info = Some(make_git_info(true, None));
1761 ctx.populate_git_vars();
1762
1763 let v = ctx.template_vars();
1764 assert_eq!(
1765 v.get_structured("IsGitDirty"),
1766 Some(&tera::Value::Bool(true))
1767 );
1768 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1769 }
1770
1771 #[test]
1772 fn test_is_snapshot_reflects_context_options() {
1773 let config = Config::default();
1774 let opts = ContextOptions {
1775 snapshot: true,
1776 ..Default::default()
1777 };
1778 let mut ctx = Context::new(config, opts);
1779 ctx.git_info = Some(make_git_info(false, None));
1780 ctx.populate_git_vars();
1781
1782 assert_eq!(
1783 ctx.template_vars().get_structured("IsSnapshot"),
1784 Some(&tera::Value::Bool(true))
1785 );
1786
1787 let config2 = Config::default();
1789 let opts2 = ContextOptions {
1790 snapshot: false,
1791 ..Default::default()
1792 };
1793 let mut ctx2 = Context::new(config2, opts2);
1794 ctx2.git_info = Some(make_git_info(false, None));
1795 ctx2.populate_git_vars();
1796
1797 assert_eq!(
1798 ctx2.template_vars().get_structured("IsSnapshot"),
1799 Some(&tera::Value::Bool(false))
1800 );
1801 }
1802
1803 #[test]
1804 fn test_is_draft_defaults_to_false() {
1805 let config = Config::default();
1806 let mut ctx = Context::new(config, ContextOptions::default());
1807 ctx.git_info = Some(make_git_info(false, None));
1808 ctx.populate_git_vars();
1809
1810 assert_eq!(
1811 ctx.template_vars().get_structured("IsDraft"),
1812 Some(&tera::Value::Bool(false))
1813 );
1814 }
1815
1816 #[test]
1817 fn test_previous_tag_empty_when_none() {
1818 let config = Config::default();
1819 let mut ctx = Context::new(config, ContextOptions::default());
1820 let mut info = make_git_info(false, None);
1821 info.previous_tag = None;
1822 ctx.git_info = Some(info);
1823 ctx.populate_git_vars();
1824
1825 assert_eq!(
1826 ctx.template_vars().get("PreviousTag"),
1827 Some(&"".to_string())
1828 );
1829 }
1830
1831 #[test]
1840 fn populate_time_vars_uses_source_date_epoch_when_set() {
1841 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1845 let config = Config::default();
1846 let mut ctx = Context::new(config, ContextOptions::default());
1847 ctx.set_env_source(env);
1848 ctx.populate_time_vars();
1849
1850 let v = ctx.template_vars();
1851 assert_eq!(
1852 v.get("Timestamp"),
1853 Some(&"1715000000".to_string()),
1854 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1855 );
1856 assert_eq!(
1857 v.get("Date"),
1858 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1859 "Date must be RFC 3339 derived from SDE"
1860 );
1861 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1862 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1863 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1864 }
1865
1866 #[test]
1867 fn test_populate_time_vars() {
1868 let env = crate::MapEnvSource::new();
1871 let config = Config::default();
1872 let mut ctx = Context::new(config, ContextOptions::default());
1873 ctx.set_env_source(env);
1874 ctx.populate_time_vars();
1875
1876 let v = ctx.template_vars();
1877
1878 let date = v
1880 .get("Date")
1881 .unwrap_or_else(|| panic!("Date should be set"));
1882 assert!(
1883 date.contains('T') && date.len() > 10,
1884 "Date should be RFC 3339, got: {date}"
1885 );
1886
1887 let ts = v
1889 .get("Timestamp")
1890 .unwrap_or_else(|| panic!("Timestamp should be set"));
1891 assert!(
1892 ts.parse::<i64>().is_ok(),
1893 "Timestamp should be a numeric string, got: {ts}"
1894 );
1895
1896 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1898 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1899 }
1900
1901 #[test]
1902 fn test_env_vars_accessible_in_templates() {
1903 let mut config = Config::default();
1904 config.project_name = "myapp".to_string();
1905 let mut ctx = Context::new(config, ContextOptions::default());
1906 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1907 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1908
1909 let result = ctx
1910 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1911 .unwrap();
1912 assert_eq!(result, "hello-world-staging");
1913 }
1914
1915 #[test]
1916 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1917 let config = Config::default();
1918 let opts = ContextOptions {
1919 snapshot: true,
1920 ..Default::default()
1921 };
1922 let mut ctx = Context::new(config, opts);
1923 ctx.populate_git_vars();
1925
1926 assert_eq!(
1927 ctx.template_vars().get_structured("IsSnapshot"),
1928 Some(&tera::Value::Bool(true))
1929 );
1930 assert_eq!(
1931 ctx.template_vars().get_structured("IsDraft"),
1932 Some(&tera::Value::Bool(false))
1933 );
1934 assert_eq!(ctx.template_vars().get("Tag"), None);
1936 }
1937
1938 #[test]
1939 fn test_is_nightly_set_when_nightly_mode_active() {
1940 let config = Config::default();
1941 let opts = ContextOptions {
1942 nightly: true,
1943 ..Default::default()
1944 };
1945 let mut ctx = Context::new(config, opts);
1946 ctx.git_info = Some(make_git_info(false, None));
1947 ctx.populate_git_vars();
1948
1949 assert_eq!(
1950 ctx.template_vars().get_structured("IsNightly"),
1951 Some(&tera::Value::Bool(true)),
1952 "IsNightly should be 'true' when nightly mode is active"
1953 );
1954 assert!(ctx.is_nightly(), "is_nightly() should return true");
1955 }
1956
1957 #[test]
1958 fn test_is_nightly_false_by_default() {
1959 let config = Config::default();
1960 let mut ctx = Context::new(config, ContextOptions::default());
1961 ctx.git_info = Some(make_git_info(false, None));
1962 ctx.populate_git_vars();
1963
1964 assert_eq!(
1965 ctx.template_vars().get_structured("IsNightly"),
1966 Some(&tera::Value::Bool(false)),
1967 "IsNightly should default to 'false'"
1968 );
1969 assert!(
1970 !ctx.is_nightly(),
1971 "is_nightly() should return false by default"
1972 );
1973 }
1974
1975 #[test]
1976 fn test_version_returns_populated_value() {
1977 let config = Config::default();
1978 let mut ctx = Context::new(config, ContextOptions::default());
1979 ctx.git_info = Some(make_git_info(false, None));
1980 ctx.populate_git_vars();
1981
1982 assert_eq!(ctx.version(), "1.2.3");
1983 }
1984
1985 #[test]
1986 fn test_version_returns_empty_when_not_set() {
1987 let config = Config::default();
1988 let ctx = Context::new(config, ContextOptions::default());
1989 assert_eq!(ctx.version(), "");
1990 }
1991
1992 #[test]
1993 fn test_is_nightly_without_git_info() {
1994 let config = Config::default();
1995 let opts = ContextOptions {
1996 nightly: true,
1997 ..Default::default()
1998 };
1999 let mut ctx = Context::new(config, opts);
2000 ctx.populate_git_vars();
2002
2003 assert_eq!(
2004 ctx.template_vars().get_structured("IsNightly"),
2005 Some(&tera::Value::Bool(true)),
2006 "IsNightly should be set even without git info"
2007 );
2008 }
2009
2010 #[test]
2011 fn test_is_git_clean_when_not_dirty() {
2012 let config = Config::default();
2013 let mut ctx = Context::new(config, ContextOptions::default());
2014 ctx.git_info = Some(make_git_info(false, None));
2015 ctx.populate_git_vars();
2016
2017 assert_eq!(
2018 ctx.template_vars().get_structured("IsGitClean"),
2019 Some(&tera::Value::Bool(true))
2020 );
2021 }
2022
2023 #[test]
2024 fn test_is_git_clean_when_dirty() {
2025 let config = Config::default();
2026 let mut ctx = Context::new(config, ContextOptions::default());
2027 ctx.git_info = Some(make_git_info(true, None));
2028 ctx.populate_git_vars();
2029
2030 assert_eq!(
2031 ctx.template_vars().get_structured("IsGitClean"),
2032 Some(&tera::Value::Bool(false))
2033 );
2034 }
2035
2036 #[test]
2037 fn test_git_url_set_from_git_info() {
2038 let config = Config::default();
2039 let mut ctx = Context::new(config, ContextOptions::default());
2040 ctx.git_info = Some(make_git_info(false, None));
2041 ctx.populate_git_vars();
2042
2043 assert_eq!(
2044 ctx.template_vars().get("GitURL"),
2045 Some(&"https://github.com/test/repo.git".to_string())
2046 );
2047 }
2048
2049 #[test]
2050 fn test_summary_set_from_git_info() {
2051 let config = Config::default();
2052 let mut ctx = Context::new(config, ContextOptions::default());
2053 ctx.git_info = Some(make_git_info(false, None));
2054 ctx.populate_git_vars();
2055
2056 assert_eq!(
2057 ctx.template_vars().get("Summary"),
2058 Some(&"v1.2.3-0-gabc123d".to_string())
2059 );
2060 }
2061
2062 #[test]
2063 fn test_tag_subject_set_from_git_info() {
2064 let config = Config::default();
2065 let mut ctx = Context::new(config, ContextOptions::default());
2066 ctx.git_info = Some(make_git_info(false, None));
2067 ctx.populate_git_vars();
2068
2069 assert_eq!(
2070 ctx.template_vars().get("TagSubject"),
2071 Some(&"Release v1.2.3".to_string())
2072 );
2073 }
2074
2075 #[test]
2076 fn test_tag_contents_set_from_git_info() {
2077 let config = Config::default();
2078 let mut ctx = Context::new(config, ContextOptions::default());
2079 ctx.git_info = Some(make_git_info(false, None));
2080 ctx.populate_git_vars();
2081
2082 assert_eq!(
2083 ctx.template_vars().get("TagContents"),
2084 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
2085 );
2086 }
2087
2088 #[test]
2089 fn test_tag_body_set_from_git_info() {
2090 let config = Config::default();
2091 let mut ctx = Context::new(config, ContextOptions::default());
2092 ctx.git_info = Some(make_git_info(false, None));
2093 ctx.populate_git_vars();
2094
2095 assert_eq!(
2096 ctx.template_vars().get("TagBody"),
2097 Some(&"Full release notes here.".to_string())
2098 );
2099 }
2100
2101 #[test]
2102 fn test_is_single_target_false_by_default() {
2103 let config = Config::default();
2104 let mut ctx = Context::new(config, ContextOptions::default());
2105 ctx.git_info = Some(make_git_info(false, None));
2106 ctx.populate_git_vars();
2107
2108 assert_eq!(
2109 ctx.template_vars().get_structured("IsSingleTarget"),
2110 Some(&tera::Value::Bool(false))
2111 );
2112 }
2113
2114 #[test]
2115 fn test_is_single_target_true_when_set() {
2116 let config = Config::default();
2117 let opts = ContextOptions {
2118 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
2119 ..Default::default()
2120 };
2121 let mut ctx = Context::new(config, opts);
2122 ctx.git_info = Some(make_git_info(false, None));
2123 ctx.populate_git_vars();
2124
2125 assert_eq!(
2126 ctx.template_vars().get_structured("IsSingleTarget"),
2127 Some(&tera::Value::Bool(true))
2128 );
2129 }
2130
2131 #[test]
2132 #[serial_test::serial]
2133 fn test_populate_runtime_vars() {
2134 let config = Config::default();
2135 let mut ctx = Context::new(config, ContextOptions::default());
2136 ctx.populate_runtime_vars();
2137
2138 let v = ctx.template_vars();
2139
2140 let goos = v
2141 .get("RuntimeGoos")
2142 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
2143 assert!(
2144 !goos.is_empty(),
2145 "RuntimeGoos should not be empty, got: {goos}"
2146 );
2147 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
2149
2150 let goarch = v
2151 .get("RuntimeGoarch")
2152 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
2153 assert!(
2154 !goarch.is_empty(),
2155 "RuntimeGoarch should not be empty, got: {goarch}"
2156 );
2157 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
2159 }
2160
2161 #[test]
2162 fn test_populate_release_notes_var_with_changelogs() {
2163 let mut config = Config::default();
2164 config.crates.push(crate::config::CrateConfig {
2165 name: "my-crate".to_string(),
2166 ..Default::default()
2167 });
2168 let mut ctx = Context::new(config, ContextOptions::default());
2169 ctx.stage_outputs
2170 .changelogs
2171 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
2172 ctx.populate_release_notes_var();
2173
2174 assert_eq!(
2175 ctx.template_vars().get("ReleaseNotes"),
2176 Some(&"## Changes\n- fix bug".to_string())
2177 );
2178 }
2179
2180 #[test]
2181 fn test_populate_release_notes_var_empty_when_no_changelogs() {
2182 let config = Config::default();
2183 let mut ctx = Context::new(config, ContextOptions::default());
2184 ctx.populate_release_notes_var();
2185
2186 assert_eq!(
2187 ctx.template_vars().get("ReleaseNotes"),
2188 Some(&"".to_string())
2189 );
2190 }
2191
2192 #[test]
2193 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
2194 let mut config = Config::default();
2195 config.crates.push(crate::config::CrateConfig {
2196 name: "crate-a".to_string(),
2197 ..Default::default()
2198 });
2199 config.crates.push(crate::config::CrateConfig {
2200 name: "crate-b".to_string(),
2201 ..Default::default()
2202 });
2203 let mut ctx = Context::new(config, ContextOptions::default());
2204 ctx.stage_outputs
2205 .changelogs
2206 .insert("crate-a".to_string(), "notes-a".to_string());
2207 ctx.stage_outputs
2208 .changelogs
2209 .insert("crate-b".to_string(), "notes-b".to_string());
2210 ctx.populate_release_notes_var();
2211
2212 assert_eq!(
2214 ctx.template_vars().get("ReleaseNotes"),
2215 Some(&"notes-a".to_string())
2216 );
2217 }
2218
2219 #[test]
2220 fn test_outputs_accessible_in_templates() {
2221 let mut config = Config::default();
2222 config.project_name = "myapp".to_string();
2223 let mut ctx = Context::new(config, ContextOptions::default());
2224 ctx.template_vars_mut().set_output("build_id", "abc123");
2225 ctx.template_vars_mut()
2226 .set_output("deploy_url", "https://example.com");
2227
2228 let result = ctx
2229 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
2230 .unwrap();
2231 assert_eq!(result, "abc123-https://example.com");
2232 }
2233
2234 #[test]
2235 fn test_artifact_ext_and_target_template_vars() {
2236 let mut config = Config::default();
2237 config.project_name = "myapp".to_string();
2238 let mut ctx = Context::new(config, ContextOptions::default());
2239 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
2240 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
2241 ctx.template_vars_mut()
2242 .set("Target", "x86_64-unknown-linux-gnu");
2243
2244 let result = ctx
2245 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
2246 .unwrap();
2247 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
2248 }
2249
2250 #[test]
2251 fn test_checksums_template_var() {
2252 let mut config = Config::default();
2253 config.project_name = "myapp".to_string();
2254 let mut ctx = Context::new(config, ContextOptions::default());
2255 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
2256 ctx.template_vars_mut().set("Checksums", checksum_text);
2257
2258 let result = ctx.render_template("{{ .Checksums }}").unwrap();
2259 assert_eq!(result, checksum_text);
2260 }
2261
2262 #[test]
2265 fn test_prefixed_tag_with_tag_prefix() {
2266 let mut config = Config::default();
2267 config.tag = Some(crate::config::TagConfig {
2268 tag_prefix: Some("api/".to_string()),
2269 ..Default::default()
2270 });
2271 let mut ctx = Context::new(config, ContextOptions::default());
2272 ctx.git_info = Some(make_git_info(false, None));
2273 ctx.populate_git_vars();
2274
2275 assert_eq!(
2276 ctx.template_vars().get("PrefixedTag"),
2277 Some(&"api/v1.2.3".to_string())
2278 );
2279 }
2280
2281 #[test]
2282 fn test_prefixed_tag_without_tag_prefix() {
2283 let config = Config::default();
2284 let mut ctx = Context::new(config, ContextOptions::default());
2285 ctx.git_info = Some(make_git_info(false, None));
2286 ctx.populate_git_vars();
2287
2288 assert_eq!(
2290 ctx.template_vars().get("PrefixedTag"),
2291 Some(&"v1.2.3".to_string())
2292 );
2293 }
2294
2295 #[test]
2296 fn test_prefixed_previous_tag_with_tag_prefix() {
2297 let mut config = Config::default();
2298 config.tag = Some(crate::config::TagConfig {
2299 tag_prefix: Some("api/".to_string()),
2300 ..Default::default()
2301 });
2302 let mut ctx = Context::new(config, ContextOptions::default());
2303 ctx.git_info = Some(make_git_info(false, None));
2304 ctx.populate_git_vars();
2305
2306 assert_eq!(
2307 ctx.template_vars().get("PrefixedPreviousTag"),
2308 Some(&"api/v1.2.2".to_string())
2309 );
2310 }
2311
2312 #[test]
2313 fn test_prefixed_previous_tag_empty_when_no_previous() {
2314 let mut config = Config::default();
2315 config.tag = Some(crate::config::TagConfig {
2316 tag_prefix: Some("api/".to_string()),
2317 ..Default::default()
2318 });
2319 let mut ctx = Context::new(config, ContextOptions::default());
2320 let mut info = make_git_info(false, None);
2321 info.previous_tag = None;
2322 ctx.git_info = Some(info);
2323 ctx.populate_git_vars();
2324
2325 assert_eq!(
2328 ctx.template_vars().get("PrefixedPreviousTag"),
2329 Some(&"".to_string())
2330 );
2331 }
2332
2333 #[test]
2334 fn test_prefixed_summary_with_tag_prefix() {
2335 let mut config = Config::default();
2336 config.tag = Some(crate::config::TagConfig {
2337 tag_prefix: Some("api/".to_string()),
2338 ..Default::default()
2339 });
2340 let mut ctx = Context::new(config, ContextOptions::default());
2341 ctx.git_info = Some(make_git_info(false, None));
2342 ctx.populate_git_vars();
2343
2344 assert_eq!(
2345 ctx.template_vars().get("PrefixedSummary"),
2346 Some(&"api/v1.2.3-0-gabc123d".to_string())
2347 );
2348 }
2349
2350 #[test]
2351 fn test_is_release_true_for_normal_release() {
2352 let config = Config::default();
2353 let opts = ContextOptions {
2354 snapshot: false,
2355 nightly: false,
2356 ..Default::default()
2357 };
2358 let mut ctx = Context::new(config, opts);
2359 ctx.git_info = Some(make_git_info(false, None));
2360 ctx.populate_git_vars();
2361
2362 assert_eq!(
2363 ctx.template_vars().get_structured("IsRelease"),
2364 Some(&tera::Value::Bool(true))
2365 );
2366 }
2367
2368 #[test]
2369 fn test_is_release_false_for_snapshot() {
2370 let config = Config::default();
2371 let opts = ContextOptions {
2372 snapshot: true,
2373 ..Default::default()
2374 };
2375 let mut ctx = Context::new(config, opts);
2376 ctx.git_info = Some(make_git_info(false, None));
2377 ctx.populate_git_vars();
2378
2379 assert_eq!(
2380 ctx.template_vars().get_structured("IsRelease"),
2381 Some(&tera::Value::Bool(false))
2382 );
2383 }
2384
2385 #[test]
2386 fn test_is_release_false_for_nightly() {
2387 let config = Config::default();
2388 let opts = ContextOptions {
2389 nightly: true,
2390 ..Default::default()
2391 };
2392 let mut ctx = Context::new(config, opts);
2393 ctx.git_info = Some(make_git_info(false, None));
2394 ctx.populate_git_vars();
2395
2396 assert_eq!(
2397 ctx.template_vars().get_structured("IsRelease"),
2398 Some(&tera::Value::Bool(false))
2399 );
2400 }
2401
2402 #[test]
2403 fn test_is_merging_true_when_merge_flag_set() {
2404 let config = Config::default();
2405 let opts = ContextOptions {
2406 merge: true,
2407 ..Default::default()
2408 };
2409 let mut ctx = Context::new(config, opts);
2410 ctx.git_info = Some(make_git_info(false, None));
2411 ctx.populate_git_vars();
2412
2413 assert_eq!(
2414 ctx.template_vars().get_structured("IsMerging"),
2415 Some(&tera::Value::Bool(true))
2416 );
2417 }
2418
2419 #[test]
2420 fn test_is_merging_false_by_default() {
2421 let config = Config::default();
2422 let mut ctx = Context::new(config, ContextOptions::default());
2423 ctx.git_info = Some(make_git_info(false, None));
2424 ctx.populate_git_vars();
2425
2426 assert_eq!(
2427 ctx.template_vars().get_structured("IsMerging"),
2428 Some(&tera::Value::Bool(false))
2429 );
2430 }
2431
2432 #[test]
2433 fn test_refresh_artifacts_var_empty() {
2434 let config = Config::default();
2435 let mut ctx = Context::new(config, ContextOptions::default());
2436 ctx.refresh_artifacts_var();
2437
2438 let result = ctx
2440 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2441 .unwrap();
2442 assert_eq!(result, "");
2443 }
2444
2445 #[test]
2446 fn test_refresh_artifacts_var_with_artifacts() {
2447 use crate::artifact::{Artifact, ArtifactKind};
2448 use std::collections::HashMap;
2449 use std::path::PathBuf;
2450
2451 let config = Config::default();
2452 let mut ctx = Context::new(config, ContextOptions::default());
2453 ctx.artifacts.add(Artifact {
2457 kind: ArtifactKind::Archive,
2458 name: String::new(),
2459 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2460 target: Some("x86_64-unknown-linux-gnu".to_string()),
2461 crate_name: "myapp".to_string(),
2462 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2463 size: None,
2464 });
2465 ctx.artifacts.add(Artifact {
2466 kind: ArtifactKind::Binary,
2467 name: String::new(),
2468 path: PathBuf::from("dist/myapp"),
2469 target: Some("x86_64-unknown-linux-gnu".to_string()),
2470 crate_name: "myapp".to_string(),
2471 metadata: HashMap::new(),
2472 size: None,
2473 });
2474 ctx.refresh_artifacts_var();
2475
2476 let result = ctx
2478 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2479 .unwrap();
2480 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2481 assert!(result.contains("myapp"));
2482
2483 let result_kinds = ctx
2485 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2486 .unwrap();
2487 assert!(result_kinds.contains("archive"));
2488 assert!(result_kinds.contains("binary"));
2489 }
2490
2491 #[test]
2492 fn test_populate_metadata_var_with_mod_timestamp() {
2493 let mut config = Config::default();
2494 config.metadata = Some(crate::config::MetadataConfig {
2495 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2496 ..Default::default()
2497 });
2498 let mut ctx = Context::new(config, ContextOptions::default());
2499 ctx.populate_metadata_var().unwrap();
2500
2501 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2503 assert_eq!(result, "{{ .CommitTimestamp }}");
2504 }
2505
2506 #[test]
2507 fn test_populate_metadata_var_empty_when_no_config() {
2508 let config = Config::default();
2509 let mut ctx = Context::new(config, ContextOptions::default());
2510 ctx.populate_metadata_var().unwrap();
2511
2512 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2514 assert_eq!(result, "");
2515 }
2516
2517 #[test]
2518 fn test_populate_metadata_var_reads_from_config() {
2519 let mut config = Config::default();
2520 config.metadata = Some(crate::config::MetadataConfig {
2521 description: Some("A test project".to_string()),
2522 homepage: Some("https://example.com".to_string()),
2523 documentation: Some("https://docs.example.com".to_string()),
2524 license: Some("MIT".to_string()),
2525 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2526 mod_timestamp: Some("1234567890".to_string()),
2527 ..Default::default()
2528 });
2529 let mut ctx = Context::new(config, ContextOptions::default());
2530 ctx.populate_metadata_var().unwrap();
2531
2532 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2533 assert_eq!(desc, "A test project");
2534
2535 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2536 assert_eq!(home, "https://example.com");
2537
2538 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2539 assert_eq!(docs, "https://docs.example.com");
2540
2541 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2542 assert_eq!(lic, "MIT");
2543
2544 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2545 assert_eq!(ts, "1234567890");
2546 }
2547
2548 #[test]
2549 fn test_populate_metadata_var_license_falls_back_to_derived() {
2550 let mut config = Config::default();
2554 config.crates = vec![crate::config::CrateConfig {
2555 name: "anodizer".to_string(),
2556 ..Default::default()
2557 }];
2558 config.derived_metadata.insert(
2559 "anodizer".to_string(),
2560 crate::config::MetadataConfig {
2561 description: Some("Derived desc".to_string()),
2562 homepage: Some("https://derived.example".to_string()),
2563 documentation: Some("https://derived.docs".to_string()),
2564 license: Some("MIT OR Apache-2.0".to_string()),
2565 ..Default::default()
2566 },
2567 );
2568 let mut ctx = Context::new(config, ContextOptions::default());
2569 ctx.populate_metadata_var().unwrap();
2570
2571 assert_eq!(
2572 ctx.render_template("{{ Metadata.License }}").unwrap(),
2573 "MIT OR Apache-2.0"
2574 );
2575 assert_eq!(
2576 ctx.render_template("{{ Metadata.Description }}").unwrap(),
2577 "Derived desc"
2578 );
2579 assert_eq!(
2580 ctx.render_template("{{ Metadata.Homepage }}").unwrap(),
2581 "https://derived.example"
2582 );
2583 assert_eq!(
2584 ctx.render_template("{{ Metadata.Documentation }}").unwrap(),
2585 "https://derived.docs"
2586 );
2587 }
2588
2589 #[test]
2590 fn test_populate_metadata_var_top_level_license_wins_over_derived() {
2591 let mut config = Config::default();
2594 config.crates = vec![crate::config::CrateConfig {
2595 name: "anodizer".to_string(),
2596 ..Default::default()
2597 }];
2598 config.derived_metadata.insert(
2599 "anodizer".to_string(),
2600 crate::config::MetadataConfig {
2601 license: Some("MIT OR Apache-2.0".to_string()),
2602 ..Default::default()
2603 },
2604 );
2605 config.metadata = Some(crate::config::MetadataConfig {
2606 license: Some("GPL-3.0".to_string()),
2607 ..Default::default()
2608 });
2609 let mut ctx = Context::new(config, ContextOptions::default());
2610 ctx.populate_metadata_var().unwrap();
2611
2612 assert_eq!(
2613 ctx.render_template("{{ Metadata.License }}").unwrap(),
2614 "GPL-3.0"
2615 );
2616 }
2617
2618 #[test]
2619 fn test_populate_metadata_var_documentation_renders() {
2620 let mut config = Config::default();
2621 config.metadata = Some(crate::config::MetadataConfig {
2622 documentation: Some("https://docs.rs/anodizer".to_string()),
2623 ..Default::default()
2624 });
2625 let mut ctx = Context::new(config, ContextOptions::default());
2626 ctx.populate_metadata_var().unwrap();
2627
2628 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2629 assert_eq!(docs, "https://docs.rs/anodizer");
2630 }
2631
2632 #[test]
2633 fn test_populate_metadata_var_documentation_empty_when_unset() {
2634 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2635 ctx.populate_metadata_var().unwrap();
2636
2637 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2638 assert_eq!(docs, "");
2639 }
2640
2641 #[test]
2642 fn test_populate_metadata_var_full_description_inline() {
2643 use crate::config::ContentSource;
2644 let mut config = Config::default();
2645 config.metadata = Some(crate::config::MetadataConfig {
2646 full_description: Some(ContentSource::Inline(
2647 "A long-form description of the project.".to_string(),
2648 )),
2649 ..Default::default()
2650 });
2651 let mut ctx = Context::new(config, ContextOptions::default());
2652 ctx.populate_metadata_var().unwrap();
2653 let rendered = ctx
2654 .render_template("{{ Metadata.FullDescription }}")
2655 .unwrap();
2656 assert_eq!(rendered, "A long-form description of the project.");
2657 }
2658
2659 #[test]
2660 fn test_populate_metadata_var_full_description_from_file() {
2661 use crate::config::ContentSource;
2662 let tmp = tempfile::tempdir().unwrap();
2663 let desc_path = tmp.path().join("DESCRIPTION.md");
2664 std::fs::write(&desc_path, "read from disk").unwrap();
2665 let mut config = Config::default();
2666 config.metadata = Some(crate::config::MetadataConfig {
2667 full_description: Some(ContentSource::FromFile {
2668 from_file: desc_path.to_string_lossy().into_owned(),
2669 }),
2670 ..Default::default()
2671 });
2672 let mut ctx = Context::new(config, ContextOptions::default());
2673 ctx.populate_metadata_var().unwrap();
2674 let rendered = ctx
2675 .render_template("{{ Metadata.FullDescription }}")
2676 .unwrap();
2677 assert_eq!(rendered, "read from disk");
2678 }
2679
2680 #[test]
2681 fn test_populate_metadata_var_full_description_from_url_resolves() {
2682 use crate::config::ContentSource;
2687 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2688
2689 let body = "long form description body";
2690 let body_len = body.len();
2691 let response: &'static str = Box::leak(
2692 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2693 );
2694 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2695
2696 let mut config = Config::default();
2697 config.metadata = Some(crate::config::MetadataConfig {
2698 full_description: Some(ContentSource::FromUrl {
2699 from_url: format!("http://{addr}/description.md"),
2700 headers: None,
2701 }),
2702 ..Default::default()
2703 });
2704 let mut ctx = Context::new(config, ContextOptions::default());
2705 ctx.populate_metadata_var()
2706 .expect("from_url should resolve through content_source");
2707 let rendered = ctx
2708 .render_template("{{ Metadata.FullDescription }}")
2709 .unwrap();
2710 assert_eq!(rendered, body);
2711 }
2712
2713 #[test]
2714 fn test_populate_metadata_var_commit_author() {
2715 use crate::config::CommitAuthorConfig;
2716 let mut config = Config::default();
2717 config.metadata = Some(crate::config::MetadataConfig {
2718 commit_author: Some(CommitAuthorConfig {
2719 name: Some("Alice Developer".to_string()),
2720 email: Some("alice@example.com".to_string()),
2721 signing: None,
2722 use_github_app_token: false,
2723 }),
2724 ..Default::default()
2725 });
2726 let mut ctx = Context::new(config, ContextOptions::default());
2727 ctx.populate_metadata_var().unwrap();
2728 let name = ctx
2729 .render_template("{{ Metadata.CommitAuthor.Name }}")
2730 .unwrap();
2731 assert_eq!(name, "Alice Developer");
2732 let email = ctx
2733 .render_template("{{ Metadata.CommitAuthor.Email }}")
2734 .unwrap();
2735 assert_eq!(email, "alice@example.com");
2736 }
2737
2738 #[test]
2739 fn test_artifact_id_template_var() {
2740 let mut config = Config::default();
2741 config.project_name = "myapp".to_string();
2742 let mut ctx = Context::new(config, ContextOptions::default());
2743 ctx.template_vars_mut().set("ArtifactID", "default");
2744
2745 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2746 assert_eq!(result, "default");
2747 }
2748
2749 #[test]
2750 fn test_artifact_id_empty_when_not_set() {
2751 let mut config = Config::default();
2752 config.project_name = "myapp".to_string();
2753 let mut ctx = Context::new(config, ContextOptions::default());
2754 ctx.template_vars_mut().set("ArtifactID", "");
2755
2756 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2757 assert_eq!(result, "");
2758 }
2759
2760 #[test]
2761 fn test_pro_vars_rendered_in_templates() {
2762 let mut config = Config::default();
2764 config.tag = Some(crate::config::TagConfig {
2765 tag_prefix: Some("api/".to_string()),
2766 ..Default::default()
2767 });
2768 let opts = ContextOptions {
2769 snapshot: false,
2770 nightly: false,
2771 merge: true,
2772 ..Default::default()
2773 };
2774 let mut ctx = Context::new(config, opts);
2775 ctx.git_info = Some(make_git_info(false, None));
2776 ctx.populate_git_vars();
2777
2778 let result = ctx
2779 .render_template(
2780 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2781 )
2782 .unwrap();
2783 assert_eq!(result, "release-merge-api/v1.2.3");
2784 }
2785
2786 #[test]
2787 fn test_is_release_without_git_info() {
2788 let config = Config::default();
2790 let opts = ContextOptions {
2791 snapshot: false,
2792 nightly: false,
2793 ..Default::default()
2794 };
2795 let mut ctx = Context::new(config, opts);
2796 ctx.populate_git_vars();
2797
2798 assert_eq!(
2799 ctx.template_vars().get_structured("IsRelease"),
2800 Some(&tera::Value::Bool(true))
2801 );
2802 }
2803
2804 #[test]
2805 fn test_is_merging_without_git_info() {
2806 let config = Config::default();
2808 let opts = ContextOptions {
2809 merge: true,
2810 ..Default::default()
2811 };
2812 let mut ctx = Context::new(config, opts);
2813 ctx.populate_git_vars();
2814
2815 assert_eq!(
2816 ctx.template_vars().get_structured("IsMerging"),
2817 Some(&tera::Value::Bool(true))
2818 );
2819 }
2820
2821 #[test]
2831 fn test_monorepo_version_matches_shared_semver_helper() {
2832 let mut config = Config::default();
2833 config.monorepo = Some(crate::config::MonorepoConfig {
2834 tag_prefix: Some("core/".to_string()),
2835 dir: None,
2836 });
2837 let mut ctx = Context::new(config, ContextOptions::default());
2838
2839 let semver = SemVer {
2840 major: 2,
2841 minor: 1,
2842 patch: 0,
2843 prerelease: Some("rc.1".to_string()),
2844 build_metadata: Some("build.7".to_string()),
2845 };
2846 let mut info = make_git_info(false, None);
2847 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2848 info.semver = semver.clone();
2849 ctx.git_info = Some(info);
2850 ctx.populate_git_vars();
2851
2852 let v = ctx.template_vars();
2853 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2856 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2857 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2858 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2859 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2861 }
2862
2863 #[test]
2864 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2865 let mut config = Config::default();
2866 config.monorepo = Some(crate::config::MonorepoConfig {
2867 tag_prefix: Some("subproject1/".to_string()),
2868 dir: None,
2869 });
2870 let mut ctx = Context::new(config, ContextOptions::default());
2871
2872 let mut info = make_git_info(false, None);
2874 info.tag = "subproject1/v1.2.3".to_string();
2875 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2876 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2877 ctx.git_info = Some(info);
2878 ctx.populate_git_vars();
2879
2880 let v = ctx.template_vars();
2881 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2883 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2885 assert_eq!(
2887 v.get("PrefixedTag"),
2888 Some(&"subproject1/v1.2.3".to_string())
2889 );
2890 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2892 assert_eq!(
2894 v.get("PrefixedPreviousTag"),
2895 Some(&"subproject1/v1.2.2".to_string())
2896 );
2897 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2899 assert_eq!(
2901 v.get("PrefixedSummary"),
2902 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2903 );
2904 }
2905
2906 #[test]
2907 fn test_monorepo_prefixed_previous_tag() {
2908 let mut config = Config::default();
2909 config.monorepo = Some(crate::config::MonorepoConfig {
2910 tag_prefix: Some("svc/".to_string()),
2911 dir: None,
2912 });
2913 let mut ctx = Context::new(config, ContextOptions::default());
2914
2915 let mut info = make_git_info(false, None);
2916 info.tag = "svc/v2.0.0".to_string();
2917 info.previous_tag = Some("svc/v1.9.0".to_string());
2918 ctx.git_info = Some(info);
2919 ctx.populate_git_vars();
2920
2921 let v = ctx.template_vars();
2922 assert_eq!(
2924 v.get("PrefixedPreviousTag"),
2925 Some(&"svc/v1.9.0".to_string())
2926 );
2927 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2929 }
2930
2931 #[test]
2932 fn test_no_monorepo_falls_back_to_tag_prefix() {
2933 let mut config = Config::default();
2935 config.tag = Some(crate::config::TagConfig {
2936 tag_prefix: Some("release/".to_string()),
2937 ..Default::default()
2938 });
2939 let mut ctx = Context::new(config, ContextOptions::default());
2940 ctx.git_info = Some(make_git_info(false, None));
2941 ctx.populate_git_vars();
2942
2943 let v = ctx.template_vars();
2944 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2946 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2948 assert_eq!(
2949 v.get("PrefixedPreviousTag"),
2950 Some(&"release/v1.2.2".to_string())
2951 );
2952 }
2953
2954 #[test]
2955 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2956 let mut config = Config::default();
2959 config.tag = Some(crate::config::TagConfig {
2960 tag_prefix: Some("release/".to_string()),
2961 ..Default::default()
2962 });
2963 config.monorepo = Some(crate::config::MonorepoConfig {
2964 tag_prefix: Some("svc/".to_string()),
2965 dir: None,
2966 });
2967 let mut ctx = Context::new(config, ContextOptions::default());
2968
2969 let mut info = make_git_info(false, None);
2970 info.tag = "svc/v1.2.3".to_string();
2971 info.previous_tag = Some("svc/v1.2.2".to_string());
2972 ctx.git_info = Some(info);
2973 ctx.populate_git_vars();
2974
2975 let v = ctx.template_vars();
2976 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2978 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2980 }
2981
2982 #[test]
2983 fn test_monorepo_prefixed_summary() {
2984 let mut config = Config::default();
2985 config.monorepo = Some(crate::config::MonorepoConfig {
2986 tag_prefix: Some("pkg/".to_string()),
2987 dir: None,
2988 });
2989 let mut ctx = Context::new(config, ContextOptions::default());
2990
2991 let mut info = make_git_info(false, None);
2992 info.tag = "pkg/v1.2.3".to_string();
2993 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
2995 ctx.git_info = Some(info);
2996 ctx.populate_git_vars();
2997
2998 assert_eq!(
3000 ctx.template_vars().get("PrefixedSummary"),
3001 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
3002 );
3003 assert_eq!(
3005 ctx.template_vars().get("Summary"),
3006 Some(&"v1.2.3-0-gabc123d".to_string())
3007 );
3008 }
3009
3010 #[test]
3011 fn test_monorepo_no_previous_tag() {
3012 let mut config = Config::default();
3013 config.monorepo = Some(crate::config::MonorepoConfig {
3014 tag_prefix: Some("svc/".to_string()),
3015 dir: None,
3016 });
3017 let mut ctx = Context::new(config, ContextOptions::default());
3018
3019 let mut info = make_git_info(false, None);
3020 info.tag = "svc/v1.0.0".to_string();
3021 info.previous_tag = None;
3022 ctx.git_info = Some(info);
3023 ctx.populate_git_vars();
3024
3025 let v = ctx.template_vars();
3026 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
3027 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
3029 }
3030
3031 #[test]
3036 fn test_monorepo_full_flow_all_vars() {
3037 let mut config = Config::default();
3040 config.project_name = "mymonorepo".to_string();
3041 config.monorepo = Some(crate::config::MonorepoConfig {
3042 tag_prefix: Some("services/api/".to_string()),
3043 dir: Some("services/api".to_string()),
3044 });
3045
3046 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
3048 assert_eq!(config.monorepo_dir(), Some("services/api"));
3049
3050 let mut ctx = Context::new(config, ContextOptions::default());
3051
3052 let mut info = make_git_info(false, None);
3055 info.tag = "services/api/v2.1.0".to_string();
3056 info.previous_tag = Some("services/api/v2.0.5".to_string());
3057 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
3058 info.semver = crate::git::SemVer {
3059 major: 2,
3060 minor: 1,
3061 patch: 0,
3062 prerelease: None,
3063 build_metadata: None,
3064 };
3065 ctx.git_info = Some(info);
3066 ctx.populate_git_vars();
3067
3068 let v = ctx.template_vars();
3069
3070 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
3072 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
3073 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
3074 assert_eq!(v.get("Major"), Some(&"2".to_string()));
3075 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
3076 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
3077 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
3078 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
3079
3080 assert_eq!(
3082 v.get("PrefixedTag"),
3083 Some(&"services/api/v2.1.0".to_string())
3084 );
3085 assert_eq!(
3086 v.get("PrefixedPreviousTag"),
3087 Some(&"services/api/v2.0.5".to_string())
3088 );
3089 assert_eq!(
3090 v.get("PrefixedSummary"),
3091 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
3092 );
3093
3094 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
3096 }
3097
3098 #[test]
3099 fn context_env_var_defaults_to_process_env_source() {
3100 let ctx = Context::new(Config::default(), ContextOptions::default());
3101 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
3103 }
3104
3105 #[test]
3106 fn context_env_var_routes_to_injected_source() {
3107 let mut ctx = Context::new(Config::default(), ContextOptions::default());
3108 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
3109 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
3110 assert_eq!(ctx.env_var("PATH"), None);
3114 }
3115
3116 #[test]
3117 #[serial_test::serial]
3118 fn populate_runtime_vars_sets_rustc_version() {
3119 let config = Config::default();
3120 let mut ctx = Context::new(config, ContextOptions::default());
3121 ctx.populate_runtime_vars();
3124
3125 let ver = ctx
3126 .template_vars()
3127 .get("RustcVersion")
3128 .expect("RustcVersion should be set after populate_runtime_vars");
3129 if !ver.is_empty() {
3133 assert!(
3134 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
3135 "RustcVersion should start with a digit: {ver}"
3136 );
3137 }
3138 }
3139}