huskies: merge 1185 story Bounded auto-retry for GatesFailed merges before requiring human intervention
This commit is contained in:
@@ -28,6 +28,8 @@ pub enum EventAction {
|
||||
},
|
||||
/// Post a new-item-created notification.
|
||||
NewItemCreated,
|
||||
/// Post a merge-auto-retry notification naming the attempt and budget.
|
||||
MergeAutoRetry,
|
||||
/// Log server-side only; do not post to chat (e.g. hard rate-limit blocks).
|
||||
LogOnly,
|
||||
/// Reload the project configuration.
|
||||
@@ -54,6 +56,7 @@ pub fn classify(event: &WatcherEvent) -> EventAction {
|
||||
EventAction::AgentCompleted { success: *success }
|
||||
}
|
||||
WatcherEvent::NewItemCreated { .. } => EventAction::NewItemCreated,
|
||||
WatcherEvent::MergeAutoRetry { .. } => EventAction::MergeAutoRetry,
|
||||
_ => EventAction::Skip,
|
||||
}
|
||||
}
|
||||
@@ -191,4 +194,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(classify(&event), EventAction::NewItemCreated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_auto_retry_is_classified_correctly() {
|
||||
let event = WatcherEvent::MergeAutoRetry {
|
||||
story_id: "1_story_foo".to_string(),
|
||||
attempt: 1,
|
||||
budget: 3,
|
||||
};
|
||||
assert_eq!(classify(&event), EventAction::MergeAutoRetry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,38 @@ pub fn format_new_item_notification(
|
||||
(plain, html)
|
||||
}
|
||||
|
||||
/// Format a merge-auto-retry notification message.
|
||||
///
|
||||
/// Sent when a `GatesFailed` merge failure is automatically retried after a
|
||||
/// delay (story 1185). Returns `(plain_text, html)` suitable for
|
||||
/// `ChatTransport::send_message`.
|
||||
pub fn format_merge_auto_retry_notification(
|
||||
item_id: &str,
|
||||
story_name: &str,
|
||||
attempt: u32,
|
||||
budget: u32,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
.unwrap_or_default();
|
||||
|
||||
let plain = format!(
|
||||
"\u{1f504} #{number} {name_plain}\u{2014} auto-retrying merge (attempt {attempt}/{budget})"
|
||||
);
|
||||
let html = format!(
|
||||
"\u{1f504} <strong>#{number}</strong> {name_html}\u{2014} auto-retrying merge \
|
||||
(attempt {attempt}/{budget})"
|
||||
);
|
||||
(plain, html)
|
||||
}
|
||||
|
||||
/// Maximum number of trailing gate-output lines included in a merge-failure
|
||||
/// chat notification.
|
||||
///
|
||||
@@ -595,6 +627,36 @@ mod tests {
|
||||
assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
|
||||
}
|
||||
|
||||
// ── format_merge_auto_retry_notification ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn format_merge_auto_retry_notification_with_story_name() {
|
||||
let (plain, html) =
|
||||
format_merge_auto_retry_notification("42_story_foo", "My Feature", 1, 3);
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{1f504} #42 My Feature \u{2014} auto-retrying merge (attempt 1/3)"
|
||||
);
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f504} <strong>#42</strong> <em>My Feature</em> \u{2014} auto-retrying merge \
|
||||
(attempt 1/3)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_merge_auto_retry_notification_falls_back_to_number() {
|
||||
let (plain, html) = format_merge_auto_retry_notification("42_story_foo", "", 2, 3);
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{1f504} #42 \u{2014} auto-retrying merge (attempt 2/3)"
|
||||
);
|
||||
assert_eq!(
|
||||
html,
|
||||
"\u{1f504} <strong>#42</strong> \u{2014} auto-retrying merge (attempt 2/3)"
|
||||
);
|
||||
}
|
||||
|
||||
// ── truncate_gate_output ──────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -16,8 +16,9 @@ use super::super::filter::{AGENT_EVENT_DEBOUNCE, should_send_rate_limit};
|
||||
use super::super::format::{
|
||||
MERGE_FAILURE_TAIL_LINES, format_agent_completed_notification,
|
||||
format_agent_started_notification, format_blocked_notification, format_error_notification,
|
||||
format_new_item_notification, format_oauth_account_swapped, format_oauth_accounts_exhausted,
|
||||
format_rate_limit_notification, truncate_gate_output,
|
||||
format_merge_auto_retry_notification, format_new_item_notification,
|
||||
format_oauth_account_swapped, format_oauth_accounts_exhausted, format_rate_limit_notification,
|
||||
truncate_gate_output,
|
||||
};
|
||||
use super::super::route::rooms_for_notification;
|
||||
use super::{find_story_name_any_stage, read_story_name};
|
||||
@@ -295,6 +296,35 @@ pub fn spawn_notification_listener(
|
||||
}
|
||||
}
|
||||
}
|
||||
EventAction::MergeAutoRetry => {
|
||||
if !config.status_push_enabled {
|
||||
continue;
|
||||
}
|
||||
let WatcherEvent::MergeAutoRetry {
|
||||
ref story_id,
|
||||
attempt,
|
||||
budget,
|
||||
} = event
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let story_name = find_story_name_any_stage(&project_root, story_id);
|
||||
let (plain, html) = format_merge_auto_retry_notification(
|
||||
story_id,
|
||||
&story_name,
|
||||
attempt,
|
||||
budget,
|
||||
);
|
||||
slog!("[bot] Sending merge-auto-retry notification: {plain}");
|
||||
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||
slog!(
|
||||
"[bot] Failed to send merge-auto-retry notification \
|
||||
to {room_id}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EventAction::LogOnly => {
|
||||
// Hard-block: log server-side for debugging; do NOT post to chat.
|
||||
// Hard-block auto-resume is normal operation — the status command
|
||||
|
||||
@@ -39,6 +39,8 @@ pub fn watcher_event_to_response(e: WatcherEvent) -> Option<WsResponse> {
|
||||
WatcherEvent::AgentCompleted { .. } => None,
|
||||
// Creation notifications are forwarded to chat transports only; no WebSocket message.
|
||||
WatcherEvent::NewItemCreated { .. } => None,
|
||||
// Merge-auto-retry notifications are forwarded to chat transports only; no WebSocket message.
|
||||
WatcherEvent::MergeAutoRetry { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user