fix(#6156): block closed issue code dispatch - #6876
Conversation
Use issue state from labeled events at the final routing boundary. Prevent close-between-check-and-label races from dispatching code. Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Shai Revivo <srevivo@redhat.com>
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
PR Summary by QodoBlock code dispatch for closed issues
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo
1.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Bind the exact open-state condition to the nested code-stage branch. Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Shai Revivo <srevivo@redhat.com>
|
This is touching on a complicated area. The conditions that are used to trigger an agent (the "predicate" of the agent) needs to move out of these .github/ files into something that is handled between the |
@ralphbean in this case both PRs need to close. I agree it is a "corner case" but the cleanest implementation need both sides.
Feel free to close it if you think it is too risky and not worthy to fix. |
waynesun09
left a comment
There was a problem hiding this comment.
[MEDIUM] "Authoritative dispatch-side guard" only covers the GitHub webhook route; GitLab/Jira poll routing has the same race unguarded
Anchor: internal/dispatch/router.go:129 (not in this diff, so noting it here rather than inline).
The PR body calls this "the authoritative dispatch-side guard for the close-between-check-and-label race", but only the GitHub Actions shell router (reusable-dispatch.yml + scaffold dispatch.yml) gained the check. Verified on head 2cc8c60:
internal/forge/gitlab/poll.go:42lists issues withupdated_after=only (nostate=opened).internal/poll/state.godetectNewLabelsemits a label-added event for any issue in the poll set whose labels changed, and only consultsisIssueClosed(state.go:103-132) in the prune path for issues NOT in the current poll set — so a just-closed issue with a freshready-to-codestill produces an event.HarnessRouter.routeLabel(router.go:129-130) mapsready-to-code->codewith no open/closed check.normevent.State(internal/normevent/event.go:129-133) carries onlyLabels/ChangeProposal/Conversation, so neitherrouteLabelnor a CELtriggercan check lifecycle state today.poll.Issuealready has aStatefield (internal/poll/client.go:64).- Jira's default JQL excludes
statusCategory != Done, but a user-supplied--jqlbypasses that (internal/jirapoll/poller.go:319-324).
Not raised in any existing thread — the discussion above covers the NormalizedEvent/trigger migration generally but does not name this gap. This finding actually reinforces that stated direction: an issue lifecycle field on NormalizedEvent would cover GitHub, GitLab, Jira and CEL triggers in one place instead of another workflow conditional.
Suggestion: Preferred: add an issue open/closed lifecycle field to normevent.State (populated from poll.Issue.State and the GitHub event) and check it in HarnessRouter.routeLabel, which is where agent predicates are migrating; that makes the guard genuinely cross-forge. Minimal alternative: skip ready-to-code in detectNewLabels/discoverAllEvents when iss.State == "closed" (field already exists). If deferred, soften the PR body to "GitHub dispatch-side guard", cross-link #1647, and file a follow-up for the poll/CEL route so the claim matches what ships.
| if [[ "${TRIGGERING_LABEL}" == "ready-for-triage" ]]; then | ||
| STAGE="triage" | ||
| elif [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then | ||
| elif [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]] && [[ "${ISSUE_STATE}" == "open" ]]; then |
There was a problem hiding this comment.
[MEDIUM] /fs-code still dispatches the code stage on a closed issue, contradicting the documented "agents don't act on closed issues" contract
Verified on head 2cc8c60: the issue_comment /fs-code branch in this same run: step (reusable-dispatch.yml:221-226; scaffold dispatch.yml:123-128) checks only ISSUE_IS_PR/ISSUE_HAS_PR == "false", COMMENT_USER_TYPE != Bot and is_authorized — it never reads ISSUE_STATE, even though github.event.issue.state is populated on issue_comment events and the env var is now exported to this step. docs/guides/user/bugfix-workflow.md:170 states "Agents don't act on closed issues (except /fs-triage which explicitly reopens)" — an explicit exception list that does not include /fs-code — so the label path and the slash-command path now enforce different close-stops-agents contracts, and scripts/pre-code.sh in fullsend-ai/agents has no issue-state check either (its only state queries are on linked PRs).
Caveat: this is pre-existing behaviour in an unmodified branch of the same modified step, not a regression introduced by the diff, and treating /fs-code as a deliberate human override is arguably intentional.
Suggestion: Given the preference not to grow workflow conditionals, the cheapest fix is docs-only: add /fs-code to the exception list at bugfix-workflow.md:170 and note in docs/agents/code.md that /fs-code is an explicit human override that runs on closed issues. Otherwise mirror the guard — add [[ "${ISSUE_STATE}" == "open" ]] to the /fs-code branch in both dispatch copies and extend TestClosedIssueReadyToCodeDoesNotDispatch to cover it. Either way, state the design decision explicitly so the two paths agree.
Summary
ready-to-codelabels only when that event reports an open issueThis is the authoritative dispatch-side guard for the close-between-check-and-label race. The producer-side optimization and post-triage regressions are in fullsend-ai/agents#1120.
Testing
GOCACHE=/private/tmp/fullsend-6156-go-cache go test ./internal/scaffold -count=1GOCACHE=/private/tmp/fullsend-6156-go-cache make lintFixes #6156