Add Daily Dedupe Digest workflow driven by AI Issue Triage output (#49907)

## Summary

Rewrites the Daily Dedupe Digest as a lightweight aggregator over the
existing **AI Issue Triage** workflow, superseding #48244.

Instead of running its own `gpt-4o-mini` duplicate-detection pass, this
workflow treats the triage workflow as the single source of truth. AI
Issue Triage already:

- posts a canonical comment per issue (marker `<!--
powertoys-ai-triage:canonical:v1 -->`) containing a `### 🔁 Possible
duplicates` section, and
- files a *pending* native duplicate-close suggestion pointing at the
strongest canonical candidate.

The digest simply collects those and drops them into one daily review
issue.

## Behavior

Runs daily (`0 8 * * *`) and via `workflow_dispatch`. Each run:

1. Ensures the `dedupe-digest` label exists.
2. Builds a candidate set from **carry-over** issues remembered in the
previous digest (hidden `<!-- dup:ISSUE=.. CANON=.. -->` markers) plus
**fresh** open issues updated in the lookback window (default 26h).
3. Re-validates each candidate against its *live* triage comment —
issues that are closed or labeled `duplicate` / `Resolution-Duplicate`
drop out automatically.
4. Opens a **new** issue each day, assigned to `@niels9001`, listing
every flagged issue (the duplicate → to close) with its suggested
canonical issue (→ keep) and the triage reason. Links to each triage
summary comment are included.
5. Closes the previous digest, superseded by the new one.
6. If nothing is flagged, it closes the previous digest and creates
none.

## Notes

- No model calls / no `models: read` permission — only `issues: write`.
- Untrusted issue/comment text is sanitized (HTML comments, control
chars, angle brackets stripped) before being written into the digest, to
avoid marker injection.
- Tunable via `env`: assignee, label, title prefix, lookback hours,
scan/flag caps, resolved-duplicate labels.

Supersedes #48244.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: niels9001 <niels9001@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26025067-259e-43e3-9dc7-a9fc4b5ba58b
This commit is contained in:
Niels Laute
2026-08-14 22:59:29 +02:00
committed by GitHub
parent 8d463be70c
commit 105ef0abcb
3 changed files with 412 additions and 0 deletions

View File

@@ -533,6 +533,17 @@ def existing_input_hash(comments):
def should_process(event, report_comment):
action = event.get("action")
issue = event.get("issue") or {}
issue_labels = {
(label.get("name", "") if isinstance(label, dict) else str(label)).lower()
for label in (issue.get("labels") or [])
}
# Never triage the automated dedupe-digest issue. It is bot-authored and
# aggregates untrusted text from many issues, so re-triaging it wastes AI
# credits and creates a prompt-injection surface. See
# .github/workflows/dedupe-digest.yml (DIGEST_LABEL).
if "dedupe-digest" in issue_labels:
return False, False
if "comment" not in event:
if (
action == "reopened"

View File

@@ -338,6 +338,17 @@ class IssueContextTests(unittest.TestCase):
}
self.assertEqual(CONTEXT.should_process(event, None), (False, False))
def test_dedupe_digest_issue_is_never_triaged(self):
event = {
"action": "opened",
"sender": {"login": "github-actions[bot]"},
"issue": {
"number": 11,
"labels": [{"name": "dedupe-digest"}],
},
}
self.assertEqual(CONTEXT.should_process(event, None), (False, False))
def test_unrelated_comment_writes_noop(self):
event = {
"action": "created",