Skip to content

fix(dispatch): distinguish permission-check failures from unauthorized skips#5219

Open
Roming22 wants to merge 1 commit into
fullsend-ai:mainfrom
Roming22:dispatch-output
Open

fix(dispatch): distinguish permission-check failures from unauthorized skips#5219
Roming22 wants to merge 1 commit into
fullsend-ai:mainfrom
Roming22:dispatch-output

Conversation

@Roming22

@Roming22 Roming22 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Give has_write_permission distinct exit codes: 1 for insufficient permission, 2 for operational failures (mktemp/gh api), so callers do not treat check failures as unauthorized.
  • Update comment_from_authorized_user in both dispatch workflows to emit the correct skip notice for each case (and fail closed on unexpected codes).
  • Keep reusable and scaffold routing helpers aligned (shared comments and case-based handling).

Related Issue

N/A

Changes

  • has_write_permission() returns 2 on mktemp/API failures (still returns 1 for no write access).
  • comment_from_authorized_user() inspects the auth exit code and logs either unauthorized or permission-verification-failure notices.
  • Apply the same routing-helper logic/comments in .github/workflows/reusable-dispatch.yml and internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml.
  • Extend scaffold tests for the new skip notice and exit-code behavior.

Testing

  • make lint passes (stage changes first, then run)
  • Tests added/updated for new or modified logic

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Commits are signed off (DCO) — human and human-directed agent sessions only
  • I wrote this contribution myself and can explain all changes in it

@Roming22
Roming22 requested a review from a team as a code owner July 16, 2026 18:05
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Log skip reasons for comment-triggered dispatches

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add shared helpers to reject bot/unauthorized slash commands with notice annotations.
• Centralize comment gating so skipped dispatches explain why in workflow logs.
• Update scaffold tests to assert helper usage and notice strings are present.
Diagram

graph TD
E{{"Issue comment"}} --> R["Determine stage"] --> D{{"Slash command"}}
D -->|"No"| X["No stage"]
D -->|"Yes"| A{{"Authorized & not bot"}}
A -->|"No"| N["::notice:: skip reason"] --> X
A -->|"Yes"| S["Set STAGE"] --> W["Dispatch workflow"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extract gating helpers into a shared script/composite action
  • ➕ Eliminates duplication between reusable workflow and scaffolded workflow
  • ➕ Makes future changes to authorization/logging behavior single-source
  • ➖ Introduces an additional file/action to distribute with the scaffold
  • ➖ Slightly more indirection when reading the workflow logic
2. Implement routing in actions/github-script (JS) instead of bash
  • ➕ More robust parsing/branching and easier unit testing of routing logic
  • ➕ Cleaner access to event payload fields
  • ➖ Adds runtime dependency on Node tooling and github-script conventions
  • ➖ A larger refactor than necessary for a logging-focused fix

Recommendation: The PR’s approach is appropriate for a minimal, low-risk improvement: it keeps behavior the same while making skips observable via ::notice:: annotations and reducing repeated conditionals via helpers. If routing logic continues to grow or more workflows need the same gate, consider extracting the helpers into a shared script or composite action to avoid maintaining the same bash functions in multiple workflow files.

Files changed (3) +56 / -16

Bug fix (2) +50 / -14
reusable-dispatch.ymlAdd helper gates and notice annotations for skipped comment dispatches +25/-7

Add helper gates and notice annotations for skipped comment dispatches

• Introduces 'comment_from_user' and 'comment_from_authorized_user' helpers to centralize bot filtering and write-permission checks for slash commands. When a dispatch is skipped due to a bot or unauthorized commenter, the workflow now emits '::notice::' annotations explaining the reason.

.github/workflows/reusable-dispatch.yml

dispatch.ymlMirror comment gating helpers in scaffolded dispatch workflow +25/-7

Mirror comment gating helpers in scaffolded dispatch workflow

• Applies the same shared helper functions and notice messages to the scaffolded 'dispatch.yml' used in generated repositories. Replaces inline 'COMMENT_USER_TYPE != Bot' and 'is_authorized' conditionals with 'comment_from_user' / 'comment_from_authorized_user' calls for consistent behavior and logging.

internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml

Tests (1) +6 / -2
scaffold_test.goAssert new helper functions and notice strings exist in scaffolded workflow +6/-2

Assert new helper functions and notice strings exist in scaffolded workflow

• Updates workflow-content tests to validate that bot/authorization filtering is implemented via the new helper functions. Adds assertions for the '::notice::' skip messages to prevent regressions in observability.

internal/scaffold/scaffold_test.go

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Site preview

Preview: https://84d85a8a-site.fullsend-ai.workers.dev

Commit: afebea024d9fddb20605d8fa6f925cfe048f0221

@qodo-code-review

qodo-code-review Bot commented Jul 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 61 rules

Grey Divider


Remediation recommended

1. Misleading unauthorized notice ✓ Resolved 🐞 Bug ◔ Observability
Description
comment_from_authorized_user() emits an “unauthorized comment” notice for any non-zero
is_authorized() result, but is_authorized() also returns non-zero on permission-check errors
(mktemp/gh api failures). This can incorrectly attribute skipped dispatches to authorization when
the real cause is an operational failure, making troubleshooting harder.
Code

.github/workflows/reusable-dispatch.yml[R187-194]

+          comment_from_authorized_user() {
+            if ! comment_from_user; then
+              return 1
+            fi
+            if ! is_authorized; then
+              echo "::notice::Skipping dispatch for unauthorized comment from ${COMMENT_USER_LOGIN}"
+              return 1
+            fi
Relevance

⭐⭐⭐ High

Repo prefers distinguishing expected failures vs operational errors; accepted error-handling
improvements around gh api/mktemp in dispatch workflows.

PR-#1688
PR-#3079
PR-#2934

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new notice is triggered on any is_authorized() failure, but has_write_permission() fails both
for genuine lack of permission and for operational errors, so the notice reason can be wrong.

.github/workflows/reusable-dispatch.yml[133-148]
.github/workflows/reusable-dispatch.yml[156-159]
.github/workflows/reusable-dispatch.yml[180-195]
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[60-76]
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[83-86]
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[110-124]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`comment_from_authorized_user()` logs `::notice::Skipping dispatch for unauthorized comment ...` whenever `is_authorized` returns non-zero. However, `is_authorized()` delegates to `has_write_permission()`, which also returns non-zero when the permission check *cannot be performed* (e.g., `mktemp` failure or `gh api` failure). This produces a misleading “unauthorized” reason for skipped dispatches.

### Issue Context
- `has_write_permission()` returns `1` both for “not authorized” and for operational failures (and already emits `::warning::...` for those failures).
- The new notice should only claim “unauthorized” when we actually determined insufficient permissions.

### Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[133-196]
- internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[60-126]

### Suggested fix approach
- Make `has_write_permission()` return a distinct exit code for operational failures (e.g., `2` for mktemp/gh failures), while returning `1` for “no write permission”.
- In `comment_from_authorized_user()`, inspect `$?` after `is_authorized`:
 - If rc==1: emit the current unauthorized notice.
 - If rc==2: emit a different notice like `::notice::Skipping dispatch: failed to verify permissions for ${COMMENT_USER_LOGIN}` (or rely on the existing `::warning::Permission API call failed...` and avoid claiming unauthorized).
- Apply the same change to both the reusable and scaffold workflows to keep them behaviorally identical.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. New dispatch notices lack requirement 📘 Rule violation § Compliance
Description
The PR adds new user-visible workflow behavior (notice annotations explaining skipped dispatches)
without referencing any tracked requirement artifact (issue/RFC/change request) in the change. This
makes it hard to audit why the behavior was introduced and whether it is in scope.
Code

.github/workflows/reusable-dispatch.yml[R180-193]

+          comment_from_user() {
+            if [[ "${COMMENT_USER_TYPE}" == "Bot" ]]; then
+              echo "::notice::Skipping dispatch for bot comment"
+              return 1
+            fi
+            return 0
+          }
+          comment_from_authorized_user() {
+            if ! comment_from_user; then
+              return 1
+            fi
+            if ! is_authorized; then
+              echo "::notice::Skipping dispatch for unauthorized comment from ${COMMENT_USER_LOGIN}"
+              return 1
Relevance

⭐ Low

Similar “link tracking issue for workflow/protected-path change” suggestions repeatedly rejected
(e.g., reusable-dispatch, e2e).

PR-#1063
PR-#1211
PR-#1030

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires new public behaviors to be tied to a tracked requirement artifact. The
changed workflow adds new ::notice:: annotations for skipped comment dispatches, but the change
itself contains no reference to a requirement artifact.

Rule 1062064: New code must map to explicit tracked requirements; no speculative features
.github/workflows/reusable-dispatch.yml[180-193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New workflow behavior was added (skip notices for bot/unauthorized comments) without an explicit reference to a tracked requirement (issue/RFC/change request) in the change.

## Issue Context
The dispatch workflows now emit `::notice::...` annotations when comment-triggered dispatches are skipped. Per compliance, new public/user-visible behavior must map to an explicit tracked requirement.

## Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[180-193]
- internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[110-123]

## Suggested remediation
- Add a short inline comment near the new `comment_from_user` / `comment_from_authorized_user` helpers referencing the requirement (e.g., `// Req: ISSUE-123` with a URL), and/or
- Update the PR description to include the tracking issue/RFC and ensure it clearly covers this behavior change.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread .github/workflows/reusable-dispatch.yml Outdated
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@waynesun09

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 16, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:24 PM UTC · Completed 8:39 PM UTC
Commit: 12bd957 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .github/workflows/reusable-dispatch.yml — This file is under .github/ (a protected path). The PR has no linked issue, which constitutes insufficient context for modifying governance/infrastructure files. Human approval is always required for protected-path changes.
    Remediation: Link a tracking issue to this PR explaining why the protected files need modification. Human reviewer must approve regardless.

Medium

  • [fail-open] .github/workflows/reusable-dispatch.ymlcomment_from_authorized_user() only checks for auth_rc values 1 and 2 before falling through to return 0. If is_authorized returns any unexpected non-zero exit code (e.g., signal-induced 126, 127, 128+N), the function would incorrectly return 0, permitting the dispatch. The dispatch.yml version correctly uses a case statement with return 0 only for code 0, defaulting to return 1 for all others. See also: [structural-consistency] finding at this location.
    Remediation: Adopt the case-statement pattern from dispatch.yml: case "${auth_rc}" in 0) return 0 ;; 1) echo ... ;; 2) echo ... ;; esac; return 1.

  • [structural-consistency] .github/workflows/reusable-dispatch.yml — AGENTS.md requires that dispatch.yml and reusable-dispatch.yml share identical routing logic, but comment_from_authorized_user uses if/elif for exit code handling in reusable-dispatch.yml while dispatch.yml uses a case statement. This structural difference also causes the fail-open behavior described above. See also: [fail-open] finding at this location.
    Remediation: Change the if/elif pattern to use a case statement matching dispatch.yml.

Low

  • [missing-authorization] .github/workflows/reusable-dispatch.yml — Non-trivial change submitted without a linked issue. The PR body describes the change clearly, but a linked issue establishes the authorization scope for non-trivial work.

  • [scope-alignment] .github/workflows/reusable-dispatch.yml — The PR title claims "log why comment-triggered dispatches are skipped" but the actual change is broader: (1) changes has_write_permission error return codes from 1 to 2, (2) adds new helper functions, (3) updates docstring comments. The return code change affects all callers, not just comment-triggered ones.

  • [consumer-completeness] .github/workflows/reusable-dispatch.yml — Non-comment trigger paths (is_event_actor_authorized) treat any non-zero has_write_permission result as "unauthorized" without distinguishing exit code 1 (denied) from 2 (operational failure). The new docstring says "Callers must distinguish 1 vs 2 when messaging" but these callers do not.

  • [documentation-consistency] .github/workflows/reusable-dispatch.yml — New helper functions in reusable-dispatch.yml lack inline comments (# Helper: check if...) present in dispatch.yml's versions.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

@Roming22 Roming22 changed the title fix(dispatch): log why comment-triggered dispatches are skipped fix(dispatch): distinguish permission-check failures from unauthorized skips Jul 16, 2026

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review squad pass (3 agents: Claude coder, Claude researcher, Grok). Fail-closed behavior verified correct for all code paths — return 1 after esac is the load-bearing line. Both workflow files are structurally identical. 2 MEDIUM findings posted inline (3/3 agent consensus on both). The stale CHANGES_REQUESTED from fullsend-ai-review[bot] (commit 6d2813e) is resolved in the current head — both files now use case + return 1.

Assisted-by: Claude (review), Grok (review)

# membership regardless of visibility (private vs public).
# See: github/gh-aw-mcpg#2862
# Returns 0 if username has write access, 1 if not, 2 on operational failure
# (mktemp/gh api errors). Callers must distinguish 1 vs 2 when messaging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Event-actor auth callers don't distinguish exit codes, violating the new docstring contract

The new docstring declares "Callers must distinguish 1 vs 2 when messaging." But is_event_actor_authorized() is still called in plain boolean if context at 3 call sites (lines ~278, ~282, ~301 in this file; same in scaffold), emitting no skip notices and treating exit codes 1 and 2 identically:

if is_event_actor_authorized "${ISSUE_USER_LOGIN}"; then
  STAGE="triage"
fi

Fail-closed is correct — dispatch is blocked for all non-zero. The issue is observability: when has_write_permission returns 2 (API failure) on the event-triggered path, there's zero audit trail, unlike the comment path which now logs distinct notices. The docstring creates a false expectation that all callers handle the distinction.

Suggestion: Either (a) narrow the docstring to "Comment-path callers should distinguish 1 vs 2 for observability; event-path callers currently treat all non-zero as skip", or (b) create an event_from_authorized_actor() wrapper with case-based notices matching comment_from_authorized_user().

Flagged by 3 agents (Claude x2, Grok) — consensus

;;
esac
return 1
}

@waynesun09 waynesun09 Jul 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[LOW] Missing * catch-all in case statement for unexpected exit codes (downgraded from MEDIUM after July 20 re-review at head afebea02 — all 3 agents now assess LOW)

case "${auth_rc}" handles 0, 1, and 2 but has no * default. Unexpected exit codes (e.g., 127 for gh not in PATH, 128+N for signal kill) fall through to return 1 with no notice — correctly fail-closed, but a silent skip in miniature. Downgraded because both failure arms in has_write_permission are caught by || { ...; return 2; }, so codes other than 0/1/2 cannot occur today; the exposure is limited to future refactors regressing silently.

Same pattern in the scaffold copy at internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml.

Suggestion: Add a catch-all before esac:

  *)
    echo "::warning::Unexpected auth exit code ${auth_rc} for ${COMMENT_USER_LOGIN}" >&2
  ;;
Previous review (superseded)

[MEDIUM] Missing * catch-all in case statement for unexpected exit codes

case "${auth_rc}" handles 0, 1, and 2 but has no * default. Unexpected exit codes (e.g., 127 for gh not in PATH, 128+N for signal kill) fall through to return 1 with zero audit trail. The return 1 after esac correctly maintains fail-closed, but the exact observability gap the PR is trying to fix (distinct notices per denial reason) repeats for codes > 2.

Same pattern in the scaffold copy at internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml.

Suggestion: Add a catch-all before esac:

  *)
    echo "::warning::Unexpected auth exit code ${auth_rc} for ${COMMENT_USER_LOGIN}" >&2
  ;;

Flagged by 3 agents (Claude x2, Grok) — consensus

…d skips

Return exit code 2 from has_write_permission on mktemp/API failures so
callers do not treat check errors as unauthorized. Emit matching skip
notices in comment_from_authorized_user and keep both dispatch workflows
aligned.

Signed-off-by: Romain Arnaud <rarnaud@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

rh-pre-commit.version: 2.3.2
rh-pre-commit.check-secrets: ENABLED

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review squad pass (3 agents: Claude coder, Claude researcher, Grok) against head afebea02. Fail-closed behavior re-verified correct on every path — an operational failure (exit 2) can never result in a dispatch, and the is_authorized || auth_rc=$? capture is safe under set -euo pipefail in if-condition context. Both workflow copies are identical modulo the pre-existing ISSUE_IS_PR/ISSUE_HAS_PR naming drift.

Posting 2 new MEDIUM findings inline (3/3 agent consensus on both). Not re-posted: the event-path 1-vs-2 gap from the July 17 round still applies unchanged at this head; the missing * catch-all comment has been updated in place — all 3 agents now assess it LOW.

echo "::warning::Permission API call failed for ${username}: $(cat "${api_err}")" >&2
rm -f "${api_err}"
return 1
return 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] premature-decision: HTTP 404 from the permission API lands in exit 2 "operational failure", conflating an authorization-relevant outcome

The docstring asserts every gh api failure is operational, but the endpoint's error taxonomy says otherwise. Verified empirically against the live API:

  • Real non-collaborator on a public repo → HTTP 200, role_name: "read" → correctly exit 1
  • Real non-collaborator on a private repo → HTTP 200, role_name: "" → correctly exit 1
  • Nonexistent user (deleted/renamed account between comment and workflow run) → HTTP 404 → gh exits non-zero → exit 2

So the common unauthorized case is classified correctly, but a deleted/renamed account gets the "failed to verify permissions" notice, which suggests a retryable infra problem when it is not. Fail-closed either way — message accuracy only.

Suggestion: Match HTTP 404 in the captured stderr and return 1 for it, reserving return 2 for network/5xx/auth errors and the mktemp path — or note in the function comment that unknown-user 404s land in exit 2 by design.

Same pattern in internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml:72-77.

Flagged by 3 agents (Claude x2, Grok) — consensus

assert.Contains(t, s, `Skipping dispatch for bot comment`)
assert.Contains(t, s, `Skipping dispatch for unauthorized comment`)
assert.Contains(t, s, `Skipping dispatch: failed to verify permissions`)
assert.Contains(t, s, `return 2`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Test assertions pin strings anywhere in the file, not the exit-code behavior being fixed

All new assertions are file-wide assert.Contains checks. assert.Contains(t, s, "return 2") passes with the string anywhere in the rendered YAML — a regression like 0|2) return 0 ;; (treating operational failure as authorized) would leave every assertion green, and the notice strings would too. Nothing executes the routing script, so the exact bug class this PR fixes (wrong exit-code mapping) would not be caught if reintroduced. Additionally, only the scaffold copy is covered — .github/workflows/reusable-dispatch.yml carries an identical hand-mirrored routing block with no drift guard.

Suggestion: Assert on compound snippets that tie exit codes to their branches (e.g. the literal is_authorized || auth_rc=$? line plus a 2) arm adjacent to its notice), and/or drive the extracted script with a fake gh on PATH asserting emitted notices and STAGE for rc 0/1/2. A normalizing drift test comparing the shared helper region of both workflow files (analogous to the existing scan-secrets sync check) would close the duplication gap.

Flagged by 3 agents (Claude x2, Grok) — consensus; severity settled at MEDIUM (assessed HIGH by one agent, LOW by another)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants