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 repository,
1377 maintainers,
1378 mod_timestamp,
1379 full_desc_src,
1380 commit_author,
1381 ) = {
1382 let meta = self.config.metadata.as_ref();
1383 let description = self
1390 .config
1391 .meta_description_project()
1392 .unwrap_or("")
1393 .to_string();
1394 let homepage = self
1395 .config
1396 .meta_homepage_project()
1397 .unwrap_or("")
1398 .to_string();
1399 let documentation = self
1400 .config
1401 .meta_documentation_project()
1402 .unwrap_or("")
1403 .to_string();
1404 let license = self.config.meta_license_project().unwrap_or("").to_string();
1405 let repository = self
1406 .config
1407 .meta_repository_project()
1408 .unwrap_or("")
1409 .to_string();
1410 let maintainers: Vec<String> = meta
1411 .and_then(|m| m.maintainers.as_ref())
1412 .cloned()
1413 .unwrap_or_default();
1414 let mod_timestamp = meta
1415 .and_then(|m| m.mod_timestamp.as_deref())
1416 .unwrap_or("")
1417 .to_string();
1418 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1419 let commit_author = meta.and_then(|m| m.commit_author.clone());
1420 (
1421 description,
1422 homepage,
1423 documentation,
1424 license,
1425 repository,
1426 maintainers,
1427 mod_timestamp,
1428 full_desc_src,
1429 commit_author,
1430 )
1431 };
1432
1433 let full_description = match full_desc_src {
1439 None => String::new(),
1440 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1441 };
1442
1443 let commit_author_map = serde_json::json!({
1444 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1445 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1446 });
1447
1448 let meta_map = serde_json::json!({
1449 "Description": description,
1450 "Homepage": homepage,
1451 "Documentation": documentation,
1452 "License": license,
1453 "Repository": repository,
1454 "Maintainers": maintainers,
1455 "ModTimestamp": mod_timestamp,
1456 "FullDescription": full_description,
1457 "CommitAuthor": commit_author_map,
1458 });
1459 self.template_vars.set_structured("Metadata", meta_map);
1461 Ok(())
1462 }
1463}
1464
1465pub fn map_os_to_goos(os: &str) -> &str {
1468 match os {
1469 "macos" => "darwin",
1470 other => other, }
1472}
1473
1474pub fn map_arch_to_goarch(arch: &str) -> &str {
1477 match arch {
1478 "x86_64" => "amd64",
1479 "x86" => "386",
1480 "aarch64" => "arm64",
1481 "powerpc64" => "ppc64",
1482 "s390x" => "s390x",
1483 "mips" => "mips",
1484 "mips64" => "mips64",
1485 "riscv64" => "riscv64",
1486 other => other,
1487 }
1488}
1489
1490#[cfg(test)]
1491#[allow(clippy::field_reassign_with_default)]
1492mod tests {
1493 use super::*;
1494 use crate::config::Config;
1495 use crate::git::{GitInfo, SemVer};
1496
1497 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1498 let tag = match prerelease {
1499 Some(pre) => format!("v1.2.3-{pre}"),
1500 None => "v1.2.3".to_string(),
1501 };
1502 GitInfo {
1503 tag,
1504 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1505 short_commit: "abc123d".to_string(),
1506 branch: "main".to_string(),
1507 dirty,
1508 semver: SemVer {
1509 major: 1,
1510 minor: 2,
1511 patch: 3,
1512 prerelease: prerelease.map(|s| s.to_string()),
1513 build_metadata: None,
1514 },
1515 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1516 commit_timestamp: "1774463400".to_string(),
1517 previous_tag: Some("v1.2.2".to_string()),
1518 remote_url: "https://github.com/test/repo.git".to_string(),
1519 summary: "v1.2.3-0-gabc123d".to_string(),
1520 tag_subject: "Release v1.2.3".to_string(),
1521 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1522 tag_body: "Full release notes here.".to_string(),
1523 first_commit: None,
1524 }
1525 }
1526
1527 #[test]
1528 fn test_context_template_vars() {
1529 let mut config = Config::default();
1530 config.project_name = "test-project".to_string();
1531 let ctx = Context::new(config, ContextOptions::default());
1532 assert_eq!(
1533 ctx.template_vars().get("ProjectName"),
1534 Some(&"test-project".to_string())
1535 );
1536 }
1537
1538 #[test]
1539 fn test_context_should_skip() {
1540 let config = Config::default();
1541 let opts = ContextOptions {
1542 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1543 ..Default::default()
1544 };
1545 let ctx = Context::new(config, opts);
1546 assert!(ctx.should_skip("publish"));
1547 assert!(ctx.should_skip("announce"));
1548 assert!(!ctx.should_skip("build"));
1549 }
1550
1551 #[test]
1552 fn publisher_deselected_empty_selectors_runs_everything() {
1553 let ctx = Context::new(Config::default(), ContextOptions::default());
1554 assert!(!ctx.publisher_deselected("npm"));
1555 assert!(!ctx.publisher_deselected("cargo"));
1556 assert!(!ctx.publisher_deselected("anything"));
1557 }
1558
1559 #[test]
1560 fn publisher_deselected_skip_denylists() {
1561 let opts = ContextOptions {
1562 skip_stages: vec!["npm".to_string()],
1563 ..Default::default()
1564 };
1565 let ctx = Context::new(Config::default(), opts);
1566 assert!(ctx.publisher_deselected("npm"));
1567 assert!(!ctx.publisher_deselected("cargo"));
1568 }
1569
1570 #[test]
1571 fn publisher_deselected_allowlist_excludes_unlisted() {
1572 let opts = ContextOptions {
1573 publisher_allowlist: vec!["cargo".to_string()],
1574 ..Default::default()
1575 };
1576 let ctx = Context::new(Config::default(), opts);
1577 assert!(!ctx.publisher_deselected("cargo"));
1578 assert!(ctx.publisher_deselected("npm"));
1579 }
1580
1581 #[test]
1582 fn publisher_deselected_skip_wins_over_allowlist() {
1583 let opts = ContextOptions {
1584 skip_stages: vec!["cargo".to_string()],
1585 publisher_allowlist: vec!["cargo".to_string()],
1586 ..Default::default()
1587 };
1588 let ctx = Context::new(Config::default(), opts);
1589 assert!(ctx.publisher_deselected("cargo"));
1590 }
1591
1592 #[test]
1593 fn test_context_render_template() {
1594 let mut config = Config::default();
1595 config.project_name = "myapp".to_string();
1596 let ctx = Context::new(config, ContextOptions::default());
1597 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1598 assert_eq!(result, "myapp-release");
1599 }
1600
1601 #[test]
1602 fn test_populate_git_vars_sets_all_expected_vars() {
1603 let config = Config::default();
1604 let mut ctx = Context::new(config, ContextOptions::default());
1605 ctx.git_info = Some(make_git_info(false, None));
1606 ctx.populate_git_vars();
1607
1608 let v = ctx.template_vars();
1609 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1610 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1611 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1612 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1613 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1614 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1615 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1616 assert_eq!(
1617 v.get("FullCommit"),
1618 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1619 );
1620 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1621 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1622 assert_eq!(
1623 v.get("CommitDate"),
1624 Some(&"2026-03-25T10:30:00+00:00".to_string())
1625 );
1626 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1627 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1628 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1631 }
1632
1633 #[test]
1634 fn test_nightly_build_defaults_to_zero_without_git_info() {
1635 let config = Config::default();
1638 let mut ctx = Context::new(config, ContextOptions::default());
1639 ctx.git_info = None;
1640 ctx.populate_git_vars();
1641 assert_eq!(
1642 ctx.template_vars().get_structured("NightlyBuild"),
1643 Some(&tera::Value::from(0u64))
1644 );
1645 }
1646
1647 #[test]
1648 fn test_commit_is_alias_for_full_commit() {
1649 let config = Config::default();
1650 let mut ctx = Context::new(config, ContextOptions::default());
1651 ctx.git_info = Some(make_git_info(false, None));
1652 ctx.populate_git_vars();
1653
1654 let v = ctx.template_vars();
1655 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1656 }
1657
1658 #[test]
1659 fn test_populate_git_vars_prerelease() {
1660 let config = Config::default();
1661 let mut ctx = Context::new(config, ContextOptions::default());
1662 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1663 ctx.populate_git_vars();
1664
1665 let v = ctx.template_vars();
1666 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1667 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1668 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1669 }
1670
1671 #[test]
1672 fn test_build_metadata_template_var() {
1673 let config = Config::default();
1674 let mut ctx = Context::new(config, ContextOptions::default());
1675 let mut info = make_git_info(false, None);
1676 info.tag = "v1.2.3+build.42".to_string();
1677 info.semver.build_metadata = Some("build.42".to_string());
1678 ctx.git_info = Some(info);
1679 ctx.populate_git_vars();
1680
1681 let v = ctx.template_vars();
1682 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1683 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1685 }
1686
1687 #[test]
1688 fn test_build_metadata_empty_when_none() {
1689 let config = Config::default();
1690 let mut ctx = Context::new(config, ContextOptions::default());
1691 ctx.git_info = Some(make_git_info(false, None));
1692 ctx.populate_git_vars();
1693
1694 assert_eq!(
1695 ctx.template_vars().get("BuildMetadata"),
1696 Some(&"".to_string())
1697 );
1698 }
1699
1700 #[test]
1701 fn test_populate_git_vars_monorepo_prefixed_tag() {
1702 let config = Config::default();
1705 let mut ctx = Context::new(config, ContextOptions::default());
1706 let mut info = make_git_info(false, None);
1707 info.tag = "core-v0.3.2".to_string();
1708 info.semver = SemVer {
1709 major: 0,
1710 minor: 3,
1711 patch: 2,
1712 prerelease: None,
1713 build_metadata: None,
1714 };
1715 ctx.git_info = Some(info);
1716 ctx.populate_git_vars();
1717
1718 let v = ctx.template_vars();
1719 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1720 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1721 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1722 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1723 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1724 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1725 }
1726
1727 #[test]
1728 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1729 let config = Config::default();
1730 let mut ctx = Context::new(config, ContextOptions::default());
1731 let mut info = make_git_info(false, None);
1732 info.tag = "operator-v1.0.0-rc.1".to_string();
1733 info.semver = SemVer {
1734 major: 1,
1735 minor: 0,
1736 patch: 0,
1737 prerelease: Some("rc.1".to_string()),
1738 build_metadata: None,
1739 };
1740 ctx.git_info = Some(info);
1741 ctx.populate_git_vars();
1742
1743 let v = ctx.template_vars();
1744 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1745 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1746 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1747 }
1748
1749 #[test]
1750 fn test_git_tree_state_clean() {
1751 let config = Config::default();
1752 let mut ctx = Context::new(config, ContextOptions::default());
1753 ctx.git_info = Some(make_git_info(false, None));
1754 ctx.populate_git_vars();
1755
1756 let v = ctx.template_vars();
1757 assert_eq!(
1758 v.get_structured("IsGitDirty"),
1759 Some(&tera::Value::Bool(false))
1760 );
1761 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1762 }
1763
1764 #[test]
1765 fn test_git_tree_state_dirty() {
1766 let config = Config::default();
1767 let mut ctx = Context::new(config, ContextOptions::default());
1768 ctx.git_info = Some(make_git_info(true, None));
1769 ctx.populate_git_vars();
1770
1771 let v = ctx.template_vars();
1772 assert_eq!(
1773 v.get_structured("IsGitDirty"),
1774 Some(&tera::Value::Bool(true))
1775 );
1776 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1777 }
1778
1779 #[test]
1780 fn test_is_snapshot_reflects_context_options() {
1781 let config = Config::default();
1782 let opts = ContextOptions {
1783 snapshot: true,
1784 ..Default::default()
1785 };
1786 let mut ctx = Context::new(config, opts);
1787 ctx.git_info = Some(make_git_info(false, None));
1788 ctx.populate_git_vars();
1789
1790 assert_eq!(
1791 ctx.template_vars().get_structured("IsSnapshot"),
1792 Some(&tera::Value::Bool(true))
1793 );
1794
1795 let config2 = Config::default();
1797 let opts2 = ContextOptions {
1798 snapshot: false,
1799 ..Default::default()
1800 };
1801 let mut ctx2 = Context::new(config2, opts2);
1802 ctx2.git_info = Some(make_git_info(false, None));
1803 ctx2.populate_git_vars();
1804
1805 assert_eq!(
1806 ctx2.template_vars().get_structured("IsSnapshot"),
1807 Some(&tera::Value::Bool(false))
1808 );
1809 }
1810
1811 #[test]
1812 fn test_is_draft_defaults_to_false() {
1813 let config = Config::default();
1814 let mut ctx = Context::new(config, ContextOptions::default());
1815 ctx.git_info = Some(make_git_info(false, None));
1816 ctx.populate_git_vars();
1817
1818 assert_eq!(
1819 ctx.template_vars().get_structured("IsDraft"),
1820 Some(&tera::Value::Bool(false))
1821 );
1822 }
1823
1824 #[test]
1825 fn test_previous_tag_empty_when_none() {
1826 let config = Config::default();
1827 let mut ctx = Context::new(config, ContextOptions::default());
1828 let mut info = make_git_info(false, None);
1829 info.previous_tag = None;
1830 ctx.git_info = Some(info);
1831 ctx.populate_git_vars();
1832
1833 assert_eq!(
1834 ctx.template_vars().get("PreviousTag"),
1835 Some(&"".to_string())
1836 );
1837 }
1838
1839 #[test]
1848 fn populate_time_vars_uses_source_date_epoch_when_set() {
1849 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1853 let config = Config::default();
1854 let mut ctx = Context::new(config, ContextOptions::default());
1855 ctx.set_env_source(env);
1856 ctx.populate_time_vars();
1857
1858 let v = ctx.template_vars();
1859 assert_eq!(
1860 v.get("Timestamp"),
1861 Some(&"1715000000".to_string()),
1862 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1863 );
1864 assert_eq!(
1865 v.get("Date"),
1866 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1867 "Date must be RFC 3339 derived from SDE"
1868 );
1869 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1870 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1871 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1872 }
1873
1874 #[test]
1875 fn test_populate_time_vars() {
1876 let env = crate::MapEnvSource::new();
1879 let config = Config::default();
1880 let mut ctx = Context::new(config, ContextOptions::default());
1881 ctx.set_env_source(env);
1882 ctx.populate_time_vars();
1883
1884 let v = ctx.template_vars();
1885
1886 let date = v
1888 .get("Date")
1889 .unwrap_or_else(|| panic!("Date should be set"));
1890 assert!(
1891 date.contains('T') && date.len() > 10,
1892 "Date should be RFC 3339, got: {date}"
1893 );
1894
1895 let ts = v
1897 .get("Timestamp")
1898 .unwrap_or_else(|| panic!("Timestamp should be set"));
1899 assert!(
1900 ts.parse::<i64>().is_ok(),
1901 "Timestamp should be a numeric string, got: {ts}"
1902 );
1903
1904 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1906 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1907 }
1908
1909 #[test]
1910 fn test_env_vars_accessible_in_templates() {
1911 let mut config = Config::default();
1912 config.project_name = "myapp".to_string();
1913 let mut ctx = Context::new(config, ContextOptions::default());
1914 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1915 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1916
1917 let result = ctx
1918 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1919 .unwrap();
1920 assert_eq!(result, "hello-world-staging");
1921 }
1922
1923 #[test]
1924 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1925 let config = Config::default();
1926 let opts = ContextOptions {
1927 snapshot: true,
1928 ..Default::default()
1929 };
1930 let mut ctx = Context::new(config, opts);
1931 ctx.populate_git_vars();
1933
1934 assert_eq!(
1935 ctx.template_vars().get_structured("IsSnapshot"),
1936 Some(&tera::Value::Bool(true))
1937 );
1938 assert_eq!(
1939 ctx.template_vars().get_structured("IsDraft"),
1940 Some(&tera::Value::Bool(false))
1941 );
1942 assert_eq!(ctx.template_vars().get("Tag"), None);
1944 }
1945
1946 #[test]
1947 fn test_is_nightly_set_when_nightly_mode_active() {
1948 let config = Config::default();
1949 let opts = ContextOptions {
1950 nightly: true,
1951 ..Default::default()
1952 };
1953 let mut ctx = Context::new(config, opts);
1954 ctx.git_info = Some(make_git_info(false, None));
1955 ctx.populate_git_vars();
1956
1957 assert_eq!(
1958 ctx.template_vars().get_structured("IsNightly"),
1959 Some(&tera::Value::Bool(true)),
1960 "IsNightly should be 'true' when nightly mode is active"
1961 );
1962 assert!(ctx.is_nightly(), "is_nightly() should return true");
1963 }
1964
1965 #[test]
1966 fn test_is_nightly_false_by_default() {
1967 let config = Config::default();
1968 let mut ctx = Context::new(config, ContextOptions::default());
1969 ctx.git_info = Some(make_git_info(false, None));
1970 ctx.populate_git_vars();
1971
1972 assert_eq!(
1973 ctx.template_vars().get_structured("IsNightly"),
1974 Some(&tera::Value::Bool(false)),
1975 "IsNightly should default to 'false'"
1976 );
1977 assert!(
1978 !ctx.is_nightly(),
1979 "is_nightly() should return false by default"
1980 );
1981 }
1982
1983 #[test]
1984 fn test_version_returns_populated_value() {
1985 let config = Config::default();
1986 let mut ctx = Context::new(config, ContextOptions::default());
1987 ctx.git_info = Some(make_git_info(false, None));
1988 ctx.populate_git_vars();
1989
1990 assert_eq!(ctx.version(), "1.2.3");
1991 }
1992
1993 #[test]
1994 fn test_version_returns_empty_when_not_set() {
1995 let config = Config::default();
1996 let ctx = Context::new(config, ContextOptions::default());
1997 assert_eq!(ctx.version(), "");
1998 }
1999
2000 #[test]
2001 fn test_is_nightly_without_git_info() {
2002 let config = Config::default();
2003 let opts = ContextOptions {
2004 nightly: true,
2005 ..Default::default()
2006 };
2007 let mut ctx = Context::new(config, opts);
2008 ctx.populate_git_vars();
2010
2011 assert_eq!(
2012 ctx.template_vars().get_structured("IsNightly"),
2013 Some(&tera::Value::Bool(true)),
2014 "IsNightly should be set even without git info"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_is_git_clean_when_not_dirty() {
2020 let config = Config::default();
2021 let mut ctx = Context::new(config, ContextOptions::default());
2022 ctx.git_info = Some(make_git_info(false, None));
2023 ctx.populate_git_vars();
2024
2025 assert_eq!(
2026 ctx.template_vars().get_structured("IsGitClean"),
2027 Some(&tera::Value::Bool(true))
2028 );
2029 }
2030
2031 #[test]
2032 fn test_is_git_clean_when_dirty() {
2033 let config = Config::default();
2034 let mut ctx = Context::new(config, ContextOptions::default());
2035 ctx.git_info = Some(make_git_info(true, None));
2036 ctx.populate_git_vars();
2037
2038 assert_eq!(
2039 ctx.template_vars().get_structured("IsGitClean"),
2040 Some(&tera::Value::Bool(false))
2041 );
2042 }
2043
2044 #[test]
2045 fn test_git_url_set_from_git_info() {
2046 let config = Config::default();
2047 let mut ctx = Context::new(config, ContextOptions::default());
2048 ctx.git_info = Some(make_git_info(false, None));
2049 ctx.populate_git_vars();
2050
2051 assert_eq!(
2052 ctx.template_vars().get("GitURL"),
2053 Some(&"https://github.com/test/repo.git".to_string())
2054 );
2055 }
2056
2057 #[test]
2058 fn test_summary_set_from_git_info() {
2059 let config = Config::default();
2060 let mut ctx = Context::new(config, ContextOptions::default());
2061 ctx.git_info = Some(make_git_info(false, None));
2062 ctx.populate_git_vars();
2063
2064 assert_eq!(
2065 ctx.template_vars().get("Summary"),
2066 Some(&"v1.2.3-0-gabc123d".to_string())
2067 );
2068 }
2069
2070 #[test]
2071 fn test_tag_subject_set_from_git_info() {
2072 let config = Config::default();
2073 let mut ctx = Context::new(config, ContextOptions::default());
2074 ctx.git_info = Some(make_git_info(false, None));
2075 ctx.populate_git_vars();
2076
2077 assert_eq!(
2078 ctx.template_vars().get("TagSubject"),
2079 Some(&"Release v1.2.3".to_string())
2080 );
2081 }
2082
2083 #[test]
2084 fn test_tag_contents_set_from_git_info() {
2085 let config = Config::default();
2086 let mut ctx = Context::new(config, ContextOptions::default());
2087 ctx.git_info = Some(make_git_info(false, None));
2088 ctx.populate_git_vars();
2089
2090 assert_eq!(
2091 ctx.template_vars().get("TagContents"),
2092 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
2093 );
2094 }
2095
2096 #[test]
2097 fn test_tag_body_set_from_git_info() {
2098 let config = Config::default();
2099 let mut ctx = Context::new(config, ContextOptions::default());
2100 ctx.git_info = Some(make_git_info(false, None));
2101 ctx.populate_git_vars();
2102
2103 assert_eq!(
2104 ctx.template_vars().get("TagBody"),
2105 Some(&"Full release notes here.".to_string())
2106 );
2107 }
2108
2109 #[test]
2110 fn test_is_single_target_false_by_default() {
2111 let config = Config::default();
2112 let mut ctx = Context::new(config, ContextOptions::default());
2113 ctx.git_info = Some(make_git_info(false, None));
2114 ctx.populate_git_vars();
2115
2116 assert_eq!(
2117 ctx.template_vars().get_structured("IsSingleTarget"),
2118 Some(&tera::Value::Bool(false))
2119 );
2120 }
2121
2122 #[test]
2123 fn test_is_single_target_true_when_set() {
2124 let config = Config::default();
2125 let opts = ContextOptions {
2126 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
2127 ..Default::default()
2128 };
2129 let mut ctx = Context::new(config, opts);
2130 ctx.git_info = Some(make_git_info(false, None));
2131 ctx.populate_git_vars();
2132
2133 assert_eq!(
2134 ctx.template_vars().get_structured("IsSingleTarget"),
2135 Some(&tera::Value::Bool(true))
2136 );
2137 }
2138
2139 #[test]
2140 #[serial_test::serial]
2141 fn test_populate_runtime_vars() {
2142 let config = Config::default();
2143 let mut ctx = Context::new(config, ContextOptions::default());
2144 ctx.populate_runtime_vars();
2145
2146 let v = ctx.template_vars();
2147
2148 let goos = v
2149 .get("RuntimeGoos")
2150 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
2151 assert!(
2152 !goos.is_empty(),
2153 "RuntimeGoos should not be empty, got: {goos}"
2154 );
2155 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
2157
2158 let goarch = v
2159 .get("RuntimeGoarch")
2160 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
2161 assert!(
2162 !goarch.is_empty(),
2163 "RuntimeGoarch should not be empty, got: {goarch}"
2164 );
2165 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
2167 }
2168
2169 #[test]
2170 fn test_populate_release_notes_var_with_changelogs() {
2171 let mut config = Config::default();
2172 config.crates.push(crate::config::CrateConfig {
2173 name: "my-crate".to_string(),
2174 ..Default::default()
2175 });
2176 let mut ctx = Context::new(config, ContextOptions::default());
2177 ctx.stage_outputs
2178 .changelogs
2179 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
2180 ctx.populate_release_notes_var();
2181
2182 assert_eq!(
2183 ctx.template_vars().get("ReleaseNotes"),
2184 Some(&"## Changes\n- fix bug".to_string())
2185 );
2186 }
2187
2188 #[test]
2189 fn test_populate_release_notes_var_empty_when_no_changelogs() {
2190 let config = Config::default();
2191 let mut ctx = Context::new(config, ContextOptions::default());
2192 ctx.populate_release_notes_var();
2193
2194 assert_eq!(
2195 ctx.template_vars().get("ReleaseNotes"),
2196 Some(&"".to_string())
2197 );
2198 }
2199
2200 #[test]
2201 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
2202 let mut config = Config::default();
2203 config.crates.push(crate::config::CrateConfig {
2204 name: "crate-a".to_string(),
2205 ..Default::default()
2206 });
2207 config.crates.push(crate::config::CrateConfig {
2208 name: "crate-b".to_string(),
2209 ..Default::default()
2210 });
2211 let mut ctx = Context::new(config, ContextOptions::default());
2212 ctx.stage_outputs
2213 .changelogs
2214 .insert("crate-a".to_string(), "notes-a".to_string());
2215 ctx.stage_outputs
2216 .changelogs
2217 .insert("crate-b".to_string(), "notes-b".to_string());
2218 ctx.populate_release_notes_var();
2219
2220 assert_eq!(
2222 ctx.template_vars().get("ReleaseNotes"),
2223 Some(&"notes-a".to_string())
2224 );
2225 }
2226
2227 #[test]
2228 fn test_outputs_accessible_in_templates() {
2229 let mut config = Config::default();
2230 config.project_name = "myapp".to_string();
2231 let mut ctx = Context::new(config, ContextOptions::default());
2232 ctx.template_vars_mut().set_output("build_id", "abc123");
2233 ctx.template_vars_mut()
2234 .set_output("deploy_url", "https://example.com");
2235
2236 let result = ctx
2237 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
2238 .unwrap();
2239 assert_eq!(result, "abc123-https://example.com");
2240 }
2241
2242 #[test]
2243 fn test_artifact_ext_and_target_template_vars() {
2244 let mut config = Config::default();
2245 config.project_name = "myapp".to_string();
2246 let mut ctx = Context::new(config, ContextOptions::default());
2247 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
2248 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
2249 ctx.template_vars_mut()
2250 .set("Target", "x86_64-unknown-linux-gnu");
2251
2252 let result = ctx
2253 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
2254 .unwrap();
2255 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
2256 }
2257
2258 #[test]
2259 fn test_checksums_template_var() {
2260 let mut config = Config::default();
2261 config.project_name = "myapp".to_string();
2262 let mut ctx = Context::new(config, ContextOptions::default());
2263 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
2264 ctx.template_vars_mut().set("Checksums", checksum_text);
2265
2266 let result = ctx.render_template("{{ .Checksums }}").unwrap();
2267 assert_eq!(result, checksum_text);
2268 }
2269
2270 #[test]
2273 fn test_prefixed_tag_with_tag_prefix() {
2274 let mut config = Config::default();
2275 config.tag = Some(crate::config::TagConfig {
2276 tag_prefix: Some("api/".to_string()),
2277 ..Default::default()
2278 });
2279 let mut ctx = Context::new(config, ContextOptions::default());
2280 ctx.git_info = Some(make_git_info(false, None));
2281 ctx.populate_git_vars();
2282
2283 assert_eq!(
2284 ctx.template_vars().get("PrefixedTag"),
2285 Some(&"api/v1.2.3".to_string())
2286 );
2287 }
2288
2289 #[test]
2290 fn test_prefixed_tag_without_tag_prefix() {
2291 let config = Config::default();
2292 let mut ctx = Context::new(config, ContextOptions::default());
2293 ctx.git_info = Some(make_git_info(false, None));
2294 ctx.populate_git_vars();
2295
2296 assert_eq!(
2298 ctx.template_vars().get("PrefixedTag"),
2299 Some(&"v1.2.3".to_string())
2300 );
2301 }
2302
2303 #[test]
2304 fn test_prefixed_previous_tag_with_tag_prefix() {
2305 let mut config = Config::default();
2306 config.tag = Some(crate::config::TagConfig {
2307 tag_prefix: Some("api/".to_string()),
2308 ..Default::default()
2309 });
2310 let mut ctx = Context::new(config, ContextOptions::default());
2311 ctx.git_info = Some(make_git_info(false, None));
2312 ctx.populate_git_vars();
2313
2314 assert_eq!(
2315 ctx.template_vars().get("PrefixedPreviousTag"),
2316 Some(&"api/v1.2.2".to_string())
2317 );
2318 }
2319
2320 #[test]
2321 fn test_prefixed_previous_tag_empty_when_no_previous() {
2322 let mut config = Config::default();
2323 config.tag = Some(crate::config::TagConfig {
2324 tag_prefix: Some("api/".to_string()),
2325 ..Default::default()
2326 });
2327 let mut ctx = Context::new(config, ContextOptions::default());
2328 let mut info = make_git_info(false, None);
2329 info.previous_tag = None;
2330 ctx.git_info = Some(info);
2331 ctx.populate_git_vars();
2332
2333 assert_eq!(
2336 ctx.template_vars().get("PrefixedPreviousTag"),
2337 Some(&"".to_string())
2338 );
2339 }
2340
2341 #[test]
2342 fn test_prefixed_summary_with_tag_prefix() {
2343 let mut config = Config::default();
2344 config.tag = Some(crate::config::TagConfig {
2345 tag_prefix: Some("api/".to_string()),
2346 ..Default::default()
2347 });
2348 let mut ctx = Context::new(config, ContextOptions::default());
2349 ctx.git_info = Some(make_git_info(false, None));
2350 ctx.populate_git_vars();
2351
2352 assert_eq!(
2353 ctx.template_vars().get("PrefixedSummary"),
2354 Some(&"api/v1.2.3-0-gabc123d".to_string())
2355 );
2356 }
2357
2358 #[test]
2359 fn test_is_release_true_for_normal_release() {
2360 let config = Config::default();
2361 let opts = ContextOptions {
2362 snapshot: false,
2363 nightly: false,
2364 ..Default::default()
2365 };
2366 let mut ctx = Context::new(config, opts);
2367 ctx.git_info = Some(make_git_info(false, None));
2368 ctx.populate_git_vars();
2369
2370 assert_eq!(
2371 ctx.template_vars().get_structured("IsRelease"),
2372 Some(&tera::Value::Bool(true))
2373 );
2374 }
2375
2376 #[test]
2377 fn test_is_release_false_for_snapshot() {
2378 let config = Config::default();
2379 let opts = ContextOptions {
2380 snapshot: true,
2381 ..Default::default()
2382 };
2383 let mut ctx = Context::new(config, opts);
2384 ctx.git_info = Some(make_git_info(false, None));
2385 ctx.populate_git_vars();
2386
2387 assert_eq!(
2388 ctx.template_vars().get_structured("IsRelease"),
2389 Some(&tera::Value::Bool(false))
2390 );
2391 }
2392
2393 #[test]
2394 fn test_is_release_false_for_nightly() {
2395 let config = Config::default();
2396 let opts = ContextOptions {
2397 nightly: true,
2398 ..Default::default()
2399 };
2400 let mut ctx = Context::new(config, opts);
2401 ctx.git_info = Some(make_git_info(false, None));
2402 ctx.populate_git_vars();
2403
2404 assert_eq!(
2405 ctx.template_vars().get_structured("IsRelease"),
2406 Some(&tera::Value::Bool(false))
2407 );
2408 }
2409
2410 #[test]
2411 fn test_is_merging_true_when_merge_flag_set() {
2412 let config = Config::default();
2413 let opts = ContextOptions {
2414 merge: true,
2415 ..Default::default()
2416 };
2417 let mut ctx = Context::new(config, opts);
2418 ctx.git_info = Some(make_git_info(false, None));
2419 ctx.populate_git_vars();
2420
2421 assert_eq!(
2422 ctx.template_vars().get_structured("IsMerging"),
2423 Some(&tera::Value::Bool(true))
2424 );
2425 }
2426
2427 #[test]
2428 fn test_is_merging_false_by_default() {
2429 let config = Config::default();
2430 let mut ctx = Context::new(config, ContextOptions::default());
2431 ctx.git_info = Some(make_git_info(false, None));
2432 ctx.populate_git_vars();
2433
2434 assert_eq!(
2435 ctx.template_vars().get_structured("IsMerging"),
2436 Some(&tera::Value::Bool(false))
2437 );
2438 }
2439
2440 #[test]
2441 fn test_refresh_artifacts_var_empty() {
2442 let config = Config::default();
2443 let mut ctx = Context::new(config, ContextOptions::default());
2444 ctx.refresh_artifacts_var();
2445
2446 let result = ctx
2448 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2449 .unwrap();
2450 assert_eq!(result, "");
2451 }
2452
2453 #[test]
2454 fn test_refresh_artifacts_var_with_artifacts() {
2455 use crate::artifact::{Artifact, ArtifactKind};
2456 use std::collections::HashMap;
2457 use std::path::PathBuf;
2458
2459 let config = Config::default();
2460 let mut ctx = Context::new(config, ContextOptions::default());
2461 ctx.artifacts.add(Artifact {
2465 kind: ArtifactKind::Archive,
2466 name: String::new(),
2467 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2468 target: Some("x86_64-unknown-linux-gnu".to_string()),
2469 crate_name: "myapp".to_string(),
2470 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2471 size: None,
2472 });
2473 ctx.artifacts.add(Artifact {
2474 kind: ArtifactKind::Binary,
2475 name: String::new(),
2476 path: PathBuf::from("dist/myapp"),
2477 target: Some("x86_64-unknown-linux-gnu".to_string()),
2478 crate_name: "myapp".to_string(),
2479 metadata: HashMap::new(),
2480 size: None,
2481 });
2482 ctx.refresh_artifacts_var();
2483
2484 let result = ctx
2486 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2487 .unwrap();
2488 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2489 assert!(result.contains("myapp"));
2490
2491 let result_kinds = ctx
2493 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2494 .unwrap();
2495 assert!(result_kinds.contains("archive"));
2496 assert!(result_kinds.contains("binary"));
2497 }
2498
2499 #[test]
2500 fn test_populate_metadata_var_with_mod_timestamp() {
2501 let mut config = Config::default();
2502 config.metadata = Some(crate::config::MetadataConfig {
2503 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2504 ..Default::default()
2505 });
2506 let mut ctx = Context::new(config, ContextOptions::default());
2507 ctx.populate_metadata_var().unwrap();
2508
2509 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2511 assert_eq!(result, "{{ .CommitTimestamp }}");
2512 }
2513
2514 #[test]
2515 fn test_populate_metadata_var_empty_when_no_config() {
2516 let config = Config::default();
2517 let mut ctx = Context::new(config, ContextOptions::default());
2518 ctx.populate_metadata_var().unwrap();
2519
2520 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2522 assert_eq!(result, "");
2523 }
2524
2525 #[test]
2526 fn test_populate_metadata_var_reads_from_config() {
2527 let mut config = Config::default();
2528 config.metadata = Some(crate::config::MetadataConfig {
2529 description: Some("A test project".to_string()),
2530 homepage: Some("https://example.com".to_string()),
2531 documentation: Some("https://docs.example.com".to_string()),
2532 license: Some("MIT".to_string()),
2533 repository: Some("https://github.com/example/test".to_string()),
2534 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2535 mod_timestamp: Some("1234567890".to_string()),
2536 ..Default::default()
2537 });
2538 let mut ctx = Context::new(config, ContextOptions::default());
2539 ctx.populate_metadata_var().unwrap();
2540
2541 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2542 assert_eq!(desc, "A test project");
2543
2544 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2545 assert_eq!(home, "https://example.com");
2546
2547 let repo = ctx.render_template("{{ Metadata.Repository }}").unwrap();
2548 assert_eq!(repo, "https://github.com/example/test");
2549
2550 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2551 assert_eq!(docs, "https://docs.example.com");
2552
2553 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2554 assert_eq!(lic, "MIT");
2555
2556 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2557 assert_eq!(ts, "1234567890");
2558 }
2559
2560 #[test]
2561 fn test_populate_metadata_var_license_falls_back_to_derived() {
2562 let mut config = Config::default();
2566 config.crates = vec![crate::config::CrateConfig {
2567 name: "anodizer".to_string(),
2568 ..Default::default()
2569 }];
2570 config.derived_metadata.insert(
2571 "anodizer".to_string(),
2572 crate::config::MetadataConfig {
2573 description: Some("Derived desc".to_string()),
2574 homepage: Some("https://derived.example".to_string()),
2575 documentation: Some("https://derived.docs".to_string()),
2576 license: Some("MIT OR Apache-2.0".to_string()),
2577 ..Default::default()
2578 },
2579 );
2580 let mut ctx = Context::new(config, ContextOptions::default());
2581 ctx.populate_metadata_var().unwrap();
2582
2583 assert_eq!(
2584 ctx.render_template("{{ Metadata.License }}").unwrap(),
2585 "MIT OR Apache-2.0"
2586 );
2587 assert_eq!(
2588 ctx.render_template("{{ Metadata.Description }}").unwrap(),
2589 "Derived desc"
2590 );
2591 assert_eq!(
2592 ctx.render_template("{{ Metadata.Homepage }}").unwrap(),
2593 "https://derived.example"
2594 );
2595 assert_eq!(
2596 ctx.render_template("{{ Metadata.Documentation }}").unwrap(),
2597 "https://derived.docs"
2598 );
2599 }
2600
2601 #[test]
2602 fn test_populate_metadata_var_top_level_license_wins_over_derived() {
2603 let mut config = Config::default();
2606 config.crates = vec![crate::config::CrateConfig {
2607 name: "anodizer".to_string(),
2608 ..Default::default()
2609 }];
2610 config.derived_metadata.insert(
2611 "anodizer".to_string(),
2612 crate::config::MetadataConfig {
2613 license: Some("MIT OR Apache-2.0".to_string()),
2614 ..Default::default()
2615 },
2616 );
2617 config.metadata = Some(crate::config::MetadataConfig {
2618 license: Some("GPL-3.0".to_string()),
2619 ..Default::default()
2620 });
2621 let mut ctx = Context::new(config, ContextOptions::default());
2622 ctx.populate_metadata_var().unwrap();
2623
2624 assert_eq!(
2625 ctx.render_template("{{ Metadata.License }}").unwrap(),
2626 "GPL-3.0"
2627 );
2628 }
2629
2630 #[test]
2631 fn test_populate_metadata_var_documentation_renders() {
2632 let mut config = Config::default();
2633 config.metadata = Some(crate::config::MetadataConfig {
2634 documentation: Some("https://docs.rs/anodizer".to_string()),
2635 ..Default::default()
2636 });
2637 let mut ctx = Context::new(config, ContextOptions::default());
2638 ctx.populate_metadata_var().unwrap();
2639
2640 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2641 assert_eq!(docs, "https://docs.rs/anodizer");
2642 }
2643
2644 #[test]
2645 fn test_populate_metadata_var_documentation_empty_when_unset() {
2646 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2647 ctx.populate_metadata_var().unwrap();
2648
2649 let docs = ctx.render_template("{{ Metadata.Documentation }}").unwrap();
2650 assert_eq!(docs, "");
2651 }
2652
2653 #[test]
2654 fn test_populate_metadata_var_full_description_inline() {
2655 use crate::config::ContentSource;
2656 let mut config = Config::default();
2657 config.metadata = Some(crate::config::MetadataConfig {
2658 full_description: Some(ContentSource::Inline(
2659 "A long-form description of the project.".to_string(),
2660 )),
2661 ..Default::default()
2662 });
2663 let mut ctx = Context::new(config, ContextOptions::default());
2664 ctx.populate_metadata_var().unwrap();
2665 let rendered = ctx
2666 .render_template("{{ Metadata.FullDescription }}")
2667 .unwrap();
2668 assert_eq!(rendered, "A long-form description of the project.");
2669 }
2670
2671 #[test]
2672 fn test_populate_metadata_var_full_description_from_file() {
2673 use crate::config::ContentSource;
2674 let tmp = tempfile::tempdir().unwrap();
2675 let desc_path = tmp.path().join("DESCRIPTION.md");
2676 std::fs::write(&desc_path, "read from disk").unwrap();
2677 let mut config = Config::default();
2678 config.metadata = Some(crate::config::MetadataConfig {
2679 full_description: Some(ContentSource::FromFile {
2680 from_file: desc_path.to_string_lossy().into_owned(),
2681 }),
2682 ..Default::default()
2683 });
2684 let mut ctx = Context::new(config, ContextOptions::default());
2685 ctx.populate_metadata_var().unwrap();
2686 let rendered = ctx
2687 .render_template("{{ Metadata.FullDescription }}")
2688 .unwrap();
2689 assert_eq!(rendered, "read from disk");
2690 }
2691
2692 #[test]
2693 fn test_populate_metadata_var_full_description_from_url_resolves() {
2694 use crate::config::ContentSource;
2699 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2700
2701 let body = "long form description body";
2702 let body_len = body.len();
2703 let response: &'static str = Box::leak(
2704 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2705 );
2706 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2707
2708 let mut config = Config::default();
2709 config.metadata = Some(crate::config::MetadataConfig {
2710 full_description: Some(ContentSource::FromUrl {
2711 from_url: format!("http://{addr}/description.md"),
2712 headers: None,
2713 }),
2714 ..Default::default()
2715 });
2716 let mut ctx = Context::new(config, ContextOptions::default());
2717 ctx.populate_metadata_var()
2718 .expect("from_url should resolve through content_source");
2719 let rendered = ctx
2720 .render_template("{{ Metadata.FullDescription }}")
2721 .unwrap();
2722 assert_eq!(rendered, body);
2723 }
2724
2725 #[test]
2726 fn test_populate_metadata_var_commit_author() {
2727 use crate::config::CommitAuthorConfig;
2728 let mut config = Config::default();
2729 config.metadata = Some(crate::config::MetadataConfig {
2730 commit_author: Some(CommitAuthorConfig {
2731 name: Some("Alice Developer".to_string()),
2732 email: Some("alice@example.com".to_string()),
2733 signing: None,
2734 use_github_app_token: false,
2735 }),
2736 ..Default::default()
2737 });
2738 let mut ctx = Context::new(config, ContextOptions::default());
2739 ctx.populate_metadata_var().unwrap();
2740 let name = ctx
2741 .render_template("{{ Metadata.CommitAuthor.Name }}")
2742 .unwrap();
2743 assert_eq!(name, "Alice Developer");
2744 let email = ctx
2745 .render_template("{{ Metadata.CommitAuthor.Email }}")
2746 .unwrap();
2747 assert_eq!(email, "alice@example.com");
2748 }
2749
2750 #[test]
2751 fn test_artifact_id_template_var() {
2752 let mut config = Config::default();
2753 config.project_name = "myapp".to_string();
2754 let mut ctx = Context::new(config, ContextOptions::default());
2755 ctx.template_vars_mut().set("ArtifactID", "default");
2756
2757 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2758 assert_eq!(result, "default");
2759 }
2760
2761 #[test]
2762 fn test_artifact_id_empty_when_not_set() {
2763 let mut config = Config::default();
2764 config.project_name = "myapp".to_string();
2765 let mut ctx = Context::new(config, ContextOptions::default());
2766 ctx.template_vars_mut().set("ArtifactID", "");
2767
2768 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2769 assert_eq!(result, "");
2770 }
2771
2772 #[test]
2773 fn test_pro_vars_rendered_in_templates() {
2774 let mut config = Config::default();
2776 config.tag = Some(crate::config::TagConfig {
2777 tag_prefix: Some("api/".to_string()),
2778 ..Default::default()
2779 });
2780 let opts = ContextOptions {
2781 snapshot: false,
2782 nightly: false,
2783 merge: true,
2784 ..Default::default()
2785 };
2786 let mut ctx = Context::new(config, opts);
2787 ctx.git_info = Some(make_git_info(false, None));
2788 ctx.populate_git_vars();
2789
2790 let result = ctx
2791 .render_template(
2792 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2793 )
2794 .unwrap();
2795 assert_eq!(result, "release-merge-api/v1.2.3");
2796 }
2797
2798 #[test]
2799 fn test_is_release_without_git_info() {
2800 let config = Config::default();
2802 let opts = ContextOptions {
2803 snapshot: false,
2804 nightly: false,
2805 ..Default::default()
2806 };
2807 let mut ctx = Context::new(config, opts);
2808 ctx.populate_git_vars();
2809
2810 assert_eq!(
2811 ctx.template_vars().get_structured("IsRelease"),
2812 Some(&tera::Value::Bool(true))
2813 );
2814 }
2815
2816 #[test]
2817 fn test_is_merging_without_git_info() {
2818 let config = Config::default();
2820 let opts = ContextOptions {
2821 merge: true,
2822 ..Default::default()
2823 };
2824 let mut ctx = Context::new(config, opts);
2825 ctx.populate_git_vars();
2826
2827 assert_eq!(
2828 ctx.template_vars().get_structured("IsMerging"),
2829 Some(&tera::Value::Bool(true))
2830 );
2831 }
2832
2833 #[test]
2843 fn test_monorepo_version_matches_shared_semver_helper() {
2844 let mut config = Config::default();
2845 config.monorepo = Some(crate::config::MonorepoConfig {
2846 tag_prefix: Some("core/".to_string()),
2847 dir: None,
2848 });
2849 let mut ctx = Context::new(config, ContextOptions::default());
2850
2851 let semver = SemVer {
2852 major: 2,
2853 minor: 1,
2854 patch: 0,
2855 prerelease: Some("rc.1".to_string()),
2856 build_metadata: Some("build.7".to_string()),
2857 };
2858 let mut info = make_git_info(false, None);
2859 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2860 info.semver = semver.clone();
2861 ctx.git_info = Some(info);
2862 ctx.populate_git_vars();
2863
2864 let v = ctx.template_vars();
2865 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2868 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2869 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2870 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2871 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2873 }
2874
2875 #[test]
2876 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2877 let mut config = Config::default();
2878 config.monorepo = Some(crate::config::MonorepoConfig {
2879 tag_prefix: Some("subproject1/".to_string()),
2880 dir: None,
2881 });
2882 let mut ctx = Context::new(config, ContextOptions::default());
2883
2884 let mut info = make_git_info(false, None);
2886 info.tag = "subproject1/v1.2.3".to_string();
2887 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2888 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2889 ctx.git_info = Some(info);
2890 ctx.populate_git_vars();
2891
2892 let v = ctx.template_vars();
2893 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2895 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2897 assert_eq!(
2899 v.get("PrefixedTag"),
2900 Some(&"subproject1/v1.2.3".to_string())
2901 );
2902 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2904 assert_eq!(
2906 v.get("PrefixedPreviousTag"),
2907 Some(&"subproject1/v1.2.2".to_string())
2908 );
2909 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2911 assert_eq!(
2913 v.get("PrefixedSummary"),
2914 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2915 );
2916 }
2917
2918 #[test]
2919 fn test_monorepo_prefixed_previous_tag() {
2920 let mut config = Config::default();
2921 config.monorepo = Some(crate::config::MonorepoConfig {
2922 tag_prefix: Some("svc/".to_string()),
2923 dir: None,
2924 });
2925 let mut ctx = Context::new(config, ContextOptions::default());
2926
2927 let mut info = make_git_info(false, None);
2928 info.tag = "svc/v2.0.0".to_string();
2929 info.previous_tag = Some("svc/v1.9.0".to_string());
2930 ctx.git_info = Some(info);
2931 ctx.populate_git_vars();
2932
2933 let v = ctx.template_vars();
2934 assert_eq!(
2936 v.get("PrefixedPreviousTag"),
2937 Some(&"svc/v1.9.0".to_string())
2938 );
2939 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2941 }
2942
2943 #[test]
2944 fn test_no_monorepo_falls_back_to_tag_prefix() {
2945 let mut config = Config::default();
2947 config.tag = Some(crate::config::TagConfig {
2948 tag_prefix: Some("release/".to_string()),
2949 ..Default::default()
2950 });
2951 let mut ctx = Context::new(config, ContextOptions::default());
2952 ctx.git_info = Some(make_git_info(false, None));
2953 ctx.populate_git_vars();
2954
2955 let v = ctx.template_vars();
2956 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2958 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2960 assert_eq!(
2961 v.get("PrefixedPreviousTag"),
2962 Some(&"release/v1.2.2".to_string())
2963 );
2964 }
2965
2966 #[test]
2967 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2968 let mut config = Config::default();
2971 config.tag = Some(crate::config::TagConfig {
2972 tag_prefix: Some("release/".to_string()),
2973 ..Default::default()
2974 });
2975 config.monorepo = Some(crate::config::MonorepoConfig {
2976 tag_prefix: Some("svc/".to_string()),
2977 dir: None,
2978 });
2979 let mut ctx = Context::new(config, ContextOptions::default());
2980
2981 let mut info = make_git_info(false, None);
2982 info.tag = "svc/v1.2.3".to_string();
2983 info.previous_tag = Some("svc/v1.2.2".to_string());
2984 ctx.git_info = Some(info);
2985 ctx.populate_git_vars();
2986
2987 let v = ctx.template_vars();
2988 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2990 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2992 }
2993
2994 #[test]
2995 fn test_monorepo_prefixed_summary() {
2996 let mut config = Config::default();
2997 config.monorepo = Some(crate::config::MonorepoConfig {
2998 tag_prefix: Some("pkg/".to_string()),
2999 dir: None,
3000 });
3001 let mut ctx = Context::new(config, ContextOptions::default());
3002
3003 let mut info = make_git_info(false, None);
3004 info.tag = "pkg/v1.2.3".to_string();
3005 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
3007 ctx.git_info = Some(info);
3008 ctx.populate_git_vars();
3009
3010 assert_eq!(
3012 ctx.template_vars().get("PrefixedSummary"),
3013 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
3014 );
3015 assert_eq!(
3017 ctx.template_vars().get("Summary"),
3018 Some(&"v1.2.3-0-gabc123d".to_string())
3019 );
3020 }
3021
3022 #[test]
3023 fn test_monorepo_no_previous_tag() {
3024 let mut config = Config::default();
3025 config.monorepo = Some(crate::config::MonorepoConfig {
3026 tag_prefix: Some("svc/".to_string()),
3027 dir: None,
3028 });
3029 let mut ctx = Context::new(config, ContextOptions::default());
3030
3031 let mut info = make_git_info(false, None);
3032 info.tag = "svc/v1.0.0".to_string();
3033 info.previous_tag = None;
3034 ctx.git_info = Some(info);
3035 ctx.populate_git_vars();
3036
3037 let v = ctx.template_vars();
3038 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
3039 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
3041 }
3042
3043 #[test]
3048 fn test_monorepo_full_flow_all_vars() {
3049 let mut config = Config::default();
3052 config.project_name = "mymonorepo".to_string();
3053 config.monorepo = Some(crate::config::MonorepoConfig {
3054 tag_prefix: Some("services/api/".to_string()),
3055 dir: Some("services/api".to_string()),
3056 });
3057
3058 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
3060 assert_eq!(config.monorepo_dir(), Some("services/api"));
3061
3062 let mut ctx = Context::new(config, ContextOptions::default());
3063
3064 let mut info = make_git_info(false, None);
3067 info.tag = "services/api/v2.1.0".to_string();
3068 info.previous_tag = Some("services/api/v2.0.5".to_string());
3069 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
3070 info.semver = crate::git::SemVer {
3071 major: 2,
3072 minor: 1,
3073 patch: 0,
3074 prerelease: None,
3075 build_metadata: None,
3076 };
3077 ctx.git_info = Some(info);
3078 ctx.populate_git_vars();
3079
3080 let v = ctx.template_vars();
3081
3082 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
3084 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
3085 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
3086 assert_eq!(v.get("Major"), Some(&"2".to_string()));
3087 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
3088 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
3089 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
3090 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
3091
3092 assert_eq!(
3094 v.get("PrefixedTag"),
3095 Some(&"services/api/v2.1.0".to_string())
3096 );
3097 assert_eq!(
3098 v.get("PrefixedPreviousTag"),
3099 Some(&"services/api/v2.0.5".to_string())
3100 );
3101 assert_eq!(
3102 v.get("PrefixedSummary"),
3103 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
3104 );
3105
3106 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
3108 }
3109
3110 #[test]
3111 fn context_env_var_defaults_to_process_env_source() {
3112 let ctx = Context::new(Config::default(), ContextOptions::default());
3113 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
3115 }
3116
3117 #[test]
3118 fn context_env_var_routes_to_injected_source() {
3119 let mut ctx = Context::new(Config::default(), ContextOptions::default());
3120 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
3121 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
3122 assert_eq!(ctx.env_var("PATH"), None);
3126 }
3127
3128 #[test]
3129 #[serial_test::serial]
3130 fn populate_runtime_vars_sets_rustc_version() {
3131 let config = Config::default();
3132 let mut ctx = Context::new(config, ContextOptions::default());
3133 ctx.populate_runtime_vars();
3136
3137 let ver = ctx
3138 .template_vars()
3139 .get("RustcVersion")
3140 .expect("RustcVersion should be set after populate_runtime_vars");
3141 if !ver.is_empty() {
3145 assert!(
3146 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
3147 "RustcVersion should start with a digit: {ver}"
3148 );
3149 }
3150 }
3151}