Commit 98e86b9556e9
Changed files (11)
dots
config
github-notif-manager
tools
github-notif-manager
dots/config/github-notif-manager/config.yaml
@@ -57,6 +57,16 @@ rules:
# ─── TARGETED ARCHIVE rules — before the general keep ─────────────────
+ # Merged Dependabot PRs (titles: "Bump ..." or "chore(deps): bump ...")
+ - name: "Archive merged Dependabot PRs"
+ description: "Merged dependency update PRs from dependabot"
+ filters:
+ subject_type: "PullRequest"
+ reason: "state_change"
+ subject_title: "*bump *"
+ action: done
+ enabled: true
+
# openshift-pipelines/operator review requests (437 notifications!)
# These are almost all automated bot/renovate PRs
- name: "Archive operator bot review requests"
tools/github-notif-manager/src/cache.rs
@@ -61,13 +61,18 @@ impl Cache {
/// Merge new notifications into cache.
/// Filters out notifications we've already marked as done.
- pub fn merge(&mut self, fresh: Vec<Notification>, full_fetch: bool) {
+ /// Returns (fresh_count, filtered_count) - number of notifications received and filtered.
+ pub fn merge(&mut self, fresh: Vec<Notification>, full_fetch: bool) -> (usize, usize) {
+ let fresh_count = fresh.len();
+
// Filter out notifications we've already marked as done
let fresh: Vec<Notification> = fresh
.into_iter()
.filter(|n| !self.done_ids.contains(&n.id))
.collect();
+ let filtered_count = fresh_count - fresh.len();
+
if full_fetch {
self.notifications = fresh;
} else {
@@ -85,6 +90,7 @@ impl Cache {
}
self.last_fetched = Some(Utc::now());
+ (fresh_count, filtered_count)
}
/// Mark a notification as done — removes from notifications and tracks ID
@@ -102,4 +108,9 @@ impl Cache {
pub fn done_count(&self) -> usize {
self.done_ids.len()
}
+
+ /// Clear all done IDs from the cache
+ pub fn clear_done_ids(&mut self) {
+ self.done_ids.clear();
+ }
}
tools/github-notif-manager/src/config.rs
@@ -57,6 +57,7 @@ impl Config {
"repository",
"reason",
"subject_type",
+ "subject_title",
"unread",
"age_days",
"updated_before",
tools/github-notif-manager/src/main.rs
@@ -80,9 +80,13 @@ enum Commands {
#[arg(long)]
cache_dir: Option<PathBuf>,
- /// Clear the cache
+ /// Clear the entire cache
#[arg(long)]
clear: bool,
+
+ /// Clear only the done IDs (keep notifications)
+ #[arg(long)]
+ clear_done: bool,
},
}
@@ -103,7 +107,7 @@ fn main() -> Result<()> {
all,
cache_dir,
} => cmd_list(limit, all, cache_dir),
- Commands::Cache { cache_dir, clear } => cmd_cache(cache_dir, clear),
+ Commands::Cache { cache_dir, clear, clear_done } => cmd_cache(cache_dir, clear, clear_done),
}
}
@@ -171,27 +175,46 @@ fn fetch_notifications(
.get_notifications(force_all, since.as_deref(), pb.as_ref())
.context("Failed to fetch notifications")?;
- let fresh_count = fresh.len();
- cache.merge(fresh, full_fetch);
+ let (fresh_count, filtered_count) = cache.merge(fresh, full_fetch);
if let Some(pb) = pb {
pb.finish_and_clear();
}
if full_fetch {
- eprintln!(
- " {} Fetched {} notifications (filtered {} done)",
- if tty { "✓".green().to_string() } else { "OK".to_string() },
- cache.notifications.len(),
- cache.done_count()
- );
+ let msg = if filtered_count > 0 {
+ format!(
+ " {} Fetched {} notifications (filtered {} previously done)",
+ if tty { "✓".green().to_string() } else { "OK".to_string() },
+ cache.notifications.len(),
+ filtered_count
+ )
+ } else {
+ format!(
+ " {} Fetched {} notifications",
+ if tty { "✓".green().to_string() } else { "OK".to_string() },
+ cache.notifications.len()
+ )
+ };
+ eprintln!("{}", msg);
} else {
- eprintln!(
- " {} Fetched {} new/updated, {} total cached",
- if tty { "✓".green().to_string() } else { "OK".to_string() },
- fresh_count,
- cache.notifications.len()
- );
+ let msg = if filtered_count > 0 {
+ format!(
+ " {} Fetched {} new/updated (filtered {} done), {} total cached",
+ if tty { "✓".green().to_string() } else { "OK".to_string() },
+ fresh_count - filtered_count,
+ filtered_count,
+ cache.notifications.len()
+ )
+ } else {
+ format!(
+ " {} Fetched {} new/updated, {} total cached",
+ if tty { "✓".green().to_string() } else { "OK".to_string() },
+ fresh_count,
+ cache.notifications.len()
+ )
+ };
+ eprintln!("{}", msg);
}
Ok(())
@@ -499,7 +522,7 @@ fn cmd_list(limit: usize, force_all: bool, cache_dir: Option<PathBuf>) -> Result
Ok(())
}
-fn cmd_cache(cache_dir: Option<PathBuf>, clear: bool) -> Result<()> {
+fn cmd_cache(cache_dir: Option<PathBuf>, clear: bool, clear_done: bool) -> Result<()> {
let cp = cache_path(cache_dir);
if clear {
@@ -512,6 +535,23 @@ fn cmd_cache(cache_dir: Option<PathBuf>, clear: bool) -> Result<()> {
return Ok(());
}
+ if clear_done {
+ if !cp.exists() {
+ println!("{}", "No cache file found".yellow());
+ return Ok(());
+ }
+ let mut cache = Cache::load(&cp)?;
+ let count = cache.done_count();
+ cache.clear_done_ids();
+ cache.save(&cp)?;
+ println!(
+ "{} Cleared {} done notification IDs from cache",
+ "✓".green(),
+ count
+ );
+ return Ok(());
+ }
+
if !cp.exists() {
println!("{}", "No cache file found".yellow());
println!("Path: {}", cp.display());
@@ -545,7 +585,11 @@ fn cmd_cache(cache_dir: Option<PathBuf>, clear: bool) -> Result<()> {
println!(" Read: {}", read.to_string().bright_black());
}
- println!("Done tracked: {}", cache.done_count());
+ println!(
+ "Done tracked: {} {}",
+ cache.done_count().to_string().yellow(),
+ "(use --clear-done to reset)".bright_black()
+ );
Ok(())
}
tools/github-notif-manager/src/rules.rs
@@ -13,6 +13,7 @@ impl RuleMatcher {
"repository" => Self::match_repository(notification, value),
"reason" => Self::match_string(¬ification.reason, value),
"subject_type" => Self::match_string(¬ification.subject.subject_type, value),
+ "subject_title" => Self::match_subject_title(notification, value),
"unread" => Self::match_bool(notification.unread, value),
"age_days" => Self::match_age_days(notification, value),
"updated_before" => Self::match_updated_before(notification, value),
@@ -50,6 +51,15 @@ impl RuleMatcher {
}
}
+ fn match_subject_title(notification: &Notification, value: &FilterValue) -> bool {
+ if let FilterValue::String(pattern) = value {
+ let matcher = WildMatch::new_case_insensitive(pattern);
+ matcher.matches(¬ification.subject.title)
+ } else {
+ false
+ }
+ }
+
fn match_string(field_value: &str, filter_value: &FilterValue) -> bool {
if let FilterValue::String(expected) = filter_value {
field_value == expected
tools/github-notif-manager/Cargo.lock
@@ -405,7 +405,7 @@ dependencies = [
[[package]]
name = "github-notif-manager"
-version = "0.2.0"
+version = "0.3.0"
dependencies = [
"anyhow",
"chrono",
tools/github-notif-manager/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "github-notif-manager"
-version = "0.2.0"
+version = "0.3.0"
edition = "2021"
[dependencies]
tools/github-notif-manager/config.example.yaml
@@ -4,6 +4,16 @@
# Rules are evaluated in order, and the first matching rule is applied.
rules:
+ # Example: Archive merged Dependabot PRs
+ - name: "Archive merged Dependabot PRs"
+ description: "Auto-archive merged dependency update PRs"
+ filters:
+ subject_type: "PullRequest"
+ reason: "state_change" # Triggered when PR is merged/closed
+ subject_title: "*bump *" # Case-insensitive; matches "Bump ..." and "chore(deps): bump ..."
+ action: done
+ enabled: true
+
# Example: Archive all Dependabot notifications from a specific repo
- name: "Archive Dependabot notifications"
description: "Auto-archive dependency update notifications"
@@ -48,8 +58,9 @@ rules:
# Available filter fields:
# - repository: string or pattern (supports wildcards like "owner/*")
-# - reason: string (subscribed, mention, team_mention, author, etc.)
+# - reason: string (subscribed, mention, team_mention, author, state_change, review_requested, etc.)
# - subject_type: string (Release, Issue, PullRequest, CheckSuite, Discussion, etc.)
+# - subject_title: string or pattern (supports wildcards, case-insensitive, e.g. "*bump *")
# - unread: boolean (true/false)
# - age_days: number (notifications older than N days)
# - updated_before: ISO date string (YYYY-MM-DD)
tools/github-notif-manager/config.yaml
@@ -57,6 +57,16 @@ rules:
# ─── TARGETED ARCHIVE rules — before the general keep ─────────────────
+ # Merged Dependabot PRs (titles: "Bump ..." or "chore(deps): bump ...")
+ - name: "Archive merged Dependabot PRs"
+ description: "Merged dependency update PRs from dependabot"
+ filters:
+ subject_type: "PullRequest"
+ reason: "state_change"
+ subject_title: "*bump *"
+ action: done
+ enabled: true
+
# openshift-pipelines/operator review requests (437 notifications!)
# These are almost all automated bot/renovate PRs
- name: "Archive operator bot review requests"
tools/github-notif-manager/default.nix
@@ -7,7 +7,7 @@
rustPlatform.buildRustPackage (finalAttrs: {
pname = "github-notif-manager";
- version = "0.2.0";
+ version = "0.3.0";
src = lib.fileset.toSource {
root = ./.;
tools/github-notif-manager/README.md
@@ -75,8 +75,9 @@ rules:
### Available Filters
- **`repository`**: Repository name or pattern (e.g., `"owner/repo"` or `"owner/*"`)
-- **`reason`**: Notification reason (e.g., `"subscribed"`, `"mention"`, `"team_mention"`, `"author"`)
+- **`reason`**: Notification reason (e.g., `"subscribed"`, `"mention"`, `"team_mention"`, `"author"`, `"state_change"`, `"review_requested"`)
- **`subject_type`**: Type of subject (e.g., `"Release"`, `"Issue"`, `"PullRequest"`, `"CheckSuite"`, `"Discussion"`)
+- **`subject_title`**: Pattern matching on notification title (e.g., `"*bump *"`) - supports wildcards, **case-insensitive**
- **`unread`**: Boolean - `true` for unread, `false` for read
- **`age_days`**: Number - notifications older than N days
- **`updated_before`**: ISO date string - notifications updated before this date (YYYY-MM-DD)
@@ -132,6 +133,18 @@ github-notif-manager process --config config.yaml --dry-run --verbose
## Examples
+### Archive Merged Dependabot PRs
+
+```yaml
+rules:
+ - name: "Archive merged Dependabot PRs"
+ filters:
+ subject_type: "PullRequest"
+ reason: "state_change"
+ subject_title: "*bump *" # Case-insensitive; matches "Bump ..." and "chore(deps): bump ..."
+ action: done
+```
+
### Archive All Dependabot Notifications
```yaml