From 837358f6370cd4742f3cd4737faabdb4e8d6927e Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 21 Jul 2026 19:07:11 +0800 Subject: [PATCH] fix(ci): tell a triage action crash apart from a silent agent (#7418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both no-response cases produced the same error, and its advice — "check the 'Run Qwen Triage' step stderr above for diagnostics" — pointed at diagnostics that do not exist for one of them: the action installs the CLI with `npm --silent`, so an install failure prints an exit code and nothing else. Observed on a PR whose triage died at exit 243 during that install, leaving a maintainer told to read an empty log. The two causes need opposite responses, and steps.triage.outcome already distinguishes them: - action failed -> no model call happened, nothing about the PR can explain it, and the fix is to re-run the job - action succeeded, empty summary -> the agent ran and returned nothing, which IS worth reading the step output for Measured baseline for the retry advice: of 19 executed triage runs, 4 failed, 3 of them on unrelated PRs, and one PR both succeeded and failed within an hour on the same head. The outcome is passed through env like RESPONSE already is, and the script is replayed under bash for both branches. Co-authored-by: wenshao --- .github/workflows/qwen-triage.yml | 17 +++++++- scripts/tests/qwen-triage-workflow.test.js | 48 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml index 1abe06bb18..89088ddcc8 100644 --- a/.github/workflows/qwen-triage.yml +++ b/.github/workflows/qwen-triage.yml @@ -295,10 +295,25 @@ jobs: shell: 'bash' env: RESPONSE: '${{ steps.triage.outputs.summary }}' + # Tells the two no-response causes apart. They need OPPOSITE + # responses, and the old message ("check the step stderr") pointed at + # diagnostics that do not exist: the action installs the CLI with + # `npm --silent`, so an install failure prints nothing but an exit + # code. Observed on a PR whose triage died at exit 243 during that + # install, with the error telling the maintainer to read an empty log. + TRIAGE_OUTCOME: '${{ steps.triage.outcome }}' run: |- set -uo pipefail if [[ -z "${RESPONSE}" || "${RESPONSE}" == "null" ]]; then - echo "::error title=Triage silent failure::Qwen Code exited without a response. Check the 'Run Qwen Triage' step stderr above for diagnostics." + if [[ "${TRIAGE_OUTCOME}" != 'success' ]]; then + # The action itself failed: no model call was ever made, so + # nothing about this PR can explain it. Retry is the fix. + echo "::error title=Triage did not start::The Qwen Code action failed before producing any response, so no triage ran. The usual cause is the global CLI install, which runs under \`npm --silent\` and prints nothing on failure. This is infrastructure, not this PR — re-run the failed job." + else + # The action succeeded but returned nothing: the agent ran and + # produced no summary. That IS worth reading the step output for. + echo "::error title=Triage silent failure::Qwen Code ran to completion but returned an empty response — a model or prompt problem, not an install one. Check the 'Run Qwen Triage' step output above." + fi exit 1 fi echo "Triage response received (${#RESPONSE} chars)." diff --git a/scripts/tests/qwen-triage-workflow.test.js b/scripts/tests/qwen-triage-workflow.test.js index 1461ec55a4..abd3dee77e 100644 --- a/scripts/tests/qwen-triage-workflow.test.js +++ b/scripts/tests/qwen-triage-workflow.test.js @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; @@ -124,6 +125,53 @@ describe('qwen-triage tmux workflow', () => { expect(checkStep).toContain('if [[ -z "${RESPONSE}"'); }); + it('tells an action crash apart from a silent agent, and replays both', () => { + const checkStep = step('Check triage response'); + // The outcome must arrive through env like RESPONSE does — inlining the + // expression into the script would be the injection shape this step + // already avoids for RESPONSE. + expect(checkStep).toContain( + "TRIAGE_OUTCOME: '${{ steps.triage.outcome }}'", + ); + expect(checkStep).not.toContain('TRIAGE_OUTCOME="${{'); + + const body = checkStep.match(/run: \|-\n([\s\S]*)$/)?.[1]; + expect(body).toBeTruthy(); + const script = body.replace(/^ {10}/gm, ''); + const run = (env) => { + const proc = spawnSync('bash', ['-c', script], { + env: { + ...process.env, + RESPONSE: '', + TRIAGE_OUTCOME: 'success', + ...env, + }, + encoding: 'utf8', + }); + return { status: proc.status, out: `${proc.stdout}${proc.stderr}` }; + }; + + // A crashed action: no model call happened, so nothing about the PR can + // explain it and the guidance must say "re-run", not "read the log" — + // the install runs under `npm --silent`, so there is no log to read. + const crashed = run({ TRIAGE_OUTCOME: 'failure' }); + expect(crashed.status).not.toBe(0); + expect(crashed.out).toContain('Triage did not start'); + expect(crashed.out).toContain('re-run the failed job'); + expect(crashed.out).not.toContain('Triage silent failure'); + + // A completed action with no summary IS worth reading the step output for. + const silent = run({ TRIAGE_OUTCOME: 'success' }); + expect(silent.status).not.toBe(0); + expect(silent.out).toContain('Triage silent failure'); + expect(silent.out).toContain('model or prompt problem'); + expect(silent.out).not.toContain('Triage did not start'); + + // A real response still passes, and 'null' still counts as no response. + expect(run({ RESPONSE: 'triaged' }).status).toBe(0); + expect(run({ RESPONSE: 'null' }).status).not.toBe(0); + }); + it('notifies the author when a manual triage re-run posts no review', () => { const notifyStep = step('Notify silent triage re-run');