Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 68 additions & 7 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ name: Issue Triage
# exfiltrate secrets. All GitHub mutations happen in steps the LLM
# cannot influence.
#
# Runs on issue open, and again when someone removes the `needs-info` label —
# the re-triage path reads the reporter's follow-up comments and classifies +
# assigns the issue (re-adding `needs-info` only if it is still too vague).
#
# What the bot does:
# 1. Removes `needs-triage`, adds `triaged`
# 2. Classifies component — one `comp:*` label
Expand All @@ -24,12 +28,21 @@ name: Issue Triage

on:
issues:
types: [opened]
# `unlabeled` re-runs triage when someone removes `needs-info` (see the job
# `if:` below) — that removal is the signal the issue now has enough detail
# to classify and assign.
types: [opened, unlabeled]

permissions:
issues: write
contents: read

# One triage run per issue at a time; a newer event supersedes an in-flight one
# (e.g. a re-label right after open won't race with the initial run).
concurrency:
group: issue-triage-${{ github.event.issue.number }}
cancel-in-progress: true

env:
OMNIGENT_SKIP_WEB_UI: "true"
UV_INDEX_URL: https://pypi.org/simple
Expand All @@ -39,9 +52,25 @@ jobs:
triage:
runs-on: ubuntu-latest
timeout-minutes: 10
# Skip issues opened by bots to avoid feedback loops.
# Run on:
# - a newly opened issue by a non-bot author (initial triage), OR
# - the `needs-info` label being REMOVED from an open issue (re-triage:
# the removal signals the issue now has enough detail to classify).
# The `unlabeled` path intentionally allows a bot actor: the removal is made
# by the omnigent-ci App (see needs-info-response.yml) whose login ends in
# `[bot]`, and only an App-token/human removal re-triggers at all — this
# workflow's own label edits use the default GITHUB_TOKEN, which never emits
# re-triggering events, so there is no loop to guard against here.
if: >-
!endsWith(github.event.issue.user.login, '[bot]')
(
github.event.action == 'opened' &&
!endsWith(github.event.issue.user.login, '[bot]')
) ||
(
github.event.action == 'unlabeled' &&
github.event.label.name == 'needs-info' &&
github.event.issue.state == 'open'
)
steps:
- name: Check LLM credentials available
id: creds
Expand Down Expand Up @@ -107,8 +136,11 @@ jobs:
set -euo pipefail

# Fetch issue metadata to a file — never interpolated into shell.
# `comments` is included so the re-triage path (needs-info removed) can
# see the detail the reporter added in comments, not just the original
# body.
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
--json number,title,body,labels,author \
--json number,title,body,labels,author,comments \
> /tmp/issue.json

# Extract key terms for duplicate search (first 200 chars of title+body).
Expand Down Expand Up @@ -258,6 +290,22 @@ jobs:
body = (issue.get("body") or "")[:8192]
labels = [l["name"] for l in issue.get("labels", [])]

# Follow-up comments by the issue author — the reporter often supplies
# the missing detail here, so the re-triage path must read them. Only
# the author's own comments count as clarification (others' comments
# are noise for this purpose and are dropped). Capped to 4 KB total.
author_login = issue.get("author", {}).get("login")
comment_section = "None."
if author_login:
author_comments = [
c.get("body", "")
for c in issue.get("comments", [])
if c.get("author", {}).get("login") == author_login and c.get("body")
]
if author_comments:
joined = "\n\n---\n\n".join(author_comments)[:4096]
comment_section = joined

dupe_section = "None found."
if dupes:
lines = [f"- #{d['number']}: {d['title']}" for d in dupes[:5]]
Expand All @@ -275,6 +323,10 @@ jobs:
Body:
{body}

## AUTHOR FOLLOW-UP COMMENTS (UNTRUSTED — later clarification from the reporter)

{comment_section}

## CANDIDATE DUPLICATES

{dupe_section}
Expand Down Expand Up @@ -342,6 +394,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
EVENT_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail

Expand Down Expand Up @@ -391,12 +444,18 @@ jobs:
dup = None

if result.get("needs_info"):
labels_add.append("needs-info")
if "needs-info" not in existing_labels:
labels_add.append("needs-info")
if "needs-triage" in existing_labels:
labels_remove.append("needs-triage")
# needs-info issues are still triaged — they just need more info.
labels_add.append("triaged")
else:
# No longer needs info. On the re-triage path the label is already
# gone (its removal triggered this run); this is a safety net for
# any case where it lingers.
if "needs-info" in existing_labels:
labels_remove.append("needs-info")
# Type
t = result.get("type")
if t and t in ALLOWED_TYPES:
Expand Down Expand Up @@ -477,8 +536,10 @@ jobs:
if labels_add or labels_remove:
cmds.append(" ".join(shlex.quote(a) for a in args))

# Duplicate comment.
if output["duplicate_of"]:
# Duplicate comment — only on the initial open. On the re-triage path
# (needs-info removed) any duplicate note was already posted at open
# time, so we skip it to avoid re-commenting.
if output["duplicate_of"] and os.environ.get("EVENT_ACTION") == "opened":
comment_args = [
"gh", "issue", "comment", issue, "--repo", repo,
"--body", f"Potential duplicate of #{output['duplicate_of']}. React 👎 to contest.",
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/needs-info-response.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Clear needs-info on author response

# When the issue AUTHOR comments on an issue that carries `needs-info`, remove
# the label — the reporter has (presumably) supplied the missing detail. That
# removal is the signal the rest of the pipeline is built around:
#
# author comments -> this workflow removes `needs-info`
# -> issue-triage.yml's `unlabeled` trigger re-triages
# (reads the follow-up comments, classifies + assigns,
# or re-adds `needs-info` if it is still too vague)
# author never responds -> stale.yml closes the issue after inactivity
#
# CRITICAL: the label MUST be removed with the omnigent-ci App token, not the
# default GITHUB_TOKEN. GitHub does not re-trigger workflows from events made
# by GITHUB_TOKEN, so a default-token removal would NOT fire issue-triage's
# `unlabeled` re-triage. The App token is a distinct actor, so its `unlabeled`
# event does re-trigger. If the App isn't configured, we skip (fail-closed):
# leaving the label is safer than removing it and stranding the issue.

on:
issue_comment:
types: [created]

permissions:
issues: write

concurrency:
group: needs-info-response-${{ github.event.issue.number }}
cancel-in-progress: true

jobs:
clear-needs-info:
runs-on: ubuntu-latest
# Only when a NON-bot commenter who IS the issue author comments on an OPEN
# issue (not a PR — issue_comment fires for PRs too) that still carries
# `needs-info`.
if: >-
!endsWith(github.event.sender.login, '[bot]') &&
!github.event.issue.pull_request &&
github.event.issue.state == 'open' &&
github.event.comment.user.login == github.event.issue.user.login &&
contains(github.event.issue.labels.*.name, 'needs-info')
steps:
- name: Mint omnigent-ci App token
id: app-token
if: vars.OMNIGENT_BOT_APP_ID != ''
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.OMNIGENT_BOT_APP_ID }}
private-key: ${{ secrets.OMNIGENT_BOT_APP_KEY }}

- name: Remove needs-info label
# Skip when the App isn't configured: removing with GITHUB_TOKEN would
# not re-trigger re-triage, so the label would just silently vanish.
if: steps.app-token.outputs.token != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail
echo "Author responded on #$ISSUE_NUMBER; removing needs-info to re-triage."
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label needs-info

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in bc04dc5. The removal step now re-checks the live labels (gh issue view --json labels | grep -qx needs-info) before calling --remove-label, so a stale event payload or a race (two quick comments / concurrent removal) no-ops cleanly instead of erroring. Also added a ::notice:: when the App is unconfigured, and suppressed the duplicate label (not just the comment) on the re-triage path per Polly's note.

Loading