forge_notify: skip-reasons drop-list filter, configurable via agent.nix

This commit is contained in:
damocles 2026-05-22 22:27:40 +02:00 committed by Mara
parent b0f6bd8ece
commit a94b504883
3 changed files with 72 additions and 4 deletions

View file

@ -104,8 +104,6 @@ pub async fn run(socket: PathBuf, is_manager: bool) {
debug!(%own_login, "forge_notify: own login resolved");
}
info!(forge_url = %forge_url, "forge_notify: polling started");
let mut interval = tokio::time::interval(Duration::from_secs(POLL_INTERVAL_SECS));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
// First tick fires immediately — skip it so we don't race the broker
@ -118,6 +116,29 @@ pub async fn run(socket: PathBuf, is_manager: bool) {
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
// Optional reason drop-list. `HIVE_FORGE_NOTIFY_SKIP_REASONS` is a
// comma-separated list of Forgejo notification `reason` values to
// suppress (e.g. `subscribed,participating`). Notifications with
// those reasons are marked read and silently dropped; everything
// else -- including notifications with a null/unrecognised reason --
// is delivered. Drop-list is safer than an allow-list: it kills the
// firehose without risking silent misses of directed signals
// (review_requested, assigned) or future unknown reason strings.
// Configurable per-agent via `hyperhive.forge.skipNotifyReasons` in agent.nix.
let skip_reasons: Vec<String> = std::env::var("HIVE_FORGE_NOTIFY_SKIP_REASONS")
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
if skip_reasons.is_empty() {
info!(forge_url = %forge_url, "forge_notify: polling started (all reasons)");
} else {
info!(forge_url = %forge_url, skip = ?skip_reasons, "forge_notify: polling started");
}
// Repos we have already unsubscribed this process lifetime. Persists
// across polls so we don't hammer DELETE on every cycle.
let mut unsubbed_repos: HashSet<String> = HashSet::new();
@ -133,6 +154,7 @@ pub async fn run(socket: PathBuf, is_manager: bool) {
keep_subscriptions,
&mut unsubbed_repos,
&own_login,
&skip_reasons,
)
.await;
}
@ -444,6 +466,7 @@ async fn poll_once(
keep_subscriptions: bool,
unsubbed_repos: &mut HashSet<String>,
own_login: &str,
skip_reasons: &[String],
) {
let url = format!("{forge_url}/api/v1/notifications?all=false&limit=50");
let resp = match client
@ -481,6 +504,18 @@ async fn poll_once(
for notif in &notifications {
let Some(id) = notif["id"].as_u64() else { continue };
// Reason drop-list: suppress noisy reasons (subscribed/participating).
// null/unknown reasons pass through — directed signals are never
// silently dropped even if Forgejo returns an unexpected value.
if !skip_reasons.is_empty() {
let reason = notif["reason"].as_str().unwrap_or("");
if !reason.is_empty() && skip_reasons.iter().any(|r| r == reason) {
debug!(%id, %reason, "forge_notify: skipping (reason in drop-list)");
mark_read(client, forge_url, token, id).await;
continue;
}
}
let body_opt = format_notification(client, token, notif, own_login).await;
// None means self-echo — mark read silently, no delivery.