This is an automated email from the ASF dual-hosted git repository.

zhangduo pushed a commit to branch branch-3
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-3 by this push:
     new 4896afaf14c HBASE-29866 Generate job summary for our yetus check 
github action (#7699)
4896afaf14c is described below

commit 4896afaf14c71ab02636749fe4bd739ddcaa1d12
Author: Duo Zhang <[email protected]>
AuthorDate: Mon Feb 2 22:42:55 2026 +0800

    HBASE-29866 Generate job summary for our yetus check github action (#7699)
    
    Signed-off-by: Nick Dimiduk <[email protected]>
    (cherry picked from commit aa97fbe7a27e2c68355549a01328ae021d0bec82)
---
 .github/workflows/yetus-general-check.yml |   6 +
 dev-support/yetus_console_to_md.py        | 275 ++++++++++++++++++++++++++++++
 2 files changed, 281 insertions(+)

diff --git a/.github/workflows/yetus-general-check.yml 
b/.github/workflows/yetus-general-check.yml
index 6a3ee831c3d..f7768503670 100644
--- a/.github/workflows/yetus-general-check.yml
+++ b/.github/workflows/yetus-general-check.yml
@@ -93,6 +93,12 @@ jobs:
           cd "${{ github.workspace }}"
           bash src/dev-support/jenkins_precommit_github_yetus.sh
 
+      - name: Publish Job Summary
+        if: always()
+        run: |
+          cd "${{ github.workspace }}"
+          python3 src/dev-support/yetus_console_to_md.py 
yetus-general-check/output/console.txt >> $GITHUB_STEP_SUMMARY
+
       - name: Publish Test Results
         if: always()
         uses: actions/upload-artifact@v4
diff --git a/dev-support/yetus_console_to_md.py 
b/dev-support/yetus_console_to_md.py
new file mode 100644
index 00000000000..3f32995e545
--- /dev/null
+++ b/dev-support/yetus_console_to_md.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+##
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Convert Apache Yetus console output to Markdown format.
+"""
+import re
+import sys
+from pathlib import Path
+from typing import List, Tuple
+
+
+# Vote to emoji mapping
+VOTE_EMOJI = {
+    '+1': '✅',
+    '-1': '❌',
+    '0': '🆗',
+    '+0': '🆗',
+    '-0': '⚠️'
+}
+
+
+def convert_vote(vote: str) -> str:
+    """Convert vote string to emoji."""
+    return VOTE_EMOJI.get(vote, vote)
+
+
+def is_runtime(text: str) -> bool:
+    """Check if text is a runtime like '41m 24s'."""
+    return bool(re.match(r'^\d+m\s+\d+s$', text))
+
+
+def parse_table_row(line: str) -> List[str]:
+    """
+    Parse a table row and return list of cell values.
+    Returns exactly 4 columns: [vote, subsystem, runtime, comment]
+    """
+    parts = line.split('|')
+    # Remove first empty element (from leading |)
+    parts = parts[1:] if len(parts) > 1 else []
+
+    result = []
+    for p in parts[:4]:  # Take first 4 columns
+        result.append(p.strip())
+
+    # Pad to 4 columns if needed
+    while len(result) < 4:
+        result.append('')
+
+    return result
+
+
+def process_first_table(lines: List[str], start_idx: int) -> Tuple[List[str], 
int]:
+    """
+    Process the first table (Vote, Subsystem, Runtime, Comment).
+
+    Returns:
+        Tuple of (markdown lines, next index to process)
+    """
+    content = []
+    i = start_idx
+
+    # Add table header
+    content.append('\n')
+    content.append('| Vote | Subsystem | Runtime | Comment |\n')
+    content.append('|------|-----------|---------|---------|\n')
+
+    # Skip the original separator line
+    if i < len(lines) and '===' in lines[i]:
+        i += 1
+
+    while i < len(lines):
+        line = lines[i]
+        stripped = line.strip()
+
+        # Check for second table start
+        if '|| Subsystem || Report/Notes ||' in line:
+            break
+
+        # Skip section separator lines (like +-----------)
+        if stripped.startswith('+--'):
+            i += 1
+            continue
+
+        # Process table rows
+        if stripped.startswith('|'):
+            parts = parse_table_row(line)
+            vote, subsystem, runtime, comment = parts[0], parts[1], parts[2], 
parts[3]
+
+            # Case 1: Section header (vote and subsystem are empty, has 
comment)
+            if not vote and not subsystem:
+                if comment:
+                    content.append(f'|  |  |  | {comment} |\n')
+                    i += 1
+                    continue
+                # If there's only runtime, it's a total time row
+                elif runtime and is_runtime(runtime):
+                    content.append(f'|  |  | {runtime} |  |\n')
+                    i += 1
+                    continue
+                else:
+                    # Empty row, skip
+                    i += 1
+                    continue
+
+            # Case 2: Data row with vote
+            if vote in VOTE_EMOJI:
+                vote_emoji = convert_vote(vote)
+                comment_parts = [comment] if comment else []
+
+                # Check for continuation lines
+                i += 1
+                while i < len(lines):
+                    next_line = lines[i]
+                    next_stripped = next_line.strip()
+
+                    if not next_stripped.startswith('|'):
+                        break
+
+                    # Check for second table start
+                    if '|| Subsystem || Report/Notes ||' in next_line:
+                        break
+
+                    next_parts = parse_table_row(next_line)
+                    next_vote, next_subsystem, next_runtime, next_comment = 
next_parts[0], next_parts[1], next_parts[2], next_parts[3]
+
+                    # Stop at new data row
+                    if next_vote in VOTE_EMOJI:
+                        break
+
+                    # If vote and subsystem are empty, check if it's a 
continuation
+                    if not next_vote and not next_subsystem:
+                        # If there's a comment, it's a continuation
+                        if next_comment:
+                            comment_parts.append(next_comment)
+                            i += 1
+                        # If there's only runtime, it's a standalone total 
time row
+                        elif next_runtime and is_runtime(next_runtime):
+                            break
+                        else:
+                            i += 1
+                    else:
+                        break
+
+                comment_text = ' '.join(comment_parts)
+                content.append(f'| {vote_emoji} | {subsystem} | {runtime} | 
{comment_text} |\n')
+                continue
+
+            # Case 3: Other cases, skip
+            i += 1
+            continue
+
+        i += 1
+
+    return content, i
+
+
+def process_second_table(lines: List[str], start_idx: int) -> Tuple[List[str], 
int]:
+    """
+    Process the second table (Subsystem, Report/Notes).
+
+    Returns:
+        Tuple of (markdown lines, next index to process)
+    """
+    content = []
+    i = start_idx
+
+    # Add table header
+    content.append('\n## Subsystem Reports\n\n')
+    content.append('| Subsystem | Report/Notes |\n')
+    content.append('|-----------|------------|\n')
+
+    # Skip the original separator line
+    if i < len(lines) and '===' in lines[i]:
+        i += 1
+
+    while i < len(lines):
+        line = lines[i]
+        stripped = line.strip()
+
+        if not stripped.startswith('|'):
+            break
+
+        # Split by | and get non-empty parts (at least 2)
+        parts = [p.strip() for p in stripped.split('|') if p.strip()]
+        if len(parts) >= 2:
+            content.append(f'| {parts[0]} | {parts[1]} |\n')
+
+        i += 1
+
+    return content, i
+
+
+def convert_console_to_markdown(input_file: str, output_file: str | None = 
None) -> str:
+    """Convert console to Markdown format."""
+    with open(input_file, 'r') as f:
+        lines = f.readlines()
+
+    content = []
+    i = 0
+
+    while i < len(lines):
+        line = lines[i]
+        stripped = line.strip()
+
+        # Handle overall line
+        if stripped == '-1 overall':
+            content.append(f'<h1><b>❌ {stripped}</b></h1>\n')
+            i += 1
+            continue
+
+        if stripped == '+1 overall':
+            content.append(f'<h1><b>✅ {stripped}</b></h1>\n')
+            i += 1
+            continue
+
+        # Detect first table start
+        if '| Vote |' in line and 'Subsystem' in line:
+            table_content, i = process_first_table(lines, i + 1)
+            content.extend(table_content)
+            continue
+
+        # Detect second table start
+        if '|| Subsystem || Report/Notes ||' in line:
+            table_content, i = process_second_table(lines, i + 1)
+            content.extend(table_content)
+            continue
+
+        i += 1
+
+    result = ''.join(content)
+
+    if output_file:
+        with open(output_file, 'w') as f:
+            f.write(result)
+        print(f'Converted {input_file} to {output_file}', file=sys.stderr)
+    else:
+        print(result, end='')
+
+    return result
+
+
+def main():
+    if len(sys.argv) < 2:
+        print(f'Usage: {sys.argv[0]} <input_file> [output_file]', 
file=sys.stderr)
+        print(f'  If output_file is not provided, output goes to stdout', 
file=sys.stderr)
+        sys.exit(1)
+
+    input_file = sys.argv[1]
+    output_file = sys.argv[2] if len(sys.argv) > 2 else None
+
+    if not Path(input_file).exists():
+        print(f'Error: Input file "{input_file}" does not exist', 
file=sys.stderr)
+        sys.exit(1)
+
+    convert_console_to_markdown(input_file, output_file)
+
+
+if __name__ == '__main__':
+    main()

Reply via email to