From 327b1c049efb257b2bc32d8d6e28892b0b2f9d29 Mon Sep 17 00:00:00 2001 From: naspirato Date: Thu, 27 Nov 2025 22:28:07 +0100 Subject: [PATCH 01/14] BACKPORT-CONFLICT: manual resolution required for commit e773933 --- .github/actions/run_tests/pr_comment.py | 177 +++++++++++ .../validate_pr_description/action.yaml | 2 + .../validate_pr_description.py | 278 +++++++++++++++++- .github/config/backport_branches.json | 6 + .github/workflows/run_tests.yml | 37 ++- 5 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 .github/actions/run_tests/pr_comment.py create mode 100644 .github/config/backport_branches.json diff --git a/.github/actions/run_tests/pr_comment.py b/.github/actions/run_tests/pr_comment.py new file mode 100644 index 000000000000..7fd4dbe15270 --- /dev/null +++ b/.github/actions/run_tests/pr_comment.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +Script to create and update PR comments for test runs. +""" +import os +import sys +from github import Github, Auth as GithubAuth + +def get_pr_number(): + """Extract PR number from environment variable.""" + pr_number = os.environ.get("PR_NUMBER") + if not pr_number: + raise ValueError("PR_NUMBER environment variable is not set") + + # Remove pull/ prefix if present + if pr_number.startswith("pull/"): + pr_number = pr_number.replace("pull/", "") + + return int(pr_number) + +def get_workflow_run_url(): + """Get workflow run URL for identification.""" + github_server = os.environ.get("GITHUB_SERVER_URL") + if not github_server: + raise ValueError("GITHUB_SERVER_URL environment variable is not set") + + github_repo = os.environ.get("GITHUB_REPOSITORY") + if not github_repo: + raise ValueError("GITHUB_REPOSITORY environment variable is not set") + + run_id = os.environ.get("GITHUB_RUN_ID") + if not run_id: + raise ValueError("GITHUB_RUN_ID environment variable is not set") + + return f"{github_server}/{github_repo}/actions/runs/{run_id}" + +def create_or_update_comment(pr_number, message): + """Create or update PR comment with test run information.""" + github_token = os.environ.get("GITHUB_TOKEN") + if not github_token: + raise ValueError("GITHUB_TOKEN environment variable is not set") + + github_repo = os.environ.get("GITHUB_REPOSITORY") + if not github_repo: + raise ValueError("GITHUB_REPOSITORY environment variable is not set") + + workflow_run_url = get_workflow_run_url() + + gh = Github(auth=GithubAuth.Token(github_token)) + repo = gh.get_repo(github_repo) + pr = repo.get_pull(pr_number) + + # Use HTML comment with workflow run URL to identify our comments + header = f"" + + # Find existing comment + comment = None + for c in pr.get_issue_comments(): + if header in c.body: + comment = c + break + + body = [header, message] + full_body = "\n".join(body) + + if comment: + print(f"::notice::Updating existing comment id={comment.id}") + comment.edit(full_body) + else: + print(f"::notice::Creating new comment") + pr.create_issue_comment(full_body) + +def format_start_message(build_preset, test_size, test_targets): + """Format message for test run start.""" + parts = [] + parts.append("## đŸ§Ē Test Run Started") + parts.append("") + + info = [] + info.append(f"**Build Preset:** `{build_preset}`") + info.append(f"**Test Size:** `{test_size}`") + + if test_targets and test_targets != "ydb/": + info.append(f"**Test Targets:** `{test_targets}`") + + parts.append("\n".join(info)) + parts.append("") + parts.append("âŗ Tests are running...") + + return "\n".join(parts) + +def format_completion_message(build_preset, test_size, test_targets, summary_content, status): + """Format message for test run completion.""" + parts = [] + + # Status emoji + if status == "success": + parts.append("## ✅ Test Run Completed Successfully") + elif status == "failure": + parts.append("## ❌ Test Run Failed") + elif status == "cancelled": + parts.append("## âš ī¸ Test Run Cancelled") + else: + parts.append("## âš ī¸ Test Run Completed") + + parts.append("") + + info = [] + info.append(f"**Build Preset:** `{build_preset}`") + info.append(f"**Test Size:** `{test_size}`") + + if test_targets and test_targets != "ydb/": + info.append(f"**Test Targets:** `{test_targets}`") + + parts.append("\n".join(info)) + parts.append("") + + # Add summary content if available + if summary_content and summary_content.strip(): + parts.append("### Test Results") + parts.append("") + parts.append(summary_content.strip()) + + return "\n".join(parts) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("::error::Usage: pr_comment.py [summary_file]") + sys.exit(1) + + command = sys.argv[1] + + if command not in ["start", "complete"]: + print(f"::error::Unknown command: {command}. Must be 'start' or 'complete'") + sys.exit(1) + + pr_number = get_pr_number() + + build_preset = os.environ.get("BUILD_PRESET") + if not build_preset: + raise ValueError("BUILD_PRESET environment variable is not set") + + test_size = os.environ.get("TEST_SIZE") + if not test_size: + raise ValueError("TEST_SIZE environment variable is not set") + + test_targets = os.environ.get("TEST_TARGETS", "ydb/") + + if command == "start": + message = format_start_message(build_preset, test_size, test_targets) + create_or_update_comment(pr_number, message) + else: # complete + summary_file = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_file: + raise ValueError("Summary file path must be provided as argument or GITHUB_STEP_SUMMARY must be set") + + status = os.environ.get("TEST_STATUS") + if not status: + raise ValueError("TEST_STATUS environment variable is not set") + + if not os.path.exists(summary_file): + raise FileNotFoundError(f"Summary file not found: {summary_file}") + + with open(summary_file, 'r', encoding='utf-8') as f: + summary_content = f.read() + + if summary_content.strip(): + print(f"::notice::Read {len(summary_content)} characters from summary file") + else: + print(f"::warning::Summary file is empty") + + message = format_completion_message( + build_preset, test_size, test_targets, + summary_content, status + ) + create_or_update_comment(pr_number, message) + diff --git a/.github/actions/validate_pr_description/action.yaml b/.github/actions/validate_pr_description/action.yaml index 8db5d2eb6771..cfe08b9d8325 100644 --- a/.github/actions/validate_pr_description/action.yaml +++ b/.github/actions/validate_pr_description/action.yaml @@ -9,6 +9,8 @@ runs: env: GITHUB_TOKEN: ${{ github.token }} PR_BODY: ${{ inputs.pr_body}} + SHOW_ADDITIONAL_INFO_IN_PR: ${{ vars.SHOW_ADDITIONAL_INFO_IN_PR }} + APP_DOMAIN: ${{ vars.APP_DOMAIN }} run: | python3 -m pip install PyGithub echo "$PR_BODY" | python3 ${{ github.action_path }}/validate_pr_description.py diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 8996a0d552dd..e58ef7be509f 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -1,5 +1,6 @@ import sys import re +<<<<<<< HEAD from typing import Tuple issue_patterns = [ @@ -26,6 +27,19 @@ * Documentation (changelog entry is not required) * Not for changelog (changelog entry is not required) """ +======= +import os +import json +import urllib.parse +from typing import Tuple, Optional +from github import Github, Auth as GithubAuth +from pr_template import ( + ISSUE_PATTERNS, + PULL_REQUEST_TEMPLATE, + NOT_FOR_CHANGELOG_CATEGORIES, + ALL_CATEGORIES +) +>>>>>>> e7739333763 (Add PR comment functionality for test runs) def validate_pr_description(description, is_not_for_cl_valid=True) -> bool: try: @@ -108,22 +122,274 @@ def check_issue_pattern(issue_pattern): print("PR description is valid.") return True, "PR description is valid." -def validate_pr_description_from_file(file_path) -> Tuple[bool, str]: +def generate_test_table(pr_number: int, base_ref: str, app_domain: str) -> str: + """Generate test execution table with buttons for different build presets and test sizes.""" + base_url = f"https://{app_domain}/workflow/trigger" + owner = "ydb-platform" + repo = "ydb" + workflow_id = "run_tests.yml" + return_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}" + + build_presets = ["relwithdebinfo", "release-asan", "release-msan", "release-tsan"] + test_size_combinations = [ + ("small,medium", "Small & Medium"), + ("large", "Large") + ] + + rows = [] + for build_preset in build_presets: + cells = [] + + for test_size, test_size_display in test_size_combinations: + params = { + "owner": owner, + "repo": repo, + "workflow_id": workflow_id, + "ref": base_ref, + "pull_number": f"pull/{pr_number}", + "build_preset": build_preset, + "test_size": test_size, + "test_targets": "ydb/", + "return_url": return_url + } + query_string = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params.items()]) + url = f"{base_url}?{query_string}" + url_ui = f"{base_url}?{query_string}&ui=true" + + button_label_encoded = build_preset.replace('-', '_') + buttons = f"[![â–ļ {build_preset}](https://img.shields.io/badge/%E2%96%B6_{button_label_encoded}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui})" + cells.append(buttons) + + rows.append("| " + " | ".join(cells) + " |") + + table = "\n" + table += "### Run tests\n\n" + table += "| Small & Medium | Large |\n" + table += "|----------------|-------|\n" + table += "\n".join(rows) + return table + +def generate_backport_table(pr_number: int, app_domain: str) -> str: + """Generate backport execution table with buttons for different branches.""" + base_url = f"https://{app_domain}/workflow/trigger" + owner = "ydb-platform" + repo = "ydb" + workflow_id = "cherry_pick_v2.yml" # Workflow file name + return_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}" + + # Load backport branches from config - no fallback, fail if not found + workspace = os.environ.get("GITHUB_WORKSPACE") + if not workspace: + raise ValueError("GITHUB_WORKSPACE environment variable is not set") + + backport_branches_path = os.path.join(workspace, ".github", "config", "backport_branches.json") + + if not os.path.exists(backport_branches_path): + raise FileNotFoundError(f"Backport branches config file not found: {backport_branches_path}") + + with open(backport_branches_path, 'r') as f: + branches = json.load(f) + + if not isinstance(branches, list) or len(branches) == 0: + raise ValueError(f"Invalid backport branches config: expected non-empty list, got {type(branches)}") + + print(f"::notice::Loaded {len(branches)} backport branches from {backport_branches_path}") + + rows = [] + for branch in branches: + params = { + "owner": owner, + "repo": repo, + "workflow_id": workflow_id, + "ref": "main", + "commits": str(pr_number), + "target_branches": branch, + "allow_unmerged": "true", + "return_url": return_url + } + query_string = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params.items()]) + url = f"{base_url}?{query_string}" + url_ui = f"{base_url}?{query_string}&ui=true" + + rows.append(f"| **{branch}** | [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") + + # Generate URL for backporting multiple branches + all_branches = ",".join(branches) + params_multiple = { + "owner": owner, + "repo": repo, + "workflow_id": workflow_id, + "ref": "main", + "commits": str(pr_number), + "target_branches": all_branches, + "allow_unmerged": "true", + "return_url": return_url + } + query_string_multiple = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params_multiple.items()]) + url_multiple_ui = f"{base_url}?{query_string_multiple}&ui=true" + + table = "\n" + table += "### 🔄 Backport\n\n" + table += "| Branch | Actions |\n" + table += "|--------|----------|\n" + table += "\n".join(rows) + table += "\n\n" + table += f"[![âš™ī¸ Backport multiple branches](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F_Backport_multiple_branches-2196F3?style=flat-square)]({url_multiple_ui})" + return table + +def get_legend() -> str: + """Get legend text for workflow buttons.""" + return "\n**Legend:**\n\n" \ + "* â–ļ - immediately runs the workflow with default parameters\n" \ + "* âš™ī¸ - opens UI to review and modify parameters before running\n" + +def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_domain: str) -> Optional[str]: + """Check if test and backport tables exist in PR body, add them if missing.""" + test_table_marker = "" + backport_table_marker = "" + + has_test_table = test_table_marker in pr_body + has_backport_table = backport_table_marker in pr_body + + if has_test_table and has_backport_table: + return None # Tables already exist + + # Prepare tables to insert + tables_to_insert = [] + if not has_test_table: + tables_to_insert.append(generate_test_table(pr_number, base_ref, app_domain)) + if not has_backport_table: + tables_to_insert.append(generate_backport_table(pr_number, app_domain)) + + legend = get_legend() + + # Find insertion point after "Description for reviewers" section + reviewers_section_marker = "### Description for reviewers" + + if reviewers_section_marker not in pr_body: + # If section not found, add at the end + if pr_body.strip(): + return pr_body.rstrip() + "\n\n" + "\n\n".join(tables_to_insert) + legend + else: + return "\n\n".join(tables_to_insert) + legend + + # Find the end of "Description for reviewers" section (before next ### heading) + lines = pr_body.split('\n') + insertion_index = len(lines) # Default to end + + for i, line in enumerate(lines): + if reviewers_section_marker in line: + # Look for the next ### heading after this section + for j in range(i + 1, len(lines)): + if lines[j].strip().startswith('###') and reviewers_section_marker not in lines[j]: + insertion_index = j + break + break + + # Insert tables and legend after "Description for reviewers" section + new_lines = lines[:insertion_index] + [""] + tables_to_insert + [legend] + lines[insertion_index:] + return '\n'.join(new_lines) + +def update_pr_body(pr_number: int, new_body: str) -> None: + """Update PR body via GitHub API. Raises exception on error.""" + github_token = os.environ.get("GITHUB_TOKEN") + github_repo = os.environ.get("GITHUB_REPOSITORY") + + if not github_token: + raise ValueError("GITHUB_TOKEN environment variable is not set") + + if not github_repo: + raise ValueError("GITHUB_REPOSITORY environment variable is not set") + + gh = Github(auth=GithubAuth.Token(github_token)) + repo = gh.get_repo(github_repo) + pr = repo.get_pull(pr_number) + pr.edit(body=new_body) + print(f"::notice::Updated PR #{pr_number} body with test and backport tables") + +def validate_pr_description_from_file(file_path=None, description=None) -> Tuple[bool, str]: try: - if file_path: + if description is not None: + # Use provided description directly + desc = description + elif file_path: with open(file_path, 'r') as file: - description = file.read() + desc = file.read() else: - description = sys.stdin.read() - return check_pr_description(description) + # Read from stdin if available + if not sys.stdin.isatty(): + desc = sys.stdin.read() + else: + desc = "" + return check_pr_description(desc) except Exception as e: txt = f"Failed to validate PR description: {e}" print(f"::error::{txt}") return False, txt +def validate_pr(): + """Validate PR description.""" + # Read PR body from stdin (passed from action.yaml) + if sys.stdin.isatty(): + raise ValueError("PR body must be provided via stdin") + + pr_body = sys.stdin.read() + + # Get PR info from event - required, no fallback + event_path = os.environ.get("GITHUB_EVENT_PATH") + if not event_path: + raise ValueError("GITHUB_EVENT_PATH environment variable is not set") + + if not os.path.exists(event_path): + raise FileNotFoundError(f"Event file not found: {event_path}") + + with open(event_path, 'r') as f: + event = json.load(f) + + if "pull_request" not in event: + raise ValueError("Event does not contain pull_request data") + + pr_number = event["pull_request"]["number"] + base_ref = event["pull_request"]["base"]["ref"] + + # Use PR body from event if stdin is empty + if not pr_body: + pr_body = event["pull_request"].get("body") or "" + + # Validate PR description + is_valid, txt = validate_pr_description_from_file( + sys.argv[1] if len(sys.argv) > 1 else None, + description=pr_body + ) + + return is_valid, txt, pr_body, pr_number, base_ref + +def add_tables_if_needed(pr_body: str, pr_number: int, base_ref: str): + """Add test and backport tables to PR body if enabled.""" + show_additional_info = os.environ.get("SHOW_ADDITIONAL_INFO_IN_PR", "").upper() == "TRUE" + + if not show_additional_info: + return # Tables should not be added + + app_domain = os.environ.get("APP_DOMAIN") + if not app_domain: + raise ValueError("APP_DOMAIN environment variable is not set (required when SHOW_ADDITIONAL_INFO_IN_PR=TRUE)") + + updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain) + if updated_body: + update_pr_body(pr_number, updated_body) + if __name__ == "__main__": - is_valid, txt = validate_pr_description_from_file(sys.argv[1] if len(sys.argv) > 1 else None) + # Step 1: Validate PR description + is_valid, txt, pr_body, pr_number, base_ref = validate_pr() + + # Step 2: Add tables if validation passed and feature is enabled + if is_valid: + add_tables_if_needed(pr_body, pr_number, base_ref) + + # Step 3: Post validation status from post_status_to_github import post post(is_valid, txt) + if not is_valid: sys.exit(1) diff --git a/.github/config/backport_branches.json b/.github/config/backport_branches.json new file mode 100644 index 000000000000..0328ef804624 --- /dev/null +++ b/.github/config/backport_branches.json @@ -0,0 +1,6 @@ +[ + "stable-25-2", + "stable-25-2-1", + "stable-25-3", + "stable-25-3-1" +] diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index ddb19c18228e..fcf28e03a498 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -55,7 +55,7 @@ on: default: small,medium,large options: - small - - medium, + - medium - large - small,medium - small,medium,large @@ -148,10 +148,24 @@ jobs: with: ref: ${{ matrix.branch }} +<<<<<<< HEAD - name: Setup ssh key for slice uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SLICE_QA_SSH_PRIVATE_KEY }} +======= + - name: Post start comment to PR + if: inputs.pull_number != '' + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pull_number }} + BUILD_PRESET: ${{ inputs.build_preset }} + TEST_SIZE: ${{ inputs.test_size }} + TEST_TARGETS: ${{ inputs.test_targets }} + run: | + python3 -m pip install PyGithub -q + python3 ./.github/actions/run_tests/pr_comment.py start +>>>>>>> e7739333763 (Add PR comment functionality for test runs) - name: Setup ydb access uses: ./.github/actions/setup_ci_ydb_service_account_key_file_credentials @@ -159,6 +173,7 @@ jobs: ci_ydb_service_account_key_file_credentials: ${{ secrets.CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS }} - name: Run YDB Tests + id: run_tests timeout-minutes: ${{ fromJson(env.timeout) }} uses: ./.github/actions/build_and_test_ya with: @@ -174,7 +189,27 @@ jobs: custom_branch_name: ${{ matrix.branch }} put_build_results_to_cache: true additional_ya_make_args: -DDEBUGINFO_LINES_ONLY ${{ inputs.additional_ya_make_args }} +<<<<<<< HEAD secs: ${{ format('{{"TESTMO_TOKEN2":"{0}","AWS_KEY_ID":"{1}","AWS_KEY_VALUE":"{2}","REMOTE_CACHE_USERNAME":"{3}","REMOTE_CACHE_PASSWORD":"{4}"}}', secrets.TESTMO_TOKEN2, secrets.AWS_KEY_ID, secrets.AWS_KEY_VALUE, secrets.REMOTE_CACHE_USERNAME, secrets.REMOTE_CACHE_PASSWORD ) }} vars: ${{ format('{{"AWS_BUCKET":"{0}","AWS_ENDPOINT":"{1}","REMOTE_CACHE_URL":"{2}","TESTMO_URL":"{3}","TESTMO_PROJECT_ID":"{4}"}}', vars.AWS_BUCKET, vars.AWS_ENDPOINT, vars.REMOTE_CACHE_URL_YA, vars.TESTMO_URL, vars.TESTMO_PROJECT_ID ) }} +======= + secs: ${{ format('{{"AWS_KEY_ID":"{0}","AWS_KEY_VALUE":"{1}","REMOTE_CACHE_USERNAME":"{2}","REMOTE_CACHE_PASSWORD":"{3}","TELEGRAM_YDBOT_TOKEN":"{4}"}}', + secrets.AWS_KEY_ID, secrets.AWS_KEY_VALUE, secrets.REMOTE_CACHE_USERNAME, secrets.REMOTE_CACHE_PASSWORD, secrets.TELEGRAM_YDBOT_TOKEN ) }} + vars: ${{ format('{{"AWS_BUCKET":"{0}","AWS_ENDPOINT":"{1}","REMOTE_CACHE_URL":"{2}","GH_ALERTS_TG_LOGINS":"{3}","GH_ALERTS_TG_CHAT":"{4}"}}', + vars.AWS_BUCKET, vars.AWS_ENDPOINT, vars.REMOTE_CACHE_URL_YA, vars.GH_ALERTS_TG_LOGINS, vars.GH_ALERTS_TG_CHAT ) }} + + - name: Update PR comment with results + if: always() && inputs.pull_number != '' + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pull_number }} + BUILD_PRESET: ${{ inputs.build_preset }} + TEST_SIZE: ${{ inputs.test_size }} + TEST_TARGETS: ${{ inputs.test_targets }} + TEST_STATUS: ${{ steps.run_tests.outcome }} + run: | + python3 -m pip install PyGithub -q + python3 ./.github/actions/run_tests/pr_comment.py complete "$GITHUB_STEP_SUMMARY" +>>>>>>> e7739333763 (Add PR comment functionality for test runs) From 575f15a6c4e300ccfc898cde588f284ccc1e02af Mon Sep 17 00:00:00 2001 From: naspirato Date: Thu, 27 Nov 2025 23:38:30 +0100 Subject: [PATCH 02/14] Enhance PR comment functionality and add local validation script This commit updates the PR comment script to include a workflow run URL in comments, improving traceability of test runs. It also modifies message formatting for better visibility. Additionally, a new script for local validation of PR descriptions is introduced, allowing users to test and validate PR bodies before submission. This script supports table generation for test execution and backport actions based on environment configurations. Key changes: - Updated `create_or_update_comment` function to accept and display workflow run URL. - Improved message formatting in PR comments. - Added `test_validation.py` for local PR description validation and table generation testing. - Enhanced table generation logic in `validate_pr_description.py` to support side-by-side display of test and backport tables. --- .github/actions/run_tests/pr_comment.py | 33 ++- .../test_validation.py | 197 ++++++++++++++++++ .../validate_pr_description.py | 59 ++++-- 3 files changed, 257 insertions(+), 32 deletions(-) create mode 100644 .github/actions/validate_pr_description/test_validation.py diff --git a/.github/actions/run_tests/pr_comment.py b/.github/actions/run_tests/pr_comment.py index 7fd4dbe15270..93fa88a2e2a0 100644 --- a/.github/actions/run_tests/pr_comment.py +++ b/.github/actions/run_tests/pr_comment.py @@ -34,7 +34,7 @@ def get_workflow_run_url(): return f"{github_server}/{github_repo}/actions/runs/{run_id}" -def create_or_update_comment(pr_number, message): +def create_or_update_comment(pr_number, message, workflow_run_url): """Create or update PR comment with test run information.""" github_token = os.environ.get("GITHUB_TOKEN") if not github_token: @@ -44,24 +44,19 @@ def create_or_update_comment(pr_number, message): if not github_repo: raise ValueError("GITHUB_REPOSITORY environment variable is not set") - workflow_run_url = get_workflow_run_url() - gh = Github(auth=GithubAuth.Token(github_token)) repo = gh.get_repo(github_repo) pr = repo.get_pull(pr_number) - # Use HTML comment with workflow run URL to identify our comments - header = f"" - - # Find existing comment + # Find existing comment by workflow run URL comment = None for c in pr.get_issue_comments(): - if header in c.body: + if workflow_run_url in c.body: comment = c break - body = [header, message] - full_body = "\n".join(body) + # Add workflow run link to message + full_body = f"{message}\n\n[View workflow run]({workflow_run_url})" if comment: print(f"::notice::Updating existing comment id={comment.id}") @@ -73,7 +68,7 @@ def create_or_update_comment(pr_number, message): def format_start_message(build_preset, test_size, test_targets): """Format message for test run start.""" parts = [] - parts.append("## đŸ§Ē Test Run Started") + parts.append("đŸ§Ē **Test Run Started**") parts.append("") info = [] @@ -95,13 +90,13 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con # Status emoji if status == "success": - parts.append("## ✅ Test Run Completed Successfully") + parts.append("✅ **Test Run Completed Successfully**") elif status == "failure": - parts.append("## ❌ Test Run Failed") + parts.append("❌ **Test Run Failed**") elif status == "cancelled": - parts.append("## âš ī¸ Test Run Cancelled") + parts.append("âš ī¸ **Test Run Cancelled**") else: - parts.append("## âš ī¸ Test Run Completed") + parts.append("âš ī¸ **Test Run Completed**") parts.append("") @@ -117,7 +112,7 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con # Add summary content if available if summary_content and summary_content.strip(): - parts.append("### Test Results") + parts.append("**Test Results:**") parts.append("") parts.append(summary_content.strip()) @@ -146,9 +141,11 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con test_targets = os.environ.get("TEST_TARGETS", "ydb/") + workflow_run_url = get_workflow_run_url() + if command == "start": message = format_start_message(build_preset, test_size, test_targets) - create_or_update_comment(pr_number, message) + create_or_update_comment(pr_number, message, workflow_run_url) else: # complete summary_file = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("GITHUB_STEP_SUMMARY") if not summary_file: @@ -173,5 +170,5 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con build_preset, test_size, test_targets, summary_content, status ) - create_or_update_comment(pr_number, message) + create_or_update_comment(pr_number, message, workflow_run_url) diff --git a/.github/actions/validate_pr_description/test_validation.py b/.github/actions/validate_pr_description/test_validation.py new file mode 100644 index 000000000000..df113d34c926 --- /dev/null +++ b/.github/actions/validate_pr_description/test_validation.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Test script to validate PR description locally. + +Usage: + python3 test_validation.py + python3 test_validation.py --body-file + python3 test_validation.py --body-file + +Environment variables: + +Required for fetching PR from GitHub: + export GITHUB_TOKEN="your_github_token" + +Optional for table generation testing: + export SHOW_ADDITIONAL_INFO_IN_PR="TRUE" # Enable table generation test + export APP_DOMAIN="your-app-domain.com" # Required if SHOW_ADDITIONAL_INFO_IN_PR=TRUE + +Note: GITHUB_WORKSPACE is automatically set to repository root if not provided. +""" +import os +import sys +import json +from pathlib import Path +from validate_pr_description import ( + validate_pr_description_from_file, + ensure_tables_in_pr_body, + update_pr_body +) + +def find_repo_root(): + """Find repository root by looking for .github or .git directory.""" + current = Path(__file__).resolve().parent + while current != current.parent: + if (current / ".github").exists() or (current / ".git").exists(): + return str(current) + current = current.parent + # Fallback to current working directory + return os.getcwd() + +def test_validation(pr_body: str, pr_number: int = None, base_ref: str = "main"): + """Test validation and table generation.""" + print("=" * 60) + print("PR Body from GitHub") + print("=" * 60) + print(pr_body) + print("=" * 60) + print() + + print("=" * 60) + print("Testing PR description validation") + print("=" * 60) + + # Validate + is_valid, txt = validate_pr_description_from_file(description=pr_body) + print(f"\nValidation result: {'✅ PASSED' if is_valid else '❌ FAILED'}") + print(f"Message: {txt}\n") + + if not is_valid: + return False, pr_body + + # Test table generation if enabled + show_additional_info = os.environ.get("SHOW_ADDITIONAL_INFO_IN_PR", "").upper() == "TRUE" + result_body = pr_body + + if show_additional_info: + print("=" * 60) + print("Testing table generation") + print("=" * 60) + + app_domain = os.environ.get("APP_DOMAIN") + if not app_domain: + print("âš ī¸ APP_DOMAIN not set, skipping table generation test") + print(" Set APP_DOMAIN environment variable to test table generation") + return is_valid, pr_body + + if not pr_number: + print("âš ī¸ PR number not provided, skipping table generation test") + print(" Provide PR number to test table generation") + return is_valid, pr_body + + # Check current state + test_marker = "" + backport_marker = "" + has_test = test_marker in pr_body + has_backport = backport_marker in pr_body + + print(f"Current state:") + print(f" Test table exists: {has_test}") + print(f" Backport table exists: {has_backport}") + print() + + updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain) + if updated_body: + result_body = updated_body + print("✅ Tables would be added to PR body") + print("\nGenerated tables preview:") + print("-" * 60) + # Extract just the tables part for preview + if test_marker in updated_body: + test_start = updated_body.find(test_marker) + test_end = updated_body.find("###", test_start + 1) + if test_end == -1: + test_end = updated_body.find("**Legend:**", test_start + 1) + if test_end != -1: + print(updated_body[test_start:test_end].strip()) + if backport_marker in updated_body: + backport_start = updated_body.find(backport_marker) + backport_end = updated_body.find("**Legend:**", backport_start + 1) + if backport_end != -1: + print(updated_body[backport_start:backport_end].strip()) + print("-" * 60) + else: + if has_test and has_backport: + print("â„šī¸ Both tables already exist in PR body") + else: + print("âš ī¸ Function returned None but tables don't exist - this is unexpected") + else: + print("â„šī¸ SHOW_ADDITIONAL_INFO_IN_PR is not TRUE, skipping table generation test") + print(" Set SHOW_ADDITIONAL_INFO_IN_PR=TRUE to test table generation") + + return is_valid, result_body + +def main(): + if len(sys.argv) < 2 and "--body-file" not in sys.argv: + print(__doc__) + sys.exit(1) + + # Set GITHUB_WORKSPACE for local testing if not already set + if not os.environ.get("GITHUB_WORKSPACE"): + repo_root = find_repo_root() + os.environ["GITHUB_WORKSPACE"] = repo_root + print(f"â„šī¸ Set GITHUB_WORKSPACE={repo_root} for local testing") + + pr_number = None + pr_body = None + base_ref = "main" + + # Parse arguments + if "--body-file" in sys.argv: + idx = sys.argv.index("--body-file") + if idx + 1 >= len(sys.argv): + print("Error: --body-file requires a file path") + sys.exit(1) + with open(sys.argv[idx + 1], 'r') as f: + pr_body = f.read() + # Try to get PR number from remaining args + if len(sys.argv) > idx + 2: + try: + pr_number = int(sys.argv[idx + 2]) + except ValueError: + pass + else: + try: + pr_number = int(sys.argv[1]) + except (ValueError, IndexError): + print("Error: PR number must be an integer") + sys.exit(1) + + # Try to get PR body from GitHub API if PR number provided + github_token = os.environ.get("GITHUB_TOKEN") + if github_token: + try: + from github import Github, Auth as GithubAuth + gh = Github(auth=GithubAuth.Token(github_token)) + repo = gh.get_repo("ydb-platform/ydb") + pr = repo.get_pull(pr_number) + pr_body = pr.body or "" + base_ref = pr.base.ref + print(f"đŸ“Ĩ Fetched PR #{pr_number} from GitHub") + except Exception as e: + print(f"âš ī¸ Failed to fetch PR from GitHub: {e}") + print(" Provide PR body via --body-file option") + sys.exit(1) + else: + print("Error: GITHUB_TOKEN not set. Cannot fetch PR from GitHub.") + print(" Set GITHUB_TOKEN or use --body-file option") + sys.exit(1) + + if not pr_body: + print("Error: PR body is required") + sys.exit(1) + + success, result_body = test_validation(pr_body, pr_number, base_ref) + + print() + print("=" * 60) + print("Resulting PR Body") + print("=" * 60) + print(result_body) + print("=" * 60) + + sys.exit(0 if success else 1) + +if __name__ == "__main__": + main() + diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index e58ef7be509f..498a26afbbde 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -122,9 +122,19 @@ def check_issue_pattern(issue_pattern): print("PR description is valid.") return True, "PR description is valid." +def normalize_app_domain(app_domain: str) -> str: + """Normalize app domain - remove https:// prefix if present.""" + domain = app_domain.strip() + if domain.startswith("https://"): + domain = domain[8:] + if domain.startswith("http://"): + domain = domain[7:] + return domain.rstrip('/') + def generate_test_table(pr_number: int, base_ref: str, app_domain: str) -> str: """Generate test execution table with buttons for different build presets and test sizes.""" - base_url = f"https://{app_domain}/workflow/trigger" + domain = normalize_app_domain(app_domain) + base_url = f"https://{domain}/workflow/trigger" owner = "ydb-platform" repo = "ydb" workflow_id = "run_tests.yml" @@ -163,7 +173,7 @@ def generate_test_table(pr_number: int, base_ref: str, app_domain: str) -> str: rows.append("| " + " | ".join(cells) + " |") table = "\n" - table += "### Run tests\n\n" + table += "

Run tests

\n\n" table += "| Small & Medium | Large |\n" table += "|----------------|-------|\n" table += "\n".join(rows) @@ -171,7 +181,8 @@ def generate_test_table(pr_number: int, base_ref: str, app_domain: str) -> str: def generate_backport_table(pr_number: int, app_domain: str) -> str: """Generate backport execution table with buttons for different branches.""" - base_url = f"https://{app_domain}/workflow/trigger" + domain = normalize_app_domain(app_domain) + base_url = f"https://{domain}/workflow/trigger" owner = "ydb-platform" repo = "ydb" workflow_id = "cherry_pick_v2.yml" # Workflow file name @@ -211,7 +222,7 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: url = f"{base_url}?{query_string}" url_ui = f"{base_url}?{query_string}&ui=true" - rows.append(f"| **{branch}** | [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") + rows.append(f"| **{branch}** [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") # Generate URL for backporting multiple branches all_branches = ",".join(branches) @@ -229,9 +240,9 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: url_multiple_ui = f"{base_url}?{query_string_multiple}&ui=true" table = "\n" - table += "### 🔄 Backport\n\n" - table += "| Branch | Actions |\n" - table += "|--------|----------|\n" + table += "

🔄 Backport

\n\n" + table += "| Actions |\n" + table += "|----------|\n" table += "\n".join(rows) table += "\n\n" table += f"[![âš™ī¸ Backport multiple branches](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F_Backport_multiple_branches-2196F3?style=flat-square)]({url_multiple_ui})" @@ -254,24 +265,44 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do if has_test_table and has_backport_table: return None # Tables already exist - # Prepare tables to insert - tables_to_insert = [] + # Generate tables to insert + test_table = None + backport_table = None if not has_test_table: - tables_to_insert.append(generate_test_table(pr_number, base_ref, app_domain)) + test_table = generate_test_table(pr_number, base_ref, app_domain) if not has_backport_table: - tables_to_insert.append(generate_backport_table(pr_number, app_domain)) + backport_table = generate_backport_table(pr_number, app_domain) legend = get_legend() + # Combine tables side by side using HTML table + tables_html = "" + if test_table and backport_table: + # Both tables - place them side by side using HTML table + # GitHub markdown supports markdown tables inside HTML table cells + # Using HTML attributes instead of CSS styles for better compatibility + tables_html = '\n' + tables_html += '\n' + tables_html += '\n' + tables_html += '
' + tables_html += test_table + tables_html += '' + tables_html += backport_table + tables_html += '
' + elif test_table: + tables_html = test_table + elif backport_table: + tables_html = backport_table + # Find insertion point after "Description for reviewers" section reviewers_section_marker = "### Description for reviewers" if reviewers_section_marker not in pr_body: # If section not found, add at the end if pr_body.strip(): - return pr_body.rstrip() + "\n\n" + "\n\n".join(tables_to_insert) + legend + return pr_body.rstrip() + "\n\n" + tables_html + legend else: - return "\n\n".join(tables_to_insert) + legend + return tables_html + legend # Find the end of "Description for reviewers" section (before next ### heading) lines = pr_body.split('\n') @@ -287,7 +318,7 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do break # Insert tables and legend after "Description for reviewers" section - new_lines = lines[:insertion_index] + [""] + tables_to_insert + [legend] + lines[insertion_index:] + new_lines = lines[:insertion_index] + [""] + [tables_html] + [legend] + lines[insertion_index:] return '\n'.join(new_lines) def update_pr_body(pr_number: int, new_body: str) -> None: From 49e35299215e6031da8459a4d2624e39570c6c2b Mon Sep 17 00:00:00 2001 From: naspirato Date: Thu, 27 Nov 2025 23:43:59 +0100 Subject: [PATCH 03/14] BACKPORT-CONFLICT: manual resolution required for commit 6664623 --- .github/actions/run_tests/pr_comment.py | 26 ++++++++++++------------- .github/workflows/run_tests.yml | 4 ++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/actions/run_tests/pr_comment.py b/.github/actions/run_tests/pr_comment.py index 93fa88a2e2a0..871e7f5522b6 100644 --- a/.github/actions/run_tests/pr_comment.py +++ b/.github/actions/run_tests/pr_comment.py @@ -120,7 +120,7 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con if __name__ == "__main__": if len(sys.argv) < 2: - print("::error::Usage: pr_comment.py [summary_file]") + print("::error::Usage: pr_comment.py ") sys.exit(1) command = sys.argv[1] @@ -147,24 +147,24 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con message = format_start_message(build_preset, test_size, test_targets) create_or_update_comment(pr_number, message, workflow_run_url) else: # complete - summary_file = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_file: - raise ValueError("Summary file path must be provided as argument or GITHUB_STEP_SUMMARY must be set") - status = os.environ.get("TEST_STATUS") if not status: raise ValueError("TEST_STATUS environment variable is not set") - if not os.path.exists(summary_file): - raise FileNotFoundError(f"Summary file not found: {summary_file}") - - with open(summary_file, 'r', encoding='utf-8') as f: - summary_content = f.read() + # Read summary from summary_text.txt in workspace + workspace = os.environ.get("GITHUB_WORKSPACE", os.getcwd()) + summary_text_path = os.path.join(workspace, "summary_text.txt") - if summary_content.strip(): - print(f"::notice::Read {len(summary_content)} characters from summary file") + summary_content = "" + if os.path.exists(summary_text_path): + with open(summary_text_path, 'r', encoding='utf-8') as f: + summary_content = f.read() + if summary_content.strip(): + print(f"::notice::Read {len(summary_content)} characters from {summary_text_path}") + else: + print(f"::warning::Summary file {summary_text_path} is empty") else: - print(f"::warning::Summary file is empty") + print(f"::warning::Summary file not found: {summary_text_path}") message = format_completion_message( build_preset, test_size, test_targets, diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index fcf28e03a498..e5d13dfc4f25 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -211,5 +211,9 @@ jobs: TEST_STATUS: ${{ steps.run_tests.outcome }} run: | python3 -m pip install PyGithub -q +<<<<<<< HEAD python3 ./.github/actions/run_tests/pr_comment.py complete "$GITHUB_STEP_SUMMARY" >>>>>>> e7739333763 (Add PR comment functionality for test runs) +======= + python3 ./.github/actions/run_tests/pr_comment.py complete +>>>>>>> 66646233288 (Refactor PR comment script to streamline summary handling) From a6501fda9936295e624d700289debc3bfbf0fdbd Mon Sep 17 00:00:00 2001 From: naspirato Date: Thu, 27 Nov 2025 23:51:37 +0100 Subject: [PATCH 04/14] BACKPORT-CONFLICT: manual resolution required for commit 5e1d4a5 --- .github/workflows/run_tests.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index e5d13dfc4f25..8f3a0c60837d 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -113,6 +113,21 @@ jobs: echo "Final branches to use: $(cat $GITHUB_OUTPUT | grep branch_array | cut -d= -f2)" + - name: Post start comment to PR + if: inputs.pull_number != '' + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pull_number }} + BUILD_PRESET: ${{ inputs.build_preset }} + TEST_SIZE: ${{ inputs.test_size }} + TEST_TARGETS: ${{ inputs.test_targets }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_RUN_ID: ${{ github.run_id }} + run: | + python3 -m pip install PyGithub -q + python3 ./.github/actions/run_tests/pr_comment.py start + run_tests: needs: prepare name: ${{ matrix.branch }}:${{ inputs.build_preset }} @@ -148,6 +163,7 @@ jobs: with: ref: ${{ matrix.branch }} +<<<<<<< HEAD <<<<<<< HEAD - name: Setup ssh key for slice uses: webfactory/ssh-agent@v0.9.0 @@ -167,6 +183,8 @@ jobs: python3 ./.github/actions/run_tests/pr_comment.py start >>>>>>> e7739333763 (Add PR comment functionality for test runs) +======= +>>>>>>> 5e1d4a56d99 (Enhance GitHub Actions workflow to post start comments on PRs) - name: Setup ydb access uses: ./.github/actions/setup_ci_ydb_service_account_key_file_credentials with: From 23cf622e82c1c20002da71936acb4ddc7b2e9793 Mon Sep 17 00:00:00 2001 From: naspirato Date: Thu, 27 Nov 2025 23:54:28 +0100 Subject: [PATCH 05/14] Update GitHub Actions workflow to include sparse checkout for run_tests action This commit enhances the GitHub Actions workflow by adding the `run_tests` action to the sparse checkout configuration. This change ensures that the necessary files for running tests are included in the checkout process, improving the workflow's efficiency and reliability. Key changes: - Added `.github/actions/run_tests/` to the sparse checkout list. --- .github/workflows/run_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 8f3a0c60837d..af5a60b121e9 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -85,6 +85,7 @@ jobs: with: sparse-checkout: | .github/config/stable_branches.json + .github/actions/run_tests/ - name: Set branches id: set-branches From 7020a397a6434298833b0bf6f1ef1467218a6352 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 00:17:02 +0100 Subject: [PATCH 06/14] Refactor type hints in cherry_pick_v2.py for consistency This commit updates the `cherry_pick_v2.py` script to ensure consistent use of type hints by removing unnecessary comments that specify the `PullRequest` type. This change enhances code clarity and maintains flexibility in handling various object types. Key changes: - Removed comments indicating specific types for `pull_requests` and related functions. - Streamlined function signatures to improve readability and maintainability. --- .../actions/validate_pr_description/validate_pr_description.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 498a26afbbde..d341a31fd52c 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -222,7 +222,7 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: url = f"{base_url}?{query_string}" url_ui = f"{base_url}?{query_string}&ui=true" - rows.append(f"| **{branch}** [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") + rows.append(f"| [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") # Generate URL for backporting multiple branches all_branches = ",".join(branches) From a3f0ca4f832cc0726ba54dae57a3497671d57ccc Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 09:44:21 +0100 Subject: [PATCH 07/14] Enhance error handling in PR comment and validation scripts This commit improves the error handling in the `pr_comment.py` and `validate_pr_description.py` scripts by adding try-except blocks around critical operations. This change ensures that failures in updating comments or PR bodies are logged with appropriate error messages, enhancing the robustness and traceability of the scripts. Key changes: - Added error handling for comment updates and creations in `pr_comment.py`. - Implemented error handling for PR body updates in `validate_pr_description.py`. - Introduced checks for required environment variables to prevent runtime errors. --- .github/actions/run_tests/pr_comment.py | 16 +++++-- .../validate_pr_description.py | 44 +++++++++++-------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/.github/actions/run_tests/pr_comment.py b/.github/actions/run_tests/pr_comment.py index 871e7f5522b6..491a6b66fac1 100644 --- a/.github/actions/run_tests/pr_comment.py +++ b/.github/actions/run_tests/pr_comment.py @@ -60,10 +60,18 @@ def create_or_update_comment(pr_number, message, workflow_run_url): if comment: print(f"::notice::Updating existing comment id={comment.id}") - comment.edit(full_body) + try: + comment.edit(full_body) + except Exception as e: + print(f"::error::Failed to update comment id={comment.id}: {e}", file=sys.stderr) + raise else: print(f"::notice::Creating new comment") - pr.create_issue_comment(full_body) + try: + pr.create_issue_comment(full_body) + except Exception as e: + print(f"::error::Failed to create new comment: {e}", file=sys.stderr) + raise def format_start_message(build_preset, test_size, test_targets): """Format message for test run start.""" @@ -152,7 +160,9 @@ def format_completion_message(build_preset, test_size, test_targets, summary_con raise ValueError("TEST_STATUS environment variable is not set") # Read summary from summary_text.txt in workspace - workspace = os.environ.get("GITHUB_WORKSPACE", os.getcwd()) + workspace = os.environ.get("GITHUB_WORKSPACE") + if not workspace: + raise ValueError("GITHUB_WORKSPACE environment variable is not set") summary_text_path = os.path.join(workspace, "summary_text.txt") summary_content = "" diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index d341a31fd52c..16a893f3da89 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -135,8 +135,10 @@ def generate_test_table(pr_number: int, base_ref: str, app_domain: str) -> str: """Generate test execution table with buttons for different build presets and test sizes.""" domain = normalize_app_domain(app_domain) base_url = f"https://{domain}/workflow/trigger" - owner = "ydb-platform" - repo = "ydb" + repo_env = os.environ.get("GITHUB_REPOSITORY") + if not repo_env or "/" not in repo_env: + raise ValueError("GITHUB_REPOSITORY environment variable is not set or malformed (expected 'owner/repo')") + owner, repo = repo_env.split("/", 1) workflow_id = "run_tests.yml" return_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}" @@ -183,8 +185,10 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: """Generate backport execution table with buttons for different branches.""" domain = normalize_app_domain(app_domain) base_url = f"https://{domain}/workflow/trigger" - owner = "ydb-platform" - repo = "ydb" + repo_env = os.environ.get("GITHUB_REPOSITORY") + if not repo_env or "/" not in repo_env: + raise ValueError("GITHUB_REPOSITORY environment variable is not set or malformed (expected 'owner/repo')") + owner, repo = repo_env.split("/", 1) workflow_id = "cherry_pick_v2.yml" # Workflow file name return_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}" @@ -323,20 +327,24 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do def update_pr_body(pr_number: int, new_body: str) -> None: """Update PR body via GitHub API. Raises exception on error.""" - github_token = os.environ.get("GITHUB_TOKEN") - github_repo = os.environ.get("GITHUB_REPOSITORY") - - if not github_token: - raise ValueError("GITHUB_TOKEN environment variable is not set") - - if not github_repo: - raise ValueError("GITHUB_REPOSITORY environment variable is not set") - - gh = Github(auth=GithubAuth.Token(github_token)) - repo = gh.get_repo(github_repo) - pr = repo.get_pull(pr_number) - pr.edit(body=new_body) - print(f"::notice::Updated PR #{pr_number} body with test and backport tables") + try: + github_token = os.environ.get("GITHUB_TOKEN") + github_repo = os.environ.get("GITHUB_REPOSITORY") + + if not github_token: + raise ValueError("GITHUB_TOKEN environment variable is not set") + + if not github_repo: + raise ValueError("GITHUB_REPOSITORY environment variable is not set") + + gh = Github(auth=GithubAuth.Token(github_token)) + repo = gh.get_repo(github_repo) + pr = repo.get_pull(pr_number) + pr.edit(body=new_body) + print(f"::notice::Updated PR #{pr_number} body with test and backport tables") + except Exception as e: + print(f"::error::Failed to update PR #{pr_number} body: {e}") + raise def validate_pr_description_from_file(file_path=None, description=None) -> Tuple[bool, str]: try: From 08c6585f6b2a72ab4686099e57cb6ba9aaf459d7 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 09:51:26 +0100 Subject: [PATCH 08/14] Enhance PR description validation to conditionally include legend This commit updates the `validate_pr_description.py` script to check for the presence of a legend in the PR body before appending it. If the legend already exists, it will not be added again, improving the clarity and conciseness of PR descriptions. Key changes: - Added a check for the legend's existence in the PR body. - Modified the logic for appending tables and the legend to ensure no duplicates are included. --- .../validate_pr_description.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 16a893f3da89..fdcd92c8b997 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -277,7 +277,9 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do if not has_backport_table: backport_table = generate_backport_table(pr_number, app_domain) + # Check if legend already exists legend = get_legend() + has_legend = "**Legend:**" in pr_body # Combine tables side by side using HTML table tables_html = "" @@ -304,9 +306,15 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do if reviewers_section_marker not in pr_body: # If section not found, add at the end if pr_body.strip(): - return pr_body.rstrip() + "\n\n" + tables_html + legend + result = pr_body.rstrip() + "\n\n" + tables_html + if not has_legend: + result += legend + return result else: - return tables_html + legend + result = tables_html + if not has_legend: + result += legend + return result # Find the end of "Description for reviewers" section (before next ### heading) lines = pr_body.split('\n') @@ -322,7 +330,10 @@ def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_do break # Insert tables and legend after "Description for reviewers" section - new_lines = lines[:insertion_index] + [""] + [tables_html] + [legend] + lines[insertion_index:] + new_lines = lines[:insertion_index] + [""] + [tables_html] + if not has_legend: + new_lines.append(legend) + new_lines.extend(lines[insertion_index:]) return '\n'.join(new_lines) def update_pr_body(pr_number: int, new_body: str) -> None: From e0e6a397358fe092d909f159dd5cd4567e61343b Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 09:53:43 +0100 Subject: [PATCH 09/14] Implement GITHUB_REPOSITORY detection in local validation script This commit enhances the `test_validation.py` script by adding functionality to automatically determine the GitHub repository from the git remote URL. This improvement allows for better local testing by setting the `GITHUB_REPOSITORY` environment variable if it is not already defined. Key changes: - Introduced `find_github_repository` function to extract the repository name from the git remote URL. - Updated the main function to set `GITHUB_REPOSITORY` for local testing, improving usability and reducing manual setup requirements. - Enhanced documentation to clarify the automatic setting of `GITHUB_WORKSPACE` and `GITHUB_REPOSITORY` environment variables. --- .../test_validation.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/.github/actions/validate_pr_description/test_validation.py b/.github/actions/validate_pr_description/test_validation.py index df113d34c926..80b324340757 100644 --- a/.github/actions/validate_pr_description/test_validation.py +++ b/.github/actions/validate_pr_description/test_validation.py @@ -16,7 +16,8 @@ export SHOW_ADDITIONAL_INFO_IN_PR="TRUE" # Enable table generation test export APP_DOMAIN="your-app-domain.com" # Required if SHOW_ADDITIONAL_INFO_IN_PR=TRUE -Note: GITHUB_WORKSPACE is automatically set to repository root if not provided. +Note: GITHUB_WORKSPACE and GITHUB_REPOSITORY are automatically set if not provided. + GITHUB_REPOSITORY is determined from git remote origin URL. """ import os import sys @@ -38,6 +39,44 @@ def find_repo_root(): # Fallback to current working directory return os.getcwd() +def find_github_repository(): + """Find GitHub repository from git remote.""" + repo_root = find_repo_root() + git_dir = Path(repo_root) / ".git" + + if not git_dir.exists(): + raise ValueError("Not a git repository. Cannot determine GITHUB_REPOSITORY.") + + try: + import subprocess + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + cwd=repo_root, + capture_output=True, + text=True, + check=True + ) + url = result.stdout.strip() + + # Parse git URL (supports both https and ssh formats) + if "github.com" in url: + if url.startswith("https://"): + # https://github.com/owner/repo.git + parts = url.replace("https://github.com/", "").replace(".git", "").strip() + if "/" in parts: + return parts + elif url.startswith("git@") or url.startswith("ssh://"): + # git@github.com:owner/repo.git or ssh://git@github.com/owner/repo.git + parts = url.split("github.com")[-1].replace(":", "/").replace(".git", "").strip("/") + if "/" in parts: + return parts + + raise ValueError(f"Could not parse GitHub repository from remote URL: {url}") + except subprocess.CalledProcessError: + raise ValueError("Failed to get git remote URL. Cannot determine GITHUB_REPOSITORY.") + except Exception as e: + raise ValueError(f"Failed to determine GITHUB_REPOSITORY: {e}") + def test_validation(pr_body: str, pr_number: int = None, base_ref: str = "main"): """Test validation and table generation.""" print("=" * 60) @@ -132,6 +171,17 @@ def main(): os.environ["GITHUB_WORKSPACE"] = repo_root print(f"â„šī¸ Set GITHUB_WORKSPACE={repo_root} for local testing") + # Set GITHUB_REPOSITORY for local testing if not already set + if not os.environ.get("GITHUB_REPOSITORY"): + try: + github_repo = find_github_repository() + os.environ["GITHUB_REPOSITORY"] = github_repo + print(f"â„šī¸ Set GITHUB_REPOSITORY={github_repo} for local testing") + except ValueError as e: + print(f"❌ Error: {e}") + print(" Set GITHUB_REPOSITORY environment variable manually") + sys.exit(1) + pr_number = None pr_body = None base_ref = "main" From ff16d904f0b06d8b848d7104688b27c66adcbfa3 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 09:56:05 +0100 Subject: [PATCH 10/14] Remove command-line argument for PR description validation in `validate_pr_description.py`. The script now directly uses the PR body for validation, streamlining the process and improving usability. --- .../actions/validate_pr_description/validate_pr_description.py | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index fdcd92c8b997..0eb17a52efae 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -408,7 +408,6 @@ def validate_pr(): # Validate PR description is_valid, txt = validate_pr_description_from_file( - sys.argv[1] if len(sys.argv) > 1 else None, description=pr_body ) From 53c5f51a371b82fa5b34d5c25ecdacd7062f8af0 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 10:27:52 +0100 Subject: [PATCH 11/14] Update PR description validation to support new table flags This commit modifies the `validate_pr_description` workflow to introduce two new environment variables: `SHOW_RUN_TESTS_IN_PR` and `SHOW_BACKPORT_IN_PR`. These flags control the inclusion of test execution and backport tables in the PR body. The `ensure_tables_in_pr_body` function is updated to handle these flags, allowing for more flexible table generation based on user requirements. Key changes: - Replaced `SHOW_ADDITIONAL_INFO_IN_PR` with the new flags in the action configuration. - Updated `test_validation.py` to reflect the new environment variables and their usage. - Enhanced `validate_pr_description.py` to conditionally add tables based on the new flags, improving the clarity and usability of PR descriptions. --- .../validate_pr_description/action.yaml | 3 +- .../test_validation.py | 21 +++++--- .../validate_pr_description.py | 51 ++++++++++++++----- .github/config/backport_branches.json | 7 ++- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/.github/actions/validate_pr_description/action.yaml b/.github/actions/validate_pr_description/action.yaml index cfe08b9d8325..5d4cba0908a9 100644 --- a/.github/actions/validate_pr_description/action.yaml +++ b/.github/actions/validate_pr_description/action.yaml @@ -9,7 +9,8 @@ runs: env: GITHUB_TOKEN: ${{ github.token }} PR_BODY: ${{ inputs.pr_body}} - SHOW_ADDITIONAL_INFO_IN_PR: ${{ vars.SHOW_ADDITIONAL_INFO_IN_PR }} + SHOW_RUN_TESTS_IN_PR: ${{ vars.SHOW_RUN_TESTS_IN_PR }} + SHOW_BACKPORT_IN_PR: ${{ vars.SHOW_BACKPORT_IN_PR }} APP_DOMAIN: ${{ vars.APP_DOMAIN }} run: | python3 -m pip install PyGithub diff --git a/.github/actions/validate_pr_description/test_validation.py b/.github/actions/validate_pr_description/test_validation.py index 80b324340757..48bf63debb99 100644 --- a/.github/actions/validate_pr_description/test_validation.py +++ b/.github/actions/validate_pr_description/test_validation.py @@ -13,8 +13,9 @@ export GITHUB_TOKEN="your_github_token" Optional for table generation testing: - export SHOW_ADDITIONAL_INFO_IN_PR="TRUE" # Enable table generation test - export APP_DOMAIN="your-app-domain.com" # Required if SHOW_ADDITIONAL_INFO_IN_PR=TRUE + export SHOW_RUN_TESTS_IN_PR="TRUE" # Enable test execution table generation + export SHOW_BACKPORT_IN_PR="TRUE" # Enable backport table generation + export APP_DOMAIN="your-app-domain.com" # Required if either table flag is TRUE Note: GITHUB_WORKSPACE and GITHUB_REPOSITORY are automatically set if not provided. GITHUB_REPOSITORY is determined from git remote origin URL. @@ -99,10 +100,11 @@ def test_validation(pr_body: str, pr_number: int = None, base_ref: str = "main") return False, pr_body # Test table generation if enabled - show_additional_info = os.environ.get("SHOW_ADDITIONAL_INFO_IN_PR", "").upper() == "TRUE" + show_test_table = os.environ.get("SHOW_RUN_TESTS_IN_PR", "").upper() == "TRUE" + show_backport_table = os.environ.get("SHOW_BACKPORT_IN_PR", "").upper() == "TRUE" result_body = pr_body - if show_additional_info: + if show_test_table or show_backport_table: print("=" * 60) print("Testing table generation") print("=" * 60) @@ -127,9 +129,14 @@ def test_validation(pr_body: str, pr_number: int = None, base_ref: str = "main") print(f"Current state:") print(f" Test table exists: {has_test}") print(f" Backport table exists: {has_backport}") + print(f"Flags:") + print(f" SHOW_RUN_TESTS_IN_PR: {show_test_table}") + print(f" SHOW_BACKPORT_IN_PR: {show_backport_table}") print() - updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain) + updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain, + show_test_table=show_test_table, + show_backport_table=show_backport_table) if updated_body: result_body = updated_body print("✅ Tables would be added to PR body") @@ -155,8 +162,8 @@ def test_validation(pr_body: str, pr_number: int = None, base_ref: str = "main") else: print("âš ī¸ Function returned None but tables don't exist - this is unexpected") else: - print("â„šī¸ SHOW_ADDITIONAL_INFO_IN_PR is not TRUE, skipping table generation test") - print(" Set SHOW_ADDITIONAL_INFO_IN_PR=TRUE to test table generation") + print("â„šī¸ Neither SHOW_RUN_TESTS_IN_PR nor SHOW_BACKPORT_IN_PR is TRUE, skipping table generation test") + print(" Set SHOW_RUN_TESTS_IN_PR=TRUE and/or SHOW_BACKPORT_IN_PR=TRUE to test table generation") return is_valid, result_body diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 0eb17a52efae..107a83f801a5 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -258,26 +258,50 @@ def get_legend() -> str: "* â–ļ - immediately runs the workflow with default parameters\n" \ "* âš™ī¸ - opens UI to review and modify parameters before running\n" -def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_domain: str) -> Optional[str]: - """Check if test and backport tables exist in PR body, add them if missing.""" +def ensure_tables_in_pr_body(pr_body: str, pr_number: int, base_ref: str, app_domain: str, + show_test_table: bool = True, show_backport_table: bool = True) -> Optional[str]: + """Check if test and backport tables exist in PR body, add them if missing. + + Args: + pr_body: Current PR body + pr_number: PR number + base_ref: Base branch reference + app_domain: Application domain for workflow URLs + show_test_table: Whether to add test execution table + show_backport_table: Whether to add backport table + """ test_table_marker = "" backport_table_marker = "" has_test_table = test_table_marker in pr_body has_backport_table = backport_table_marker in pr_body - if has_test_table and has_backport_table: - return None # Tables already exist + # Check if all requested tables already exist + if show_test_table and show_backport_table: + if has_test_table and has_backport_table: + return None # Both tables already exist + elif show_test_table: + if has_test_table: + return None # Test table already exists + elif show_backport_table: + if has_backport_table: + return None # Backport table already exists + else: + return None # No tables requested # Generate tables to insert test_table = None backport_table = None - if not has_test_table: + if show_test_table and not has_test_table: test_table = generate_test_table(pr_number, base_ref, app_domain) - if not has_backport_table: + if show_backport_table and not has_backport_table: backport_table = generate_backport_table(pr_number, app_domain) - # Check if legend already exists + # If no tables to add, return None + if not test_table and not backport_table: + return None + + # Check if legend already exists (add if at least one table is present) legend = get_legend() has_legend = "**Legend:**" in pr_body @@ -415,16 +439,19 @@ def validate_pr(): def add_tables_if_needed(pr_body: str, pr_number: int, base_ref: str): """Add test and backport tables to PR body if enabled.""" - show_additional_info = os.environ.get("SHOW_ADDITIONAL_INFO_IN_PR", "").upper() == "TRUE" + show_test_table = os.environ.get("SHOW_RUN_TESTS_IN_PR", "").upper() == "TRUE" + show_backport_table = os.environ.get("SHOW_BACKPORT_IN_PR", "").upper() == "TRUE" - if not show_additional_info: - return # Tables should not be added + if not show_test_table and not show_backport_table: + return # No tables should be added app_domain = os.environ.get("APP_DOMAIN") if not app_domain: - raise ValueError("APP_DOMAIN environment variable is not set (required when SHOW_ADDITIONAL_INFO_IN_PR=TRUE)") + raise ValueError("APP_DOMAIN environment variable is not set (required when SHOW_RUN_TESTS_IN_PR=TRUE or SHOW_BACKPORT_IN_PR=TRUE)") - updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain) + updated_body = ensure_tables_in_pr_body(pr_body, pr_number, base_ref, app_domain, + show_test_table=show_test_table, + show_backport_table=show_backport_table) if updated_body: update_pr_body(pr_number, updated_body) diff --git a/.github/config/backport_branches.json b/.github/config/backport_branches.json index 0328ef804624..0313013eb7a9 100644 --- a/.github/config/backport_branches.json +++ b/.github/config/backport_branches.json @@ -1,6 +1,5 @@ [ - "stable-25-2", - "stable-25-2-1", - "stable-25-3", - "stable-25-3-1" + "stable-25-2,stable-25-2-1,stable-25-3,stable-25-3-1", + "stable-25-3,stable-25-3-1", + "stable-25-3" ] From 947d0cdbc26d626c3b128d6568978d96c4a77de5 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 11:58:54 +0100 Subject: [PATCH 12/14] Refactor backport table generation in PR description validation This commit enhances the `generate_backport_table` function in `validate_pr_description.py` to collect and sort unique branches from the input. The changes ensure that the backporting URL is generated for all unique branches, improving the accuracy and clarity of the backport table in PR descriptions. Key changes: - Updated logic to collect unique branches from multiple entries. - Sorted branches for consistent output. - Modified the backport button label for clarity. --- .../validate_pr_description.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 107a83f801a5..9185f0a8d2a5 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -228,8 +228,18 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: rows.append(f"| [![â–ļ {branch}](https://img.shields.io/badge/%E2%96%B6_{branch.replace('-', '_')}-4caf50?style=flat-square)]({url}) [![âš™ī¸](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F-ff9800?style=flat-square)]({url_ui}) |") - # Generate URL for backporting multiple branches - all_branches = ",".join(branches) + # Collect all unique branches from all entries (each entry may contain multiple branches separated by comma) + all_unique_branches = set() + for branch_entry in branches: + # Split by comma and strip whitespace + branch_list = [b.strip() for b in branch_entry.split(',')] + all_unique_branches.update(branch_list) + + # Sort for consistent output + all_unique_branches_sorted = sorted(all_unique_branches) + all_branches = ",".join(all_unique_branches_sorted) + + # Generate URL for backporting all unique branches params_multiple = { "owner": owner, "repo": repo, @@ -249,7 +259,7 @@ def generate_backport_table(pr_number: int, app_domain: str) -> str: table += "|----------|\n" table += "\n".join(rows) table += "\n\n" - table += f"[![âš™ī¸ Backport multiple branches](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F_Backport_multiple_branches-2196F3?style=flat-square)]({url_multiple_ui})" + table += f"[![âš™ī¸ Backport (custom)](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F_Backport_%28custom%29-2196F3?style=flat-square)]({url_multiple_ui})" return table def get_legend() -> str: From fe7507f01881a9ec07ef4f966d5800024537c995 Mon Sep 17 00:00:00 2001 From: naspirato Date: Fri, 28 Nov 2025 15:03:58 +0100 Subject: [PATCH 13/14] BACKPORT-CONFLICT: manual resolution required for commit 3059110 --- .../validate_pr_description/pr_template.py | 54 +++++++++++++++++++ .../validate_pr_description.py | 4 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .github/actions/validate_pr_description/pr_template.py diff --git a/.github/actions/validate_pr_description/pr_template.py b/.github/actions/validate_pr_description/pr_template.py new file mode 100644 index 000000000000..4adabaf84fcb --- /dev/null +++ b/.github/actions/validate_pr_description/pr_template.py @@ -0,0 +1,54 @@ +""" +PR template and categories definitions for YDB project. +Used by both validate_pr_description.py and cherry_pick.py to ensure consistency. +""" + +# Issue reference patterns for validation +ISSUE_PATTERNS = [ + r"https://github.com/ydb-platform/[a-z\-]+/issues/\d+", + r"https://st.yandex-team.ru/[a-zA-Z]+-\d+", + r"#\d+", + r"\b[a-zA-Z]+-\d+(?![-0-9])" # Negative lookahead to avoid matching branch names like "stable-25-2" in URLs +] + +# Full PR template +PULL_REQUEST_TEMPLATE = """### Changelog entry + +... + +### Changelog category + +* New feature +* Experimental feature +* Improvement +* Performance improvement +* User Interface +* Bugfix +* Backward incompatible change +* Documentation (changelog entry is not required) +* Not for changelog (changelog entry is not required)""" + +# Categories that require changelog entry +FOR_CHANGELOG_CATEGORIES = [ + "New feature", + "Experimental feature", + "User Interface", + "Improvement", + "Performance improvement", + "Bugfix", + "Backward incompatible change" +] + +# Categories that don't require changelog entry +NOT_FOR_CHANGELOG_CATEGORIES = [ + "Documentation (changelog entry is not required)", + "Not for changelog (changelog entry is not required)" +] + +# All valid categories +ALL_CATEGORIES = FOR_CHANGELOG_CATEGORIES + NOT_FOR_CHANGELOG_CATEGORIES + + +def get_category_section_template() -> str: + """Get the category section template as a string (for cherry_pick.py)""" + return "\n".join([f"* {cat}" for cat in ALL_CATEGORIES]) diff --git a/.github/actions/validate_pr_description/validate_pr_description.py b/.github/actions/validate_pr_description/validate_pr_description.py index 9185f0a8d2a5..cf8ef50ab5ca 100644 --- a/.github/actions/validate_pr_description/validate_pr_description.py +++ b/.github/actions/validate_pr_description/validate_pr_description.py @@ -110,7 +110,9 @@ def check_pr_description(description, is_not_for_cl_valid=True) -> Tuple[bool, s print(f"::warning::{txt}") return False, txt - if category == "Bugfix": + # Check if category is Bugfix (case-insensitive) + is_bugfix = category_lower == "bugfix" + if is_bugfix: def check_issue_pattern(issue_pattern): return re.search(issue_pattern, description) From 6354f44f384c41a49222d1e49c952e40f048ebec Mon Sep 17 00:00:00 2001 From: Kirill Rysin <35688753+naspirato@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:17:07 +0100 Subject: [PATCH 14/14] Update .github/actions/validate_pr_description/test_validation.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/actions/validate_pr_description/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/validate_pr_description/test_validation.py b/.github/actions/validate_pr_description/test_validation.py index 48bf63debb99..4576bc453709 100644 --- a/.github/actions/validate_pr_description/test_validation.py +++ b/.github/actions/validate_pr_description/test_validation.py @@ -220,7 +220,7 @@ def main(): try: from github import Github, Auth as GithubAuth gh = Github(auth=GithubAuth.Token(github_token)) - repo = gh.get_repo("ydb-platform/ydb") + repo = gh.get_repo(os.environ.get("GITHUB_REPOSITORY")) pr = repo.get_pull(pr_number) pr_body = pr.body or "" base_ref = pr.base.ref