Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/actions/run_tests/pr_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/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, workflow_run_url):
"""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")

gh = Github(auth=GithubAuth.Token(github_token))
repo = gh.get_repo(github_repo)
pr = repo.get_pull(pr_number)

# Find existing comment by workflow run URL
comment = None
for c in pr.get_issue_comments():
if workflow_run_url in c.body:
comment = c
break

# 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}")
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 <start|complete>")
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/")

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, workflow_run_url)
else: # complete
status = os.environ.get("TEST_STATUS")
if not status:
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())
summary_text_path = os.path.join(workspace, "summary_text.txt")

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 not found: {summary_text_path}")

message = format_completion_message(
build_preset, test_size, test_targets,
summary_content, status
)
create_or_update_comment(pr_number, message, workflow_run_url)

2 changes: 2 additions & 0 deletions .github/actions/validate_pr_description/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
197 changes: 197 additions & 0 deletions .github/actions/validate_pr_description/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""
Test script to validate PR description locally.

Usage:
python3 test_validation.py <pr_number>
python3 test_validation.py <pr_number> --body-file <file_path>
python3 test_validation.py --body-file <file_path>

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 = "<!-- test-execution-table -->"
backport_marker = "<!-- backport-table -->"
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()

Loading