Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 86 additions & 17 deletions .github/workflows/qwen-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,11 @@ jobs:
verdict: '${{ steps.run.outputs.verdict || steps.prepare.outputs.verdict || steps.pr.outputs.verdict }}'
failure_phase: '${{ steps.prepare.outputs.failure_phase }}'
steps:
- name: 'Install tmux runner tools'
- name: 'Install PR resolver tools'
run: |-
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends ca-certificates curl git gnupg jq tmux util-linux
apt-get install -y --no-install-recommends ca-certificates curl git gnupg jq

install -d -m 755 /etc/apt/keyrings
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
Expand All @@ -287,10 +287,7 @@ jobs:
apt-get update
apt-get install -y --no-install-recommends gh

npm install -g '@qwen-code/qwen-code@latest'
qwen --version
gh --version
tmux -V

- name: 'Resolve PR and check state'
id: 'pr'
Expand Down Expand Up @@ -348,6 +345,17 @@ jobs:
echo "decision=na" >> "$GITHUB_OUTPUT"
fi

# Install before checkout so PR-controlled .npmrc cannot affect npm.
- name: 'Install tmux runner tools'
if: "steps.pr.outputs.decision == 'run'"
run: |-
set -euo pipefail
apt-get install -y --no-install-recommends tmux util-linux

npm install -g --registry=https://registry.npmjs.org '@qwen-code/qwen-code@latest'
qwen --version
tmux -V

- name: 'Clean stale review worktrees'
if: "steps.pr.outputs.decision == 'run'"
run: |-
Expand Down Expand Up @@ -381,13 +389,15 @@ jobs:
set +e
{
printf '%s\n' '$ npm ci --prefer-offline --no-audit --progress=false'
runuser -u node -- npm ci --prefer-offline --no-audit --progress=false
runuser -u node -- env -u GITHUB_OUTPUT -u GITHUB_STATE -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_STEP_SUMMARY \
npm ci --prefer-offline --no-audit --progress=false
install_status=$?
if [ "$install_status" -ne 0 ]; then
printf '\n%s\n' "npm ci failed with exit code ${install_status}."
else
printf '\n%s\n' '$ npm run build'
runuser -u node -- npm run build
runuser -u node -- env -u GITHUB_OUTPUT -u GITHUB_STATE -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_STEP_SUMMARY \
npm run build
build_status=$?
if [ "$build_status" -ne 0 ]; then
printf '\n%s\n' "npm run build failed with exit code ${build_status}."
Expand Down Expand Up @@ -663,6 +673,9 @@ jobs:
"QWEN_CI_REAL_GH=${QWEN_CI_REAL_GH:-}"
"QWEN_CI_REAL_GIT=${QWEN_CI_REAL_GIT:-}"
)
if [ -n "${OPENAI_MODEL:-}" ]; then
QWEN_ENV+=("OPENAI_MODEL=$OPENAI_MODEL")
fi

set +e
timeout --kill-after=10s 20m runuser -u node -- env -i "${QWEN_ENV[@]}" "${QWEN_CMD[@]}" \
Expand Down Expand Up @@ -771,17 +784,27 @@ jobs:
# as markdown — which a
# backtick fence (breakable by a long enough ``` run) cannot guarantee.
# Order matters: escape & first so the < / > entities aren't re-escaped.
html_escape() {
sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}

emit_block() {
local summary="$1" file="$2" max="$3" content
local summary="$1" file="$2" max="$3" content truncated='' summary_html
[ -n "$file" ] && [ -f "$file" ] || return 0
content="$(head -c "$max" "$file")"
summary_html="$(printf '%s' "$summary" | html_escape)"
if [ "$(wc -c < "$file")" -gt "$max" ]; then
content="${content}"$'\n\n...truncated -- full log in the run artifacts.'
truncated=$'\n\n...truncated -- full log in the run artifacts.'
fi
if ! content="$(
set -o pipefail
head -c "$max" "$file" | tr -d '\000' | html_escape
)"; then
echo "::warning::emit_block failed while rendering $summary; see run artifacts." >&2
content='<em>Log could not be rendered; see run artifacts.</em>'
Comment thread
yiliang114 marked this conversation as resolved.
elif [ -n "$truncated" ]; then
content="${content}${truncated}"
fi
content="${content//&/&amp;}"
content="${content//</&lt;}"
content="${content//>/&gt;}"
printf '<details>\n<summary>%s</summary>\n\n<pre><code>\n' "$summary"
printf '<details>\n<summary>%s</summary>\n\n<pre><code>\n' "$summary_html"
printf '%s\n' "$content"
printf '</code></pre>\n\n</details>\n\n'
}
Comment thread
yiliang114 marked this conversation as resolved.
Expand All @@ -807,22 +830,68 @@ jobs:
case "$PREPARE_FAILURE_PHASE" in
install) PREPARE_COMMAND='npm ci' ;;
build) PREPARE_COMMAND='npm run build' ;;
*) PREPARE_COMMAND="$PREPARE_FAILURE_PHASE" ;;
*)
PREPARE_COMMAND='install/build'
UNKNOWN_PREPARE_PHASE="$(
printf '%s' "$PREPARE_FAILURE_PHASE" | tr -d '\000' | tr '\r\n' ' ' | head -c 200 | html_escape
)"
echo "::warning::Unrecognized prepare failure phase: ${UNKNOWN_PREPARE_PHASE}"
;;
esac
if [ -z "$PREPARE_LOG" ]; then
PREPARE_LOG_NOTE='No prepare.log was found in tmux-results, so the install/build log section is omitted.'
echo "::warning::${PREPARE_LOG_NOTE}"
fi
{
printf '%s\n\n' '<!-- qwen-triage:tmux -->'
printf '**tmux real-user testing: fail** - [workflow run](%s)\n\n' "$RUN_URL"
printf 'The PR app could not be launched because `%s` failed before the tmux session started. This is treated as a PR failure verdict rather than an infrastructure failure.\n\n' "$PREPARE_COMMAND"
if [ -n "${PREPARE_LOG_NOTE:-}" ]; then
printf '%s\n\n' "$PREPARE_LOG_NOTE"
fi
emit_block 'Install/build log' "$PREPARE_LOG" 20000
printf '%s\n' '— _Qwen Code · tmux real-user testing_'
} > "$BODY_FILE"
else
Comment thread
yiliang114 marked this conversation as resolved.
REPORT="$(find tmux-results -name 'report.md' 2>/dev/null | head -1 || true)"
TRANSCRIPT="$(find tmux-results -name 'tmux-readable-full.log' 2>/dev/null | head -1 || true)"
if [ -z "$REPORT" ] && [ -z "$TRANSCRIPT" ]; then
MISSING_ARTIFACTS_NOTE='No report.md or tmux-readable-full.log was found in tmux-results, so detailed report sections are omitted.'
echo "::warning::${MISSING_ARTIFACTS_NOTE}"
fi
case "${VERDICT:-}" in
infra-error)
VERDICT_LABEL='infra-error (crash/OOM)'
DESCRIPTION='The tmux test did not complete because the qwen process failed or was killed. This is not a pass/fail result for the affected flow; check runner resources and PR code for crashes or memory leaks.'
;;
timeout)
VERDICT_LABEL='timeout'
DESCRIPTION='The tmux test did not complete before the time limit. This is not a pass/fail result for the affected flow; see the workflow run and artifacts for details.'
;;
pass)
VERDICT_LABEL='pass'
DESCRIPTION='Launched the changed app in a real tmux session and exercised the affected flow.'
;;
fail)
VERDICT_LABEL='fail'
DESCRIPTION='Launched the changed app in a real tmux session and exercised the affected flow.'

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] The pass) and fail) case arms produce byte-identical DESCRIPTION strings. A fail verdict renders as "tmux real-user testing: fail" followed by text that reads as if the test succeeded ("exercised the affected flow"), which can mislead readers skimming the PR comment.

Consolidating to pass|fail) also prevents a future edit from accidentally drifting one arm but not the other:

Suggested change
DESCRIPTION='Launched the changed app in a real tmux session and exercised the affected flow.'
pass|fail)
VERDICT_LABEL="$VERDICT"
DESCRIPTION='Launched the changed app in a real tmux session and exercised the affected flow.'
;;

— qwen3.7-max via Qwen Code /review

;;
*)
VERDICT_LABEL='unknown'
UNKNOWN_VERDICT="$(
printf '%s' "${VERDICT:-}" | tr -d '\000' | tr '\r\n' ' ' | head -c 200 | html_escape
Comment thread
yiliang114 marked this conversation as resolved.
)"
echo "::warning::Unrecognized tmux verdict: ${UNKNOWN_VERDICT}"
DESCRIPTION="The tmux test produced an unrecognized verdict (<code>${UNKNOWN_VERDICT}</code>), so this is not a pass/fail result for the affected flow. See the workflow run and artifacts for details."
;;
Comment thread
yiliang114 marked this conversation as resolved.
esac
{
printf '%s\n\n' '<!-- qwen-triage:tmux -->'
printf '**tmux real-user testing: %s** - [workflow run](%s)\n\n' "$VERDICT" "$RUN_URL"
printf 'Launched the changed app in a real tmux session and exercised the affected flow.\n\n'
printf '**tmux real-user testing: %s** - [workflow run](%s)\n\n' "$VERDICT_LABEL" "$RUN_URL"
printf '%s\n\n' "$DESCRIPTION"
if [ -n "${MISSING_ARTIFACTS_NOTE:-}" ]; then
printf '%s\n\n' "$MISSING_ARTIFACTS_NOTE"
fi
emit_block 'E2E test report' "$REPORT" 20000
emit_block 'Full tmux transcript' "$TRANSCRIPT" 30000
printf '%s\n' '— _Qwen Code · tmux real-user testing_'
Expand Down
142 changes: 142 additions & 0 deletions scripts/tests/qwen-triage-workflow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';

const workflow = readFileSync('.github/workflows/qwen-triage.yml', 'utf8');

function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function step(name) {
const escaped = escapeRegExp(name);
const match = workflow.match(
new RegExp(
Comment thread
yiliang114 marked this conversation as resolved.
`\\n\\s+- name:\\s*(['"])${escaped}\\1[\\s\\S]*?(?=\\n\\s+- name:\\s*['"]|\\n\\s{2}[a-zA-Z0-9_-]+:|$)`,
),
);
return match?.[0] ?? '';
}

describe('qwen-triage tmux workflow', () => {
it('escapes embedded tmux artifacts without bash pattern replacement ampersands', () => {
const postStep = step('Post tmux result comment');

expect(postStep).not.toContain('content="${content//&/&amp;}"');
expect(postStep).not.toContain('content="${content//</&lt;}"');
expect(postStep).not.toContain('content="${content//>/&gt;}"');
expect(postStep).toContain("sed -e 's/&/\\&amp;/g'");
expect(postStep).toContain("-e 's/</\\&lt;/g'");
expect(postStep).toContain("-e 's/>/\\&gt;/g'");
expect(postStep).toContain('html_escape()');
expect(postStep).toContain("tr -d '\\000'");
expect(postStep).toContain('Log could not be rendered');
expect(postStep).toContain('if ! content="$(');
expect(postStep).toContain('set -o pipefail');
expect(postStep).toContain('::warning::emit_block failed');
expect(postStep).toContain(
'summary_html="$(printf \'%s\' "$summary" | html_escape)"',
);
expect(postStep).toContain(
'\'<details>\\n<summary>%s</summary>\\n\\n<pre><code>\\n\' "$summary_html"',
);
});

it('passes the selected OpenAI model into the app under tmux test', () => {
const runStep = step('Run tmux real-user testing');

expect(runStep).toContain('if [ -n "${OPENAI_MODEL:-}" ]; then');
expect(runStep).toContain('"OPENAI_MODEL=$OPENAI_MODEL"');
});

it('reports timeout and infra-error without claiming the flow was exercised', () => {
Comment thread
yiliang114 marked this conversation as resolved.
const postStep = step('Post tmux result comment');

expect(postStep).toContain('case "${VERDICT:-}" in');
expect(postStep).toContain("VERDICT_LABEL='infra-error (crash/OOM)'");
expect(postStep).toContain("VERDICT_LABEL='timeout'");
Comment thread
yiliang114 marked this conversation as resolved.
expect(postStep).toContain("VERDICT_LABEL='pass'");
expect(postStep).toContain("VERDICT_LABEL='fail'");
expect(postStep).toContain("VERDICT_LABEL='unknown'");
expect(postStep).toContain('The tmux test did not complete');
expect(postStep).toContain(
'The tmux test did not complete before the time limit',
);
expect(postStep).toContain('not a pass/fail result');
expect(postStep).toContain('crashes or memory leaks');
Comment thread
yiliang114 marked this conversation as resolved.
expect(postStep).toContain(
'Launched the changed app in a real tmux session and exercised the affected flow.',
);
expect(postStep).toContain('produced an unrecognized verdict');
expect(postStep).toContain('UNKNOWN_VERDICT="$(');
expect(postStep).toContain('::warning::Unrecognized tmux verdict');
expect(postStep).toContain("tr '\\r\\n' ' '");
expect(postStep).toContain('<code>${UNKNOWN_VERDICT}</code>');
expect(postStep).toContain('"$VERDICT_LABEL" "$RUN_URL"');
expect(postStep).toContain('printf \'%s\\n\\n\' "$DESCRIPTION"');
expect(postStep).toContain('MISSING_ARTIFACTS_NOTE=');
expect(postStep).toContain(
'No report.md or tmux-readable-full.log was found in tmux-results',
);
});

it('removes GitHub command files from PR-controlled lifecycle scripts', () => {
const prepareStep = step('Install and build PR app');
const strippedEnv =
'env -u GITHUB_OUTPUT -u GITHUB_STATE -u GITHUB_ENV -u GITHUB_PATH -u GITHUB_STEP_SUMMARY';

expect(prepareStep).toContain('-u GITHUB_OUTPUT');
expect(prepareStep).toContain('-u GITHUB_STATE');
expect(prepareStep).toContain('-u GITHUB_ENV');
expect(prepareStep).toContain('-u GITHUB_PATH');
expect(prepareStep).toContain('-u GITHUB_STEP_SUMMARY');
Comment thread
yiliang114 marked this conversation as resolved.
expect(prepareStep).toMatch(
new RegExp(`${escapeRegExp(strippedEnv)} \\\\\\s+npm ci`),
);
expect(prepareStep).toMatch(
new RegExp(`${escapeRegExp(strippedEnv)} \\\\\\s+npm run build`),
);
});

it('does not echo unrecognized prepare failure phases into comments', () => {
const postStep = step('Post tmux result comment');

expect(postStep).toContain("PREPARE_COMMAND='install/build'");
expect(postStep).toContain('::warning::Unrecognized prepare failure phase');
expect(postStep).toContain('PREPARE_LOG_NOTE=');
expect(postStep).toContain(
'No prepare.log was found in tmux-results, so the install/build log section is omitted.',
);
expect(postStep).not.toContain('PREPARE_COMMAND="$PREPARE_FAILURE_PHASE"');
});

it('installs the heavy tmux test harness only for runnable PRs', () => {
Comment thread
yiliang114 marked this conversation as resolved.
const installStep = step('Install tmux runner tools');
const resolverStep = step('Install PR resolver tools');

expect(resolverStep).toContain('apt-get install');
expect(installStep).toContain('if: "steps.pr.outputs.decision == \'run\'"');
expect(installStep).toContain(
'apt-get install -y --no-install-recommends tmux util-linux',
);
expect(installStep).toContain(
"npm install -g --registry=https://registry.npmjs.org '@qwen-code/qwen-code@latest'",
);
expect(installStep).toContain('qwen --version');
expect(installStep).toContain('tmux -V');
expect(resolverStep).not.toContain('tmux');
Comment thread
yiliang114 marked this conversation as resolved.
expect(resolverStep).not.toContain('npm install');
expect(resolverStep).not.toContain('qwen --version');
expect(
Comment thread
yiliang114 marked this conversation as resolved.
workflow.indexOf("- name: 'Resolve PR and check state'"),
).toBeLessThan(workflow.indexOf("- name: 'Install tmux runner tools'"));
expect(
workflow.indexOf("- name: 'Install tmux runner tools'"),
).toBeLessThan(workflow.indexOf("- name: 'Checkout PR merge ref'"));
});
});
Loading