1use crate::artifact::ArtifactRegistry;
2use crate::config::Config;
3use crate::env_source::{EnvSource, ProcessEnvSource};
4use crate::git::GitInfo;
5use crate::log::{StageLogger, Verbosity};
6use crate::partial::PartialTarget;
7use crate::publish_report::PublishReport;
8use crate::scm::ScmTokenType;
9use crate::template::TemplateVars;
10use anyhow::Context as _;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum RollbackMode {
24 None,
27 #[default]
32 BestEffort,
33}
34
35pub const VALID_RELEASE_SKIPS: &[&str] = &[
42 "publish",
43 "announce",
44 "sign",
45 "validate",
46 "sbom",
47 "attest",
48 "docker",
49 "docker-sign",
50 "winget",
51 "choco",
52 "snapcraft",
53 "snapcraft-publish",
54 "scoop",
55 "brew",
56 "nix",
57 "aur",
58 "cargo",
59 "krew",
60 "nfpm",
61 "makeself",
62 "appimage",
63 "flatpak",
64 "srpm",
65 "before",
66 "before-publish",
67 "notarize",
68 "archive",
69 "source",
70 "build",
71 "changelog",
72 "release",
73 "checksum",
74 "upx",
75 "blob",
76 "templatefiles",
77 "dmg",
78 "msi",
79 "nsis",
80 "pkg",
81 "appbundle",
82 "verify-release",
83];
84
85pub const VALID_BUILD_SKIPS: &[&str] = &["pre-hooks", "post-hooks", "validate", "before"];
87
88pub fn validate_skip_values(skip: &[String], valid: &[&str]) -> Result<(), String> {
93 let invalid: Vec<&str> = skip
94 .iter()
95 .map(|s| s.as_str())
96 .filter(|s| !valid.contains(s))
97 .collect();
98 if invalid.is_empty() {
99 Ok(())
100 } else {
101 Err(format!(
102 "invalid --skip value(s): {}. Valid options: {}",
103 invalid.join(", "),
104 valid.join(", "),
105 ))
106 }
107}
108
109pub struct ContextOptions {
110 pub snapshot: bool,
111 pub nightly: bool,
112 pub dry_run: bool,
113 pub quiet: bool,
114 pub verbose: bool,
115 pub debug: bool,
116 pub skip_stages: Vec<String>,
117 pub selected_crates: Vec<String>,
118 pub token: Option<String>,
119 pub parallelism: usize,
121 pub single_target: Option<String>,
123 pub release_notes_path: Option<PathBuf>,
125 pub fail_fast: bool,
127 pub partial_target: Option<PartialTarget>,
130 pub merge: bool,
132 pub publish_only: bool,
142 pub project_root: Option<PathBuf>,
145 pub strict: bool,
147 pub resume_release: bool,
152 pub replace_existing_artifacts: bool,
156 pub skip_post_publish_poll: bool,
164 pub gate_submitter: Option<bool>,
172 pub rollback_mode: Option<RollbackMode>,
177 pub simulate_failure_publishers: Vec<String>,
185 pub rollback_only: bool,
191 pub allow_rerun: bool,
206 pub from_run: Option<String>,
210 pub runtime_nondeterministic_allowlist: Vec<(String, String)>,
217 pub summary_json_path: Option<PathBuf>,
221 pub allow_ai_failure: bool,
228 pub changelog_from: Option<String>,
235 pub changelog_full_history: bool,
242 pub changelog_to: Option<String>,
251 pub changelog_preview: bool,
267}
268
269impl Default for ContextOptions {
270 fn default() -> Self {
271 Self {
272 snapshot: false,
273 nightly: false,
274 dry_run: false,
275 quiet: false,
276 verbose: false,
277 debug: false,
278 skip_stages: Vec::new(),
279 selected_crates: Vec::new(),
280 token: None,
281 parallelism: 4,
282 single_target: None,
283 release_notes_path: None,
284 fail_fast: false,
285 partial_target: None,
286 merge: false,
287 publish_only: false,
288 project_root: None,
289 strict: false,
290 resume_release: false,
291 replace_existing_artifacts: false,
292 skip_post_publish_poll: false,
293 gate_submitter: None,
294 rollback_mode: None,
295 simulate_failure_publishers: Vec::new(),
296 rollback_only: false,
297 allow_rerun: false,
298 from_run: None,
299 runtime_nondeterministic_allowlist: Vec::new(),
300 summary_json_path: None,
301 allow_ai_failure: false,
302 changelog_from: None,
303 changelog_full_history: false,
304 changelog_to: None,
305 changelog_preview: false,
306 }
307 }
308}
309
310#[derive(Debug, Default)]
315pub struct StageOutputs {
316 pub github_native_changelog: bool,
320 pub changelogs: HashMap<String, String>,
322 pub changelog_header: Option<String>,
327 pub changelog_footer: Option<String>,
330 pub post_publish_results: Vec<serde_json::Value>,
338}
339
340pub struct Context {
341 pub config: Config,
342 pub artifacts: ArtifactRegistry,
343 pub options: ContextOptions,
344 pub stage_outputs: StageOutputs,
346 template_vars: TemplateVars,
347 pub git_info: Option<GitInfo>,
348 pub token_type: ScmTokenType,
350 pub skip_memento: crate::pipe_skip::SkipMemento,
356 pub publish_report: Option<PublishReport>,
364 pub determinism: Option<crate::DeterminismState>,
371 pub pending_outcome: Option<crate::PublisherOutcome>,
381 built_crate_names: Option<std::collections::HashSet<String>>,
392 env_source: Arc<dyn EnvSource>,
400 #[cfg(feature = "test-helpers")]
408 pub log_capture: Option<crate::log::LogCapture>,
409}
410
411impl Context {
412 pub fn new(config: Config, options: ContextOptions) -> Self {
413 let mut vars = TemplateVars::new();
414 vars.set("ProjectName", &config.project_name);
415 Self {
416 config,
417 artifacts: ArtifactRegistry::new(),
418 options,
419 stage_outputs: StageOutputs::default(),
420 template_vars: vars,
421 git_info: None,
422 token_type: ScmTokenType::GitHub,
423 skip_memento: crate::pipe_skip::SkipMemento::new(),
424 publish_report: None,
425 determinism: None,
426 pending_outcome: None,
427 built_crate_names: None,
428 env_source: Arc::new(ProcessEnvSource),
429 #[cfg(feature = "test-helpers")]
430 log_capture: None,
431 }
432 }
433
434 pub fn env_var(&self, name: &str) -> Option<String> {
442 self.env_source.var(name)
443 }
444
445 pub fn set_env_source<S: EnvSource + 'static>(&mut self, src: S) {
451 self.env_source = Arc::new(src);
452 }
453
454 pub fn env_source(&self) -> &dyn EnvSource {
459 self.env_source.as_ref()
460 }
461
462 pub fn env_source_arc(&self) -> Arc<dyn EnvSource> {
468 Arc::clone(&self.env_source)
469 }
470
471 #[cfg(feature = "test-helpers")]
477 pub fn with_log_capture(&mut self, capture: crate::log::LogCapture) {
478 self.log_capture = Some(capture);
479 }
480
481 pub fn record_publisher_outcome(&mut self, outcome: crate::PublisherOutcome) {
489 self.pending_outcome = Some(outcome);
490 }
491
492 pub fn take_pending_outcome(&mut self) -> Option<crate::PublisherOutcome> {
496 self.pending_outcome.take()
497 }
498
499 pub fn publish_report(&self) -> Option<&PublishReport> {
502 self.publish_report.as_ref()
503 }
504
505 pub fn set_publish_report(&mut self, r: PublishReport) {
511 self.publish_report = Some(r);
512 }
513
514 pub fn built_crate_names(&self) -> Option<&std::collections::HashSet<String>> {
517 self.built_crate_names.as_ref()
518 }
519
520 pub fn set_built_crate_names(&mut self, names: std::collections::HashSet<String>) {
523 self.built_crate_names = Some(names);
524 }
525
526 pub fn remember_skip(&self, stage: &str, label: &str, reason: &str) {
533 self.skip_memento.remember(stage, label, reason);
534 }
535
536 pub fn template_vars(&self) -> &TemplateVars {
537 &self.template_vars
538 }
539
540 pub fn template_vars_mut(&mut self) -> &mut TemplateVars {
541 &mut self.template_vars
542 }
543
544 pub fn render_template(&self, template: &str) -> anyhow::Result<String> {
545 crate::template::render(template, &self.template_vars)
546 }
547
548 pub fn render_template_opt(&self, template: Option<&str>) -> anyhow::Result<Option<String>> {
550 template.map(|t| self.render_template(t)).transpose()
551 }
552
553 pub fn skip_with_log(
562 &self,
563 skip: &Option<crate::config::StringOrBool>,
564 log: &StageLogger,
565 label: &str,
566 ) -> anyhow::Result<bool> {
567 let Some(d) = skip else {
568 return Ok(false);
569 };
570 let should_skip = d
571 .try_evaluates_to_true(|s| self.render_template(s))
572 .with_context(|| format!("evaluate skip expression for {label}"))?;
573 if should_skip {
574 log.status(&format!("{} skipped", label));
575 }
576 Ok(should_skip)
577 }
578
579 pub fn should_skip(&self, stage_name: &str) -> bool {
580 self.options.skip_stages.iter().any(|s| s == stage_name)
581 }
582
583 pub fn skip_validate(&self) -> bool {
585 self.should_skip("validate")
586 }
587
588 pub fn is_dry_run(&self) -> bool {
589 self.options.dry_run
590 }
591
592 pub fn is_snapshot(&self) -> bool {
593 self.options.snapshot
594 }
595
596 pub fn is_strict(&self) -> bool {
597 self.options.strict
598 }
599
600 pub fn strict_guard(&self, log: &crate::log::StageLogger, msg: &str) -> anyhow::Result<()> {
603 if self.options.strict {
604 anyhow::bail!("{} (strict mode)", msg);
605 }
606 log.warn(msg);
607 Ok(())
608 }
609
610 pub fn skip_in_snapshot(&self, log: &crate::log::StageLogger, stage: &str) -> bool {
619 if self.is_snapshot() {
620 log.status(&format!("{}: skipped (snapshot mode)", stage));
621 true
622 } else {
623 false
624 }
625 }
626
627 pub fn render_template_strict(
629 &self,
630 template: &str,
631 label: &str,
632 log: &crate::log::StageLogger,
633 ) -> anyhow::Result<String> {
634 match self.render_template(template) {
635 Ok(rendered) => Ok(rendered),
636 Err(e) => {
637 if self.options.strict {
638 anyhow::bail!("{}: failed to render template: {} (strict mode)", label, e);
639 }
640 log.warn(&format!("{}: failed to render template: {}", label, e));
641 Ok(template.to_string())
642 }
643 }
644 }
645
646 pub fn is_nightly(&self) -> bool {
647 self.options.nightly
648 }
649
650 pub fn set_release_url(&mut self, url: &str) {
655 self.template_vars.set("ReleaseURL", url);
656 }
657
658 pub fn version(&self) -> String {
661 self.template_vars
662 .get("Version")
663 .cloned()
664 .unwrap_or_default()
665 }
666
667 pub fn verbosity(&self) -> Verbosity {
669 Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
670 }
671
672 pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
678 self.config.retry.unwrap_or_default().to_policy()
679 }
680
681 pub fn logger(&self, stage: &'static str) -> StageLogger {
689 #[allow(unused_mut)]
690 let mut log = StageLogger::new(stage, self.verbosity()).with_env(self.env_for_redact());
691 #[cfg(feature = "test-helpers")]
692 if let Some(cap) = &self.log_capture {
693 log = log.with_capture_handle(cap.clone());
694 }
695 log
696 }
697
698 fn env_for_redact(&self) -> Vec<(String, String)> {
704 use std::collections::HashMap;
705 let mut map: HashMap<String, String> = std::env::vars().collect();
706 for (k, v) in self.template_vars.all_env() {
707 map.insert(k.clone(), v.clone());
708 }
709 map.into_iter().collect()
710 }
711
712 pub fn populate_git_vars(&mut self) {
754 if let Some(ref info) = self.git_info {
755 let raw_version = info.semver.raw_version_string();
757
758 let version = info.semver.version_string();
764
765 self.template_vars.set("Tag", &info.tag);
766 self.template_vars.set("Version", &version);
767 self.template_vars.set("RawVersion", &raw_version);
768 self.template_vars.set("Base", &raw_version);
773 self.template_vars
774 .set("Major", &info.semver.major.to_string());
775 self.template_vars
776 .set("Minor", &info.semver.minor.to_string());
777 self.template_vars
778 .set("Patch", &info.semver.patch.to_string());
779 self.template_vars.set(
780 "Prerelease",
781 info.semver.prerelease.as_deref().unwrap_or(""),
782 );
783 self.template_vars.set(
784 "BuildMetadata",
785 info.semver.build_metadata.as_deref().unwrap_or(""),
786 );
787 self.template_vars.set("FullCommit", &info.commit);
788 self.template_vars.set("Commit", &info.commit);
789 self.template_vars.set("ShortCommit", &info.short_commit);
790 self.template_vars.set("Branch", &info.branch);
791 self.template_vars.set("CommitDate", &info.commit_date);
792 self.template_vars
793 .set("CommitTimestamp", &info.commit_timestamp);
794 self.template_vars
795 .set("IsGitDirty", if info.dirty { "true" } else { "false" });
796 self.template_vars
797 .set("IsGitClean", if info.dirty { "false" } else { "true" });
798 self.template_vars
799 .set("GitTreeState", if info.dirty { "dirty" } else { "clean" });
800 self.template_vars.set("GitURL", &info.remote_url);
801 self.template_vars.set("Summary", &info.summary);
802 self.template_vars.set("TagSubject", &info.tag_subject);
803 self.template_vars.set("TagContents", &info.tag_contents);
804 self.template_vars.set("TagBody", &info.tag_body);
805 self.template_vars
806 .set("PreviousTag", info.previous_tag.as_deref().unwrap_or(""));
807 self.template_vars
808 .set("FirstCommit", info.first_commit.as_deref().unwrap_or(""));
809
810 let monorepo_prefix = self.config.monorepo_tag_prefix();
821
822 if let Some(prefix) = monorepo_prefix {
828 self.template_vars.set("PrefixedTag", &info.tag);
831
832 let stripped_tag = crate::git::strip_monorepo_prefix(&info.tag, prefix);
834 self.template_vars.set("Tag", stripped_tag);
835
836 let version = info.semver.version_string();
846 self.template_vars.set("Version", &version);
847
848 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
850 self.template_vars.set("PrefixedPreviousTag", prev_tag);
851
852 let stripped_prev = crate::git::strip_monorepo_prefix(prev_tag, prefix);
854 self.template_vars.set("PreviousTag", stripped_prev);
855
856 self.template_vars.set("PrefixedSummary", &info.summary);
860 let stripped_summary = crate::git::strip_monorepo_prefix(&info.summary, prefix);
862 self.template_vars.set("Summary", stripped_summary);
863 } else {
864 let tag_prefix = self
866 .config
867 .tag
868 .as_ref()
869 .and_then(|t| t.tag_prefix.as_deref())
870 .unwrap_or("");
871 self.template_vars
872 .set("PrefixedTag", &format!("{}{}", tag_prefix, info.tag));
873 let prev_tag = info.previous_tag.as_deref().unwrap_or("");
874 let prefixed_prev = if prev_tag.is_empty() {
875 String::new()
876 } else {
877 format!("{}{}", tag_prefix, prev_tag)
878 };
879 self.template_vars
880 .set("PrefixedPreviousTag", &prefixed_prev);
881 self.template_vars.set(
882 "PrefixedSummary",
883 &format!("{}{}", tag_prefix, info.summary),
884 );
885 }
886 }
887
888 let nightly_build = if self.git_info.is_some() {
901 let root = self
902 .options
903 .project_root
904 .clone()
905 .unwrap_or_else(|| PathBuf::from("."));
906 let monorepo_prefix = self.config.monorepo_tag_prefix();
907 crate::git::count_commits_since_last_tag_in(&root, monorepo_prefix).unwrap_or(0)
908 } else {
909 0
910 };
911 self.template_vars
912 .set("NightlyBuild", &nightly_build.to_string());
913
914 self.template_vars.set(
915 "IsSnapshot",
916 if self.options.snapshot {
917 "true"
918 } else {
919 "false"
920 },
921 );
922 self.template_vars.set(
923 "IsNightly",
924 if self.options.nightly {
925 "true"
926 } else {
927 "false"
928 },
929 );
930 self.template_vars.set(
934 "IsHarness",
935 if self.env_var("ANODIZER_IN_DETERMINISM_HARNESS").is_some() {
936 "true"
937 } else {
938 "false"
939 },
940 );
941 let is_draft = self
943 .config
944 .release
945 .as_ref()
946 .and_then(|r| r.draft)
947 .unwrap_or(false);
948 self.template_vars
949 .set("IsDraft", if is_draft { "true" } else { "false" });
950 self.template_vars.set(
951 "IsSingleTarget",
952 if self.options.single_target.is_some() {
953 "true"
954 } else {
955 "false"
956 },
957 );
958
959 let is_release = !self.options.snapshot && !self.options.nightly;
961 self.template_vars
962 .set("IsRelease", if is_release { "true" } else { "false" });
963
964 self.template_vars.set(
966 "IsMerging",
967 if self.options.merge { "true" } else { "false" },
968 );
969 }
970
971 pub fn populate_time_vars(&mut self) {
999 let now = crate::sde::resolve_now_with_env(self.env_source());
1007 self.template_vars.set("Date", &now.to_rfc3339());
1008 self.template_vars
1009 .set("Timestamp", &now.timestamp().to_string());
1010 self.template_vars.set("Now", &now.to_rfc3339());
1011 self.template_vars
1012 .set("Year", &now.format("%Y").to_string());
1013 self.template_vars
1014 .set("Month", &now.format("%m").to_string());
1015 self.template_vars.set("Day", &now.format("%d").to_string());
1016 self.template_vars
1017 .set("Hour", &now.format("%H").to_string());
1018 self.template_vars
1019 .set("Minute", &now.format("%M").to_string());
1020 }
1021
1022 pub fn populate_runtime_vars(&mut self) {
1031 let goos = map_os_to_goos(std::env::consts::OS);
1032 let goarch = map_arch_to_goarch(std::env::consts::ARCH);
1033 self.template_vars.set("RuntimeGoos", goos);
1034 self.template_vars.set("RuntimeGoarch", goarch);
1035 self.template_vars.set("Runtime_Goos", goos);
1038 self.template_vars.set("Runtime_Goarch", goarch);
1039 self.populate_rustc_vars();
1043 }
1044
1045 fn populate_rustc_vars(&mut self) {
1052 let ver = crate::partial::detect_rustc_version().unwrap_or_default();
1053 self.template_vars.set("RustcVersion", &ver);
1054 }
1055
1056 pub fn populate_release_notes_var(&mut self) {
1064 let notes = self
1066 .config
1067 .crates
1068 .iter()
1069 .find_map(|c| self.stage_outputs.changelogs.get(&c.name))
1070 .cloned()
1071 .unwrap_or_default();
1072 self.template_vars.set("ReleaseNotes", ¬es);
1073 }
1074
1075 pub fn refresh_artifacts_var(&mut self) {
1090 const CSV_LIST_KEYS: &[&str] = &["extra_binaries", "extra_files"];
1095 const JSON_LIST_KEYS: &[&str] = &["Platforms"];
1101
1102 let artifacts_value: Vec<serde_json::Value> = self
1103 .artifacts
1104 .all()
1105 .iter()
1106 .map(|a| {
1107 let mut metadata_map = serde_json::Map::with_capacity(a.metadata.len());
1109 for (k, v) in &a.metadata {
1110 if CSV_LIST_KEYS.contains(&k.as_str()) {
1111 let items: Vec<serde_json::Value> = if v.is_empty() {
1112 Vec::new()
1113 } else {
1114 v.split(',')
1115 .map(|s| serde_json::Value::String(s.to_string()))
1116 .collect()
1117 };
1118 metadata_map.insert(k.clone(), serde_json::Value::Array(items));
1119 } else if JSON_LIST_KEYS.contains(&k.as_str()) {
1120 let parsed = serde_json::from_str::<serde_json::Value>(v)
1124 .unwrap_or_else(|_| serde_json::Value::String(v.clone()));
1125 metadata_map.insert(k.clone(), parsed);
1126 } else {
1127 metadata_map.insert(k.clone(), serde_json::Value::String(v.clone()));
1128 }
1129 }
1130 serde_json::json!({
1131 "name": a.name,
1132 "path": a.path.to_string_lossy(),
1133 "target": a.target.as_deref().unwrap_or(""),
1134 "kind": a.kind.as_str(),
1135 "crate_name": a.crate_name,
1136 "metadata": serde_json::Value::Object(metadata_map),
1137 })
1138 })
1139 .collect();
1140 let tera_value = tera::Value::Array(artifacts_value);
1143 self.template_vars.set_structured("Artifacts", tera_value);
1144 }
1145
1146 pub fn populate_metadata_var(&mut self) -> anyhow::Result<()> {
1159 let (
1162 description,
1163 homepage,
1164 license,
1165 maintainers,
1166 mod_timestamp,
1167 full_desc_src,
1168 commit_author,
1169 ) = {
1170 let meta = self.config.metadata.as_ref();
1171 let description = meta
1172 .and_then(|m| m.description.as_deref())
1173 .unwrap_or("")
1174 .to_string();
1175 let homepage = meta
1176 .and_then(|m| m.homepage.as_deref())
1177 .unwrap_or("")
1178 .to_string();
1179 let license = meta
1180 .and_then(|m| m.license.as_deref())
1181 .unwrap_or("")
1182 .to_string();
1183 let maintainers: Vec<String> = meta
1184 .and_then(|m| m.maintainers.as_ref())
1185 .cloned()
1186 .unwrap_or_default();
1187 let mod_timestamp = meta
1188 .and_then(|m| m.mod_timestamp.as_deref())
1189 .unwrap_or("")
1190 .to_string();
1191 let full_desc_src = meta.and_then(|m| m.full_description.clone());
1192 let commit_author = meta.and_then(|m| m.commit_author.clone());
1193 (
1194 description,
1195 homepage,
1196 license,
1197 maintainers,
1198 mod_timestamp,
1199 full_desc_src,
1200 commit_author,
1201 )
1202 };
1203
1204 let full_description = match full_desc_src {
1210 None => String::new(),
1211 Some(src) => crate::content_source::resolve(&src, "metadata.full_description", self)?,
1212 };
1213
1214 let commit_author_map = serde_json::json!({
1215 "Name": commit_author.as_ref().and_then(|c| c.name.clone()).unwrap_or_default(),
1216 "Email": commit_author.as_ref().and_then(|c| c.email.clone()).unwrap_or_default(),
1217 });
1218
1219 let meta_map = serde_json::json!({
1220 "Description": description,
1221 "Homepage": homepage,
1222 "License": license,
1223 "Maintainers": maintainers,
1224 "ModTimestamp": mod_timestamp,
1225 "FullDescription": full_description,
1226 "CommitAuthor": commit_author_map,
1227 });
1228 self.template_vars.set_structured("Metadata", meta_map);
1230 Ok(())
1231 }
1232}
1233
1234pub fn map_os_to_goos(os: &str) -> &str {
1237 match os {
1238 "macos" => "darwin",
1239 other => other, }
1241}
1242
1243pub fn map_arch_to_goarch(arch: &str) -> &str {
1246 match arch {
1247 "x86_64" => "amd64",
1248 "x86" => "386",
1249 "aarch64" => "arm64",
1250 "powerpc64" => "ppc64",
1251 "s390x" => "s390x",
1252 "mips" => "mips",
1253 "mips64" => "mips64",
1254 "riscv64" => "riscv64",
1255 other => other,
1256 }
1257}
1258
1259#[cfg(test)]
1260#[allow(clippy::field_reassign_with_default)]
1261mod tests {
1262 use super::*;
1263 use crate::config::Config;
1264 use crate::git::{GitInfo, SemVer};
1265
1266 fn make_git_info(dirty: bool, prerelease: Option<&str>) -> GitInfo {
1267 let tag = match prerelease {
1268 Some(pre) => format!("v1.2.3-{pre}"),
1269 None => "v1.2.3".to_string(),
1270 };
1271 GitInfo {
1272 tag,
1273 commit: "abc123def456abc123def456abc123def456abc1".to_string(),
1274 short_commit: "abc123d".to_string(),
1275 branch: "main".to_string(),
1276 dirty,
1277 semver: SemVer {
1278 major: 1,
1279 minor: 2,
1280 patch: 3,
1281 prerelease: prerelease.map(|s| s.to_string()),
1282 build_metadata: None,
1283 },
1284 commit_date: "2026-03-25T10:30:00+00:00".to_string(),
1285 commit_timestamp: "1774463400".to_string(),
1286 previous_tag: Some("v1.2.2".to_string()),
1287 remote_url: "https://github.com/test/repo.git".to_string(),
1288 summary: "v1.2.3-0-gabc123d".to_string(),
1289 tag_subject: "Release v1.2.3".to_string(),
1290 tag_contents: "Release v1.2.3\n\nFull release notes here.".to_string(),
1291 tag_body: "Full release notes here.".to_string(),
1292 first_commit: None,
1293 }
1294 }
1295
1296 #[test]
1297 fn test_context_template_vars() {
1298 let mut config = Config::default();
1299 config.project_name = "test-project".to_string();
1300 let ctx = Context::new(config, ContextOptions::default());
1301 assert_eq!(
1302 ctx.template_vars().get("ProjectName"),
1303 Some(&"test-project".to_string())
1304 );
1305 }
1306
1307 #[test]
1308 fn test_context_should_skip() {
1309 let config = Config::default();
1310 let opts = ContextOptions {
1311 skip_stages: vec!["publish".to_string(), "announce".to_string()],
1312 ..Default::default()
1313 };
1314 let ctx = Context::new(config, opts);
1315 assert!(ctx.should_skip("publish"));
1316 assert!(ctx.should_skip("announce"));
1317 assert!(!ctx.should_skip("build"));
1318 }
1319
1320 #[test]
1321 fn test_context_render_template() {
1322 let mut config = Config::default();
1323 config.project_name = "myapp".to_string();
1324 let ctx = Context::new(config, ContextOptions::default());
1325 let result = ctx.render_template("{{ .ProjectName }}-release").unwrap();
1326 assert_eq!(result, "myapp-release");
1327 }
1328
1329 #[test]
1330 fn test_populate_git_vars_sets_all_expected_vars() {
1331 let config = Config::default();
1332 let mut ctx = Context::new(config, ContextOptions::default());
1333 ctx.git_info = Some(make_git_info(false, None));
1334 ctx.populate_git_vars();
1335
1336 let v = ctx.template_vars();
1337 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
1338 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
1339 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1340 assert_eq!(v.get("Major"), Some(&"1".to_string()));
1341 assert_eq!(v.get("Minor"), Some(&"2".to_string()));
1342 assert_eq!(v.get("Patch"), Some(&"3".to_string()));
1343 assert_eq!(v.get("Prerelease"), Some(&"".to_string()));
1344 assert_eq!(
1345 v.get("FullCommit"),
1346 Some(&"abc123def456abc123def456abc123def456abc1".to_string())
1347 );
1348 assert_eq!(v.get("ShortCommit"), Some(&"abc123d".to_string()));
1349 assert_eq!(v.get("Branch"), Some(&"main".to_string()));
1350 assert_eq!(
1351 v.get("CommitDate"),
1352 Some(&"2026-03-25T10:30:00+00:00".to_string())
1353 );
1354 assert_eq!(v.get("CommitTimestamp"), Some(&"1774463400".to_string()));
1355 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
1356 assert_eq!(v.get("Base"), Some(&"1.2.3".to_string()));
1359 }
1360
1361 #[test]
1362 fn test_nightly_build_defaults_to_zero_without_git_info() {
1363 let config = Config::default();
1366 let mut ctx = Context::new(config, ContextOptions::default());
1367 ctx.git_info = None;
1368 ctx.populate_git_vars();
1369 assert_eq!(
1370 ctx.template_vars().get("NightlyBuild"),
1371 Some(&"0".to_string())
1372 );
1373 }
1374
1375 #[test]
1376 fn test_commit_is_alias_for_full_commit() {
1377 let config = Config::default();
1378 let mut ctx = Context::new(config, ContextOptions::default());
1379 ctx.git_info = Some(make_git_info(false, None));
1380 ctx.populate_git_vars();
1381
1382 let v = ctx.template_vars();
1383 assert_eq!(v.get("Commit"), v.get("FullCommit"));
1384 }
1385
1386 #[test]
1387 fn test_populate_git_vars_prerelease() {
1388 let config = Config::default();
1389 let mut ctx = Context::new(config, ContextOptions::default());
1390 ctx.git_info = Some(make_git_info(false, Some("rc.1")));
1391 ctx.populate_git_vars();
1392
1393 let v = ctx.template_vars();
1394 assert_eq!(v.get("Version"), Some(&"1.2.3-rc.1".to_string()));
1395 assert_eq!(v.get("RawVersion"), Some(&"1.2.3".to_string()));
1396 assert_eq!(v.get("Prerelease"), Some(&"rc.1".to_string()));
1397 }
1398
1399 #[test]
1400 fn test_build_metadata_template_var() {
1401 let config = Config::default();
1402 let mut ctx = Context::new(config, ContextOptions::default());
1403 let mut info = make_git_info(false, None);
1404 info.tag = "v1.2.3+build.42".to_string();
1405 info.semver.build_metadata = Some("build.42".to_string());
1406 ctx.git_info = Some(info);
1407 ctx.populate_git_vars();
1408
1409 let v = ctx.template_vars();
1410 assert_eq!(v.get("BuildMetadata"), Some(&"build.42".to_string()));
1411 assert_eq!(v.get("Version"), Some(&"1.2.3+build.42".to_string()));
1413 }
1414
1415 #[test]
1416 fn test_build_metadata_empty_when_none() {
1417 let config = Config::default();
1418 let mut ctx = Context::new(config, ContextOptions::default());
1419 ctx.git_info = Some(make_git_info(false, None));
1420 ctx.populate_git_vars();
1421
1422 assert_eq!(
1423 ctx.template_vars().get("BuildMetadata"),
1424 Some(&"".to_string())
1425 );
1426 }
1427
1428 #[test]
1429 fn test_populate_git_vars_monorepo_prefixed_tag() {
1430 let config = Config::default();
1433 let mut ctx = Context::new(config, ContextOptions::default());
1434 let mut info = make_git_info(false, None);
1435 info.tag = "core-v0.3.2".to_string();
1436 info.semver = SemVer {
1437 major: 0,
1438 minor: 3,
1439 patch: 2,
1440 prerelease: None,
1441 build_metadata: None,
1442 };
1443 ctx.git_info = Some(info);
1444 ctx.populate_git_vars();
1445
1446 let v = ctx.template_vars();
1447 assert_eq!(v.get("Tag"), Some(&"core-v0.3.2".to_string()));
1448 assert_eq!(v.get("Version"), Some(&"0.3.2".to_string()));
1449 assert_eq!(v.get("RawVersion"), Some(&"0.3.2".to_string()));
1450 assert_eq!(v.get("Major"), Some(&"0".to_string()));
1451 assert_eq!(v.get("Minor"), Some(&"3".to_string()));
1452 assert_eq!(v.get("Patch"), Some(&"2".to_string()));
1453 }
1454
1455 #[test]
1456 fn test_populate_git_vars_monorepo_prefixed_tag_with_prerelease() {
1457 let config = Config::default();
1458 let mut ctx = Context::new(config, ContextOptions::default());
1459 let mut info = make_git_info(false, None);
1460 info.tag = "operator-v1.0.0-rc.1".to_string();
1461 info.semver = SemVer {
1462 major: 1,
1463 minor: 0,
1464 patch: 0,
1465 prerelease: Some("rc.1".to_string()),
1466 build_metadata: None,
1467 };
1468 ctx.git_info = Some(info);
1469 ctx.populate_git_vars();
1470
1471 let v = ctx.template_vars();
1472 assert_eq!(v.get("Tag"), Some(&"operator-v1.0.0-rc.1".to_string()));
1473 assert_eq!(v.get("Version"), Some(&"1.0.0-rc.1".to_string()));
1474 assert_eq!(v.get("RawVersion"), Some(&"1.0.0".to_string()));
1475 }
1476
1477 #[test]
1478 fn test_git_tree_state_clean() {
1479 let config = Config::default();
1480 let mut ctx = Context::new(config, ContextOptions::default());
1481 ctx.git_info = Some(make_git_info(false, None));
1482 ctx.populate_git_vars();
1483
1484 let v = ctx.template_vars();
1485 assert_eq!(v.get("IsGitDirty"), Some(&"false".to_string()));
1486 assert_eq!(v.get("GitTreeState"), Some(&"clean".to_string()));
1487 }
1488
1489 #[test]
1490 fn test_git_tree_state_dirty() {
1491 let config = Config::default();
1492 let mut ctx = Context::new(config, ContextOptions::default());
1493 ctx.git_info = Some(make_git_info(true, None));
1494 ctx.populate_git_vars();
1495
1496 let v = ctx.template_vars();
1497 assert_eq!(v.get("IsGitDirty"), Some(&"true".to_string()));
1498 assert_eq!(v.get("GitTreeState"), Some(&"dirty".to_string()));
1499 }
1500
1501 #[test]
1502 fn test_is_snapshot_reflects_context_options() {
1503 let config = Config::default();
1504 let opts = ContextOptions {
1505 snapshot: true,
1506 ..Default::default()
1507 };
1508 let mut ctx = Context::new(config, opts);
1509 ctx.git_info = Some(make_git_info(false, None));
1510 ctx.populate_git_vars();
1511
1512 assert_eq!(
1513 ctx.template_vars().get("IsSnapshot"),
1514 Some(&"true".to_string())
1515 );
1516
1517 let config2 = Config::default();
1519 let opts2 = ContextOptions {
1520 snapshot: false,
1521 ..Default::default()
1522 };
1523 let mut ctx2 = Context::new(config2, opts2);
1524 ctx2.git_info = Some(make_git_info(false, None));
1525 ctx2.populate_git_vars();
1526
1527 assert_eq!(
1528 ctx2.template_vars().get("IsSnapshot"),
1529 Some(&"false".to_string())
1530 );
1531 }
1532
1533 #[test]
1534 fn test_is_draft_defaults_to_false() {
1535 let config = Config::default();
1536 let mut ctx = Context::new(config, ContextOptions::default());
1537 ctx.git_info = Some(make_git_info(false, None));
1538 ctx.populate_git_vars();
1539
1540 assert_eq!(
1541 ctx.template_vars().get("IsDraft"),
1542 Some(&"false".to_string())
1543 );
1544 }
1545
1546 #[test]
1547 fn test_previous_tag_empty_when_none() {
1548 let config = Config::default();
1549 let mut ctx = Context::new(config, ContextOptions::default());
1550 let mut info = make_git_info(false, None);
1551 info.previous_tag = None;
1552 ctx.git_info = Some(info);
1553 ctx.populate_git_vars();
1554
1555 assert_eq!(
1556 ctx.template_vars().get("PreviousTag"),
1557 Some(&"".to_string())
1558 );
1559 }
1560
1561 #[test]
1570 fn populate_time_vars_uses_source_date_epoch_when_set() {
1571 let env = crate::MapEnvSource::new().with("SOURCE_DATE_EPOCH", "1715000000");
1575 let config = Config::default();
1576 let mut ctx = Context::new(config, ContextOptions::default());
1577 ctx.set_env_source(env);
1578 ctx.populate_time_vars();
1579
1580 let v = ctx.template_vars();
1581 assert_eq!(
1582 v.get("Timestamp"),
1583 Some(&"1715000000".to_string()),
1584 "Timestamp must equal SOURCE_DATE_EPOCH seconds"
1585 );
1586 assert_eq!(
1587 v.get("Date"),
1588 Some(&"2024-05-06T12:53:20+00:00".to_string()),
1589 "Date must be RFC 3339 derived from SDE"
1590 );
1591 assert_eq!(v.get("Year"), Some(&"2024".to_string()));
1592 assert_eq!(v.get("Month"), Some(&"05".to_string()));
1593 assert_eq!(v.get("Day"), Some(&"06".to_string()));
1594 }
1595
1596 #[test]
1597 fn test_populate_time_vars() {
1598 let env = crate::MapEnvSource::new();
1601 let config = Config::default();
1602 let mut ctx = Context::new(config, ContextOptions::default());
1603 ctx.set_env_source(env);
1604 ctx.populate_time_vars();
1605
1606 let v = ctx.template_vars();
1607
1608 let date = v
1610 .get("Date")
1611 .unwrap_or_else(|| panic!("Date should be set"));
1612 assert!(
1613 date.contains('T') && date.len() > 10,
1614 "Date should be RFC 3339, got: {date}"
1615 );
1616
1617 let ts = v
1619 .get("Timestamp")
1620 .unwrap_or_else(|| panic!("Timestamp should be set"));
1621 assert!(
1622 ts.parse::<i64>().is_ok(),
1623 "Timestamp should be a numeric string, got: {ts}"
1624 );
1625
1626 let now = v.get("Now").unwrap_or_else(|| panic!("Now should be set"));
1628 assert!(now.contains('T'), "Now should be ISO 8601, got: {now}");
1629 }
1630
1631 #[test]
1632 fn test_env_vars_accessible_in_templates() {
1633 let mut config = Config::default();
1634 config.project_name = "myapp".to_string();
1635 let mut ctx = Context::new(config, ContextOptions::default());
1636 ctx.template_vars_mut().set_env("MY_VAR", "hello-world");
1637 ctx.template_vars_mut().set_env("DEPLOY_ENV", "staging");
1638
1639 let result = ctx
1640 .render_template("{{ .Env.MY_VAR }}-{{ .Env.DEPLOY_ENV }}")
1641 .unwrap();
1642 assert_eq!(result, "hello-world-staging");
1643 }
1644
1645 #[test]
1646 fn test_populate_git_vars_without_git_info_still_sets_snapshot() {
1647 let config = Config::default();
1648 let opts = ContextOptions {
1649 snapshot: true,
1650 ..Default::default()
1651 };
1652 let mut ctx = Context::new(config, opts);
1653 ctx.populate_git_vars();
1655
1656 assert_eq!(
1657 ctx.template_vars().get("IsSnapshot"),
1658 Some(&"true".to_string())
1659 );
1660 assert_eq!(
1661 ctx.template_vars().get("IsDraft"),
1662 Some(&"false".to_string())
1663 );
1664 assert_eq!(ctx.template_vars().get("Tag"), None);
1666 }
1667
1668 #[test]
1669 fn test_is_nightly_set_when_nightly_mode_active() {
1670 let config = Config::default();
1671 let opts = ContextOptions {
1672 nightly: true,
1673 ..Default::default()
1674 };
1675 let mut ctx = Context::new(config, opts);
1676 ctx.git_info = Some(make_git_info(false, None));
1677 ctx.populate_git_vars();
1678
1679 assert_eq!(
1680 ctx.template_vars().get("IsNightly"),
1681 Some(&"true".to_string()),
1682 "IsNightly should be 'true' when nightly mode is active"
1683 );
1684 assert!(ctx.is_nightly(), "is_nightly() should return true");
1685 }
1686
1687 #[test]
1688 fn test_is_nightly_false_by_default() {
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("IsNightly"),
1696 Some(&"false".to_string()),
1697 "IsNightly should default to 'false'"
1698 );
1699 assert!(
1700 !ctx.is_nightly(),
1701 "is_nightly() should return false by default"
1702 );
1703 }
1704
1705 #[test]
1706 fn test_version_returns_populated_value() {
1707 let config = Config::default();
1708 let mut ctx = Context::new(config, ContextOptions::default());
1709 ctx.git_info = Some(make_git_info(false, None));
1710 ctx.populate_git_vars();
1711
1712 assert_eq!(ctx.version(), "1.2.3");
1713 }
1714
1715 #[test]
1716 fn test_version_returns_empty_when_not_set() {
1717 let config = Config::default();
1718 let ctx = Context::new(config, ContextOptions::default());
1719 assert_eq!(ctx.version(), "");
1720 }
1721
1722 #[test]
1723 fn test_is_nightly_without_git_info() {
1724 let config = Config::default();
1725 let opts = ContextOptions {
1726 nightly: true,
1727 ..Default::default()
1728 };
1729 let mut ctx = Context::new(config, opts);
1730 ctx.populate_git_vars();
1732
1733 assert_eq!(
1734 ctx.template_vars().get("IsNightly"),
1735 Some(&"true".to_string()),
1736 "IsNightly should be set even without git info"
1737 );
1738 }
1739
1740 #[test]
1741 fn test_is_git_clean_when_not_dirty() {
1742 let config = Config::default();
1743 let mut ctx = Context::new(config, ContextOptions::default());
1744 ctx.git_info = Some(make_git_info(false, None));
1745 ctx.populate_git_vars();
1746
1747 assert_eq!(
1748 ctx.template_vars().get("IsGitClean"),
1749 Some(&"true".to_string())
1750 );
1751 }
1752
1753 #[test]
1754 fn test_is_git_clean_when_dirty() {
1755 let config = Config::default();
1756 let mut ctx = Context::new(config, ContextOptions::default());
1757 ctx.git_info = Some(make_git_info(true, None));
1758 ctx.populate_git_vars();
1759
1760 assert_eq!(
1761 ctx.template_vars().get("IsGitClean"),
1762 Some(&"false".to_string())
1763 );
1764 }
1765
1766 #[test]
1767 fn test_git_url_set_from_git_info() {
1768 let config = Config::default();
1769 let mut ctx = Context::new(config, ContextOptions::default());
1770 ctx.git_info = Some(make_git_info(false, None));
1771 ctx.populate_git_vars();
1772
1773 assert_eq!(
1774 ctx.template_vars().get("GitURL"),
1775 Some(&"https://github.com/test/repo.git".to_string())
1776 );
1777 }
1778
1779 #[test]
1780 fn test_summary_set_from_git_info() {
1781 let config = Config::default();
1782 let mut ctx = Context::new(config, ContextOptions::default());
1783 ctx.git_info = Some(make_git_info(false, None));
1784 ctx.populate_git_vars();
1785
1786 assert_eq!(
1787 ctx.template_vars().get("Summary"),
1788 Some(&"v1.2.3-0-gabc123d".to_string())
1789 );
1790 }
1791
1792 #[test]
1793 fn test_tag_subject_set_from_git_info() {
1794 let config = Config::default();
1795 let mut ctx = Context::new(config, ContextOptions::default());
1796 ctx.git_info = Some(make_git_info(false, None));
1797 ctx.populate_git_vars();
1798
1799 assert_eq!(
1800 ctx.template_vars().get("TagSubject"),
1801 Some(&"Release v1.2.3".to_string())
1802 );
1803 }
1804
1805 #[test]
1806 fn test_tag_contents_set_from_git_info() {
1807 let config = Config::default();
1808 let mut ctx = Context::new(config, ContextOptions::default());
1809 ctx.git_info = Some(make_git_info(false, None));
1810 ctx.populate_git_vars();
1811
1812 assert_eq!(
1813 ctx.template_vars().get("TagContents"),
1814 Some(&"Release v1.2.3\n\nFull release notes here.".to_string())
1815 );
1816 }
1817
1818 #[test]
1819 fn test_tag_body_set_from_git_info() {
1820 let config = Config::default();
1821 let mut ctx = Context::new(config, ContextOptions::default());
1822 ctx.git_info = Some(make_git_info(false, None));
1823 ctx.populate_git_vars();
1824
1825 assert_eq!(
1826 ctx.template_vars().get("TagBody"),
1827 Some(&"Full release notes here.".to_string())
1828 );
1829 }
1830
1831 #[test]
1832 fn test_is_single_target_false_by_default() {
1833 let config = Config::default();
1834 let mut ctx = Context::new(config, ContextOptions::default());
1835 ctx.git_info = Some(make_git_info(false, None));
1836 ctx.populate_git_vars();
1837
1838 assert_eq!(
1839 ctx.template_vars().get("IsSingleTarget"),
1840 Some(&"false".to_string())
1841 );
1842 }
1843
1844 #[test]
1845 fn test_is_single_target_true_when_set() {
1846 let config = Config::default();
1847 let opts = ContextOptions {
1848 single_target: Some("x86_64-unknown-linux-gnu".to_string()),
1849 ..Default::default()
1850 };
1851 let mut ctx = Context::new(config, opts);
1852 ctx.git_info = Some(make_git_info(false, None));
1853 ctx.populate_git_vars();
1854
1855 assert_eq!(
1856 ctx.template_vars().get("IsSingleTarget"),
1857 Some(&"true".to_string())
1858 );
1859 }
1860
1861 #[test]
1862 #[serial_test::serial]
1863 fn test_populate_runtime_vars() {
1864 let config = Config::default();
1865 let mut ctx = Context::new(config, ContextOptions::default());
1866 ctx.populate_runtime_vars();
1867
1868 let v = ctx.template_vars();
1869
1870 let goos = v
1871 .get("RuntimeGoos")
1872 .unwrap_or_else(|| panic!("RuntimeGoos should be set"));
1873 assert!(
1874 !goos.is_empty(),
1875 "RuntimeGoos should not be empty, got: {goos}"
1876 );
1877 assert_eq!(goos, map_os_to_goos(std::env::consts::OS));
1879
1880 let goarch = v
1881 .get("RuntimeGoarch")
1882 .unwrap_or_else(|| panic!("RuntimeGoarch should be set"));
1883 assert!(
1884 !goarch.is_empty(),
1885 "RuntimeGoarch should not be empty, got: {goarch}"
1886 );
1887 assert_eq!(goarch, map_arch_to_goarch(std::env::consts::ARCH));
1889 }
1890
1891 #[test]
1892 fn test_populate_release_notes_var_with_changelogs() {
1893 let mut config = Config::default();
1894 config.crates.push(crate::config::CrateConfig {
1895 name: "my-crate".to_string(),
1896 ..Default::default()
1897 });
1898 let mut ctx = Context::new(config, ContextOptions::default());
1899 ctx.stage_outputs
1900 .changelogs
1901 .insert("my-crate".to_string(), "## Changes\n- fix bug".to_string());
1902 ctx.populate_release_notes_var();
1903
1904 assert_eq!(
1905 ctx.template_vars().get("ReleaseNotes"),
1906 Some(&"## Changes\n- fix bug".to_string())
1907 );
1908 }
1909
1910 #[test]
1911 fn test_populate_release_notes_var_empty_when_no_changelogs() {
1912 let config = Config::default();
1913 let mut ctx = Context::new(config, ContextOptions::default());
1914 ctx.populate_release_notes_var();
1915
1916 assert_eq!(
1917 ctx.template_vars().get("ReleaseNotes"),
1918 Some(&"".to_string())
1919 );
1920 }
1921
1922 #[test]
1923 fn test_populate_release_notes_var_deterministic_with_multiple_crates() {
1924 let mut config = Config::default();
1925 config.crates.push(crate::config::CrateConfig {
1926 name: "crate-a".to_string(),
1927 ..Default::default()
1928 });
1929 config.crates.push(crate::config::CrateConfig {
1930 name: "crate-b".to_string(),
1931 ..Default::default()
1932 });
1933 let mut ctx = Context::new(config, ContextOptions::default());
1934 ctx.stage_outputs
1935 .changelogs
1936 .insert("crate-a".to_string(), "notes-a".to_string());
1937 ctx.stage_outputs
1938 .changelogs
1939 .insert("crate-b".to_string(), "notes-b".to_string());
1940 ctx.populate_release_notes_var();
1941
1942 assert_eq!(
1944 ctx.template_vars().get("ReleaseNotes"),
1945 Some(&"notes-a".to_string())
1946 );
1947 }
1948
1949 #[test]
1950 fn test_outputs_accessible_in_templates() {
1951 let mut config = Config::default();
1952 config.project_name = "myapp".to_string();
1953 let mut ctx = Context::new(config, ContextOptions::default());
1954 ctx.template_vars_mut().set_output("build_id", "abc123");
1955 ctx.template_vars_mut()
1956 .set_output("deploy_url", "https://example.com");
1957
1958 let result = ctx
1959 .render_template("{{ .Outputs.build_id }}-{{ .Outputs.deploy_url }}")
1960 .unwrap();
1961 assert_eq!(result, "abc123-https://example.com");
1962 }
1963
1964 #[test]
1965 fn test_artifact_ext_and_target_template_vars() {
1966 let mut config = Config::default();
1967 config.project_name = "myapp".to_string();
1968 let mut ctx = Context::new(config, ContextOptions::default());
1969 ctx.template_vars_mut().set("ArtifactName", "myapp.tar.gz");
1970 ctx.template_vars_mut().set("ArtifactExt", ".tar.gz");
1971 ctx.template_vars_mut()
1972 .set("Target", "x86_64-unknown-linux-gnu");
1973
1974 let result = ctx
1975 .render_template("{{ .ArtifactExt }}_{{ .Target }}")
1976 .unwrap();
1977 assert_eq!(result, ".tar.gz_x86_64-unknown-linux-gnu");
1978 }
1979
1980 #[test]
1981 fn test_checksums_template_var() {
1982 let mut config = Config::default();
1983 config.project_name = "myapp".to_string();
1984 let mut ctx = Context::new(config, ContextOptions::default());
1985 let checksum_text = "abc123 myapp.tar.gz\ndef456 myapp.zip\n";
1986 ctx.template_vars_mut().set("Checksums", checksum_text);
1987
1988 let result = ctx.render_template("{{ .Checksums }}").unwrap();
1989 assert_eq!(result, checksum_text);
1990 }
1991
1992 #[test]
1995 fn test_prefixed_tag_with_tag_prefix() {
1996 let mut config = Config::default();
1997 config.tag = Some(crate::config::TagConfig {
1998 tag_prefix: Some("api/".to_string()),
1999 ..Default::default()
2000 });
2001 let mut ctx = Context::new(config, ContextOptions::default());
2002 ctx.git_info = Some(make_git_info(false, None));
2003 ctx.populate_git_vars();
2004
2005 assert_eq!(
2006 ctx.template_vars().get("PrefixedTag"),
2007 Some(&"api/v1.2.3".to_string())
2008 );
2009 }
2010
2011 #[test]
2012 fn test_prefixed_tag_without_tag_prefix() {
2013 let config = Config::default();
2014 let mut ctx = Context::new(config, ContextOptions::default());
2015 ctx.git_info = Some(make_git_info(false, None));
2016 ctx.populate_git_vars();
2017
2018 assert_eq!(
2020 ctx.template_vars().get("PrefixedTag"),
2021 Some(&"v1.2.3".to_string())
2022 );
2023 }
2024
2025 #[test]
2026 fn test_prefixed_previous_tag_with_tag_prefix() {
2027 let mut config = Config::default();
2028 config.tag = Some(crate::config::TagConfig {
2029 tag_prefix: Some("api/".to_string()),
2030 ..Default::default()
2031 });
2032 let mut ctx = Context::new(config, ContextOptions::default());
2033 ctx.git_info = Some(make_git_info(false, None));
2034 ctx.populate_git_vars();
2035
2036 assert_eq!(
2037 ctx.template_vars().get("PrefixedPreviousTag"),
2038 Some(&"api/v1.2.2".to_string())
2039 );
2040 }
2041
2042 #[test]
2043 fn test_prefixed_previous_tag_empty_when_no_previous() {
2044 let mut config = Config::default();
2045 config.tag = Some(crate::config::TagConfig {
2046 tag_prefix: Some("api/".to_string()),
2047 ..Default::default()
2048 });
2049 let mut ctx = Context::new(config, ContextOptions::default());
2050 let mut info = make_git_info(false, None);
2051 info.previous_tag = None;
2052 ctx.git_info = Some(info);
2053 ctx.populate_git_vars();
2054
2055 assert_eq!(
2058 ctx.template_vars().get("PrefixedPreviousTag"),
2059 Some(&"".to_string())
2060 );
2061 }
2062
2063 #[test]
2064 fn test_prefixed_summary_with_tag_prefix() {
2065 let mut config = Config::default();
2066 config.tag = Some(crate::config::TagConfig {
2067 tag_prefix: Some("api/".to_string()),
2068 ..Default::default()
2069 });
2070 let mut ctx = Context::new(config, ContextOptions::default());
2071 ctx.git_info = Some(make_git_info(false, None));
2072 ctx.populate_git_vars();
2073
2074 assert_eq!(
2075 ctx.template_vars().get("PrefixedSummary"),
2076 Some(&"api/v1.2.3-0-gabc123d".to_string())
2077 );
2078 }
2079
2080 #[test]
2081 fn test_is_release_true_for_normal_release() {
2082 let config = Config::default();
2083 let opts = ContextOptions {
2084 snapshot: false,
2085 nightly: false,
2086 ..Default::default()
2087 };
2088 let mut ctx = Context::new(config, opts);
2089 ctx.git_info = Some(make_git_info(false, None));
2090 ctx.populate_git_vars();
2091
2092 assert_eq!(
2093 ctx.template_vars().get("IsRelease"),
2094 Some(&"true".to_string())
2095 );
2096 }
2097
2098 #[test]
2099 fn test_is_release_false_for_snapshot() {
2100 let config = Config::default();
2101 let opts = ContextOptions {
2102 snapshot: true,
2103 ..Default::default()
2104 };
2105 let mut ctx = Context::new(config, opts);
2106 ctx.git_info = Some(make_git_info(false, None));
2107 ctx.populate_git_vars();
2108
2109 assert_eq!(
2110 ctx.template_vars().get("IsRelease"),
2111 Some(&"false".to_string())
2112 );
2113 }
2114
2115 #[test]
2116 fn test_is_release_false_for_nightly() {
2117 let config = Config::default();
2118 let opts = ContextOptions {
2119 nightly: true,
2120 ..Default::default()
2121 };
2122 let mut ctx = Context::new(config, opts);
2123 ctx.git_info = Some(make_git_info(false, None));
2124 ctx.populate_git_vars();
2125
2126 assert_eq!(
2127 ctx.template_vars().get("IsRelease"),
2128 Some(&"false".to_string())
2129 );
2130 }
2131
2132 #[test]
2133 fn test_is_merging_true_when_merge_flag_set() {
2134 let config = Config::default();
2135 let opts = ContextOptions {
2136 merge: true,
2137 ..Default::default()
2138 };
2139 let mut ctx = Context::new(config, opts);
2140 ctx.git_info = Some(make_git_info(false, None));
2141 ctx.populate_git_vars();
2142
2143 assert_eq!(
2144 ctx.template_vars().get("IsMerging"),
2145 Some(&"true".to_string())
2146 );
2147 }
2148
2149 #[test]
2150 fn test_is_merging_false_by_default() {
2151 let config = Config::default();
2152 let mut ctx = Context::new(config, ContextOptions::default());
2153 ctx.git_info = Some(make_git_info(false, None));
2154 ctx.populate_git_vars();
2155
2156 assert_eq!(
2157 ctx.template_vars().get("IsMerging"),
2158 Some(&"false".to_string())
2159 );
2160 }
2161
2162 #[test]
2163 fn test_refresh_artifacts_var_empty() {
2164 let config = Config::default();
2165 let mut ctx = Context::new(config, ContextOptions::default());
2166 ctx.refresh_artifacts_var();
2167
2168 let result = ctx
2170 .render_template("{% for a in Artifacts %}{{ a.name }}{% endfor %}")
2171 .unwrap();
2172 assert_eq!(result, "");
2173 }
2174
2175 #[test]
2176 fn test_refresh_artifacts_var_with_artifacts() {
2177 use crate::artifact::{Artifact, ArtifactKind};
2178 use std::collections::HashMap;
2179 use std::path::PathBuf;
2180
2181 let config = Config::default();
2182 let mut ctx = Context::new(config, ContextOptions::default());
2183 ctx.artifacts.add(Artifact {
2187 kind: ArtifactKind::Archive,
2188 name: String::new(),
2189 path: PathBuf::from("dist/myapp-1.0.0-linux-amd64.tar.gz"),
2190 target: Some("x86_64-unknown-linux-gnu".to_string()),
2191 crate_name: "myapp".to_string(),
2192 metadata: HashMap::from([("format".to_string(), "tar.gz".to_string())]),
2193 size: None,
2194 });
2195 ctx.artifacts.add(Artifact {
2196 kind: ArtifactKind::Binary,
2197 name: String::new(),
2198 path: PathBuf::from("dist/myapp"),
2199 target: Some("x86_64-unknown-linux-gnu".to_string()),
2200 crate_name: "myapp".to_string(),
2201 metadata: HashMap::new(),
2202 size: None,
2203 });
2204 ctx.refresh_artifacts_var();
2205
2206 let result = ctx
2208 .render_template("{% for a in Artifacts %}{{ a.name }},{% endfor %}")
2209 .unwrap();
2210 assert!(result.contains("myapp-1.0.0-linux-amd64.tar.gz"));
2211 assert!(result.contains("myapp"));
2212
2213 let result_kinds = ctx
2215 .render_template("{% for a in Artifacts %}{{ a.kind }},{% endfor %}")
2216 .unwrap();
2217 assert!(result_kinds.contains("archive"));
2218 assert!(result_kinds.contains("binary"));
2219 }
2220
2221 #[test]
2222 fn test_populate_metadata_var_with_mod_timestamp() {
2223 let mut config = Config::default();
2224 config.metadata = Some(crate::config::MetadataConfig {
2225 mod_timestamp: Some("{{ .CommitTimestamp }}".to_string()),
2226 ..Default::default()
2227 });
2228 let mut ctx = Context::new(config, ContextOptions::default());
2229 ctx.populate_metadata_var().unwrap();
2230
2231 let result = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2233 assert_eq!(result, "{{ .CommitTimestamp }}");
2234 }
2235
2236 #[test]
2237 fn test_populate_metadata_var_empty_when_no_config() {
2238 let config = Config::default();
2239 let mut ctx = Context::new(config, ContextOptions::default());
2240 ctx.populate_metadata_var().unwrap();
2241
2242 let result = ctx.render_template("{{ Metadata.Description }}").unwrap();
2244 assert_eq!(result, "");
2245 }
2246
2247 #[test]
2248 fn test_populate_metadata_var_reads_from_config() {
2249 let mut config = Config::default();
2250 config.metadata = Some(crate::config::MetadataConfig {
2251 description: Some("A test project".to_string()),
2252 homepage: Some("https://example.com".to_string()),
2253 license: Some("MIT".to_string()),
2254 maintainers: Some(vec!["Alice".to_string(), "Bob".to_string()]),
2255 mod_timestamp: Some("1234567890".to_string()),
2256 ..Default::default()
2257 });
2258 let mut ctx = Context::new(config, ContextOptions::default());
2259 ctx.populate_metadata_var().unwrap();
2260
2261 let desc = ctx.render_template("{{ Metadata.Description }}").unwrap();
2262 assert_eq!(desc, "A test project");
2263
2264 let home = ctx.render_template("{{ Metadata.Homepage }}").unwrap();
2265 assert_eq!(home, "https://example.com");
2266
2267 let lic = ctx.render_template("{{ Metadata.License }}").unwrap();
2268 assert_eq!(lic, "MIT");
2269
2270 let ts = ctx.render_template("{{ Metadata.ModTimestamp }}").unwrap();
2271 assert_eq!(ts, "1234567890");
2272 }
2273
2274 #[test]
2275 fn test_populate_metadata_var_full_description_inline() {
2276 use crate::config::ContentSource;
2277 let mut config = Config::default();
2278 config.metadata = Some(crate::config::MetadataConfig {
2279 full_description: Some(ContentSource::Inline(
2280 "A long-form description of the project.".to_string(),
2281 )),
2282 ..Default::default()
2283 });
2284 let mut ctx = Context::new(config, ContextOptions::default());
2285 ctx.populate_metadata_var().unwrap();
2286 let rendered = ctx
2287 .render_template("{{ Metadata.FullDescription }}")
2288 .unwrap();
2289 assert_eq!(rendered, "A long-form description of the project.");
2290 }
2291
2292 #[test]
2293 fn test_populate_metadata_var_full_description_from_file() {
2294 use crate::config::ContentSource;
2295 let tmp = tempfile::tempdir().unwrap();
2296 let desc_path = tmp.path().join("DESCRIPTION.md");
2297 std::fs::write(&desc_path, "read from disk").unwrap();
2298 let mut config = Config::default();
2299 config.metadata = Some(crate::config::MetadataConfig {
2300 full_description: Some(ContentSource::FromFile {
2301 from_file: desc_path.to_string_lossy().into_owned(),
2302 }),
2303 ..Default::default()
2304 });
2305 let mut ctx = Context::new(config, ContextOptions::default());
2306 ctx.populate_metadata_var().unwrap();
2307 let rendered = ctx
2308 .render_template("{{ Metadata.FullDescription }}")
2309 .unwrap();
2310 assert_eq!(rendered, "read from disk");
2311 }
2312
2313 #[test]
2314 fn test_populate_metadata_var_full_description_from_url_resolves() {
2315 use crate::config::ContentSource;
2320 use crate::test_helpers::responder::spawn_oneshot_http_responder;
2321
2322 let body = "long form description body";
2323 let body_len = body.len();
2324 let response: &'static str = Box::leak(
2325 format!("HTTP/1.1 200 OK\r\nContent-Length: {body_len}\r\n\r\n{body}").into_boxed_str(),
2326 );
2327 let (addr, _calls) = spawn_oneshot_http_responder(vec![response]);
2328
2329 let mut config = Config::default();
2330 config.metadata = Some(crate::config::MetadataConfig {
2331 full_description: Some(ContentSource::FromUrl {
2332 from_url: format!("http://{addr}/description.md"),
2333 headers: None,
2334 }),
2335 ..Default::default()
2336 });
2337 let mut ctx = Context::new(config, ContextOptions::default());
2338 ctx.populate_metadata_var()
2339 .expect("from_url should resolve through content_source");
2340 let rendered = ctx
2341 .render_template("{{ Metadata.FullDescription }}")
2342 .unwrap();
2343 assert_eq!(rendered, body);
2344 }
2345
2346 #[test]
2347 fn test_populate_metadata_var_commit_author() {
2348 use crate::config::CommitAuthorConfig;
2349 let mut config = Config::default();
2350 config.metadata = Some(crate::config::MetadataConfig {
2351 commit_author: Some(CommitAuthorConfig {
2352 name: Some("Alice Developer".to_string()),
2353 email: Some("alice@example.com".to_string()),
2354 signing: None,
2355 use_github_app_token: false,
2356 }),
2357 ..Default::default()
2358 });
2359 let mut ctx = Context::new(config, ContextOptions::default());
2360 ctx.populate_metadata_var().unwrap();
2361 let name = ctx
2362 .render_template("{{ Metadata.CommitAuthor.Name }}")
2363 .unwrap();
2364 assert_eq!(name, "Alice Developer");
2365 let email = ctx
2366 .render_template("{{ Metadata.CommitAuthor.Email }}")
2367 .unwrap();
2368 assert_eq!(email, "alice@example.com");
2369 }
2370
2371 #[test]
2372 fn test_artifact_id_template_var() {
2373 let mut config = Config::default();
2374 config.project_name = "myapp".to_string();
2375 let mut ctx = Context::new(config, ContextOptions::default());
2376 ctx.template_vars_mut().set("ArtifactID", "default");
2377
2378 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2379 assert_eq!(result, "default");
2380 }
2381
2382 #[test]
2383 fn test_artifact_id_empty_when_not_set() {
2384 let mut config = Config::default();
2385 config.project_name = "myapp".to_string();
2386 let mut ctx = Context::new(config, ContextOptions::default());
2387 ctx.template_vars_mut().set("ArtifactID", "");
2388
2389 let result = ctx.render_template("{{ .ArtifactID }}").unwrap();
2390 assert_eq!(result, "");
2391 }
2392
2393 #[test]
2394 fn test_pro_vars_rendered_in_templates() {
2395 let mut config = Config::default();
2397 config.tag = Some(crate::config::TagConfig {
2398 tag_prefix: Some("api/".to_string()),
2399 ..Default::default()
2400 });
2401 let opts = ContextOptions {
2402 snapshot: false,
2403 nightly: false,
2404 merge: true,
2405 ..Default::default()
2406 };
2407 let mut ctx = Context::new(config, opts);
2408 ctx.git_info = Some(make_git_info(false, None));
2409 ctx.populate_git_vars();
2410
2411 let result = ctx
2412 .render_template(
2413 "{% if IsRelease %}release{% endif %}-{% if IsMerging %}merge{% endif %}-{{ .PrefixedTag }}",
2414 )
2415 .unwrap();
2416 assert_eq!(result, "release-merge-api/v1.2.3");
2417 }
2418
2419 #[test]
2420 fn test_is_release_without_git_info() {
2421 let config = Config::default();
2423 let opts = ContextOptions {
2424 snapshot: false,
2425 nightly: false,
2426 ..Default::default()
2427 };
2428 let mut ctx = Context::new(config, opts);
2429 ctx.populate_git_vars();
2430
2431 assert_eq!(
2432 ctx.template_vars().get("IsRelease"),
2433 Some(&"true".to_string())
2434 );
2435 }
2436
2437 #[test]
2438 fn test_is_merging_without_git_info() {
2439 let config = Config::default();
2441 let opts = ContextOptions {
2442 merge: true,
2443 ..Default::default()
2444 };
2445 let mut ctx = Context::new(config, opts);
2446 ctx.populate_git_vars();
2447
2448 assert_eq!(
2449 ctx.template_vars().get("IsMerging"),
2450 Some(&"true".to_string())
2451 );
2452 }
2453
2454 #[test]
2464 fn test_monorepo_version_matches_shared_semver_helper() {
2465 let mut config = Config::default();
2466 config.monorepo = Some(crate::config::MonorepoConfig {
2467 tag_prefix: Some("core/".to_string()),
2468 dir: None,
2469 });
2470 let mut ctx = Context::new(config, ContextOptions::default());
2471
2472 let semver = SemVer {
2473 major: 2,
2474 minor: 1,
2475 patch: 0,
2476 prerelease: Some("rc.1".to_string()),
2477 build_metadata: Some("build.7".to_string()),
2478 };
2479 let mut info = make_git_info(false, None);
2480 info.tag = "core/v2.1.0-rc.1+build.7".to_string();
2481 info.semver = semver.clone();
2482 ctx.git_info = Some(info);
2483 ctx.populate_git_vars();
2484
2485 let v = ctx.template_vars();
2486 assert_eq!(v.get("Version"), Some(&semver.version_string()));
2489 assert_eq!(v.get("Version"), Some(&"2.1.0-rc.1+build.7".to_string()));
2490 assert_eq!(v.get("RawVersion"), Some(&semver.raw_version_string()));
2491 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2492 assert_eq!(v.get("Tag"), Some(&"v2.1.0-rc.1+build.7".to_string()));
2494 }
2495
2496 #[test]
2497 fn test_monorepo_tag_prefix_strips_tag_for_template_var() {
2498 let mut config = Config::default();
2499 config.monorepo = Some(crate::config::MonorepoConfig {
2500 tag_prefix: Some("subproject1/".to_string()),
2501 dir: None,
2502 });
2503 let mut ctx = Context::new(config, ContextOptions::default());
2504
2505 let mut info = make_git_info(false, None);
2507 info.tag = "subproject1/v1.2.3".to_string();
2508 info.previous_tag = Some("subproject1/v1.2.2".to_string());
2509 info.summary = "subproject1/v1.2.3-0-gabc123d".to_string();
2510 ctx.git_info = Some(info);
2511 ctx.populate_git_vars();
2512
2513 let v = ctx.template_vars();
2514 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2516 assert_eq!(v.get("Version"), Some(&"1.2.3".to_string()));
2518 assert_eq!(
2520 v.get("PrefixedTag"),
2521 Some(&"subproject1/v1.2.3".to_string())
2522 );
2523 assert_eq!(v.get("PreviousTag"), Some(&"v1.2.2".to_string()));
2525 assert_eq!(
2527 v.get("PrefixedPreviousTag"),
2528 Some(&"subproject1/v1.2.2".to_string())
2529 );
2530 assert_eq!(v.get("Summary"), Some(&"v1.2.3-0-gabc123d".to_string()));
2532 assert_eq!(
2534 v.get("PrefixedSummary"),
2535 Some(&"subproject1/v1.2.3-0-gabc123d".to_string())
2536 );
2537 }
2538
2539 #[test]
2540 fn test_monorepo_prefixed_previous_tag() {
2541 let mut config = Config::default();
2542 config.monorepo = Some(crate::config::MonorepoConfig {
2543 tag_prefix: Some("svc/".to_string()),
2544 dir: None,
2545 });
2546 let mut ctx = Context::new(config, ContextOptions::default());
2547
2548 let mut info = make_git_info(false, None);
2549 info.tag = "svc/v2.0.0".to_string();
2550 info.previous_tag = Some("svc/v1.9.0".to_string());
2551 ctx.git_info = Some(info);
2552 ctx.populate_git_vars();
2553
2554 let v = ctx.template_vars();
2555 assert_eq!(
2557 v.get("PrefixedPreviousTag"),
2558 Some(&"svc/v1.9.0".to_string())
2559 );
2560 assert_eq!(v.get("PreviousTag"), Some(&"v1.9.0".to_string()));
2562 }
2563
2564 #[test]
2565 fn test_no_monorepo_falls_back_to_tag_prefix() {
2566 let mut config = Config::default();
2568 config.tag = Some(crate::config::TagConfig {
2569 tag_prefix: Some("release/".to_string()),
2570 ..Default::default()
2571 });
2572 let mut ctx = Context::new(config, ContextOptions::default());
2573 ctx.git_info = Some(make_git_info(false, None));
2574 ctx.populate_git_vars();
2575
2576 let v = ctx.template_vars();
2577 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2579 assert_eq!(v.get("PrefixedTag"), Some(&"release/v1.2.3".to_string()));
2581 assert_eq!(
2582 v.get("PrefixedPreviousTag"),
2583 Some(&"release/v1.2.2".to_string())
2584 );
2585 }
2586
2587 #[test]
2588 fn test_monorepo_overrides_tag_prefix_for_prefixed_vars() {
2589 let mut config = Config::default();
2592 config.tag = Some(crate::config::TagConfig {
2593 tag_prefix: Some("release/".to_string()),
2594 ..Default::default()
2595 });
2596 config.monorepo = Some(crate::config::MonorepoConfig {
2597 tag_prefix: Some("svc/".to_string()),
2598 dir: None,
2599 });
2600 let mut ctx = Context::new(config, ContextOptions::default());
2601
2602 let mut info = make_git_info(false, None);
2603 info.tag = "svc/v1.2.3".to_string();
2604 info.previous_tag = Some("svc/v1.2.2".to_string());
2605 ctx.git_info = Some(info);
2606 ctx.populate_git_vars();
2607
2608 let v = ctx.template_vars();
2609 assert_eq!(v.get("Tag"), Some(&"v1.2.3".to_string()));
2611 assert_eq!(v.get("PrefixedTag"), Some(&"svc/v1.2.3".to_string()));
2613 }
2614
2615 #[test]
2616 fn test_monorepo_prefixed_summary() {
2617 let mut config = Config::default();
2618 config.monorepo = Some(crate::config::MonorepoConfig {
2619 tag_prefix: Some("pkg/".to_string()),
2620 dir: None,
2621 });
2622 let mut ctx = Context::new(config, ContextOptions::default());
2623
2624 let mut info = make_git_info(false, None);
2625 info.tag = "pkg/v1.2.3".to_string();
2626 info.summary = "pkg/v1.2.3-0-gabc123d".to_string();
2628 ctx.git_info = Some(info);
2629 ctx.populate_git_vars();
2630
2631 assert_eq!(
2633 ctx.template_vars().get("PrefixedSummary"),
2634 Some(&"pkg/v1.2.3-0-gabc123d".to_string())
2635 );
2636 assert_eq!(
2638 ctx.template_vars().get("Summary"),
2639 Some(&"v1.2.3-0-gabc123d".to_string())
2640 );
2641 }
2642
2643 #[test]
2644 fn test_monorepo_no_previous_tag() {
2645 let mut config = Config::default();
2646 config.monorepo = Some(crate::config::MonorepoConfig {
2647 tag_prefix: Some("svc/".to_string()),
2648 dir: None,
2649 });
2650 let mut ctx = Context::new(config, ContextOptions::default());
2651
2652 let mut info = make_git_info(false, None);
2653 info.tag = "svc/v1.0.0".to_string();
2654 info.previous_tag = None;
2655 ctx.git_info = Some(info);
2656 ctx.populate_git_vars();
2657
2658 let v = ctx.template_vars();
2659 assert_eq!(v.get("PrefixedPreviousTag"), Some(&"".to_string()));
2660 assert_eq!(v.get("PreviousTag"), Some(&"".to_string()));
2662 }
2663
2664 #[test]
2669 fn test_monorepo_full_flow_all_vars() {
2670 let mut config = Config::default();
2673 config.project_name = "mymonorepo".to_string();
2674 config.monorepo = Some(crate::config::MonorepoConfig {
2675 tag_prefix: Some("services/api/".to_string()),
2676 dir: Some("services/api".to_string()),
2677 });
2678
2679 assert_eq!(config.monorepo_tag_prefix(), Some("services/api/"));
2681 assert_eq!(config.monorepo_dir(), Some("services/api"));
2682
2683 let mut ctx = Context::new(config, ContextOptions::default());
2684
2685 let mut info = make_git_info(false, None);
2688 info.tag = "services/api/v2.1.0".to_string();
2689 info.previous_tag = Some("services/api/v2.0.5".to_string());
2690 info.summary = "services/api/v2.1.0-0-gabc123d".to_string();
2691 info.semver = crate::git::SemVer {
2692 major: 2,
2693 minor: 1,
2694 patch: 0,
2695 prerelease: None,
2696 build_metadata: None,
2697 };
2698 ctx.git_info = Some(info);
2699 ctx.populate_git_vars();
2700
2701 let v = ctx.template_vars();
2702
2703 assert_eq!(v.get("Tag"), Some(&"v2.1.0".to_string()));
2705 assert_eq!(v.get("Version"), Some(&"2.1.0".to_string()));
2706 assert_eq!(v.get("RawVersion"), Some(&"2.1.0".to_string()));
2707 assert_eq!(v.get("Major"), Some(&"2".to_string()));
2708 assert_eq!(v.get("Minor"), Some(&"1".to_string()));
2709 assert_eq!(v.get("Patch"), Some(&"0".to_string()));
2710 assert_eq!(v.get("PreviousTag"), Some(&"v2.0.5".to_string()));
2711 assert_eq!(v.get("Summary"), Some(&"v2.1.0-0-gabc123d".to_string()));
2712
2713 assert_eq!(
2715 v.get("PrefixedTag"),
2716 Some(&"services/api/v2.1.0".to_string())
2717 );
2718 assert_eq!(
2719 v.get("PrefixedPreviousTag"),
2720 Some(&"services/api/v2.0.5".to_string())
2721 );
2722 assert_eq!(
2723 v.get("PrefixedSummary"),
2724 Some(&"services/api/v2.1.0-0-gabc123d".to_string())
2725 );
2726
2727 assert_eq!(v.get("ProjectName"), Some(&"mymonorepo".to_string()));
2729 }
2730
2731 #[test]
2732 fn context_env_var_defaults_to_process_env_source() {
2733 let ctx = Context::new(Config::default(), ContextOptions::default());
2734 assert_eq!(ctx.env_var("ANODIZER_T3_UNSET_VAR"), None);
2736 }
2737
2738 #[test]
2739 fn context_env_var_routes_to_injected_source() {
2740 let mut ctx = Context::new(Config::default(), ContextOptions::default());
2741 ctx.set_env_source(crate::MapEnvSource::new().with("INJECTED", "yes"));
2742 assert_eq!(ctx.env_var("INJECTED"), Some("yes".to_string()));
2743 assert_eq!(ctx.env_var("PATH"), None);
2747 }
2748
2749 #[test]
2750 #[serial_test::serial]
2751 fn populate_runtime_vars_sets_rustc_version() {
2752 let config = Config::default();
2753 let mut ctx = Context::new(config, ContextOptions::default());
2754 ctx.populate_runtime_vars();
2757
2758 let ver = ctx
2759 .template_vars()
2760 .get("RustcVersion")
2761 .expect("RustcVersion should be set after populate_runtime_vars");
2762 if !ver.is_empty() {
2766 assert!(
2767 ver.chars().next().is_some_and(|c| c.is_ascii_digit()),
2768 "RustcVersion should start with a digit: {ver}"
2769 );
2770 }
2771 }
2772}