-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(ci): harden tmux triage reporting #5548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e7efb4b
669a92e
e01a1ed
0293810
f423fb4
7fb69d6
d8f4d7c
b7fa622
78f9085
a67aea4
7190ad6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 \ | ||||||||||||
|
|
@@ -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' | ||||||||||||
|
|
@@ -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: |- | ||||||||||||
|
|
@@ -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}." | ||||||||||||
|
|
@@ -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[@]}" \ | ||||||||||||
|
|
@@ -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/&/\&/g' -e 's/</\</g' -e 's/>/\>/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>' | ||||||||||||
| elif [ -n "$truncated" ]; then | ||||||||||||
| content="${content}${truncated}" | ||||||||||||
| fi | ||||||||||||
| content="${content//&/&}" | ||||||||||||
| content="${content//</<}" | ||||||||||||
| content="${content//>/>}" | ||||||||||||
| 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' | ||||||||||||
| } | ||||||||||||
|
yiliang114 marked this conversation as resolved.
|
||||||||||||
|
|
@@ -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 | ||||||||||||
|
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.' | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The Consolidating to
Suggested change
— 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 | ||||||||||||
|
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." | ||||||||||||
| ;; | ||||||||||||
|
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_' | ||||||||||||
|
|
||||||||||||
| 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( | ||
|
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//&/&}"'); | ||
| expect(postStep).not.toContain('content="${content//</<}"'); | ||
| expect(postStep).not.toContain('content="${content//>/>}"'); | ||
| expect(postStep).toContain("sed -e 's/&/\\&/g'"); | ||
| expect(postStep).toContain("-e 's/</\\</g'"); | ||
| expect(postStep).toContain("-e 's/>/\\>/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', () => { | ||
|
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'"); | ||
|
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'); | ||
|
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'); | ||
|
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', () => { | ||
|
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'); | ||
|
yiliang114 marked this conversation as resolved.
|
||
| expect(resolverStep).not.toContain('npm install'); | ||
| expect(resolverStep).not.toContain('qwen --version'); | ||
| expect( | ||
|
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'")); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.