Skip to main content

anodizer_core/context/
runtime.rs

1use super::*;
2
3impl Context {
4    /// Return the current `Version` template variable, or an empty string if
5    /// not yet populated.
6    pub fn version(&self) -> String {
7        self.template_vars
8            .get("Version")
9            .cloned()
10            .unwrap_or_default()
11    }
12
13    /// Reproducible-mtime seed shared by every stage that stamps a build
14    /// timestamp into a produced artifact (release archives, source archives,
15    /// PyPI wheels + sdists).
16    ///
17    /// Resolution ladder, single-sourced here so archives and wheels never
18    /// pick different timestamps in one run:
19    ///
20    /// 1. when ANY build in the crate universe is `reproducible: true`, the
21    ///    commit timestamp wins outright — a reproducible build pins its own
22    ///    output to the commit, so a stray ambient `SOURCE_DATE_EPOCH` must
23    ///    not override it;
24    /// 2. otherwise `SOURCE_DATE_EPOCH` (the standard reproducibility
25    ///    contract, set by the determinism harness on every child), falling
26    ///    back to the commit timestamp.
27    ///
28    /// Returns `None` when neither a commit timestamp nor `SOURCE_DATE_EPOCH`
29    /// is available (writers then leave the default wall-clock stamp).
30    pub fn resolve_reproducible_mtime(&self) -> Option<u64> {
31        let any_reproducible = self.config.crate_universe().into_iter().any(|c| {
32            c.builds
33                .as_ref()
34                .is_some_and(|builds| builds.iter().any(|b| b.reproducible.unwrap_or(false)))
35        });
36        let commit_ts = self
37            .template_vars()
38            .get("CommitTimestamp")
39            .and_then(|ts| ts.parse::<u64>().ok());
40        if any_reproducible {
41            commit_ts
42        } else {
43            self.env_var("SOURCE_DATE_EPOCH")
44                .and_then(|s| s.parse::<u64>().ok())
45                .or(commit_ts)
46        }
47    }
48
49    /// Derive the verbosity level from context options.
50    pub fn verbosity(&self) -> Verbosity {
51        Verbosity::from_flags(self.options.quiet, self.options.verbose, self.options.debug)
52    }
53
54    /// Resolve the user's `retry:` block into a concrete `RetryPolicy`,
55    /// applying defaults when `retry:` is unset. Equivalent to
56    /// `ctx.config.retry.unwrap_or_default().to_policy()` but centralizes
57    /// the lookup so a future refactor can hang validation / clamping off
58    /// a single seam.
59    pub fn retry_policy(&self) -> crate::retry::RetryPolicy {
60        self.config.retry.unwrap_or_default().to_policy()
61    }
62
63    /// The absolute wall-clock deadline the current publisher invocation's
64    /// retry ladders must not sleep past: the invocation's anchor plus
65    /// `retry.max_elapsed` (or [`crate::retry::DEFAULT_MAX_ELAPSED`], 15 min,
66    /// when unset) — so a publisher that threads this into
67    /// [`crate::retry::retry_sync_deadline`] /
68    /// [`crate::retry::retry_async_deadline`] is bounded by default and the
69    /// operator can raise or lower the ceiling with one config field.
70    ///
71    /// This is a READ, not a mint. The anchor belongs to the enclosing
72    /// [`crate::retry::PublisherRetryScope`], installed once per publisher
73    /// invocation at the dispatch seam, so every seam inside one invocation —
74    /// an auth exchange, the publish request, a cleanup — observes the SAME
75    /// deadline and a wedged remote cannot spend the budget once per seam. A
76    /// caller outside any such scope (a stage-level retry ladder) anchors at
77    /// the moment of the call.
78    ///
79    /// Always `Some`, except when the configured budget is so large that the
80    /// deadline is not representable — an unrepresentable ceiling is no
81    /// ceiling, and `None` is how the engines spell unbounded. That `None` is
82    /// also what a caller with no [`Context`] at all passes them.
83    pub fn retry_deadline(&self) -> Option<std::time::Instant> {
84        let budget = self
85            .config
86            .retry
87            .unwrap_or_default()
88            .max_elapsed_duration()
89            .unwrap_or(crate::retry::DEFAULT_MAX_ELAPSED);
90        crate::retry::current_budget_anchor()
91            .unwrap_or_else(std::time::Instant::now)
92            .checked_add(budget)
93    }
94
95    /// Create a [`StageLogger`] for the given stage name, pre-attached to
96    /// the context's env-pairs list so that subprocess stderr / stdout
97    /// flowing through [`StageLogger::check_output`] is automatically
98    /// redacted. The env list combines the template-engine env
99    /// (process + config + `.env` files) and the current `std::env::vars`
100    /// snapshot, so any secret value reachable to a hook or subprocess is
101    /// available for scrubbing.
102    pub fn logger(&self, stage: &'static str) -> StageLogger {
103        // Snapshot the current redaction env into the shared cell at
104        // construction so a secret injected via `template_vars_mut().set_env`
105        // (which has no mutation hook) is captured, matching the historical
106        // snapshot-at-`logger()` behavior. The cell stays shared afterward, so
107        // a secret minted later through an `env_source` mutation still reaches
108        // this logger via `refresh_secret_env` at that mutation point.
109        self.refresh_secret_env();
110        #[allow(unused_mut)]
111        let mut log =
112            StageLogger::new(stage, self.verbosity()).with_shared_env(Arc::clone(&self.secret_env));
113        #[cfg(feature = "test-helpers")]
114        if let Some(cap) = &self.log_capture {
115            log = log.with_capture_handle(cap.clone());
116        }
117        log
118    }
119}