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');