diff --git a/.github/workflows/fullsend.yaml b/.github/workflows/fullsend.yaml index 6552b6f..d35d6ab 100644 --- a/.github/workflows/fullsend.yaml +++ b/.github/workflows/fullsend.yaml @@ -43,25 +43,25 @@ jobs: group: fullsend-dispatch-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false if: >- - github.event_name != 'issue_comment' - || github.event.comment.user.type != 'Bot' + (github.event_name != 'pull_request_target' && github.event_name != 'pull_request_review' + || github.event.pull_request.head.ref != 'fullsend/scaffold-install') + && (github.event_name != 'issue_comment' + || github.event.comment.user.type != 'Bot') uses: fullsend-ai/.fullsend/.github/workflows/dispatch.yml@main with: event_action: ${{ github.event.action }} stop-fix: + # Job-level if: is intentionally coarse — it only screens for the + # /fs-fix-stop command on a PR from a non-bot. The authoritative + # authorization decision (collaborator permission API + PR-author escape + # hatch) is made in the step below, so a maintainer whose author_association + # is not MEMBER (e.g. private org membership) is not filtered out (ADR 0054). if: >- github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.user.type != 'Bot' && github.event.comment.body == '/fs-fix-stop' - && ( - github.event.comment.author_association == 'OWNER' - || github.event.comment.author_association == 'MEMBER' - || github.event.comment.author_association == 'COLLABORATOR' - || github.event.comment.author_association == 'CONTRIBUTOR' - || github.event.comment.user.login == github.event.issue.user.login - ) runs-on: ubuntu-24.04 permissions: contents: read @@ -73,7 +73,37 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.issue.number }} REPO: ${{ github.repository }} + COMMENT_USER_LOGIN: ${{ github.event.comment.user.login }} + ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }} run: | + set -euo pipefail + # ADR 0054: authorize via the collaborator permission API + # (admin|maintain|write), not author_association — the latter grants + # contributor status to anyone with a single merged PR (issue #5421). + # Mirrors has_repo_permission() in dispatch.yml; keep the two in sync. + # The PR author may always stop the fix agent on their own PR. + authorized=false + if [[ -n "$COMMENT_USER_LOGIN" && "$COMMENT_USER_LOGIN" == "$ISSUE_USER_LOGIN" ]]; then + authorized=true + else + if api_err=$(mktemp); then + if role=$(gh api "repos/$REPO/collaborators/$COMMENT_USER_LOGIN/permission" \ + --jq '.role_name' 2>"$api_err"); then + case "$role" in + admin|maintain|write) authorized=true ;; + esac + else + echo "::warning::Permission API call failed for $COMMENT_USER_LOGIN: $(cat "$api_err")" + fi + rm -f "$api_err" + else + echo "::warning::Failed to create temp file for permission check of $COMMENT_USER_LOGIN" + fi + fi + if [[ "$authorized" != "true" ]]; then + echo "::notice::User $COMMENT_USER_LOGIN is not authorized to stop the fix agent (requires write access or PR authorship)" + exit 0 + fi gh label create "fullsend-no-fix" --repo "$REPO" \ --description "Skip bot-triggered fix agent runs" --color "FBCA04" \ --force 2>/dev/null || true