From c8466aa0b2d67ec01bc83ab5ccf38810e8c9c1d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:56:35 +0000 Subject: [PATCH 1/3] Initial plan From df269288bdc4ec996960e431c09cd81d42ee47b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:00:03 +0000 Subject: [PATCH 2/3] fix(guard): ignore stale maintainer reactions after content edits Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/3064f93b-a433-4244-be07-a6af9cc4d7ec Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../rust-guard/src/labels/helpers.rs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/guards/github-guard/rust-guard/src/labels/helpers.rs b/guards/github-guard/rust-guard/src/labels/helpers.rs index 83ddf79d..34eb1b73 100644 --- a/guards/github-guard/rust-guard/src/labels/helpers.rs +++ b/guards/github-guard/rust-guard/src/labels/helpers.rs @@ -467,6 +467,10 @@ pub fn has_maintainer_reaction_with_callback( }; let endorser_min_rank = integrity_level_rank(endorser_min); + let item_updated_at = item + .get("updatedAt") + .or_else(|| item.get("updated_at")) + .and_then(|v| v.as_str()); for node in nodes.iter().take(MAX_REACTIONS_TO_CHECK) { let content = match node.get("content").and_then(|v| v.as_str()) { @@ -490,6 +494,26 @@ pub fn has_maintainer_reaction_with_callback( None => continue, }; + let reaction_created_at = node + .get("createdAt") + .or_else(|| node.get("created_at")) + .and_then(|v| v.as_str()); + if let (Some(item_updated), Some(reaction_created)) = (item_updated_at, reaction_created_at) { + if item_updated > reaction_created { + crate::log_debug(&format!( + "[integrity] {}: skipping stale {} reaction {} from @{} \ + (item updatedAt={} > reaction createdAt={})", + repo_full_name, + reaction_kind, + content, + login, + item_updated, + reaction_created + )); + continue; + } + } + // Fetch reactor's collaborator permission to determine their integrity level. let perm = super::backend::get_collaborator_permission_with_callback( callback, owner, repo, login, @@ -2008,6 +2032,101 @@ mod tests { )); } + #[test] + fn test_has_maintainer_reaction_honors_unmodified_item_endorsement() { + let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]); + let item = serde_json::json!({ + "number": 42, + "updatedAt": "2026-04-20T00:00:00Z", + "reactions": {"nodes": [{ + "user": {"login": "alice"}, + "content": "THUMBS_UP", + "createdAt": "2026-04-20T00:00:00Z" + }]} + }); + assert!(has_maintainer_reaction_with_callback( + &item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx, + admin_permission_callback, "endorsement" + )); + } + + #[test] + fn test_has_maintainer_reaction_skips_stale_endorsement_after_edit() { + let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]); + let item = serde_json::json!({ + "number": 42, + "updatedAt": "2026-04-21T00:00:00Z", + "reactions": {"nodes": [{ + "user": {"login": "alice"}, + "content": "THUMBS_UP", + "createdAt": "2026-04-20T00:00:00Z" + }]} + }); + assert!(!has_maintainer_reaction_with_callback( + &item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx, + admin_permission_callback, "endorsement" + )); + } + + #[test] + fn test_has_maintainer_reaction_honors_endorsement_added_after_edit() { + let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]); + let item = serde_json::json!({ + "number": 42, + "updated_at": "2026-04-20T00:00:00Z", + "reactions": {"nodes": [{ + "user": {"login": "alice"}, + "content": "THUMBS_UP", + "createdAt": "2026-04-21T00:00:00Z" + }]} + }); + assert!(has_maintainer_reaction_with_callback( + &item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx, + admin_permission_callback, "endorsement" + )); + } + + #[test] + fn test_has_maintainer_reaction_counts_fresh_when_stale_and_fresh_mixed() { + let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]); + let item = serde_json::json!({ + "number": 42, + "updatedAt": "2026-04-21T00:00:00Z", + "reactions": {"nodes": [ + { + "user": {"login": "alice"}, + "content": "THUMBS_UP", + "createdAt": "2026-04-20T00:00:00Z" + }, + { + "user": {"login": "bob"}, + "content": "THUMBS_UP", + "createdAt": "2026-04-22T00:00:00Z" + } + ]} + }); + assert!(has_maintainer_reaction_with_callback( + &item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx, + admin_permission_callback, "endorsement" + )); + } + + #[test] + fn test_has_maintainer_reaction_missing_timestamps_keeps_existing_behavior() { + let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]); + let item = serde_json::json!({ + "number": 42, + "reactions": {"nodes": [{ + "user": {"login": "alice"}, + "content": "THUMBS_UP" + }]} + }); + assert!(has_maintainer_reaction_with_callback( + &item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx, + admin_permission_callback, "endorsement" + )); + } + #[test] fn test_cap_integrity_at_none() { let ctx = test_ctx(); From 2db66858b01877614f7d747c5e7cd07858e52055 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 20 Apr 2026 20:21:11 -0700 Subject: [PATCH 3/3] Update guards/github-guard/rust-guard/src/labels/helpers.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- guards/github-guard/rust-guard/src/labels/helpers.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/guards/github-guard/rust-guard/src/labels/helpers.rs b/guards/github-guard/rust-guard/src/labels/helpers.rs index 34eb1b73..ea190ba7 100644 --- a/guards/github-guard/rust-guard/src/labels/helpers.rs +++ b/guards/github-guard/rust-guard/src/labels/helpers.rs @@ -468,7 +468,11 @@ pub fn has_maintainer_reaction_with_callback( let endorser_min_rank = integrity_level_rank(endorser_min); let item_updated_at = item - .get("updatedAt") + .get("lastEditedAt") + .or_else(|| item.get("editedAt")) + .or_else(|| item.get("last_edited_at")) + .or_else(|| item.get("edited_at")) + .or_else(|| item.get("updatedAt")) .or_else(|| item.get("updated_at")) .and_then(|v| v.as_str());