feat(ci): add /assign GitHub Action with stale cleanup#2634
Conversation
Implement the documented self-assignment slash command and a scheduled cleanup for auto-assigned issues with no PR activity for two weeks. Fixes vybestack#2630.
📝 WalkthroughWalkthroughAdds ChangesIssue Assignment Automation
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8).github/workflows/assign-stale-cleanup.ymlTraceback (most recent call last): .github/workflows/assign.ymlTraceback (most recent call last): Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
LLxprt PR Review – PR #2634Issue AlignmentImplements the Side Effects
Code Quality
Tests and CoverageCoverage impact: unknown / effectively no behavioral coverage.
VerdictNeeds Work |
| # Exempt: never unassign acoliver (ticket exception). | ||
| for login in "${ASSIGNEES[@]}"; do | ||
| if [[ "${login}" == "${EXEMPT_LOGIN}" ]]; then | ||
| echo " 🛡️ Skipping #${issue_number}: assignee @${EXEMPT_LOGIN} is exempt" | ||
| return 0 | ||
| fi | ||
| done |
There was a problem hiding this comment.
The acoliver exemption returns for the entire issue, so if acoliver is one of multiple assignees, other stale assignees are never evaluated for unassignment. If the intent is only to protect acoliver, move the exemption check inside the per-assignee loop so other assignees can still be cleaned up independently.
| mapfile -t CANDIDATES < <( | ||
| gh issue list --repo "${REPO}" --state open --label "${AUTO_ASSIGNED_LABEL}" \ | ||
| --limit 200 \ | ||
| --json number,assignees \ | ||
| --jq '.[] | "\(.number)\t\([.assignees[].login] | join(","))"' \ | ||
| 2>/dev/null || true | ||
| ) |
There was a problem hiding this comment.
The candidate issue list is fetched with
gh issue list --limit 200and no pagination. If the repository has more than 200 open issues with theauto-assignedlabel, stale issues beyond that window are never processed, resulting in incomplete cleanup. Consider adding--paginateor iterating pages to ensure all candidates are scanned.
| assignee_count="$( | ||
| gh issue view "${ISSUE_NUMBER}" --repo "${REPO}" --json assignees \ | ||
| --jq '.assignees | length' 2>/dev/null || echo "0" | ||
| )" |
There was a problem hiding this comment.
Redundant API call: the script invokes
gh issue viewtwice to retrieve assignees—first to count them (.assignees | length) and then to list their logins ([.assignees[].login] | join(", ")). Combine these into a single API call to reduce latency and GitHub API usage. For example, fetch the full assignee list once, compute the count via${#assignees[@]}, and extract logins from the same response.
|
|
||
| require_env GITHUB_REPOSITORY | ||
| require_env ISSUE_NUMBER | ||
| require_env COMMENTER_LOGIN |
There was a problem hiding this comment.
Missing input validation for
COMMENTER_LOGIN: the script does not validate that the login follows GitHub username conventions (alphanumeric + hyphens, no consecutive hyphens, max 39 characters). While GitHub generally validates this upstream, adding a defensive check prevents unexpected behavior in API calls or future modifications that might interpolate the variable into queries.
| - name: Run unassign-stale-issues script | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| GITHUB_TOKEN: ${{ github.token }} | ||
| GITHUB_REPOSITORY: ${{ github.repository }} | ||
| run: | | ||
| set -euo pipefail | ||
| chmod +x ./.github/scripts/unassign-stale-issues.sh | ||
| ./.github/scripts/unassign-stale-issues.sh |
There was a problem hiding this comment.
The
gh issue listcommand is limited to 200 results (--limit 200). If the repository ever has more than 200 open issues with theauto-assignedlabel, some stale issues will be silently skipped. Consider removing the limit or using a pagination strategy to ensure all candidates are evaluated.
|
|
||
| function loadWorkflow(relPath) { | ||
| const source = readRootFile(relPath); | ||
| const parsed = yaml.load(source); |
There was a problem hiding this comment.
The test uses
yaml.load(source)without specifying a safe schema, which defaults to the unsafe YAML schema that can execute arbitrary JavaScript through custom tags. While the YAML files are trusted repository files, this is a security anti-pattern that could lead to code execution if the test is ever reused with untrusted input. Useyaml.load(source, { schema: yaml.SAFE_SCHEMA })oryaml.load(source, { schema: yaml.JSON_SCHEMA })instead.
| expect(script).toContain('trusted-contributors.txt'); | ||
| expect(script).toContain('gh search prs'); | ||
| expect(script).toContain('--merged'); | ||
| expect(script).toContain('--add-assignee'); | ||
| expect(script).toContain('post_sticky_feedback'); |
There was a problem hiding this comment.
These tests only verify the presence of strings in the shell scripts, not the actual behavior. A test could pass even if the string appears in a comment or the functionality is broken. Consider testing the actual behavior (e.g., by mocking environment variables and verifying outputs, or at minimum testing that the strings appear in executable code paths rather than comments).
| expect(normalize(job?.if)).toContain( | ||
| "github.repository == 'vybestack/llxprt-code'", | ||
| ); |
There was a problem hiding this comment.
Consider extracting repeated hardcoded business values into constants at the top of the test file. This creates maintenance overhead and makes it harder to update these values in the future. For example:
const CANONICAL_REPO = 'vybestack/llxprt-code'; const EXEMPT_LOGIN = 'acoliver'; const CRON_SCHEDULE = '0 7 * * *'; const STALE_WINDOW_DAYS = 14; const MAX_ASSIGNMENTS = 3;
OpenCodeReview — PR #2634
|
|
@Ayush7614 run shellcheck |
|
@CodeRabbit is the shell necessary in here or could this be done entirely in yaml? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Candidate files =='
fd -a '^(assign\.yml|assign-issue\.sh|assign-stale-cleanup\.yml|unassign-stale-issues\.sh|assign-workflow\.test\.js)$' . \
-E node_modules -E dist -E build
printf '%s\n' '== Workflow outlines =='
for f in $(fd -a '^(assign\.yml|assign-stale-cleanup\.yml)$' . -E node_modules -E dist -E build); do
echo "--- $f"
nl -ba "$f"
done
printf '%s\n' '== Script function outlines =='
for f in $(fd -a '^(assign-issue\.sh|unassign-stale-issues\.sh)$' . -E node_modules -E dist -E build); do
echo "--- $f"
rg -n '^(#!/|[[:alpha:]_][[:alnum:]_]*\(\)|[[:alpha:]_][[:alnum:]_]*\(\s*\{)|\b(gh|jq|curl)\b|case |if |while |for )' "$f" || true
doneLength of output: 984 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Repository change summary =='
git status --short
git diff --name-status HEAD~1..HEAD 2>/dev/null || true
printf '%s\n' '== /assign implementation references =='
rg -n -i -C 3 '/assign|auto-assigned|stale cleanup|unassign.*stale' \
-g '*.yml' -g '*.yaml' -g '*.sh' -g 'assign-workflow.test.js' . \
-g '!node_modules/**' -g '!dist/**' -g '!build/**' || true
printf '%s\n' '== Contract test =='
test_file="$(fd -a '^assign-workflow\.test\.js$' scripts/tests | head -n 1)"
if [ -n "$test_file" ]; then
nl -ba "$test_file"
fiLength of output: 8443 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in \
.github/workflows/assign.yml \
.github/workflows/assign-stale-cleanup.yml \
.github/scripts/assign-issue.sh \
.github/scripts/unassign-stale-issues.sh
do
if [ -f "$f" ]; then
printf '\n%s\n' "===== $f ====="
awk '{ printf "%4d %s\n", NR, $0 }' "$f"
else
printf '%s\n' "MISSING: $f"
fi
doneLength of output: 20136
YAML is appropriate for the triggers, exact-comment gate, permissions, concurrency, and scheduling. But the implementation also needs dynamic GitHub API work and control flow:
Those require imperative code. The current checked-in shell scripts are a reasonable choice, particularly since the logic is independently contract-tested. The alternative would be |
|
@CodeRabbit is there an attack vector here and if so can we limit it? thinking like 1000 bots smash us with /assign ? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
workflow="$(fd -a '^assign\.yml$' . -E node_modules -E dist -E build | head -n1)"
script="$(fd -a '^assign-issue\.sh$' . -E node_modules -E dist -E build | head -n1)"
test_file="$(fd -a '^assign-workflow\.test\.js$' . -E node_modules -E dist -E build | head -n1)"
printf '%s\n' '== Candidate paths =='
printf 'workflow=%s\nscript=%s\ntest=%s\n' "$workflow" "$script" "$test_file"
for file in "$workflow" "$script" "$test_file"; do
if [ -n "$file" ] && [ -f "$file" ]; then
printf '\n===== %s =====\n' "$file"
nl -ba "$file"
fi
doneLength of output: 1095 🏁 Script executed: #!/bin/bash
set -euo pipefail
find . \
-path '*/node_modules' -prune -o \
-path '*/dist' -prune -o \
-path '*/build' -prune -o \
-type f \( \
-name 'assign.yml' -o \
-name 'assign-issue.sh' -o \
-name 'assign-workflow.test.js' \
\) -print |
while IFS= read -r file; do
printf '\n===== %s =====\n' "$file"
sed -n '=' "$file" | sed 'N;s/\n/\t/'
doneLength of output: 1959 🏁 Script executed: #!/bin/bash
set -euo pipefail
for file in \
.github/workflows/assign.yml \
.github/scripts/assign-issue.sh \
scripts/tests/assign-workflow.test.js
do
if [ -f "$file" ]; then
printf '\n===== %s =====\n' "$file"
sed -n '1,260p' "$file"
else
printf 'MISSING: %s\n' "$file"
fi
doneLength of output: 14287
I would limit this in two layers:
Also, for failed eligibility, I would avoid posting/updating sticky feedback for every request—or only provide that feedback for known contributors. Otherwise attackers can deliberately generate persistent issue-comment noise and write API traffic. The three-assignment cap protects issue ownership; it does not rate-limit invocation volume. I recommend treating global concurrency plus early rejection as required hardening before enabling this on a public repository. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/assign-issue.sh:
- Around line 95-137: Update the workflow permissions associated with the
assign-issue script to include pull-requests: read alongside the existing
permissions. Ensure the gh search prs invocation in the merged_pr_count
eligibility check can read pull requests and preserve the current eligibility
behavior.
In @.github/scripts/unassign-stale-issues.sh:
- Around line 148-166: Update the assignee evaluation in the function containing
the exemption loop so the `EXEMPT_LOGIN` check occurs inside the per-assignee
loop. Skip only `acoliver` while continuing to evaluate other co-assignees
through `assignment_age_ok_to_unassign`; remove the issue-level `return 0`
behavior and preserve the existing unassignment decision flow.
In @.github/workflows/assign.yml:
- Around line 11-13: Update the workflow permissions block in assign.yml to add
pull-requests: read alongside contents: read and issues: write, allowing the
merged pull-request search used by the workflow to function correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f3d8a5f-64b7-4b6f-a790-aa65ed95e103
📒 Files selected for processing (6)
.github/scripts/assign-issue.sh.github/scripts/unassign-stale-issues.sh.github/workflows/assign-stale-cleanup.yml.github/workflows/assign.ymlCONTRIBUTING.mdscripts/tests/assign-workflow.test.js
| merged_pr_count="$( | ||
| gh search prs --repo "${REPO}" --author "${USER_LOGIN}" --merged \ | ||
| --json number --jq 'length' 2>/dev/null || echo "0" | ||
| )" | ||
|
|
||
| if [[ "${merged_pr_count}" -gt 0 ]]; then | ||
| is_eligible=true | ||
| eligibility_reason="prior merged PR(s)" | ||
| fi | ||
|
|
||
| if [[ "${is_eligible}" != "true" ]]; then | ||
| prior_assigned_count="$( | ||
| gh search issues --repo "${REPO}" --assignee "${USER_LOGIN}" --state all \ | ||
| --json number --jq 'length' 2>/dev/null || echo "0" | ||
| )" | ||
| if [[ "${prior_assigned_count}" -gt 0 ]]; then | ||
| is_eligible=true | ||
| eligibility_reason="prior issue assignment" | ||
| fi | ||
| fi | ||
|
|
||
| if [[ "${is_eligible}" != "true" && -f "${TRUSTED_FILE}" ]]; then | ||
| if grep -Fqx "${USER_LOGIN}" "${TRUSTED_FILE}"; then | ||
| is_eligible=true | ||
| eligibility_reason="trusted contributor list" | ||
| fi | ||
| fi | ||
|
|
||
| # Owners/members/collaborators are always eligible (author_association not | ||
| # always available here; treat repository permission via association env). | ||
| AUTHOR_ASSOCIATION="${AUTHOR_ASSOCIATION:-}" | ||
| case "${AUTHOR_ASSOCIATION}" in | ||
| OWNER | MEMBER | COLLABORATOR) | ||
| is_eligible=true | ||
| eligibility_reason="repository ${AUTHOR_ASSOCIATION}" | ||
| ;; | ||
| esac | ||
|
|
||
| if [[ "${is_eligible}" != "true" ]]; then | ||
| echo "⚠️ @${USER_LOGIN} is not eligible for self-assignment" | ||
| post_sticky_feedback "❌ Could not assign @${USER_LOGIN}: eligibility requires at least one merged PR in this repository, a prior issue assignment, or membership in \`.github/trusted-contributors.txt\`. Please open a PR first or ask a maintainer to assign you." | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does GitHub Actions GITHUB_TOKEN need pull-requests read permission for gh search prs / search API reads on a public repository, or are public repo reads exempt from the permissions block?
💡 Result:
Yes, you need to explicitly grant pull-requests: read permission if you use the permissions block in your workflow file and intend to use gh search prs or similar API calls that require those scopes [1][2]. There is no exemption for public repositories regarding the permissions block [3][1]. When you define a permissions block in your GitHub Actions workflow file, you must explicitly state all the permissions you require [1][2]. Any permission not explicitly listed in that block is set to none (with the exception of metadata, which always receives read access) [1][2]. While public repositories may have different default GITHUB_TOKEN settings compared to private ones depending on your organization or repository configuration [4][5], these defaults only apply when you do not define a custom permissions block [4][6]. Once you include a permissions key in your YAML file, the default permissions are overridden by your explicit configuration [3][1][2]. Therefore, if your workflow needs to search for pull requests, you must include pull-requests: read in your permissions configuration [1][2].
Citations:
- 1: https://dev.to/github/the-githubtoken-in-github-actions-how-it-works-change-permissions-customizations-3cgp
- 2: https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/
- 3: https://dev-error-db.com/errors/github-actions-could-not-read-username-for-https-github-com-terminal-prompts-disabled/
- 4: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository
- 5: https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/
- 6: https://docs.github.com/en/enterprise-server@3.2/actions/reference/authentication-in-a-workflow
Add pull-requests: read to the workflow permissions. gh search prs runs under the workflow’s explicit permissions: block, so contents: read and issues: write aren’t enough. Without pull-requests: read, the merged-PR eligibility path falls back to 0 and rejects valid commenters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/assign-issue.sh around lines 95 - 137, Update the workflow
permissions associated with the assign-issue script to include pull-requests:
read alongside the existing permissions. Ensure the gh search prs invocation in
the merged_pr_count eligibility check can read pull requests and preserve the
current eligibility behavior.
|
@Ayush7614 look at #2634 (comment) and the other coderabbit reviews. LLet's assume at some point someone will comment /assign on everything so let's add a cap for how many issues they can be assigned at once, the concurrency cap. ask your agent for any other hardening recommendations to prevent also spammers (github name BuyMyStupidThing /assign every issue it can) |
Satisfy shellcheck, add pull-requests:read, reject Bot actors, serialize per-actor concurrency, keep the 3-issue cap, and unassign stale co-assignees without skipping whole issues for acoliver.
|
@acoliver addressed shellcheck + the spam-hardening notes (and CodeRabbit’s actionable items): CI / shellcheck
Spam / flood hardening
CodeRabbit
Happy to add more rate-limiting (e.g. daily attempt budget) if you want that next. |
| if [[ "${assignee_count}" -gt 0 ]]; then | ||
| current="$( | ||
| gh issue view "${ISSUE}" --repo "${REPO}" --json assignees \ | ||
| --jq '[.assignees[].login] | join(", ")' 2>/dev/null || echo "someone" | ||
| )" | ||
| echo "⚠️ Issue #${ISSUE} is already assigned to: ${current}" | ||
| post_sticky_feedback "❌ Could not assign @${USER_LOGIN}: this issue is already assigned to **${current}**." | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
Race condition in assignment logic due to concurrent workflow runs from different actors. The workflow uses per-issue-per-actor concurrency, so different users assigning the same issue can still execute in parallel. Both will pass the "issue unassigned" check, both will attempt assignment, and both will call
post_sticky_feedback, potentially creating duplicate sticky comments. Consider adding a global per-issue lock (e.g., using a GitHub API lock file or issue comment lock) or at least handling the case where the issue becomes assigned between the check and the mutation.
| gh label create "${AUTO_ASSIGNED_LABEL}" \ | ||
| --repo "${REPO}" \ | ||
| --color "0E8A16" \ | ||
| --description "Assigned via /assign automation" \ | ||
| >/dev/null 2>&1 || true |
There was a problem hiding this comment.
gh label createis invoked unconditionally on every assignment, even when the label already exists. This generates unnecessary API calls and consumes rate limit quota. Use an idempotent check likegh label viewfirst, or usegh label editwhich creates or updates the label in a single call.
| pr_hits="$( | ||
| gh search prs \ | ||
| --repo "${REPO}" \ | ||
| --author "${assignee}" \ | ||
| --json number \ | ||
| --jq 'length' \ | ||
| "#${issue_number} in:title,body" \ | ||
| 2>/dev/null || echo "0" | ||
| )" | ||
|
|
||
| [[ "${pr_hits}" =~ ^[0-9]+$ ]] && [[ "${pr_hits}" -gt 0 ]] |
There was a problem hiding this comment.
This script calls
gh search prs, which requires the GitHub Search API scope, but the workflow does not grantsearchpermission (assign-stale-cleanup.ymlhascontents: read,issues: write,pull-requests: readonly). Additionally, errors are swallowed with|| echo "0", so on permission denial, rate limiting, or API unavailability the script will incorrectly conclude there is no linked PR activity and proceed to unassign users. Please either addsearch: readto the workflow permissions or add explicit failure handling here (and avoid silently treating errors as zero hits).
| if [[ -z "${assigned_at}" ]]; then | ||
| # Fall back to issue updatedAt when timeline is unavailable. | ||
| assigned_at="$( | ||
| gh issue view "${issue_number}" --repo "${REPO}" --json updatedAt \ | ||
| --jq '.updatedAt' 2>/dev/null || true | ||
| )" | ||
| fi |
There was a problem hiding this comment.
The
updatedAtfallback is semantically wrong for assignment age:updatedAtchanges on any issue mutation, including comments or label changes, so it can prevent stale cleanup for an old assignment with recent unrelated activity. If a reliable assignment timestamp is needed, store or derive it from an explicit assignment event/comment rather than using generic issue update time.
| mapfile -t CANDIDATES < <( | ||
| gh issue list --repo "${REPO}" --state open --label "${AUTO_ASSIGNED_LABEL}" \ | ||
| --limit 200 \ | ||
| --json number,assignees \ | ||
| --jq '.[] | "\(.number)\t\([.assignees[].login] | join(","))"' \ | ||
| 2>/dev/null || true | ||
| ) |
There was a problem hiding this comment.
The candidate issue scan is capped at 200 issues (
gh issue list --limit 200). If the repository ever has more than 200 open issues with theauto-assignedlabel, the remainder will be skipped during each run. Please either increase the limit to the maximum supported by GitHub, paginate through results, or document this as a known limitation with a remediation plan.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.github/scripts/assign-issue.sh (2)
87-133: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not grant self-assignment eligibility from repository roles or the trusted file.
The eligibility block accepts
OWNER,MEMBER,COLLABORATOR, and.github/trusted-contributors.txtmembership without a merged PR or prior issue assignment, while the failure feedback only lists those two conditions plus the trusted file. Remove the role bypass or document and test the role/trusted-file exceptions as intentional policy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/scripts/assign-issue.sh around lines 87 - 133, Restrict self-assignment eligibility in the eligibility logic around merged_pr_count and prior_assigned_count to users with a merged PR or prior issue assignment. Remove the TRUSTED_FILE membership check and the AUTHOR_ASSOCIATION role bypass, including their eligibility reasons, so OWNER, MEMBER, COLLABORATOR, and trusted-file entries cannot bypass those requirements.
137-169: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake assignment and tracking-label updates a single failure domain.
If
gh issue edit --add-labelfails, or the run is canceled between the assignment and label write, the issue stays assigned withoutauto-assigned. The stale cleanup workflow selects open issues bylabel:auto-assigned, so this leave-behind assignment will not be discovered and may run against the per-user cap. Use one API update that sets the assignee and preserves existing labels, and report failure unless both states persist before printing success.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/scripts/assign-issue.sh around lines 137 - 169, The assignment and auto-assigned label writes in the post-eligibility flow must be one atomic tracking update. Replace the separate gh issue edit calls around the assignment verification and label addition with a single API update that sets USER_LOGIN as assignee while preserving all existing labels, then re-read the issue and report failure unless both the assignee and AUTO_ASSIGNED_LABEL are present; only print success after both states persist.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/assign-issue.sh:
- Around line 39-43: Update the existing_id lookup in the issue-assignment flow
to capture gh api output and its exit status separately, rather than masking
failures with || true. Fail closed when the API request returns non-zero, while
preserving the current behavior when the request succeeds but no comment matches
MARKER.
- Around line 40-42: Update the comment lookup in the assignment feedback flow
to match the marker only when the comment was authored by the configured bot
account, preventing user-authored marker copies from being patched. Preserve
pagination and first-match behavior, and use the existing bot identity
configuration rather than hardcoding an unrelated author.
- Around line 60-68: Serialize the entire assignment check-and-mutation flow in
the `/assign` workflow per issue, rather than per commenter, by updating the
concurrency group in the workflow configuration and setting cancel-in-progress
to false. Ensure the existing zero-assignee check and add-assignee operation in
the assignment script execute within that shared per-issue queue.
- Around line 60-63: Update the assignee count read in the assign guard to fail
closed: remove the fallback that converts gh issue view errors into 0, capture
or propagate the command failure, and abort with clear feedback when reading
current assignees fails. Continue to the assignment flow only when
assignee_count is successfully obtained.
---
Outside diff comments:
In @.github/scripts/assign-issue.sh:
- Around line 87-133: Restrict self-assignment eligibility in the eligibility
logic around merged_pr_count and prior_assigned_count to users with a merged PR
or prior issue assignment. Remove the TRUSTED_FILE membership check and the
AUTHOR_ASSOCIATION role bypass, including their eligibility reasons, so OWNER,
MEMBER, COLLABORATOR, and trusted-file entries cannot bypass those requirements.
- Around line 137-169: The assignment and auto-assigned label writes in the
post-eligibility flow must be one atomic tracking update. Replace the separate
gh issue edit calls around the assignment verification and label addition with a
single API update that sets USER_LOGIN as assignee while preserving all existing
labels, then re-read the issue and report failure unless both the assignee and
AUTO_ASSIGNED_LABEL are present; only print success after both states persist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 472bf4a3-4bb3-470f-8835-e583148e1084
📒 Files selected for processing (6)
.github/scripts/assign-issue.sh.github/scripts/unassign-stale-issues.sh.github/workflows/assign-stale-cleanup.yml.github/workflows/assign.ymlCONTRIBUTING.mdscripts/tests/assign-workflow.test.js
🚧 Files skipped from review as they are similar to previous changes (5)
- .github/workflows/assign-stale-cleanup.yml
- .github/workflows/assign.yml
- CONTRIBUTING.md
- scripts/tests/assign-workflow.test.js
- .github/scripts/unassign-stale-issues.sh
| existing_id="$( | ||
| gh api "repos/${REPO}/issues/${ISSUE}/comments" --paginate \ | ||
| --jq ".[] | select(.body != null and (.body | contains(\"${MARKER}\"))) | .id" 2>/dev/null \ | ||
| | head -n 1 || true | ||
| )" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "File exists:"
test -f .github/scripts/assign-issue.sh && echo yes || echo no
echo
echo "Relevant script section:"
sed -n '1,90p' .github/scripts/assign-issue.sh | cat -n
echo
echo "Shell behavior for set -e with pipeline containing gh api && head with failed command:"
set -euo pipefail
bash -c 'false || true' && echo "pipeline_with_trued_succeeded" || echo "pipeline_with_trued_failed"
bash -c 'set -euo pipefail; false | true' && echo "pipefail_pipeline_succeeded" || echo "pipefail_pipeline_failed"
printf '1\n2\n' | false | head -n 1 && echo "pipe_before_head_succeeded" || echo "pipe_before_head_failed"Repository: vybestack/llxprt-code
Length of output: 3983
Handle gh api lookup failures separately from missing comments.
|| true hides the exit status of gh api, so when the API read fails assignment can proceed and post another comment instead of failing closed. Capture the list first, then distinguish non-zero API failures from an absent sticky match.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/assign-issue.sh around lines 39 - 43, Update the existing_id
lookup in the issue-assignment flow to capture gh api output and its exit status
separately, rather than masking failures with || true. Fail closed when the API
request returns non-zero, while preserving the current behavior when the request
succeeds but no comment matches MARKER.
| gh api "repos/${REPO}/issues/${ISSUE}/comments" --paginate \ | ||
| --jq ".[] | select(.body != null and (.body | contains(\"${MARKER}\"))) | .id" 2>/dev/null \ | ||
| | head -n 1 || true |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file exists and relevant lines =="
if [ -f .github/scripts/assign-issue.sh ]; then
sed -n '1,120p' .github/scripts/assign-issue.sh | nl -ba
else
echo ".github/scripts/assign-issue.sh not found"
fd -a 'assign-issue\.sh' .
fi
echo
echo "== searches for markers/comments assign issues =="
rg -n "MARKER|create comment|assign issue|sticky|comments|assign_issue|assign-issue" .github scripts . 2>/dev/null | head -200Repository: vybestack/llxprt-code
Length of output: 236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file exists and relevant lines =="
if [ -f .github/scripts/assign-issue.sh ]; then
sed -n '1,120p' .github/scripts/assign-issue.sh
else
echo ".github/scripts/assign-issue.sh not found"
find . -name 'assign-issue.sh' -print
fi
echo
echo "== searches for markers/comments assign issues =="
grep -RInE "MARKER|create comment|assign issue|sticky|comments|assign_issue|assign-issue" .github scripts . 2>/dev/null | head -200 || trueRepository: vybestack/llxprt-code
Length of output: 30029
Restrict sticky updates to bot-authored comments.
The selector matches any comment containing <!-- llxprt-assign-feedback -->, including a user-supplied copy of the marker, so gh api --method PATCH can overwrite that comment. Select the bot’s existing comment (for example by author login) or store the created comment ID.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/assign-issue.sh around lines 40 - 42, Update the comment
lookup in the assignment feedback flow to match the marker only when the comment
was authored by the configured bot account, preventing user-authored marker
copies from being patched. Preserve pagination and first-match behavior, and use
the existing bot identity configuration rather than hardcoding an unrelated
author.
| assignee_count="$( | ||
| gh issue view "${ISSUE}" --repo "${REPO}" --json assignees \ | ||
| --jq '.assignees | length' 2>/dev/null || echo "0" | ||
| )" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate file =="
git ls-files | rg '(^|/)\.github/scripts/assign-issue\.sh$|assign-issue\.sh$' || true
echo "== File excerpt =="
if [ -f .github/scripts/assign-issue.sh ]; then
nl -ba .github/scripts/assign-issue.sh | sed -n '1,130p'
fi
echo "== GitHub issue assignees docs excerpt from network (if gh/curl ok) =="
if command -v gh >/dev/null 2>&1; then
gh api repos/github/docs-content/content/rest/issues/assignees.md -q '.content' | base64 -d | sed -n '1,200p' || true
else
curl -sSL 'https://raw.githubusercontent.com/github/docs-content/main/content/rest/issues/assignees.md' | sed -n '1,200p' || true
fi
echo "== Behavioral model of assignee_count/add logic from source =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/scripts/assign-issue.sh')
if p.exists():
s=p.read_text()
for needle in ['assignee_count','assignees | length','add-assignee','assignee_assigned']:
print(needle, 'present' if needle in s else 'absent')
else:
print('file_missing')
PYRepository: vybestack/llxprt-code
Length of output: 268
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== File excerpt =="
awk '{ printf "%6d\t%s\n", NR, $0 }' .github/scripts/assign-issue.sh | sed -n '1,160p'
echo "== Relevant symbols present =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/scripts/assign-issue.sh')
if not p.exists():
print('file_missing')
else:
s = p.read_text()
for needle in ['assignee_count', '.assignees | length', 'add-assignee', 'assignee_assigned']:
print(f'{needle!r}:', 'present' if needle in s else 'absent')
PY
echo "== Read-only behavioral parser/snippet model =="
python3 - <<'PY'
from pathlib import Path
import re
p = Path('.github/scripts/assign-issue.sh')
s = p.read_text().splitlines()
for i, line in enumerate(s, start=1):
if 'assignee_count' in line or 'assignees | length' in line or '--add-assignee' in line or '--add-assignees' in line or 'assignee_assigned' in line:
# print up to 3 lines around each match
lo=max(1,i-3); hi=len(s)+1 if i==len(s) else i+4
print(f"\n--- occurrences near line {i} ---")
for n in range(lo,hi):
if 1<=n<=len(s):
print(f"{n}: {s[n-1]}")
PY
echo "== Check for add-assignee/add-assignees and failure-handling guard pattern =="
python3 - <<'PY'
from pathlib import Path
import re
p = Path('.github/scripts/assign-issue.sh')
lines = [l.strip() for l in p.read_text().splitlines()]
print("gh assign calls:")
for line in lines:
if 'gh ' in line and ('add-assignee' in line or 'add-assignees' in line):
print(line)
print("assignee assignment condition lines:")
for line in lines:
if 'add-assignee' in line or 'add-assignees' in line or 'assignee_assigned' in line:
print(line)
PYRepository: vybestack/llxprt-code
Length of output: 8758
🌐 Web query:
GitHub REST API Add assignees to an issue updates existing assignees not replace
💡 Result:
The GitHub REST API provides two distinct ways to manage issue assignees, depending on whether you wish to add users or replace the entire list [1][2]. To add assignees without removing existing ones, use the Add assignees to an issue endpoint [1][3]: POST /repos/{owner}/{repo}/issues/{issue_number}/assignees According to official GitHub documentation, this endpoint adds the specified users to the issue while ensuring that users already assigned are not replaced [1][4]. You can add up to 10 assignees in a single request by providing an array of usernames in the body [1][3]. In contrast, the Update an issue endpoint (PATCH /repos/{owner}/{repo}/issues/{issue_number}) behaves differently [2]. When you provide an assignees array via the PATCH method, it replaces the entire set of current assignees with the new list provided in the request [2]. Passing an empty array ([]) to the PATCH endpoint will clear all assignees from the issue [2]. Only users with push access to the repository have the authority to modify issue assignees [1][2]. If a user without sufficient permissions attempts these changes, the assignee modifications are silently ignored [1][2].
Citations:
- 1: https://docs.github.com/en/rest/issues/assignees?apiVersion=2022-11-28
- 2: https://docs.github.com/en/rest/issues/issues
- 3: https://docs.github.com/rest/issues/assignees?apiVersion=2022-11-28
- 4: https://docs.github.com/en/enterprise-cloud@latest/rest/issues/assignees
🌐 Web query:
GitHub CLI issue edit --add-assignee REST Add Assignees PATCH issue body assignees replaces behavior
💡 Result:
The behavior of assignee management in the GitHub CLI (gh) and the underlying GitHub REST API differs fundamentally: GitHub CLI (gh issue edit) The gh issue edit command does not perform a direct replacement of assignees when using flags. Instead, it provides specific flags to manage the assignee list incrementally [1][2]: --add-assignee: Adds the specified user(s) to the existing list of assignees [1][3]. --remove-assignee: Removes the specified user(s) from the existing list of assignees [1][3]. This design was explicitly implemented to avoid accidental overwrites and to allow piecemeal updates to issue metadata [2]. You can combine these flags in a single command (e.g., gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa) to achieve the effect of a reassign or update [1][3]. GitHub REST API (PATCH /repos/{owner}/{repo}/issues/{issue_number}) In contrast, the GitHub REST API's PATCH endpoint for updating an issue behaves as a replacement operation [4][5]. When you provide the assignees array in the request body, it replaces the entire set of current assignees with the new list provided [4][5]. Sending an empty array ([]) to this endpoint will clear all assignees from the issue [4][5]. For targeted additions or removals via the REST API without replacing the entire list, you should use the dedicated Assignees endpoints [6][7]: POST /repos/{owner}/{repo}/issues/{issue_number}/assignees: Adds assignees without removing existing ones [6][7]. DELETE /repos/{owner}/{repo}/issues/{issue_number}/assignees: Removes specific assignees from the issue [6][7].
Citations:
- 1: https://cli.github.com/manual/gh_issue_edit
- 2: Change behavior of slice flags for issue edit and pr edit commands cli/cli#2949
- 3: https://man.archlinux.org/man/gh-issue-edit.1.en.raw
- 4: https://docs.github.com/en/rest/issues/issues
- 5: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28
- 6: https://docs.github.com/en/rest/issues/assignees
- 7: https://docs.github.com/en/rest/issues/assignees?apiVersion=2022-11-28
Fail closed when reading current assignees.
|| echo "0" makes API failures indistinguishable from an unassigned issue. That allows the assign guard to be skipped and gh issue edit --add-assignee ... to proceed; gh issue edit --add-assignee is additive and does not replace existing assignees, so a failed read can create a co-assignment.
Abort and surface feedback when the assignee read fails; only continue when the assignee_count read succeeds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/assign-issue.sh around lines 60 - 63, Update the assignee
count read in the assign guard to fail closed: remove the fallback that converts
gh issue view errors into 0, capture or propagate the command failure, and abort
with clear feedback when reading current assignees fails. Continue to the
assignment flow only when assignee_count is successfully obtained.
| assignee_count="$( | ||
| gh issue view "${ISSUE}" --repo "${REPO}" --json assignees \ | ||
| --jq '.assignees | length' 2>/dev/null || echo "0" | ||
| )" | ||
|
|
||
| if [[ "${assignee_count}" -gt 0 ]]; then | ||
| current="$( | ||
| gh issue view "${ISSUE}" --repo "${REPO}" --json assignees \ | ||
| --jq '[.assignees[].login] | join(", ")' 2>/dev/null || echo "someone" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)(assign-issue\.sh|.*workflow.*\.ya?ml|.*workflow.*\.ya?ml)$' || true
echo
echo "Target file outline/lines:"
if [ -f .github/scripts/assign-issue.sh ]; then
wc -l .github/scripts/assign-issue.sh
sed -n '1,190p' .github/scripts/assign-issue.sh | nl -ba
else
fd -a 'assign-issue\.sh$' .github || true
fi
echo
echo "Workflow references to assign issue:"
rg -n "assign-issue|assign|add-assignee|concurrency|issues: create|issue_comment" .github -S || trueRepository: vybestack/llxprt-code
Length of output: 894
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "assign-issue.sh:"
cat -n .github/scripts/assign-issue.sh
echo
echo "All workflow trigger/concurrency/assignment references:"
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
[ -f "$f" ] || continue
echo "--- $f"
rg -n "on:|issue|comment|assign|concurrency|runs-on|uses:" "$f" -S || true
doneRepository: vybestack/llxprt-code
Length of output: 50377
Serialize /assign checks with the assignment mutation.
.github/workflows/assign.yml uses a concurrency group that includes the commenter login, so different commenters on the same issue can both pass the zero-assignee check and --add-assignee can add both users. Move the queue to a per-issue group with cancel-in-progress: false, or re-fetch after assignment and reject/roll back the actor when another assignee was added.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/assign-issue.sh around lines 60 - 68, Serialize the entire
assignment check-and-mutation flow in the `/assign` workflow per issue, rather
than per commenter, by updating the concurrency group in the workflow
configuration and setting cancel-in-progress to false. Ensure the existing
zero-assignee check and add-assignee operation in the assignment script execute
within that shared per-issue queue.
Summary
/assignself-assignment workflow (assign.yml+assign-issue.sh): exact-match comment gate, eligibility (merged PR / prior assignment / trusted contributors / owner-member-collaborator), 3-issue cap,auto-assignedlabel, and sticky feedback when assignment succeeds or silently fails.assign-stale-cleanup.yml+unassign-stale-issues.sh) to unassign auto-assigned issues with no linked PR activity for 14 days (exemptsacoliver).CONTRIBUTING.mdand adds YAML/script contract tests inscripts/tests/assign-workflow.test.js.Fixes #2630.
Test plan
bash -non both scriptsbunx vitest run --config scripts/tests/vitest.config.ts scripts/tests/assign-workflow.test.js(11/11 passed)/assignon an unassigned eligible issue and confirm assignment + sticky feedback +auto-assignedlabel/assignon an already-assigned issue and confirm refusal feedbackAssign Stale Cleanupviaworkflow_dispatchafter labeling a stale test issueSummary by CodeRabbit
New Features
/assigncomment.auto-assignedafter 14 days without linked PR activity, with a maintainer exemption.Documentation
/assignrequirements, eligibility/limits, and stale cleanup behavior.Tests