Skip to content

Commit df26928

Browse files
Copilotlpcox
andauthored
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>
1 parent c8466aa commit df26928

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

  • guards/github-guard/rust-guard/src/labels

guards/github-guard/rust-guard/src/labels/helpers.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,10 @@ pub fn has_maintainer_reaction_with_callback(
467467
};
468468

469469
let endorser_min_rank = integrity_level_rank(endorser_min);
470+
let item_updated_at = item
471+
.get("updatedAt")
472+
.or_else(|| item.get("updated_at"))
473+
.and_then(|v| v.as_str());
470474

471475
for node in nodes.iter().take(MAX_REACTIONS_TO_CHECK) {
472476
let content = match node.get("content").and_then(|v| v.as_str()) {
@@ -490,6 +494,26 @@ pub fn has_maintainer_reaction_with_callback(
490494
None => continue,
491495
};
492496

497+
let reaction_created_at = node
498+
.get("createdAt")
499+
.or_else(|| node.get("created_at"))
500+
.and_then(|v| v.as_str());
501+
if let (Some(item_updated), Some(reaction_created)) = (item_updated_at, reaction_created_at) {
502+
if item_updated > reaction_created {
503+
crate::log_debug(&format!(
504+
"[integrity] {}: skipping stale {} reaction {} from @{} \
505+
(item updatedAt={} > reaction createdAt={})",
506+
repo_full_name,
507+
reaction_kind,
508+
content,
509+
login,
510+
item_updated,
511+
reaction_created
512+
));
513+
continue;
514+
}
515+
}
516+
493517
// Fetch reactor's collaborator permission to determine their integrity level.
494518
let perm = super::backend::get_collaborator_permission_with_callback(
495519
callback, owner, repo, login,
@@ -2008,6 +2032,101 @@ mod tests {
20082032
));
20092033
}
20102034

2035+
#[test]
2036+
fn test_has_maintainer_reaction_honors_unmodified_item_endorsement() {
2037+
let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]);
2038+
let item = serde_json::json!({
2039+
"number": 42,
2040+
"updatedAt": "2026-04-20T00:00:00Z",
2041+
"reactions": {"nodes": [{
2042+
"user": {"login": "alice"},
2043+
"content": "THUMBS_UP",
2044+
"createdAt": "2026-04-20T00:00:00Z"
2045+
}]}
2046+
});
2047+
assert!(has_maintainer_reaction_with_callback(
2048+
&item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx,
2049+
admin_permission_callback, "endorsement"
2050+
));
2051+
}
2052+
2053+
#[test]
2054+
fn test_has_maintainer_reaction_skips_stale_endorsement_after_edit() {
2055+
let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]);
2056+
let item = serde_json::json!({
2057+
"number": 42,
2058+
"updatedAt": "2026-04-21T00:00:00Z",
2059+
"reactions": {"nodes": [{
2060+
"user": {"login": "alice"},
2061+
"content": "THUMBS_UP",
2062+
"createdAt": "2026-04-20T00:00:00Z"
2063+
}]}
2064+
});
2065+
assert!(!has_maintainer_reaction_with_callback(
2066+
&item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx,
2067+
admin_permission_callback, "endorsement"
2068+
));
2069+
}
2070+
2071+
#[test]
2072+
fn test_has_maintainer_reaction_honors_endorsement_added_after_edit() {
2073+
let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]);
2074+
let item = serde_json::json!({
2075+
"number": 42,
2076+
"updated_at": "2026-04-20T00:00:00Z",
2077+
"reactions": {"nodes": [{
2078+
"user": {"login": "alice"},
2079+
"content": "THUMBS_UP",
2080+
"createdAt": "2026-04-21T00:00:00Z"
2081+
}]}
2082+
});
2083+
assert!(has_maintainer_reaction_with_callback(
2084+
&item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx,
2085+
admin_permission_callback, "endorsement"
2086+
));
2087+
}
2088+
2089+
#[test]
2090+
fn test_has_maintainer_reaction_counts_fresh_when_stale_and_fresh_mixed() {
2091+
let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]);
2092+
let item = serde_json::json!({
2093+
"number": 42,
2094+
"updatedAt": "2026-04-21T00:00:00Z",
2095+
"reactions": {"nodes": [
2096+
{
2097+
"user": {"login": "alice"},
2098+
"content": "THUMBS_UP",
2099+
"createdAt": "2026-04-20T00:00:00Z"
2100+
},
2101+
{
2102+
"user": {"login": "bob"},
2103+
"content": "THUMBS_UP",
2104+
"createdAt": "2026-04-22T00:00:00Z"
2105+
}
2106+
]}
2107+
});
2108+
assert!(has_maintainer_reaction_with_callback(
2109+
&item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx,
2110+
admin_permission_callback, "endorsement"
2111+
));
2112+
}
2113+
2114+
#[test]
2115+
fn test_has_maintainer_reaction_missing_timestamps_keeps_existing_behavior() {
2116+
let ctx = ctx_with_endorsement_reactions(vec!["THUMBS_UP"]);
2117+
let item = serde_json::json!({
2118+
"number": 42,
2119+
"reactions": {"nodes": [{
2120+
"user": {"login": "alice"},
2121+
"content": "THUMBS_UP"
2122+
}]}
2123+
});
2124+
assert!(has_maintainer_reaction_with_callback(
2125+
&item, "owner/repo", &ctx.endorsement_reactions, "approved", &ctx,
2126+
admin_permission_callback, "endorsement"
2127+
));
2128+
}
2129+
20112130
#[test]
20122131
fn test_cap_integrity_at_none() {
20132132
let ctx = test_ctx();

0 commit comments

Comments
 (0)