From 6c595485db114b7d22eab1abc7459bf4182046d1 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 28 Jul 2026 12:16:50 +0800 Subject: [PATCH 1/6] ci: auto-minimize comments from org-blocked users Adds a scheduled workflow that runs every hour to scan recent issue/PR comments and minimize any from users blocked at the org level. This cleans up spam comments that were posted before a block was applied. The workflow: 1. Fetches the org's blocked-user list via REST API 2. Queries recent comments (last 2h) via GraphQL 3. Matches comment authors against the blocked list 4. Minimizes unmatched comments as OFF_TOPIC via GraphQL Also triggerable manually via workflow_dispatch with a configurable lookback window. --- .github/workflows/auto-minimize-spam.yml | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 .github/workflows/auto-minimize-spam.yml diff --git a/.github/workflows/auto-minimize-spam.yml b/.github/workflows/auto-minimize-spam.yml new file mode 100644 index 00000000000..1aa8c676944 --- /dev/null +++ b/.github/workflows/auto-minimize-spam.yml @@ -0,0 +1,192 @@ +name: 'Auto-minimize blocked user comments' + +# Periodically scan recent issue/PR comments and minimize any from +# users blocked at the org level. This cleans up comments that were +# posted before the block was applied, and catches any that slip +# through during the window between a spam comment and the manual +# block action. +# +# The workflow reads the org's blocked-user list via the REST API, +# then paginates recent comments across all open issues and PRs. +# Comments from blocked users that are not already minimized are +# collapsed with the OFF_TOPIC classifier via GraphQL. + +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: 'string' + +permissions: + 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: 'Minimize comments from blocked users' + env: + GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' + LOOKBACK_HOURS: '${{ inputs.hours || ''2'' }}' + run: |- + set -euo pipefail + + ORG="${GITHUB_REPOSITORY%%/*}" + REPO="$GITHUB_REPOSITORY" + SINCE="$(date -u -d "${LOOKBACK_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)" + + echo "Scanning comments since $SINCE in $REPO" + + # ── 1. Fetch org-level blocked users ────────────────────────── + BLOCKED_USERS="$( + gh api "orgs/${ORG}/blocks" --paginate --jq '.[].login' 2>/dev/null \ + | tr '[:upper:]' '[:lower:]' \ + | sort -u + )" + BLOCKED_COUNT="$(printf '%s\n' "$BLOCKED_USERS" | grep -c . || true)" + echo "Org blocked users: ${BLOCKED_COUNT}" + if [ "$BLOCKED_COUNT" -eq 0 ]; then + echo "No blocked users; nothing to do." + exit 0 + fi + + # ── 2. Fetch recent comments from open issues and PRs ───────── + # Use the search API to find comments updated since $SINCE. + # The issues endpoint returns both issue and PR comments when + # we paginate /issues/{n}/comments, but the search API is more + # efficient for a time-windowed scan across all issues. + COMMENT_IDS="$( + 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)) + | map(select((.author.login | ascii_downcase) as $login + | $login | inside("'"$(printf '%s' "$BLOCKED_USERS" | tr '\n' '|')"' | rtrimstr("|"))")) + | .[].id + ' 2>/dev/null + )" + + # The jq `inside` trick above is fragile for multi-user matching. + # Fall back to a simpler shell-level filter. + 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)" + ' 2>/dev/null + )" + + 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)) + fi + done <<< "$ALL_UNMINIMIZED" + + echo "Unminimized comments from blocked users: ${MATCHED_COUNT}" + if [ "$MATCHED_COUNT" -eq 0 ]; then + echo "Nothing to minimize." + exit 0 + fi + + # ── 3. Minimize each matched comment ────────────────────────── + SUCCESS=0 + FAIL=0 + while IFS= read -r node_id; do + [ -z "$node_id" ] && continue + result="$( + gh api graphql -f query=" + mutation { + minimizeComment(input: {subjectId: \"${node_id}\", classifier: OFF_TOPIC}) { + minimizedComment { isMinimized } + } + } + " --jq '.data.minimizeComment.minimizedComment.isMinimized' 2>/dev/null + )" || true + if [ "$result" = "true" ]; then + SUCCESS=$((SUCCESS + 1)) + else + FAIL=$((FAIL + 1)) + echo "::warning::Failed to minimize ${node_id}" + fi + done <<< "$MATCHED_IDS" + + echo "Minimized ${SUCCESS} comments, ${FAIL} failed." + echo "## Summary" >> "$GITHUB_STEP_SUMMARY" + echo "- Scanned comments since: ${SINCE}" >> "$GITHUB_STEP_SUMMARY" + echo "- Org blocked users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" + echo "- Comments minimized: ${SUCCESS}" >> "$GITHUB_STEP_SUMMARY" + if [ "$FAIL" -gt 0 ]; then + echo "- Failures: ${FAIL}" >> "$GITHUB_STEP_SUMMARY" + fi From 704ee8ee7df7376310e6cba8cf0738c6462bd320 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 28 Jul 2026 12:35:48 +0800 Subject: [PATCH 2/6] ci: use repo blocklist file instead of org blocked-users API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org blocked-users API requires admin:org scope which the CI bot PAT doesn't have. Switch to a plain-text blocklist file at .github/spam-blocklist.txt — one username per line, case-insensitive, # for comments. No special scopes needed. Also adds danialzivehdadr as the first entry. --- .github/spam-blocklist.txt | 4 + .github/workflows/auto-minimize-spam.yml | 102 ++++++++--------------- 2 files changed, 40 insertions(+), 66 deletions(-) create mode 100644 .github/spam-blocklist.txt diff --git a/.github/spam-blocklist.txt b/.github/spam-blocklist.txt new file mode 100644 index 00000000000..406bc985432 --- /dev/null +++ b/.github/spam-blocklist.txt @@ -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 diff --git a/.github/workflows/auto-minimize-spam.yml b/.github/workflows/auto-minimize-spam.yml index 1aa8c676944..e53c5bc1f06 100644 --- a/.github/workflows/auto-minimize-spam.yml +++ b/.github/workflows/auto-minimize-spam.yml @@ -1,15 +1,14 @@ -name: 'Auto-minimize blocked user comments' +name: 'Auto-minimize spam comments' # Periodically scan recent issue/PR comments and minimize any from -# users blocked at the org level. This cleans up comments that were -# posted before the block was applied, and catches any that slip -# through during the window between a spam comment and the manual -# block action. +# 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 workflow reads the org's blocked-user list via the REST API, -# then paginates recent comments across all open issues and PRs. -# Comments from blocked users that are not already minimized are -# collapsed with the OFF_TOPIC classifier via GraphQL. +# 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. on: schedule: @@ -23,6 +22,7 @@ on: type: 'string' permissions: + contents: 'read' issues: 'write' pull-requests: 'write' @@ -36,81 +36,46 @@ jobs: runs-on: 'ubuntu-latest' timeout-minutes: 10 steps: - - name: 'Minimize comments from blocked users' + - 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 - ORG="${GITHUB_REPOSITORY%%/*}" REPO="$GITHUB_REPOSITORY" SINCE="$(date -u -d "${LOOKBACK_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ)" + BLOCKLIST=".github/spam-blocklist.txt" echo "Scanning comments since $SINCE in $REPO" - # ── 1. Fetch org-level blocked users ────────────────────────── + # ── 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. BLOCKED_USERS="$( - gh api "orgs/${ORG}/blocks" --paginate --jq '.[].login' 2>/dev/null \ + grep -v '^\s*#' "$BLOCKLIST" \ + | grep -v '^\s*$' \ + | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ | tr '[:upper:]' '[:lower:]' \ | sort -u )" BLOCKED_COUNT="$(printf '%s\n' "$BLOCKED_USERS" | grep -c . || true)" - echo "Org blocked users: ${BLOCKED_COUNT}" + echo "Blocklisted users: ${BLOCKED_COUNT}" if [ "$BLOCKED_COUNT" -eq 0 ]; then - echo "No blocked users; nothing to do." + echo "Blocklist is empty; nothing to do." exit 0 fi - # ── 2. Fetch recent comments from open issues and PRs ───────── - # Use the search API to find comments updated since $SINCE. - # The issues endpoint returns both issue and PR comments when - # we paginate /issues/{n}/comments, but the search API is more - # efficient for a time-windowed scan across all issues. - COMMENT_IDS="$( - 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)) - | map(select((.author.login | ascii_downcase) as $login - | $login | inside("'"$(printf '%s' "$BLOCKED_USERS" | tr '\n' '|')"' | rtrimstr("|"))")) - | .[].id - ' 2>/dev/null - )" - - # The jq `inside` trick above is fragile for multi-user matching. - # Fall back to a simpler shell-level filter. + # ── 2. Fetch recent unminimized comments via GraphQL ────────── ALL_UNMINIMIZED="$( gh api graphql -f query=" query { @@ -151,12 +116,17 @@ jobs: 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 blocked users: ${MATCHED_COUNT}" + echo "Unminimized comments from blocklisted users: ${MATCHED_COUNT}" if [ "$MATCHED_COUNT" -eq 0 ]; then echo "Nothing to minimize." + echo "## Summary" >> "$GITHUB_STEP_SUMMARY" + echo "- Scanned comments since: ${SINCE}" >> "$GITHUB_STEP_SUMMARY" + echo "- Blocklisted users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" + echo "- Comments minimized: 0" >> "$GITHUB_STEP_SUMMARY" exit 0 fi @@ -185,7 +155,7 @@ jobs: echo "Minimized ${SUCCESS} comments, ${FAIL} failed." echo "## Summary" >> "$GITHUB_STEP_SUMMARY" echo "- Scanned comments since: ${SINCE}" >> "$GITHUB_STEP_SUMMARY" - echo "- Org blocked users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" + echo "- Blocklisted users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" echo "- Comments minimized: ${SUCCESS}" >> "$GITHUB_STEP_SUMMARY" if [ "$FAIL" -gt 0 ]; then echo "- Failures: ${FAIL}" >> "$GITHUB_STEP_SUMMARY" From 5f914b739ef5e2a34e85842f19c852e50f004de0 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 28 Jul 2026 06:17:08 +0000 Subject: [PATCH 3/6] ci: make auto-minimize-spam failures visible, handle empty blocklist --- .github/workflows/auto-minimize-spam.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-minimize-spam.yml b/.github/workflows/auto-minimize-spam.yml index e53c5bc1f06..1984b10746a 100644 --- a/.github/workflows/auto-minimize-spam.yml +++ b/.github/workflows/auto-minimize-spam.yml @@ -9,6 +9,10 @@ name: 'Auto-minimize spam comments' # 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. on: schedule: @@ -61,12 +65,15 @@ jobs: 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 + | sort -u \ + || true )" BLOCKED_COUNT="$(printf '%s\n' "$BLOCKED_USERS" | grep -c . || true)" echo "Blocklisted users: ${BLOCKED_COUNT}" @@ -105,7 +112,7 @@ jobs: ] | map(select(.isMinimized == false and .author != null)) | .[] | "\(.author.login)\t\(.id)" - ' 2>/dev/null + ' )" MATCHED_IDS="" @@ -159,4 +166,5 @@ jobs: echo "- Comments minimized: ${SUCCESS}" >> "$GITHUB_STEP_SUMMARY" if [ "$FAIL" -gt 0 ]; then echo "- Failures: ${FAIL}" >> "$GITHUB_STEP_SUMMARY" + exit 1 fi From e91b8c2bec1bcab3d09edfbe2db501b478f1e9e9 Mon Sep 17 00:00:00 2001 From: qwen-code-ci-bot Date: Tue, 28 Jul 2026 08:32:52 +0000 Subject: [PATCH 4/6] fix(ci): address review feedback on auto-minimize-spam workflow (#7899) - Change hours input type from string to number (project convention) - Extract duplicated step-summary writes into write_summary() function - Capture gh stderr (2>&1) and include it in failure warnings - Document coverage limits in header comment --- .github/workflows/auto-minimize-spam.yml | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/auto-minimize-spam.yml b/.github/workflows/auto-minimize-spam.yml index 1984b10746a..cfde2893781 100644 --- a/.github/workflows/auto-minimize-spam.yml +++ b/.github/workflows/auto-minimize-spam.yml @@ -13,6 +13,11 @@ name: 'Auto-minimize spam comments' # 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: @@ -22,8 +27,8 @@ on: hours: description: 'Look back this many hours (default 2)' required: false - default: '2' - type: 'string' + default: 2 + type: 'number' permissions: contents: 'read' @@ -57,6 +62,15 @@ jobs: 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 ──────────────────────────────────────── @@ -130,10 +144,7 @@ jobs: echo "Unminimized comments from blocklisted users: ${MATCHED_COUNT}" if [ "$MATCHED_COUNT" -eq 0 ]; then echo "Nothing to minimize." - echo "## Summary" >> "$GITHUB_STEP_SUMMARY" - echo "- Scanned comments since: ${SINCE}" >> "$GITHUB_STEP_SUMMARY" - echo "- Blocklisted users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" - echo "- Comments minimized: 0" >> "$GITHUB_STEP_SUMMARY" + write_summary 0 exit 0 fi @@ -149,21 +160,18 @@ jobs: minimizedComment { isMinimized } } } - " --jq '.data.minimizeComment.minimizedComment.isMinimized' 2>/dev/null + " --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}" + echo "::warning::Failed to minimize ${node_id}: ${result}" fi done <<< "$MATCHED_IDS" echo "Minimized ${SUCCESS} comments, ${FAIL} failed." - echo "## Summary" >> "$GITHUB_STEP_SUMMARY" - echo "- Scanned comments since: ${SINCE}" >> "$GITHUB_STEP_SUMMARY" - echo "- Blocklisted users: ${BLOCKED_COUNT}" >> "$GITHUB_STEP_SUMMARY" - echo "- Comments minimized: ${SUCCESS}" >> "$GITHUB_STEP_SUMMARY" + write_summary "$SUCCESS" if [ "$FAIL" -gt 0 ]; then echo "- Failures: ${FAIL}" >> "$GITHUB_STEP_SUMMARY" exit 1 From 4e0118725c710319452f229b55a148901ec9e409 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Tue, 28 Jul 2026 12:51:18 +0000 Subject: [PATCH 5/6] fix(ci): quote auto-minimize-spam expressions to pass yamllint (#7899) --- .github/workflows/auto-minimize-spam.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-minimize-spam.yml b/.github/workflows/auto-minimize-spam.yml index cfde2893781..10b05f84186 100644 --- a/.github/workflows/auto-minimize-spam.yml +++ b/.github/workflows/auto-minimize-spam.yml @@ -41,7 +41,7 @@ concurrency: jobs: minimize: - if: github.repository == 'QwenLM/qwen-code' + if: "${{ github.repository == 'QwenLM/qwen-code' }}" runs-on: 'ubuntu-latest' timeout-minutes: 10 steps: @@ -54,7 +54,7 @@ jobs: - name: 'Minimize comments from blocklisted users' env: GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' - LOOKBACK_HOURS: '${{ inputs.hours || ''2'' }}' + LOOKBACK_HOURS: "${{ inputs.hours || '2' }}" run: |- set -euo pipefail From 5b05bd98e2441f740a4c3d33fe13159e95326f74 Mon Sep 17 00:00:00 2001 From: qwen-code-ci-bot Date: Tue, 28 Jul 2026 14:14:13 +0000 Subject: [PATCH 6/6] test(ci): add regression guards for auto-minimize-spam workflow invariants (#7899) --- .github/scripts/auto-minimize-spam.test.mjs | 73 +++++++++++++++++++++ .github/workflows/ci.yml | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/auto-minimize-spam.test.mjs diff --git a/.github/scripts/auto-minimize-spam.test.mjs b/.github/scripts/auto-minimize-spam.test.mjs new file mode 100644 index 00000000000..12a53861492 --- /dev/null +++ b/.github/scripts/auto-minimize-spam.test.mjs @@ -0,0 +1,73 @@ +// Regression guards for the security-critical invariants of the +// auto-minimize-spam workflow. Follows the pattern established by +// qwen-triage-workflow.test.mjs: a future edit that removes the repository +// guard, widens permissions, moves GH_TOKEN to job-level env, or drops +// persist-credentials would ship without any other test to catch it. +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, it } from 'node:test'; +import { parse } from 'yaml'; + +const workflowPath = join( + dirname(fileURLToPath(import.meta.url)), + '..', + 'workflows', + 'auto-minimize-spam.yml', +); +const doc = parse(readFileSync(workflowPath, 'utf8')); +const minimizeJob = doc.jobs.minimize; +const steps = minimizeJob.steps; +const checkoutStep = steps.find((s) => s.uses?.startsWith('actions/checkout')); +const minimizeStep = steps.find((s) => + s.name?.includes('Minimize comments'), +); + +describe('auto-minimize-spam: repository guard', () => { + it('gates the job on the canonical repository', () => { + assert.match( + String(minimizeJob.if), + /github\.repository == 'QwenLM\/qwen-code'/, + ); + }); +}); + +describe('auto-minimize-spam: permissions', () => { + it('has a minimal top-level permissions block', () => { + const perms = doc.permissions; + assert.deepEqual(perms, { + contents: 'read', + issues: 'write', + 'pull-requests': 'write', + }); + }); + + it('does not set job-level permissions', () => { + assert.equal( + minimizeJob.permissions, + undefined, + 'job-level permissions override the top-level block', + ); + }); +}); + +describe('auto-minimize-spam: credential scoping', () => { + it('disables persist-credentials on checkout', () => { + assert.ok(checkoutStep, 'checkout step must exist'); + assert.equal(checkoutStep.with['persist-credentials'], false); + }); + + it('scopes GH_TOKEN to step-level env, not job-level', () => { + assert.equal( + minimizeJob.env, + undefined, + 'job-level env would expose GH_TOKEN to every step', + ); + assert.ok(minimizeStep, 'minimize step must exist'); + assert.ok( + minimizeStep.env?.GH_TOKEN, + 'GH_TOKEN must be set in the minimize step env', + ); + }); +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a09a904d73..5519fae3331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ env: # BOTH the github_ci_only helper step and the full-profile Test step, so a # new helper test can't be added to one path and silently dropped from the # other. - HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/qwen-triage-workflow.test.mjs' + HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/qwen-triage-workflow.test.mjs .github/scripts/auto-minimize-spam.test.mjs' jobs: classify_pr: