summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKirill Rysin <[email protected]>2026-01-15 12:53:10 +0100
committerGitHub <[email protected]>2026-01-15 14:53:10 +0300
commit4e81309fbd7720ab7a080ad1de9e49af2fcec2e0 (patch)
tree13443e19566d8f723f16bac3218c43e19d5bafc9
parent0d21c6608dd7238557a88bd4ab1d8be6d143d34a (diff)
Add scripts for collecting and saving test summaries as artifacts (#32063)
-rw-r--r--.github/scripts/tests/collect_combined_summary.py574
-rw-r--r--.github/scripts/tests/save_summary_artifact.sh31
-rw-r--r--.github/workflows/collect_combined_summary.yml29
-rw-r--r--.github/workflows/run_and_debug_tests.yml115
4 files changed, 749 insertions, 0 deletions
diff --git a/.github/scripts/tests/collect_combined_summary.py b/.github/scripts/tests/collect_combined_summary.py
new file mode 100644
index 00000000000..f4ea6b35506
--- /dev/null
+++ b/.github/scripts/tests/collect_combined_summary.py
@@ -0,0 +1,574 @@
+#!/usr/bin/env python3
+"""
+Collect step summaries from all matrix jobs and generate a combined markdown table.
+
+This script:
+1. Collects GITHUB_STEP_SUMMARY files from all matrix jobs (via artifacts)
+2. Parses test statistics from each summary
+3. Generates a combined markdown table with branches as rows and build_presets as columns
+4. Writes the combined summary to GITHUB_STEP_SUMMARY
+"""
+import argparse
+import json
+import os
+import re
+import sys
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+
+
+def parse_all_attempts(summary_text: str) -> List[Dict]:
+ """
+ Parse all attempt tables from markdown summary.
+
+ Returns: list of dicts, each containing stats and URL for one attempt (try_1, try_2, try_3, etc.)
+ """
+ if not summary_text:
+ return []
+
+ # Pattern to match markdown table with test statistics
+ pattern = r'\|\s*TESTS\s*\|\s*PASSED\s*\|\s*ERRORS\s*\|\s*FAILED\s*\|\s*SKIPPED\s*\|\s*MUTED'
+ url_pattern = r'https://storage\.yandexcloud\.net/[^\s\)]+ya-test\.html[^\s\)]*'
+
+ lines = summary_text.split('\n')
+
+ # Find all table headers
+ table_starts = []
+ for i, line in enumerate(lines):
+ if re.search(pattern, line, re.IGNORECASE):
+ table_starts.append(i)
+
+ if not table_starts:
+ return []
+
+ attempts = []
+
+ def extract_number(s):
+ match = re.search(r'\[?(\d+)\]?', s)
+ return int(match.group(1)) if match else 0
+
+ # Parse each table
+ for table_idx, start_idx in enumerate(table_starts):
+ attempt_num = table_idx + 1
+ stats = {
+ 'tests': 0,
+ 'passed': 0,
+ 'errors': 0,
+ 'failed': 0,
+ 'skipped': 0,
+ 'muted': 0
+ }
+ attempt_url = None
+
+ # Find data row for this table
+ i = start_idx + 1
+ while i < len(lines):
+ line = lines[i].strip()
+ if '---' in line:
+ i += 1
+ continue
+ if line.startswith('|') and line.count('|') >= 6:
+ parts = [p.strip() for p in line.split('|')]
+ parts = [p for p in parts if p]
+
+ if len(parts) >= 6:
+ try:
+ stats['tests'] = extract_number(parts[0])
+ stats['passed'] = extract_number(parts[1])
+ stats['errors'] = extract_number(parts[2])
+ stats['failed'] = extract_number(parts[3])
+ stats['skipped'] = extract_number(parts[4])
+ stats['muted'] = extract_number(parts[5])
+
+ # Extract URL from FAILED or TESTS column
+ failed_cell = parts[3] if len(parts) > 3 else ''
+ tests_cell = parts[0] if len(parts) > 0 else ''
+
+ match = re.search(url_pattern, failed_cell)
+ if match:
+ attempt_url = match.group(0)
+ else:
+ match = re.search(url_pattern, tests_cell)
+ if match:
+ attempt_url = match.group(0)
+
+ break
+ except (ValueError, IndexError):
+ pass
+ # Check if next line is a new table header
+ if i + 1 < len(lines) and re.search(pattern, lines[i + 1], re.IGNORECASE):
+ break
+ i += 1
+
+ if stats['tests'] > 0 or stats['failed'] > 0 or stats['errors'] > 0 or attempt_url:
+ attempts.append({
+ 'attempt': attempt_num,
+ 'stats': stats,
+ 'url': attempt_url
+ })
+
+ return attempts
+
+
+def parse_summary_markdown(summary_text: str) -> Optional[Dict]:
+ """
+ Parse test statistics from markdown summary.
+
+ Looks for markdown table format:
+ | TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED |
+
+ There can be multiple tables (for retries: try_1, try_2, try_3).
+ We only take the LAST table (last attempt) for combined summary.
+
+ Returns: dict with test statistics from last attempt or None if not found
+ """
+ if not summary_text:
+ return None
+
+ # Pattern to match markdown table with test statistics
+ pattern = r'\|\s*TESTS\s*\|\s*PASSED\s*\|\s*ERRORS\s*\|\s*FAILED\s*\|\s*SKIPPED\s*\|\s*MUTED'
+
+ lines = summary_text.split('\n')
+
+ # Find all table headers
+ table_starts = []
+ for i, line in enumerate(lines):
+ if re.search(pattern, line, re.IGNORECASE):
+ table_starts.append(i)
+
+ if not table_starts:
+ return None
+
+ # Use the LAST table (last attempt)
+ start_idx = table_starts[-1]
+
+ stats = {
+ 'tests': 0,
+ 'passed': 0,
+ 'errors': 0,
+ 'failed': 0,
+ 'skipped': 0,
+ 'muted': 0
+ }
+
+ def extract_number(s):
+ # Handle format: [624](url) or just 624
+ match = re.search(r'\[?(\d+)\]?', s)
+ return int(match.group(1)) if match else 0
+
+ # Look for data row after the header (only from last table)
+ i = start_idx + 1
+ while i < len(lines):
+ line = lines[i].strip()
+ # Skip separator line
+ if '---' in line:
+ i += 1
+ continue
+ # Check if this is a data row
+ if line.startswith('|') and line.count('|') >= 6:
+ # Parse the row - this is the data from last attempt
+ parts = [p.strip() for p in line.split('|')]
+ parts = [p for p in parts if p] # Remove empty
+
+ if len(parts) >= 6:
+ try:
+ # Take values from last attempt only (don't sum)
+ stats['tests'] = extract_number(parts[0])
+ stats['passed'] = extract_number(parts[1])
+ stats['errors'] = extract_number(parts[2])
+ stats['failed'] = extract_number(parts[3])
+ stats['skipped'] = extract_number(parts[4])
+ stats['muted'] = extract_number(parts[5])
+ # Found the data row, break
+ break
+ except (ValueError, IndexError):
+ pass
+ # Check if next line is a new table header (shouldn't happen for last table, but just in case)
+ if i + 1 < len(lines) and re.search(pattern, lines[i + 1], re.IGNORECASE):
+ # This shouldn't happen since we're using last table, but break anyway
+ break
+ i += 1
+
+ # Only return stats if we found data
+ if stats['tests'] > 0 or stats['failed'] > 0 or stats['errors'] > 0:
+ return stats
+
+ return None
+
+
+def extract_test_report_url(summary_text: str) -> Optional[str]:
+ """
+ Extract test report URL from summary.
+ Takes URL from the LAST attempt (last table) - from FAILED column if available, otherwise from TESTS column.
+ """
+ if not summary_text:
+ return None
+
+ # Pattern to match markdown table with test statistics
+ table_pattern = r'\|\s*TESTS\s*\|\s*PASSED\s*\|\s*ERRORS\s*\|\s*FAILED\s*\|\s*SKIPPED\s*\|\s*MUTED'
+ lines = summary_text.split('\n')
+
+ # Find all table headers
+ table_starts = []
+ for i, line in enumerate(lines):
+ if re.search(table_pattern, line, re.IGNORECASE):
+ table_starts.append(i)
+
+ if not table_starts:
+ return None
+
+ # Use the LAST table (last attempt)
+ start_idx = table_starts[-1]
+
+ # Pattern for storage.yandexcloud.net URLs with ya-test.html
+ url_pattern = r'https://storage\.yandexcloud\.net/[^\s\)]+ya-test\.html[^\s\)]*'
+
+ # Look for data row after the header and extract URL from FAILED column (preferred) or TESTS column
+ i = start_idx + 1
+ while i < len(lines):
+ line = lines[i].strip()
+ # Skip separator line
+ if '---' in line:
+ i += 1
+ continue
+ # Check if this is a data row
+ if line.startswith('|') and line.count('|') >= 6:
+ # Parse the row
+ parts = [p.strip() for p in line.split('|')]
+ parts = [p for p in parts if p] # Remove empty
+
+ if len(parts) >= 6:
+ # Try FAILED column first (column index 3, but parts[0] is TESTS, parts[1] is PASSED, etc.)
+ # Actually, parts array: [TESTS, PASSED, ERRORS, FAILED, SKIPPED, MUTED]
+ # So FAILED is at index 3
+ failed_cell = parts[3] if len(parts) > 3 else ''
+ tests_cell = parts[0] if len(parts) > 0 else ''
+
+ # Try to extract URL from FAILED column first
+ match = re.search(url_pattern, failed_cell)
+ if match:
+ return match.group(0)
+
+ # Fallback to TESTS column
+ match = re.search(url_pattern, tests_cell)
+ if match:
+ return match.group(0)
+
+ # Found the data row, break
+ break
+ i += 1
+
+ # Fallback: search in entire summary (but prefer last attempt)
+ # Search backwards from end to find last URL
+ all_matches = list(re.finditer(url_pattern, summary_text))
+ if all_matches:
+ return all_matches[-1].group(0)
+
+ return None
+
+
+def extract_job_info_from_name(job_name: str) -> Tuple[Optional[str], Optional[str]]:
+ """
+ Extract branch and build_preset from job name.
+
+ Format: "Regression-run_... (build_preset) / branch:build_preset"
+ Returns: (branch, build_preset)
+ """
+ branch = None
+ build_preset = None
+
+ if ':' in job_name:
+ # Format: "branch:build_preset"
+ parts = job_name.split(':')
+ if len(parts) >= 2:
+ branch = parts[0].strip()
+ build_preset = parts[1].strip()
+ elif ' / ' in job_name:
+ # Format: "Regression-run_... (build_preset) / branch:build_preset"
+ parts = job_name.split(' / ')
+ if len(parts) >= 2:
+ branch_preset = parts[-1].strip()
+ if ':' in branch_preset:
+ branch, build_preset = branch_preset.split(':', 1)
+ branch = branch.strip()
+ build_preset = build_preset.strip()
+
+ return branch, build_preset
+
+
+def collect_summaries_from_artifacts(artifacts_dir: str) -> Dict[str, Dict]:
+ """
+ Collect summaries from artifact files.
+
+ Artifacts are downloaded with pattern "job-summary-*" and contain files like:
+ "summary-{branch}:{build_preset}.md"
+
+ Returns: dict mapping "branch:build_preset" to summary data
+ """
+ summaries = {}
+ artifacts_path = Path(artifacts_dir)
+
+ if not artifacts_path.exists():
+ print(f"Warning: Artifacts directory {artifacts_dir} does not exist", file=sys.stderr)
+ return summaries
+
+ # Look for summary files - they might be in subdirectories (one per artifact)
+ # Files can be named: "summary-*.md" or "{branch}:{build_preset} summary"
+ summary_files = list(artifacts_path.rglob("summary-*.md"))
+ # Also look for files ending with " summary" (without .md extension)
+ summary_files.extend(artifacts_path.rglob("* summary"))
+
+ for summary_file in summary_files:
+ try:
+ with open(summary_file, 'r', encoding='utf-8') as f:
+ summary_text = f.read()
+
+ # Extract job name from filename
+ # Format: "summary-{branch}:{build_preset}.md" or " {branch}:{build_preset} summary"
+ filename = summary_file.name.strip() # Remove leading/trailing whitespace
+
+ # Handle format: "summary-{branch}:{build_preset}.md"
+ if filename.startswith('summary-') and filename.endswith('.md'):
+ job_name = filename[8:-3].strip() # Remove "summary-" prefix and ".md" suffix
+ # Handle format: "{branch}:{build_preset} summary" (without .md extension)
+ elif filename.endswith(' summary'):
+ job_name = filename[:-8].strip() # Remove " summary" suffix
+ else:
+ job_name = summary_file.stem.replace('summary-', '').strip()
+
+ branch, build_preset = extract_job_info_from_name(job_name)
+
+ if not branch or not build_preset:
+ # Try to extract from filename directly
+ if ':' in job_name:
+ parts = job_name.split(':', 1)
+ branch = parts[0].strip()
+ build_preset = parts[1].strip()
+
+ if not branch or not build_preset:
+ print(f"Warning: Could not extract branch/build_preset from {job_name} (file: {summary_file})", file=sys.stderr)
+ continue
+
+ key = f"{branch}:{build_preset}"
+
+ # Parse statistics from last attempt
+ stats = parse_summary_markdown(summary_text)
+ test_report_url = extract_test_report_url(summary_text)
+
+ # Parse all attempts for detailed table
+ all_attempts = parse_all_attempts(summary_text)
+
+ summaries[key] = {
+ 'branch': branch,
+ 'build_preset': build_preset,
+ 'job_name': job_name,
+ 'stats': stats or {},
+ 'test_report_url': test_report_url,
+ 'all_attempts': all_attempts, # List of all attempts with stats and URLs
+ }
+ print(f"Collected summary for {key}", file=sys.stderr)
+
+ except Exception as e:
+ print(f"Warning: Error processing {summary_file}: {e}", file=sys.stderr)
+ continue
+
+ return summaries
+
+
+def generate_combined_markdown_table(summaries: Dict[str, Dict]) -> str:
+ """
+ Generate combined markdown table.
+
+ Rows: branches
+ Columns: build_presets
+ Cells: error counts with links (always show link, even if no errors)
+ Plus detailed table with all attempts below.
+ """
+ if not summaries:
+ return "No job summaries available.\n"
+
+ # Collect all branches and build_presets
+ branches = sorted(set(s['branch'] for s in summaries.values()))
+ presets = sorted(set(s['build_preset'] for s in summaries.values()))
+
+ if not branches or not presets:
+ return "No valid job summaries found.\n"
+
+ lines = []
+ lines.append("## 📊 Combined Test Summary\n")
+ lines.append("| Branch | " + " | ".join(presets) + " |")
+ lines.append("|" + "---|" * (len(presets) + 1))
+
+ for branch in branches:
+ row = [branch]
+ for preset in presets:
+ key = f"{branch}:{preset}"
+ if key in summaries:
+ summary = summaries[key]
+ stats = summary.get('stats', {})
+ failed = stats.get('failed', 0)
+ errors = stats.get('errors', 0)
+ total_errors = failed + errors
+ test_report_url = summary.get('test_report_url', '')
+
+ if test_report_url:
+ if total_errors > 0:
+ cell = f"[{total_errors}]({test_report_url})"
+ if failed > 0 and errors > 0:
+ cell += f" ({failed} failed, {errors} errors)"
+ elif failed > 0:
+ cell += f" ({failed} failed)"
+ else:
+ cell += f" ({errors} errors)"
+ else:
+ # No errors, but still show link
+ cell = f"[✓]({test_report_url})"
+ else:
+ # No URL available
+ if total_errors > 0:
+ cell = str(total_errors)
+ if failed > 0 and errors > 0:
+ cell += f" ({failed} failed, {errors} errors)"
+ elif failed > 0:
+ cell += f" ({failed} failed)"
+ else:
+ cell += f" ({errors} errors)"
+ else:
+ cell = "✓"
+ row.append(cell)
+ else:
+ row.append("—")
+
+ lines.append("| " + " | ".join(row) + " |")
+
+ # Add detailed table with all attempts
+ # Same structure as main table: branches as rows, build_presets as columns
+ lines.append("\n### Detailed Results (All Attempts)\n")
+ lines.append("| Branch | " + " | ".join(presets) + " |")
+ lines.append("|" + "---|" * (len(presets) + 1))
+
+ for branch in branches:
+ row = [branch]
+ for preset in presets:
+ key = f"{branch}:{preset}"
+ if key in summaries:
+ summary = summaries[key]
+ all_attempts = summary.get('all_attempts', [])
+
+ if all_attempts:
+ # Format all attempts in this cell
+ attempt_parts = []
+ for attempt in all_attempts:
+ attempt_num = attempt.get('attempt', 0)
+ attempt_stats = attempt.get('stats', {})
+ failed = attempt_stats.get('failed', 0)
+ errors = attempt_stats.get('errors', 0)
+ attempt_url = attempt.get('url', '')
+
+ total_errors = failed + errors
+
+ if attempt_url:
+ if total_errors > 0:
+ attempt_text = f"try_{attempt_num}: [{total_errors}]({attempt_url})"
+ if failed > 0 and errors > 0:
+ attempt_text += f" ({failed}f, {errors}e)"
+ elif failed > 0:
+ attempt_text += f" ({failed}f)"
+ else:
+ attempt_text += f" ({errors}e)"
+ else:
+ attempt_text = f"try_{attempt_num}: [✓]({attempt_url})"
+ else:
+ if total_errors > 0:
+ attempt_text = f"try_{attempt_num}: {total_errors}"
+ if failed > 0 and errors > 0:
+ attempt_text += f" ({failed}f, {errors}e)"
+ elif failed > 0:
+ attempt_text += f" ({failed}f)"
+ else:
+ attempt_text += f" ({errors}e)"
+ else:
+ attempt_text = f"try_{attempt_num}: ✓"
+
+ attempt_parts.append(attempt_text)
+
+ cell = "<br>".join(attempt_parts)
+ else:
+ # Fallback if all_attempts not parsed
+ stats = summary.get('stats', {})
+ failed = stats.get('failed', 0)
+ errors = stats.get('errors', 0)
+ test_report_url = summary.get('test_report_url', '')
+
+ total_errors = failed + errors
+ if test_report_url:
+ if total_errors > 0:
+ cell = f"last: [{total_errors}]({test_report_url})"
+ if failed > 0 and errors > 0:
+ cell += f" ({failed}f, {errors}e)"
+ elif failed > 0:
+ cell += f" ({failed}f)"
+ else:
+ cell += f" ({errors}e)"
+ else:
+ cell = f"last: [✓]({test_report_url})"
+ else:
+ if total_errors > 0:
+ cell = f"last: {total_errors}"
+ if failed > 0 and errors > 0:
+ cell += f" ({failed}f, {errors}e)"
+ elif failed > 0:
+ cell += f" ({failed}f)"
+ else:
+ cell += f" ({errors}e)"
+ else:
+ cell = "last: ✓"
+
+ row.append(cell)
+ else:
+ row.append("—")
+
+ lines.append("| " + " | ".join(row) + " |")
+
+ return "\n".join(lines) + "\n"
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Collect step summaries from matrix jobs and generate combined markdown table'
+ )
+ parser.add_argument(
+ '--artifacts-dir',
+ required=True,
+ help='Directory containing summary artifacts from matrix jobs'
+ )
+ parser.add_argument(
+ '--output',
+ help='Output file for combined summary (default: GITHUB_STEP_SUMMARY)'
+ )
+
+ args = parser.parse_args()
+
+ # Collect summaries
+ print("Collecting summaries from artifacts...", file=sys.stderr)
+ summaries = collect_summaries_from_artifacts(args.artifacts_dir)
+ print(f"Found {len(summaries)} job summaries", file=sys.stderr)
+
+ # Generate combined markdown table
+ combined_markdown = generate_combined_markdown_table(summaries)
+
+ # Write output
+ output_file = args.output or os.environ.get('GITHUB_STEP_SUMMARY')
+ if output_file:
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(combined_markdown)
+ print(f"Combined summary written to {output_file}", file=sys.stderr)
+ else:
+ # Write to stdout if no output file specified
+ print(combined_markdown)
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.github/scripts/tests/save_summary_artifact.sh b/.github/scripts/tests/save_summary_artifact.sh
new file mode 100644
index 00000000000..6305f0c1c75
--- /dev/null
+++ b/.github/scripts/tests/save_summary_artifact.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Save GITHUB_STEP_SUMMARY to artifact for later collection
+
+set -e
+
+# Get job name from environment or construct it
+JOB_NAME="${GITHUB_JOB:-unknown}"
+# If job name contains branch:build_preset format, use it
+# Otherwise try to construct from matrix variables
+if [[ "$JOB_NAME" == *":"* ]]; then
+ SUMMARY_NAME="summary-${JOB_NAME}.md"
+else
+ # Try to get from matrix context
+ BRANCH="${MATRIX_BRANCH:-${GITHUB_REF_NAME:-unknown}}"
+ BUILD_PRESET="${MATRIX_BUILD_PRESET:-${INPUTS_BUILD_PRESET:-unknown}}"
+ SUMMARY_NAME="summary-${BRANCH}:${BUILD_PRESET}.md"
+fi
+
+SUMMARY_DIR="${GITHUB_WORKSPACE}/summary_artifacts"
+mkdir -p "$SUMMARY_DIR"
+
+SUMMARY_FILE="${SUMMARY_DIR}/${SUMMARY_NAME}"
+
+if [ -f "$GITHUB_STEP_SUMMARY" ]; then
+ cp "$GITHUB_STEP_SUMMARY" "$SUMMARY_FILE"
+ echo "Saved summary to $SUMMARY_FILE"
+else
+ echo "Warning: GITHUB_STEP_SUMMARY not found"
+ # Create empty file so artifact upload doesn't fail
+ touch "$SUMMARY_FILE"
+fi
diff --git a/.github/workflows/collect_combined_summary.yml b/.github/workflows/collect_combined_summary.yml
new file mode 100644
index 00000000000..fd83c614b5c
--- /dev/null
+++ b/.github/workflows/collect_combined_summary.yml
@@ -0,0 +1,29 @@
+name: Collect Combined Summary
+
+on:
+ workflow_call:
+
+jobs:
+ collect_combined_summary:
+ name: Collect Combined Summary
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Download all summary artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: job-summary-*
+ merge-multiple: true
+ path: summary_artifacts
+
+ - name: Clear old summary and add combined
+ run: |
+ # Clear existing summary
+ > $GITHUB_STEP_SUMMARY
+ # Add combined summary
+ python3 .github/scripts/tests/collect_combined_summary.py \
+ --artifacts-dir summary_artifacts \
+ --output $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/run_and_debug_tests.yml b/.github/workflows/run_and_debug_tests.yml
new file mode 100644
index 00000000000..4e4e54799d9
--- /dev/null
+++ b/.github/workflows/run_and_debug_tests.yml
@@ -0,0 +1,115 @@
+name: Run and debug tests
+
+on:
+ workflow_dispatch:
+ inputs:
+ test_targets:
+ description: 'Paths to tests (e.g., "ydb/tests/olap/" or "ydb/tests/olap/, ydb/tests/functional/")'
+ required: false
+ type: string
+ default: 'ydb/tests/olap/'
+ build_preset:
+ description: 'Build preset types (comma-separated: "relwithdebinfo, release-asan" or single: "relwithdebinfo")'
+ required: false
+ type: string
+ default: 'relwithdebinfo, release-asan'
+ branches:
+ description: 'Branches to test (comma-separated: "main, stable-25-3" or JSON array: ["main", "stable-25-3"], empty = use branches_config_path)'
+ required: false
+ type: string
+ default: ''
+ use_branches_config:
+ description: 'If true, use branches from .github/config/stable_tests_branches.json'
+ required: false
+ type: boolean
+ default: false
+
+jobs:
+ prepare:
+ runs-on: ubuntu-latest
+ outputs:
+ branches_json: ${{ steps.format-branches.outputs.branches_json }}
+ build_preset_array: ${{ steps.format-build-preset.outputs.build_preset_array }}
+ steps:
+ - name: Format branches
+ id: format-branches
+ run: |
+ if [ -z "${{ inputs.branches }}" ]; then
+ # Empty - will use branches_config_path or default
+ echo "branches_json=" >> $GITHUB_OUTPUT
+ elif [[ "${{ inputs.branches }}" == \[* ]]; then
+ # Already JSON array
+ echo "branches_json=${{ inputs.branches }}" >> $GITHUB_OUTPUT
+ else
+ # Comma-separated format: "main, stable-25-3" -> ["main", "stable-25-3"]
+ IFS=',' read -ra BRANCHES <<< "${{ inputs.branches }}"
+ JSON_BRANCHES="["
+ FIRST=true
+ for branch in "${BRANCHES[@]}"; do
+ branch=$(echo "$branch" | xargs) # trim whitespace
+ if [ -n "$branch" ]; then
+ if [ "$FIRST" = true ]; then
+ FIRST=false
+ else
+ JSON_BRANCHES+=", "
+ fi
+ JSON_BRANCHES+="\"$branch\""
+ fi
+ done
+ JSON_BRANCHES+="]"
+ echo "branches_json=$JSON_BRANCHES" >> $GITHUB_OUTPUT
+ echo "Converted '${{ inputs.branches }}' to $JSON_BRANCHES"
+ fi
+
+ - name: Format build presets
+ id: format-build-preset
+ run: |
+ BUILD_PRESET_INPUT="${{ inputs.build_preset }}"
+ if [ -z "$BUILD_PRESET_INPUT" ]; then
+ BUILD_PRESET_INPUT="relwithdebinfo"
+ fi
+
+ # Check if already JSON array
+ if [[ "$BUILD_PRESET_INPUT" == \[* ]]; then
+ echo "build_preset_array=$BUILD_PRESET_INPUT" >> $GITHUB_OUTPUT
+ else
+ # Comma-separated format: "relwithdebinfo, release-asan" -> ["relwithdebinfo", "release-asan"]
+ IFS=',' read -ra PRESETS <<< "$BUILD_PRESET_INPUT"
+ JSON_PRESETS="["
+ FIRST=true
+ for preset in "${PRESETS[@]}"; do
+ preset=$(echo "$preset" | xargs) # trim whitespace
+ if [ -n "$preset" ]; then
+ if [ "$FIRST" = true ]; then
+ FIRST=false
+ else
+ JSON_PRESETS+=", "
+ fi
+ JSON_PRESETS+="\"$preset\""
+ fi
+ done
+ JSON_PRESETS+="]"
+ echo "build_preset_array=$JSON_PRESETS" >> $GITHUB_OUTPUT
+ echo "Converted '$BUILD_PRESET_INPUT' to $JSON_PRESETS"
+ fi
+
+ main:
+ name: Run and debug tests
+ needs: prepare
+ uses: ./.github/workflows/run_tests.yml
+ secrets: inherit
+ strategy:
+ fail-fast: false
+ matrix:
+ build_preset: ${{ fromJson(needs.prepare.outputs.build_preset_array) }}
+ with:
+ test_targets: ${{ inputs.test_targets }}
+ test_size: small,medium
+ build_preset: ${{ matrix.build_preset }}
+ branches: ${{ needs.prepare.outputs.branches_json || '' }}
+ branches_config_path: ${{ inputs.use_branches_config && '.github/config/stable_tests_branches.json' || '' }}
+
+ collect_combined_summary:
+ needs: main
+ if: always()
+ uses: ./.github/workflows/collect_combined_summary.yml