Skip to main content

anodizer_core/config/
post_publish_poll.rs

1//! Post-publish polling configuration shared by the Chocolatey and WinGet
2//! publishers.
3//!
4//! Both publishers report `HTTP 2xx` from the submission endpoint long
5//! before the upstream actually approves the package (Chocolatey
6//! moderation queue) or merges the PR (winget-pkgs validation pipeline).
7//! When polling is enabled, the publish stage waits for a terminal
8//! moderation/validation state up to `timeout`, sampling every
9//! `interval`, and surfaces the result as part of the release summary.
10//!
11//! Defaults: `enabled: true`, `interval: 30s`, `timeout: 30m`. Callers
12//! that want a fire-and-forget publish (CI without long-running waits)
13//! either set `enabled: false` per-publisher or pass
14//! `--no-post-publish-poll` globally.
15
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19use super::HumanDuration;
20
21/// Per-publisher post-publish polling config block.
22///
23/// See module-level docs for the polling lifecycle. Default values:
24/// `enabled: true`, `interval: 30s`, `timeout: 30m`.
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
26#[serde(default, deny_unknown_fields)]
27pub struct PostPublishPollConfig {
28    /// Whether to poll at all. Default `true`. Setting `false` disables
29    /// polling without removing the config block (parity with every
30    /// `skip:` toggle elsewhere in the schema).
31    pub enabled: bool,
32    /// How long to wait between successive status checks. Default `30s`.
33    pub interval: HumanDuration,
34    /// Total wall-clock budget for polling. When exhausted, the poller
35    /// emits `PostPublishStatus::Timeout` with the last observed state.
36    /// Default `30m`.
37    pub timeout: HumanDuration,
38}
39
40impl PostPublishPollConfig {
41    /// Default interval between successive polls (30 seconds).
42    pub const DEFAULT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
43    /// Default total polling budget (30 minutes).
44    pub const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60);
45}
46
47impl Default for PostPublishPollConfig {
48    fn default() -> Self {
49        Self {
50            enabled: true,
51            interval: HumanDuration(Self::DEFAULT_INTERVAL),
52            timeout: HumanDuration(Self::DEFAULT_TIMEOUT),
53        }
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn defaults_match_spec() {
63        let c = PostPublishPollConfig::default();
64        assert!(c.enabled);
65        assert_eq!(c.interval.duration(), std::time::Duration::from_secs(30));
66        assert_eq!(
67            c.timeout.duration(),
68            std::time::Duration::from_secs(30 * 60)
69        );
70    }
71
72    #[test]
73    fn empty_yaml_yields_defaults() {
74        let c: PostPublishPollConfig = serde_yaml_ng::from_str("{}").unwrap();
75        assert!(c.enabled);
76        assert_eq!(c.interval.duration(), std::time::Duration::from_secs(30));
77        assert_eq!(
78            c.timeout.duration(),
79            std::time::Duration::from_secs(30 * 60)
80        );
81    }
82
83    #[test]
84    fn parses_explicit_yaml() {
85        let yaml = "enabled: false\ninterval: 1m\ntimeout: 5m\n";
86        let c: PostPublishPollConfig = serde_yaml_ng::from_str(yaml).unwrap();
87        assert!(!c.enabled);
88        assert_eq!(c.interval.duration(), std::time::Duration::from_secs(60));
89        assert_eq!(c.timeout.duration(), std::time::Duration::from_secs(5 * 60));
90    }
91
92    #[test]
93    fn unknown_field_rejected() {
94        let yaml = "interval: 1m\nbogus: true\n";
95        let res: Result<PostPublishPollConfig, _> = serde_yaml_ng::from_str(yaml);
96        assert!(res.is_err(), "deny_unknown_fields must reject typos");
97    }
98}