diff options
| author | Kirill Rysin <[email protected]> | 2026-01-15 12:53:10 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-15 14:53:10 +0300 |
| commit | 4e81309fbd7720ab7a080ad1de9e49af2fcec2e0 (patch) | |
| tree | 13443e19566d8f723f16bac3218c43e19d5bafc9 | |
| parent | 0d21c6608dd7238557a88bd4ab1d8be6d143d34a (diff) | |
Add scripts for collecting and saving test summaries as artifacts (#32063)
| -rw-r--r-- | .github/scripts/tests/collect_combined_summary.py | 574 | ||||
| -rw-r--r-- | .github/scripts/tests/save_summary_artifact.sh | 31 | ||||
| -rw-r--r-- | .github/workflows/collect_combined_summary.yml | 29 | ||||
| -rw-r--r-- | .github/workflows/run_and_debug_tests.yml | 115 |
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 |
