mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-27 05:37:15 -05:00
* 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 ) 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>
266 lines
10 KiB
YAML
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"
|