Repository navigation
ci: auto-minimize comments from org-blocked users #7899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
6c59548
704ee8e
8868e83
5f914b7
e40f468
e91b8c2
ed2670e
e810102
0a1e4f2
fb44158
4e01187
19f9962
5b05bd9
64943e3
28db4f3
8874021
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Users whose comments should be auto-minimized as off-topic. | ||
| # One username per line, case-insensitive. Lines starting with # are comments. | ||
| # Add entries here when a user is blocked at the org level or identified as spam. | ||
| danialzivehdadr |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| name: 'Auto-minimize spam comments' | ||
|
|
||
| # Periodically scan recent issue/PR comments and minimize any from | ||
| # users listed in .github/spam-blocklist.txt. This cleans up spam | ||
| # comments that were posted before a block was applied, and catches | ||
| # any that slip through during the window between a spam comment | ||
| # and the manual block action. | ||
| # | ||
| # The blocklist is a plain-text file in the repo — one username per | ||
| # line, case-insensitive, # for comments. No special API scopes | ||
| # needed beyond issues:write + pull-requests:write. | ||
| # | ||
| # Note: the lookback window (LOOKBACK_HOURS) only filters issues — | ||
| # the GraphQL pullRequests connection has no `since` filter, so PRs | ||
| # are always scoped to the 100 most recently updated. | ||
| # | ||
| # Coverage limits: only issue-style comments are scanned — PR review | ||
| # (inline) comments, review bodies, and Discussions are not. | ||
| # comments(last: 30) means a >30-comment flood on a single thread is | ||
| # only partially cleaned per run. | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '30 * * * *' # Every hour at :30 | ||
| workflow_dispatch: | ||
| inputs: | ||
| hours: | ||
| description: 'Look back this many hours (default 2)' | ||
| required: false | ||
| default: 2 | ||
| type: 'number' | ||
|
|
||
| permissions: | ||
| contents: 'read' | ||
| issues: 'write' | ||
| pull-requests: 'write' | ||
|
|
||
| concurrency: | ||
| group: 'auto-minimize-spam' | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| minimize: | ||
| if: github.repository == 'QwenLM/qwen-code' | ||
| runs-on: 'ubuntu-latest' | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: 'Checkout blocklist' | ||
| uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 | ||
| with: | ||
| sparse-checkout: '.github/spam-blocklist.txt' | ||
| persist-credentials: false | ||
|
|
||
| - name: 'Minimize comments from blocklisted users' | ||
| env: | ||
| GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' | ||
| LOOKBACK_HOURS: '${{ inputs.hours || ''2'' }}' | ||
| run: |- | ||
| set -euo pipefail | ||
|
|
||
| REPO="$GITHUB_REPOSITORY" | ||
| SINCE="$(date -u -d "${LOOKBACK_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)" | ||
| BLOCKLIST=".github/spam-blocklist.txt" | ||
|
|
||
| write_summary() { | ||
| { | ||
| echo "## Summary" | ||
| echo "- Scanned comments since: ${SINCE}" | ||
| echo "- Blocklisted users: ${BLOCKED_COUNT}" | ||
| echo "- Comments minimized: $1" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| } | ||
|
|
||
| echo "Scanning comments since $SINCE in $REPO" | ||
|
|
||
| # ── 1. Parse blocklist ──────────────────────────────────────── | ||
| if [ ! -f "$BLOCKLIST" ]; then | ||
| echo "::notice::No blocklist file found at $BLOCKLIST; nothing to do." | ||
| exit 0 | ||
| fi | ||
| # Strip comments, blank lines, whitespace; lowercase for matching. | ||
| # `|| true` keeps an all-comment/blank blocklist from tripping | ||
| # `set -e` (grep exits 1 when nothing passes the filter). | ||
| BLOCKED_USERS="$( | ||
| grep -v '^\s*#' "$BLOCKLIST" \ | ||
| | grep -v '^\s*$' \ | ||
| | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ | ||
| | tr '[:upper:]' '[:lower:]' \ | ||
| | sort -u \ | ||
| || true | ||
| )" | ||
| BLOCKED_COUNT="$(printf '%s\n' "$BLOCKED_USERS" | grep -c . || true)" | ||
| echo "Blocklisted users: ${BLOCKED_COUNT}" | ||
| if [ "$BLOCKED_COUNT" -eq 0 ]; then | ||
| echo "Blocklist is empty; nothing to do." | ||
| exit 0 | ||
| fi | ||
|
|
||
| # ── 2. Fetch recent unminimized comments via GraphQL ────────── | ||
| ALL_UNMINIMIZED="$( | ||
| gh api graphql -f query=" | ||
| query { | ||
| repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") { | ||
| issues(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}, filterBy: {since: \"${SINCE}\"}) { | ||
| nodes { | ||
| number | ||
| comments(last: 30) { | ||
| nodes { id author { login } isMinimized } | ||
| } | ||
| } | ||
| } | ||
| pullRequests(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) { | ||
| nodes { | ||
| number | ||
| comments(last: 30) { | ||
| nodes { id author { login } isMinimized } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| " --jq ' | ||
| [ | ||
| .data.repository.issues.nodes[].comments.nodes[], | ||
| .data.repository.pullRequests.nodes[].comments.nodes[] | ||
| ] | ||
| | map(select(.isMinimized == false and .author != null)) | ||
| | .[] | "\(.author.login)\t\(.id)" | ||
| ' | ||
| )" | ||
|
|
||
| MATCHED_IDS="" | ||
| MATCHED_COUNT=0 | ||
| while IFS=$'\t' read -r login node_id; do | ||
| [ -z "$login" ] && continue | ||
| login_lc="$(printf '%s' "$login" | tr '[:upper:]' '[:lower:]')" | ||
| if printf '%s\n' "$BLOCKED_USERS" | grep -qxF "$login_lc"; then | ||
| MATCHED_IDS="${MATCHED_IDS}${node_id}"$'\n' | ||
| MATCHED_COUNT=$((MATCHED_COUNT + 1)) | ||
| echo " matched: @${login} → ${node_id}" | ||
| fi | ||
| done <<< "$ALL_UNMINIMIZED" | ||
|
|
||
| echo "Unminimized comments from blocklisted users: ${MATCHED_COUNT}" | ||
| if [ "$MATCHED_COUNT" -eq 0 ]; then | ||
| echo "Nothing to minimize." | ||
| write_summary 0 | ||
| exit 0 | ||
| fi | ||
|
|
||
| # ── 3. Minimize each matched comment ────────────────────────── | ||
| SUCCESS=0 | ||
| FAIL=0 | ||
| while IFS= read -r node_id; do | ||
| [ -z "$node_id" ] && continue | ||
| result="$( | ||
|
Comment on lines
+154
to
+156
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Each matched comment spawns a separate 中文说明每个匹配的评论都会启动一个独立的 — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declined — the per-comment 中文说明已拒绝——逐条评论调用 |
||
| gh api graphql -f query=" | ||
| mutation { | ||
| minimizeComment(input: {subjectId: \"${node_id}\", classifier: OFF_TOPIC}) { | ||
| minimizedComment { isMinimized } | ||
| } | ||
| } | ||
| " --jq '.data.minimizeComment.minimizedComment.isMinimized' 2>&1 | ||
| )" || true | ||
| if [ "$result" = "true" ]; then | ||
| SUCCESS=$((SUCCESS + 1)) | ||
| else | ||
| FAIL=$((FAIL + 1)) | ||
| echo "::warning::Failed to minimize ${node_id}: ${result}" | ||
| fi | ||
| done <<< "$MATCHED_IDS" | ||
|
|
||
| echo "Minimized ${SUCCESS} comments, ${FAIL} failed." | ||
| write_summary "$SUCCESS" | ||
| if [ "$FAIL" -gt 0 ]; then | ||
| echo "- Failures: ${FAIL}" >> "$GITHUB_STEP_SUMMARY" | ||
| exit 1 | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declined — not worth the diff growth in this PR.
The suggestion to extract the bash into
.github/scripts/auto-minimize-spam.mjswith paired tests is well-founded as a convention matter, but it would be a major rewrite that significantly expands this PR's scope. The maintainer (@wenshao) already verified the verbatim bash with a 31-check scenario harness (including a 5-mutation teeth check) and live read-only runs against the repository, then gave LGTM. That harness provides the regression coverage this suggestion seeks.If the mjs convention is desired here, a follow-up PR is the better vehicle — it keeps this PR focused and avoids re-litigating logic that has already been thoroughly validated.
中文说明
拒绝 — 在本 PR 中不值得增加这么多 diff。
将 bash 提取到
.github/scripts/auto-minimize-spam.mjs并配套测试的建议,从惯例角度来说完全合理,但这将是一次重大重写,会显著扩大本 PR 的范围。维护者(@wenshao)已通过 31 项场景 harness(含 5 个变异杀伤力检查)和对仓库的真实只读运行充分验证了原版 bash 脚本,并给出了 LGTM。该 harness 已提供了本建议所寻求的回归覆盖。如果此处需要遵循 mjs 惯例,后续 PR 是更好的载体——既保持本 PR 的聚焦,也避免对已充分验证的逻辑进行重新审查。