
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Summary
Add an org-wide reusable workflow in this repo — resolve_bot_pr_threads.yml — called from our other repos, that lets our automated review bot (rtibblesbot) resolve the review threads it has marked as addressed — something it currently cannot do directly because it has no write access to our repos.
This is the repo-side half of a two-part design. The bot-side half lands in the coding-agent review pipeline.
Background / problem
rtibblesbot posts automated code reviews on PRs across our repos (kolibri, studio, kolibri-design-system, …). On a delta re-review it correctly classifies each prior finding (RESOLVED / ACKNOWLEDGED / CONTESTED / UNADDRESSED) and already emits a collapsed "Prior-finding status" summary in the review body.
What it cannot do is resolve the corresponding conversation threads in the GitHub UI. Every attempt fails:
gh: rtibblesbot does not have the correct permissions to execute `ResolveReviewThread`
Root cause: resolveReviewThread requires the actor to have write (push) access to the base repo (or be the PR author). rtibblesbot is an external reviewer with neither — and most of our PRs come from forks (e.g. rtibbles/kolibri → learningequality/kolibri). We do not want to grant the bot write access, nor stand up a GitHub App just for this.
Proposed design
Split the privileged action to where the privilege already exists:
-
Bot side (in coding-agent): instead of calling resolveReviewThread, the bot edits its own root review comment for each settled thread (author permission — no write access needed) to inject:
- a visible marker, e.g.
✅ **Resolved** — addressed in current code, and
- a hidden HTML-comment marker
<!-- agent:thread-resolved --> (invisible in rendered markdown, greppable). Idempotent: skip if already present.
-
Repo side (this request): an automation, triggered by that comment edit, that runs inside the repo's own Actions context — where GITHUB_TOKEN carries write permission — finds the thread whose root comment is the edited one, and resolves it. Gated on the comment author being rtibblesbot and the marker being present.
Result: the cosmetic ✅ shows immediately, and the thread genuinely collapses / the unresolved-conversation count drops — with no extra permissions granted to the bot.
Why this works on fork PRs (permissions research)
The load-bearing question was whether the Actions GITHUB_TOKEN can resolve a thread when the PR is from a fork. It can:
- The read-only-token-on-forks downgrade is specific to the
pull_request event. Comment/review events (issue_comment, pull_request_review, pull_request_review_comment) instead run from the base repo's default branch with a writable token, even for fork PRs. (peter-evans/create-pull-request concepts guide; Wiz GitHub Actions threat model.)
resolveReviewThread requires contents: write on the token (not merely pull-requests: write) — confirmed in GitHub community discussion #44650. The caller grants both.
Security
These comment-triggered events are powerful precisely because they grant a write token to externally-triggered runs. The standard exploit requires the workflow to check out and run PR-head code — this automation never checks out anything. It runs one fixed GraphQL mutation, gated on a trusted author login + the hidden marker. No untrusted code path.
Deployment: reusable workflow + per-repo caller
We propagate org automations as reusable workflows called from each repo, so that's the shape used here: a dedicated reusable workflow resolve_bot_pr_threads.yml in this repo, plus a thin caller on each target repo's default branch. (Note: org "required workflows" / rulesets-require-workflows are not an option — they require GitHub Enterprise, which we don't have, and they're a merge gate on pull_request, not event-triggered automation. Not applicable.)
Reusable workflow (in this repo)
.github/workflows/resolve_bot_pr_threads.yml. Because the github context inside a reusable workflow reflects the caller's original event, the author/marker gate lives here:
# .github/workflows/resolve_bot_pr_threads.yml
on:
workflow_call:
jobs:
resolve-addressed-review-threads:
if: >
github.event_name == 'pull_request_review_comment' &&
github.event.comment.user.login == 'rtibblesbot' &&
contains(github.event.comment.body, '<!-- agent:thread-resolved -->')
runs-on: ubuntu-latest
steps:
- name: Resolve the thread containing this comment
env:
GH_TOKEN: ${{ github.token }}
OWNER: ${{ github.event.repository.owner.login }}
NAME: ${{ github.event.repository.name }}
PR: ${{ github.event.pull_request.number }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
set -euo pipefail
thread_id=$(gh api graphql -f query='
query($owner:String!,$name:String!,$pr:Int!){
repository(owner:$owner,name:$name){
pullRequest(number:$pr){
reviewThreads(first:100){
nodes{ id isResolved comments(first:1){ nodes{ databaseId } } }
}
}
}
}' -F owner="$OWNER" -F name="$NAME" -F pr="$PR" \
--jq ".data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved==false)
| select(.comments.nodes[0].databaseId==$COMMENT_ID) | .id")
if [ -z "$thread_id" ]; then
echo "No matching unresolved thread for comment $COMMENT_ID"; exit 0
fi
gh api graphql -f query='
mutation($id:ID!){ resolveReviewThread(input:{threadId:$id}){ thread{ id isResolved } } }' \
-f id="$thread_id"
echo "Resolved thread $thread_id"
Per-repo caller (on each repo's default branch)
The caller owns the trigger and grants the token permissions the reusable workflow needs:
name: Automation
on:
pull_request_review_comment:
types: [edited]
# ...any other events the consolidated automation handles...
jobs:
resolve-bot-pr-threads:
permissions:
contents: write # required by resolveReviewThread (gh community #44650)
pull-requests: write
uses: learningequality/.github/.github/workflows/resolve_bot_pr_threads.yml@main
secrets: inherit
A dedicated caller (rather than folding into a future catch-all entry point) keeps the contents: write grant scoped to just this automation.
Notes:
reviewThreads(first:100) covers all but very large PRs; add pagination if needed.
github.event.comment.id (REST id) equals the GraphQL databaseId, so the match is exact.
- No loop risk: resolving a thread doesn't emit a comment-edit event.
Validation plan
- Land the reusable job here + a caller on one repo's default branch (suggest kolibri) to prove the fork-PR token can resolve.
- Manually edit one existing
rtibblesbot review comment to add the marker → confirm the thread resolves.
- Once proven, ship the bot-side comment-edit change in
coding-agent and add the caller to the remaining repos.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Summary
Add an org-wide reusable workflow in this repo —
resolve_bot_pr_threads.yml— called from our other repos, that lets our automated review bot (rtibblesbot) resolve the review threads it has marked as addressed — something it currently cannot do directly because it has no write access to our repos.This is the repo-side half of a two-part design. The bot-side half lands in the
coding-agentreview pipeline.Background / problem
rtibblesbotposts automated code reviews on PRs across our repos (kolibri, studio, kolibri-design-system, …). On a delta re-review it correctly classifies each prior finding (RESOLVED / ACKNOWLEDGED / CONTESTED / UNADDRESSED) and already emits a collapsed "Prior-finding status" summary in the review body.What it cannot do is resolve the corresponding conversation threads in the GitHub UI. Every attempt fails:
Root cause:
resolveReviewThreadrequires the actor to have write (push) access to the base repo (or be the PR author).rtibblesbotis an external reviewer with neither — and most of our PRs come from forks (e.g.rtibbles/kolibri→learningequality/kolibri). We do not want to grant the bot write access, nor stand up a GitHub App just for this.Proposed design
Split the privileged action to where the privilege already exists:
Bot side (in
coding-agent): instead of callingresolveReviewThread, the bot edits its own root review comment for each settled thread (author permission — no write access needed) to inject:✅ **Resolved** — addressed in current code, and<!-- agent:thread-resolved -->(invisible in rendered markdown, greppable). Idempotent: skip if already present.Repo side (this request): an automation, triggered by that comment edit, that runs inside the repo's own Actions context — where
GITHUB_TOKENcarries write permission — finds the thread whose root comment is the edited one, and resolves it. Gated on the comment author beingrtibblesbotand the marker being present.Result: the cosmetic ✅ shows immediately, and the thread genuinely collapses / the unresolved-conversation count drops — with no extra permissions granted to the bot.
Why this works on fork PRs (permissions research)
The load-bearing question was whether the Actions
GITHUB_TOKENcan resolve a thread when the PR is from a fork. It can:pull_requestevent. Comment/review events (issue_comment,pull_request_review,pull_request_review_comment) instead run from the base repo's default branch with a writable token, even for fork PRs. (peter-evans/create-pull-request concepts guide; Wiz GitHub Actions threat model.)resolveReviewThreadrequirescontents: writeon the token (not merelypull-requests: write) — confirmed in GitHub community discussion #44650. The caller grants both.Security
These comment-triggered events are powerful precisely because they grant a write token to externally-triggered runs. The standard exploit requires the workflow to check out and run PR-head code — this automation never checks out anything. It runs one fixed GraphQL mutation, gated on a trusted author login + the hidden marker. No untrusted code path.
Deployment: reusable workflow + per-repo caller
We propagate org automations as reusable workflows called from each repo, so that's the shape used here: a dedicated reusable workflow
resolve_bot_pr_threads.ymlin this repo, plus a thin caller on each target repo's default branch. (Note: org "required workflows" / rulesets-require-workflows are not an option — they require GitHub Enterprise, which we don't have, and they're a merge gate onpull_request, not event-triggered automation. Not applicable.)Reusable workflow (in this repo)
.github/workflows/resolve_bot_pr_threads.yml. Because thegithubcontext inside a reusable workflow reflects the caller's original event, the author/marker gate lives here:Per-repo caller (on each repo's default branch)
The caller owns the trigger and grants the token permissions the reusable workflow needs:
A dedicated caller (rather than folding into a future catch-all entry point) keeps the
contents: writegrant scoped to just this automation.Notes:
reviewThreads(first:100)covers all but very large PRs; add pagination if needed.github.event.comment.id(REST id) equals the GraphQLdatabaseId, so the match is exact.Validation plan
rtibblesbotreview comment to add the marker → confirm the thread resolves.coding-agentand add the caller to the remaining repos.