Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6c59548
ci: auto-minimize comments from org-blocked users
qwen-code-dev-bot Jul 28, 2026
704ee8e
ci: use repo blocklist file instead of org blocked-users API
qwen-code-dev-bot Jul 28, 2026
8868e83
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
qwen-code-dev-bot Jul 28, 2026
5f914b7
ci: make auto-minimize-spam failures visible, handle empty blocklist
qwen-code-dev-bot Jul 28, 2026
e40f468
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
wenshao Jul 28, 2026
e91b8c2
fix(ci): address review feedback on auto-minimize-spam workflow (#7899)
qwen-code-ci-bot Jul 28, 2026
ed2670e
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
qwen-code-dev-bot Jul 28, 2026
e810102
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
wenshao Jul 28, 2026
0a1e4f2
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
qwen-code-dev-bot Jul 28, 2026
fb44158
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
wenshao Jul 28, 2026
4e01187
fix(ci): quote auto-minimize-spam expressions to pass yamllint (#7899)
qwen-code-dev-bot Jul 28, 2026
19f9962
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
wenshao Jul 28, 2026
5b05bd9
test(ci): add regression guards for auto-minimize-spam workflow invar…
qwen-code-ci-bot Jul 28, 2026
64943e3
Merge remote-tracking branch 'origin/main' into ci/auto-minimize-bloc…
qwen-code-ci-bot Jul 29, 2026
28db4f3
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
qwen-code-dev-bot Jul 29, 2026
8874021
Merge branch 'main' into ci/auto-minimize-blocked-user-comments
wenshao Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/spam-blocklist.txt
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
178 changes: 178 additions & 0 deletions .github/workflows/auto-minimize-spam.yml
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'

Check failure on line 44 in .github/workflows/auto-minimize-spam.yml

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, Node 22.x)

44:9 [quoted-strings] string value is not quoted with single quotes
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 }}'
Comment on lines +54 to +56

Copy link
Copy Markdown
Collaborator Author

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.mjs with 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 的聚焦,也避免对已充分验证的逻辑进行重新审查。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Each matched comment spawns a separate gh api graphql process for the minimizeComment mutation — an N+1 API-call pattern. — Failure scenario: a spam flood matching 100+ comments would spend 20–50 seconds on per-process overhead (Go binary startup, auth, TLS, network roundtrip), eating a large share of the 10-minute timeout-minutes budget. Concrete cost: ~200–500 ms × N matches of pure overhead.

中文说明

每个匹配的评论都会启动一个独立的 gh api graphql 进程来执行 minimizeComment mutation——这是 N+1 API 调用模式。失败场景:如果垃圾信息泛滥匹配到 100+ 条评论,每个进程的开销(Go 二进制启动、认证、TLS、网络往返)约 200–500ms,累计 20–50 秒,会占用 10 分钟 timeout-minutes 预算的很大一部分。建议使用 GraphQL aliases 批量 mutation(每批 ~50 个)以减少 API 调用次数。

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Declined — the per-comment gh api graphql call is intentional for this workflow. It runs hourly with a typically 0–1 match blocklist; even a 100+ match flood spends 20–50 s of overhead, well within the 10-minute timeout-minutes. Batching with GraphQL aliases in shell script adds substantial complexity (dynamic query construction, batch chunking, per-alias result parsing, partial-failure handling) for marginal gain. The current loop also gives individual success/failure tracking with per-ID ::warning:: annotations, which is simpler to debug.

中文说明

已拒绝——逐条评论调用 gh api graphql 是该工作流的有意设计。工作流每小时运行一次,blocklist 通常只有 0–1 个匹配;即使出现 100+ 匹配的垃圾信息泛滥,20–50 秒的开销也远在 10 分钟 timeout-minutes 预算之内。在 shell 脚本中使用 GraphQL aliases 进行批量操作会引入大量复杂度(动态查询构造、分批处理、逐 alias 结果解析、部分失败处理),收益却很小。当前的逐条循环还提供逐个成功/失败跟踪和逐 ID 的 ::warning:: 注释,更易于调试。

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
Loading