Files
mattermost/.github/workflows/server-ci-report.yml
T
Maria A NunezandCursor Agent a08d806a48 Fix flaky-test Mattermost table for colspan section rows (#36993)
* Fix flaky table conversion for colspan section rows

The mikepenz flaky_summary HTML contains per-suite section-header rows
(<td colspan="2"><strong>...</strong></td>) that markdown tables cannot
represent. The sed pipeline only matched bare <td>/<th> with text-only
content, so those rows leaked raw HTML and broke the rendered table in
Mattermost.

Replace the sed/awk conversion with an inline python3 HTML parser that keeps
only rows matching the header column count (dropping the section-header
rows), unescapes HTML entities, escapes in-cell pipes, and emits a flat
markdown table.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

* Collapse whitespace in flaky table cells

str.strip() only trims leading/trailing whitespace, so an embedded newline
(including one decoded from an entity like &#10;) would remain and break the
single-line markdown table row. Collapse all internal whitespace to single
spaces when rendering each cell.

Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-11 10:20:27 -04:00

266 lines
10 KiB
YAML

# Server CI Report is invoked via workflow_call from server-ci.yml after test jobs finish.
name: Server CI Report
on:
workflow_call:
inputs:
source-run-id:
description: Server CI workflow run ID (for artifact download)
required: true
type: string
source-event:
description: github.event_name from the Server CI run
required: true
type: string
head-commit-id:
description: Commit ID for junit report annotations
required: true
type: string
head-repository:
description: head_repository.full_name from the Server CI run
required: true
type: string
workflow-url:
description: HTML URL of the Server CI workflow run
required: true
type: string
secrets:
WEBHOOK_URL_FLAKY_TEST:
required: false
WEBHOOK_AUTH_TOKEN_FLAKY_TEST:
required: false
WEBHOOK_URL_FLAKY_TEST_MM:
required: false
permissions: {}
jobs:
generate-report-matrix:
permissions:
actions: read
contents: read
if: inputs.head-repository == github.repository
runs-on: ubuntu-22.04
outputs:
REPORT_MATRIX: ${{ steps.report.outputs.REPORT_MATRIX }}
steps:
- name: report/download-artifacts-from-PR-workflow
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
run-id: ${{ inputs.source-run-id }}
github-token: ${{ github.token }}
pattern: "*-test-logs"
path: reports
- name: report/validate-and-prepare-data
id: validate
run: |
# Create validated data file
> /tmp/validated-tests.json
find "reports" -type f -name "test-name" | while read -r test_file; do
folder=$(basename "$(dirname "$test_file")")
test_name_raw=$(cat "$test_file" | tr -d '\n\r')
# Validate test name: allow alphanumeric, spaces, hyphens, underscores, parentheses, and dots
if [[ "$test_name_raw" =~ ^[a-zA-Z0-9\ \(\)_.-]+$ ]] && [[ ${#test_name_raw} -le 100 ]]; then
# Use jq to safely escape the test name as JSON
test_name_escaped=$(echo -n "$test_name_raw" | jq -R .)
echo "{\"artifact\": \"$folder\", \"name\": $test_name_escaped}" >> /tmp/validated-tests.json
else
echo "Warning: Skipping invalid test name in $test_file: '$test_name_raw'" >&2
fi
done
# Verify we have at least some valid tests
if [[ ! -s /tmp/validated-tests.json ]]; then
echo "Error: No valid test names found" >&2
exit 1
fi
- name: report/generate-report-matrix
id: report
run: |
# Convert validated JSON objects to matrix format
jq -s '{ "test": . }' /tmp/validated-tests.json | tee /tmp/report-matrix
echo REPORT_MATRIX=$(cat /tmp/report-matrix | jq --compact-output --monochrome-output) >> ${GITHUB_OUTPUT}
publish-report:
runs-on: ubuntu-22.04
name: Publish Report ${{ matrix.test.name }}
needs:
- generate-report-matrix
permissions:
actions: read
pull-requests: write
checks: write
strategy:
matrix: ${{ fromJson(needs.generate-report-matrix.outputs.REPORT_MATRIX) }}
steps:
- name: report/download-artifacts-from-PR-workflow
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
run-id: ${{ inputs.source-run-id }}
github-token: ${{ github.token }}
name: ${{ matrix.test.artifact }}
path: ${{ matrix.test.artifact }}
- name: report/fetch-pr-number
if: inputs.source-event == 'pull_request'
id: incoming-pr
env:
ARTIFACT: "${{ matrix.test.artifact }}"
run: |
if [[ -f "$ARTIFACT/pr-number" ]]; then
pr_number=$(cat "$ARTIFACT/pr-number" | tr -d '\n\r' | grep -E '^[0-9]+$')
if [[ -n "$pr_number" ]] && [[ ${#pr_number} -le 10 ]]; then
echo "NUMBER=$pr_number" >> ${GITHUB_OUTPUT}
else
echo "Invalid PR number format" >&2
exit 1
fi
else
echo "PR number file not found" >&2
exit 1
fi
- name: Publish test report
id: report
uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v6.3.1
with:
report_paths: ${{ matrix.test.artifact }}/report.xml
check_name: ${{ matrix.test.name }} (Results)
job_name: ${{ matrix.test.name }}
commit: ${{ inputs.head-commit-id }}
require_tests: true
check_retries: true
flaky_summary: true
include_passed: true
check_annotations: true
- name: Report retried tests to Mattermost channel (pull request)
if: >-
steps.report.outputs.flaky_summary != '<table><tr><th>Test</th><th>Retries</th></tr></table>'
&& steps.report.outputs.failed == '0'
&& inputs.source-event == 'pull_request'
&& env.WEBHOOK_URL_FLAKY_TEST_MM != ''
continue-on-error: true
env:
WEBHOOK_URL_FLAKY_TEST_MM: ${{ secrets.WEBHOOK_URL_FLAKY_TEST_MM }}
FLAKY_SUMMARY: ${{ steps.report.outputs.flaky_summary }}
PR_NUMBER: ${{ steps.incoming-pr.outputs.NUMBER }}
TEST_NAME: ${{ matrix.test.name }}
REPO: ${{ github.repository }}
WORKFLOW_RUN_HTML_URL: ${{ inputs.workflow-url }}
SERVER_URL: ${{ github.server_url }}
run: |
PR_URL="${SERVER_URL}/${REPO}/pull/${PR_NUMBER}"
# Convert the HTML <table> flaky summary into a Mattermost markdown table.
# The summary contains a header row plus, per suite, a section-header row
# (<td colspan="2"><strong>...</strong></td>) that markdown tables cannot
# represent. Parse the HTML, keep only rows matching the header's column
# count (dropping section-header rows), and emit a flat markdown table.
TABLE_MD=$(python3 - <<'PY'
import os, html
from html.parser import HTMLParser
class FlakyTableParser(HTMLParser):
def __init__(self):
super().__init__()
self.rows = []
self.row = None
self.cell = None
def handle_starttag(self, tag, attrs):
if tag == "tr":
self.row = []
elif tag in ("td", "th"):
self.cell = []
def handle_endtag(self, tag):
if tag in ("td", "th") and self.cell is not None:
self.row.append("".join(self.cell))
self.cell = None
elif tag == "tr" and self.row is not None:
self.rows.append(self.row)
self.row = None
def handle_data(self, data):
if self.cell is not None:
self.cell.append(data)
parser = FlakyTableParser()
parser.feed(os.environ.get("FLAKY_SUMMARY", ""))
rows = parser.rows
if rows:
width = len(rows[0])
# Keep header + data rows; drop colspan section-header rows.
rows = [r for r in rows if len(r) == width]
def cell(text):
# Collapse all whitespace (incl. newlines) so a cell stays on one
# line; otherwise an embedded newline would break the markdown row.
text = " ".join(html.unescape(text).split())
return text.replace("|", "\\|")
if len(rows) >= 2:
lines = ["| " + " | ".join(cell(c) for c in rows[0]) + " |",
"|" + "|".join(["---"] * width) + "|"]
lines += ["| " + " | ".join(cell(c) for c in r) + " |" for r in rows[1:]]
print("\n".join(lines))
PY
)
# Use real newlines; a literal "\n" renders verbatim in Mattermost.
NL=$'\n'
TEXT=":warning: **Flaky test(s) detected** in [${REPO}#${PR_NUMBER}](${PR_URL})"
TEXT="${TEXT}${NL}_Test job:_ [${TEST_NAME}](${WORKFLOW_RUN_HTML_URL})${NL}${NL}${TABLE_MD}"
PAYLOAD=$(jq -n \
--arg text "$TEXT" \
'{
username: "Flaky Test Report",
icon_url: "https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png",
attachments: [{color: "#CCCC00", text: $text}]
}')
curl -X POST -fsSL \
--connect-timeout 5 \
--max-time 30 \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$WEBHOOK_URL_FLAKY_TEST_MM"
- name: Report retried tests to flaky-test webhook (pull request)
if: >-
steps.report.outputs.flaky_summary != '<table><tr><th>Test</th><th>Retries</th></tr></table>'
&& steps.report.outputs.failed == '0'
&& inputs.source-event == 'pull_request'
&& env.WEBHOOK_URL_FLAKY_TEST != ''
&& env.WEBHOOK_AUTH_TOKEN_FLAKY_TEST != ''
continue-on-error: true
env:
WEBHOOK_URL_FLAKY_TEST: ${{ secrets.WEBHOOK_URL_FLAKY_TEST }}
WEBHOOK_AUTH_TOKEN_FLAKY_TEST: ${{ secrets.WEBHOOK_AUTH_TOKEN_FLAKY_TEST }}
FLAKY_SUMMARY: ${{ steps.report.outputs.flaky_summary }}
PR_NUMBER: ${{ steps.incoming-pr.outputs.NUMBER }}
REPO: ${{ github.repository }}
run: |
PAYLOAD=$(jq -n \
--arg repo "$REPO" \
--arg pr_number "$PR_NUMBER" \
--arg flaky_summary "$FLAKY_SUMMARY" \
'{repo:$repo, pr_number:$pr_number, flaky_summary:$flaky_summary}')
curl -X POST -fsSL \
--connect-timeout 5 \
--max-time 30 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $WEBHOOK_AUTH_TOKEN_FLAKY_TEST" \
-d "$PAYLOAD" \
"$WEBHOOK_URL_FLAKY_TEST"