Skip to main content

anodizer_core/
pipe_skip.rs

1//! Aggregated per-stage skip tracking.
2//!
3//! Mirrors GoReleaser's `internal/pipe/pipe.go::SkipMemento`. Pipeline
4//! stages that iterate multiple sub-configs (signs, docker_signs, custom
5//! publishers, archives, nfpms, …) occasionally need to skip a sub-config
6//! for a legitimate reason: `artifacts: none`, an `if:` conditional that
7//! rendered to `"false"`, an `ids` filter that matched nothing, an empty
8//! `cmd`. Before this module those skips used a bare `continue;` — the
9//! end-of-pipeline summary lost all visibility into intentional skips, so a
10//! misconfigured sign block and a deliberately-disabled one looked
11//! identical in the logs.
12//!
13//! `SkipMemento` collects a (stage, config_label, reason) tuple per skip.
14//! The pipeline runner drains it at end-of-pipeline and prints a grouped
15//! summary so users know which sub-configs were intentionally skipped.
16
17use std::sync::{Arc, Mutex};
18
19/// A single skip event: which stage, which sub-config, and why.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SkipEvent {
22    /// Stage that raised the skip (`sign`, `docker-sign`, `publisher`, …).
23    pub stage: String,
24    /// Human-readable label for the sub-config (e.g. the sign config's `id`,
25    /// the publisher's `name`, or a positional `publisher[2]`).
26    pub label: String,
27    /// Reason the skip happened (short, single-line).
28    pub reason: String,
29}
30
31/// Thread-safe aggregator. Cheap to clone (wraps `Arc<Mutex<…>>`).
32///
33/// Stages record via `remember`. The pipeline runner calls `drain` at
34/// end-of-pipeline to print the summary. Duplicate `(stage, label, reason)`
35/// tuples are dropped on insert so a per-artifact inner `continue` doesn't
36/// emit N copies of the same skip message.
37#[derive(Debug, Clone, Default)]
38pub struct SkipMemento {
39    inner: Arc<Mutex<Vec<SkipEvent>>>,
40}
41
42impl SkipMemento {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Record a skip. Duplicate `(stage, label, reason)` tuples are dropped.
48    pub fn remember(&self, stage: &str, label: &str, reason: &str) {
49        let event = SkipEvent {
50            stage: stage.to_string(),
51            label: label.to_string(),
52            reason: reason.to_string(),
53        };
54        let mut guard = match self.inner.lock() {
55            Ok(g) => g,
56            Err(poisoned) => poisoned.into_inner(),
57        };
58        if !guard.iter().any(|e| e == &event) {
59            guard.push(event);
60        }
61    }
62
63    /// Current number of recorded skips. Useful for tests and the summary
64    /// header (`"3 intentional skips"`).
65    pub fn len(&self) -> usize {
66        self.inner.lock().map(|g| g.len()).unwrap_or(0)
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.len() == 0
71    }
72
73    /// Take a snapshot of recorded skips without clearing. Used by tests.
74    pub fn snapshot(&self) -> Vec<SkipEvent> {
75        self.inner.lock().map(|g| g.clone()).unwrap_or_default()
76    }
77
78    /// Drain all recorded skips, leaving the memento empty.
79    pub fn drain(&self) -> Vec<SkipEvent> {
80        self.inner
81            .lock()
82            .map(|mut g| std::mem::take(&mut *g))
83            .unwrap_or_default()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn records_and_drains() {
93        let m = SkipMemento::new();
94        assert!(m.is_empty());
95        m.remember("sign", "cosign", "artifacts: none");
96        m.remember("sign", "gpg", "if: false");
97        assert_eq!(m.len(), 2);
98
99        let drained = m.drain();
100        assert_eq!(drained.len(), 2);
101        assert_eq!(drained[0].stage, "sign");
102        assert_eq!(drained[0].label, "cosign");
103        assert_eq!(drained[0].reason, "artifacts: none");
104        assert!(m.is_empty());
105    }
106
107    #[test]
108    fn deduplicates_identical_events() {
109        let m = SkipMemento::new();
110        // A per-artifact inner loop may fire "ids filter" skip N times for
111        // the same sign config; only one summary line should survive.
112        for _ in 0..10 {
113            m.remember("sign", "cosign", "ids filter matched no artifacts");
114        }
115        assert_eq!(m.len(), 1);
116    }
117
118    #[test]
119    fn keeps_distinct_reasons_per_label() {
120        let m = SkipMemento::new();
121        m.remember("sign", "cosign", "artifacts: none");
122        m.remember("sign", "cosign", "if: false");
123        // Same label, different reasons → both survive so the user sees
124        // each distinct skip path.
125        assert_eq!(m.len(), 2);
126    }
127
128    #[test]
129    fn snapshot_does_not_clear() {
130        let m = SkipMemento::new();
131        m.remember("publisher", "my-tool", "empty cmd");
132        let snap = m.snapshot();
133        assert_eq!(snap.len(), 1);
134        // Still present after snapshot.
135        assert_eq!(m.len(), 1);
136    }
137
138    #[test]
139    fn clone_shares_state() {
140        let m = SkipMemento::new();
141        let m2 = m.clone();
142        m2.remember("docker-sign", "cosign-docker", "artifacts: none");
143        assert_eq!(m.len(), 1);
144        assert_eq!(m.snapshot(), m2.snapshot());
145    }
146}