1use serde::{Deserialize, Serialize};
12use std::time::Duration;
13
14use crate::contracts::WebhookConfig;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum WebhookEventType {
19 TaskCreated,
21 TaskStarted,
23 TaskCompleted,
25 TaskFailed,
27 TaskStatusChanged,
29 LoopStarted,
31 LoopStopped,
33 PhaseStarted,
35 PhaseCompleted,
37 QueueUnblocked,
39}
40
41impl WebhookEventType {
42 pub fn as_str(&self) -> &'static str {
43 match self {
44 WebhookEventType::TaskCreated => "task_created",
45 WebhookEventType::TaskStarted => "task_started",
46 WebhookEventType::TaskCompleted => "task_completed",
47 WebhookEventType::TaskFailed => "task_failed",
48 WebhookEventType::TaskStatusChanged => "task_status_changed",
49 WebhookEventType::LoopStarted => "loop_started",
50 WebhookEventType::LoopStopped => "loop_stopped",
51 WebhookEventType::PhaseStarted => "phase_started",
52 WebhookEventType::PhaseCompleted => "phase_completed",
53 WebhookEventType::QueueUnblocked => "queue_unblocked",
54 }
55 }
56}
57
58impl std::str::FromStr for WebhookEventType {
59 type Err = anyhow::Error;
60
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 Ok(match s {
63 "task_created" => Self::TaskCreated,
64 "task_started" => Self::TaskStarted,
65 "task_completed" => Self::TaskCompleted,
66 "task_failed" => Self::TaskFailed,
67 "task_status_changed" => Self::TaskStatusChanged,
68 "loop_started" => Self::LoopStarted,
69 "loop_stopped" => Self::LoopStopped,
70 "phase_started" => Self::PhaseStarted,
71 "phase_completed" => Self::PhaseCompleted,
72 "queue_unblocked" => Self::QueueUnblocked,
73 other => anyhow::bail!(
74 "Unknown event type: {}. Supported: task_created, task_started, task_completed, task_failed, task_status_changed, loop_started, loop_stopped, phase_started, phase_completed, queue_unblocked",
75 other
76 ),
77 })
78 }
79}
80
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct WebhookContext {
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub runner: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub model: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub phase: Option<u8>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub phase_count: Option<u8>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub duration_ms: Option<u64>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub repo_root: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub branch: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub commit: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub ci_gate: Option<String>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct WebhookPayload {
117 pub event: String,
119 pub timestamp: String,
121 #[serde(skip_serializing_if = "Option::is_none")]
124 pub task_id: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
128 pub task_title: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub previous_status: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub current_status: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub note: Option<String>,
138 #[serde(flatten)]
140 pub context: WebhookContext,
141}
142
143#[derive(Debug, Clone)]
145pub struct ResolvedWebhookConfig {
146 pub enabled: bool,
147 pub url: Option<String>,
148 pub secret: Option<String>,
149 pub timeout: Duration,
150 pub retry_count: u32,
151 pub retry_backoff: Duration,
152}
153
154impl ResolvedWebhookConfig {
155 pub fn from_config(config: &WebhookConfig) -> Self {
157 Self {
158 enabled: config.enabled.unwrap_or(false),
159 url: config.url.clone(),
160 secret: config.secret.clone(),
161 timeout: Duration::from_secs(config.timeout_secs.unwrap_or(30) as u64),
162 retry_count: config.retry_count.unwrap_or(3),
163 retry_backoff: Duration::from_millis(config.retry_backoff_ms.unwrap_or(1000) as u64),
164 }
165 }
166}
167
168#[derive(Debug, Clone)]
170pub(crate) struct WebhookMessage {
171 pub(crate) payload: WebhookPayload,
172 pub(crate) config: ResolvedWebhookConfig,
173}
174
175pub(crate) fn resolve_webhook_config(config: &WebhookConfig) -> ResolvedWebhookConfig {
177 ResolvedWebhookConfig::from_config(config)
178}