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: false`, `interval: 30s`, `timeout: 30m`. The
12//! human-moderation queues these publishers feed routinely take
13//! HOURS to DAYS to clear; blocking a CI job for that long is wrong by
14//! default. Operators who genuinely want in-band verification opt in
15//! per-publisher with `post_publish_poll: { enabled: true }`. Operators
16//! who want global opt-out (e.g., already opted-in via top-level
17//! config) keep `--no-post-publish-poll` as the override.
18
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22use super::HumanDuration;
23
24/// Per-publisher post-publish polling config block.
25///
26/// See module-level docs for the polling lifecycle. Default values:
27/// `enabled: true`, `interval: 30s`, `timeout: 30m`.
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
29#[serde(default, deny_unknown_fields)]
30pub struct PostPublishPollConfig {
31 /// Whether to poll at all. Default `false` — the upstream moderation
32 /// queues these publishers feed (Chocolatey, winget-pkgs) routinely
33 /// take HOURS to DAYS; the publish stage cannot reasonably block on
34 /// them in a CI workflow. Opt in to in-band verification by setting
35 /// `true` per-publisher (e.g. when running locally and willing to
36 /// wait).
37 pub enabled: bool,
38 /// How long to wait between successive status checks. Default `30s`.
39 pub interval: HumanDuration,
40 /// Total wall-clock budget for polling. When exhausted, the poller
41 /// emits `PostPublishStatus::Timeout` with the last observed state.
42 /// Default `30m`.
43 pub timeout: HumanDuration,
44}
45
46impl PostPublishPollConfig {
47 /// Default interval between successive polls (30 seconds).
48 pub const DEFAULT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
49 /// Default total polling budget (30 minutes).
50 pub const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60);
51}
52
53impl Default for PostPublishPollConfig {
54 fn default() -> Self {
55 Self {
56 enabled: false,
57 interval: HumanDuration(Self::DEFAULT_INTERVAL),
58 timeout: HumanDuration(Self::DEFAULT_TIMEOUT),
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn defaults_match_spec() {
69 let c = PostPublishPollConfig::default();
70 assert!(!c.enabled, "polling is opt-in — defaults to disabled");
71 assert_eq!(c.interval.duration(), std::time::Duration::from_secs(30));
72 assert_eq!(
73 c.timeout.duration(),
74 std::time::Duration::from_secs(30 * 60)
75 );
76 }
77
78 #[test]
79 fn empty_yaml_yields_defaults() {
80 let c: PostPublishPollConfig = serde_yaml_ng::from_str("{}").unwrap();
81 assert!(!c.enabled);
82 assert_eq!(c.interval.duration(), std::time::Duration::from_secs(30));
83 assert_eq!(
84 c.timeout.duration(),
85 std::time::Duration::from_secs(30 * 60)
86 );
87 }
88
89 #[test]
90 fn parses_explicit_yaml() {
91 let yaml = "enabled: false\ninterval: 1m\ntimeout: 5m\n";
92 let c: PostPublishPollConfig = serde_yaml_ng::from_str(yaml).unwrap();
93 assert!(!c.enabled);
94 assert_eq!(c.interval.duration(), std::time::Duration::from_secs(60));
95 assert_eq!(c.timeout.duration(), std::time::Duration::from_secs(5 * 60));
96 }
97
98 #[test]
99 fn unknown_field_rejected() {
100 let yaml = "interval: 1m\nbogus: true\n";
101 let res: Result<PostPublishPollConfig, _> = serde_yaml_ng::from_str(yaml);
102 assert!(res.is_err(), "deny_unknown_fields must reject typos");
103 }
104}