SERVER-126353: Added required patch on github PR (#53563)

GitOrigin-RevId: b5663bb0c96bdeb9d98046aa52622d353a1cfb83
This commit is contained in:
Alexander Neben 2026-05-14 11:05:51 -07:00 committed by MongoDB Bot
parent 145524fa5e
commit 5316914cc7
5 changed files with 298 additions and 1 deletions

View File

@ -562,6 +562,31 @@ py_binary(
],
)
py_binary(
name = "validate_evergreen_patch_link",
srcs = ["validate_evergreen_patch_link.py"],
main = "validate_evergreen_patch_link.py",
visibility = ["//visibility:public"],
deps = [
dependency(
"requests",
group = "core",
),
dependency(
"structlog",
group = "evergreen",
),
dependency(
"typer",
group = "core",
),
dependency(
"typing-extensions",
group = "core",
),
],
)
py_binary(
name = "github_merge_queue_metrics",
srcs = ["github_merge_queue_metrics.py"],

View File

@ -19,6 +19,16 @@ mongo_toolchain_py_cxx_test(
],
)
py_test(
name = "test_validate_evergreen_patch_link",
srcs = ["test_validate_evergreen_patch_link.py"],
main = "test_validate_evergreen_patch_link.py",
visibility = ["//visibility:public"],
deps = [
"//buildscripts:validate_evergreen_patch_link",
],
)
py_test(
name = "test_sync_repo_with_copybara",
srcs = [

View File

@ -0,0 +1,111 @@
"""Unit tests for validate_evergreen_patch_link."""
import unittest
from unittest.mock import patch
from buildscripts.validate_evergreen_patch_link import (
find_patch_link,
has_patch_link,
)
class HasPatchLinkTest(unittest.TestCase):
def test_spruce_version(self):
self.assertTrue(
has_patch_link("see https://spruce.mongodb.com/version/abc123_DEF for results")
)
def test_spruce_patch(self):
self.assertTrue(has_patch_link("https://spruce.mongodb.com/patch/abc123"))
def test_evergreen_version(self):
self.assertTrue(has_patch_link("https://evergreen.mongodb.com/version/abc123"))
def test_evergreen_patch(self):
self.assertTrue(has_patch_link("https://evergreen.mongodb.com/patch/abc123"))
def test_case_insensitive(self):
self.assertTrue(has_patch_link("HTTPS://Spruce.MongoDB.com/Version/abc123"))
def test_empty(self):
self.assertFalse(has_patch_link(""))
self.assertFalse(has_patch_link(None))
def test_unrelated_url(self):
self.assertFalse(has_patch_link("https://github.com/foo/bar/pull/1"))
def test_host_only_does_not_match(self):
self.assertFalse(has_patch_link("see spruce.mongodb.com for details"))
class FindPatchLinkTest(unittest.TestCase):
def test_link_in_comment(self):
comments = [{"body": "https://spruce.mongodb.com/version/x"}]
self.assertTrue(find_patch_link(comments))
def test_no_link_in_comments(self):
comments = [{"body": "lgtm"}, {"body": "needs tests"}]
self.assertFalse(find_patch_link(comments))
def test_empty_comments(self):
self.assertFalse(find_patch_link([]))
def test_link_in_any_comment(self):
comments = [
{"body": "lgtm"},
{"body": "ok: https://spruce.mongodb.com/version/abc"},
]
self.assertTrue(find_patch_link(comments))
class MainFlowTest(unittest.TestCase):
"""Exercise the main() flow end-to-end with the GitHub API stubbed out."""
@patch("buildscripts.validate_evergreen_patch_link.get_pr_comments")
def test_passes_when_link_in_comment(self, get_comments):
from buildscripts.validate_evergreen_patch_link import main
get_comments.return_value = [{"body": "https://spruce.mongodb.com/version/abc"}]
# Should not raise.
main(
github_org="o",
github_repo="r",
pr_number=1,
github_token="t",
requester="github_pr",
)
@patch("buildscripts.validate_evergreen_patch_link.get_pr_comments")
def test_fails_when_missing(self, get_comments):
import typer
from buildscripts.validate_evergreen_patch_link import main
get_comments.return_value = [{"body": "looks good"}]
with self.assertRaises(typer.Exit) as cm:
main(
github_org="o",
github_repo="r",
pr_number=1,
github_token="t",
requester="github_pr",
)
self.assertEqual(cm.exception.exit_code, 1)
@patch("buildscripts.validate_evergreen_patch_link.get_pr_comments")
def test_skips_when_not_github_pr(self, get_comments):
from buildscripts.validate_evergreen_patch_link import main
main(
github_org="o",
github_repo="r",
pr_number=1,
github_token="t",
requester="patch",
)
get_comments.assert_not_called()
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,151 @@
#!/usr/bin/env python3
# Copyright (C) 2026-present MongoDB, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the Server Side Public License, version 1,
# as published by MongoDB, Inc.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Server Side Public License for more details.
#
# You should have received a copy of the Server Side Public License
# along with this program. If not, see
# <http://www.mongodb.com/licensing/server-side-public-license>.
#
# As a special exception, the copyright holders give permission to link the
# code of portions of this program with the OpenSSL library under certain
# conditions as described in each individual source file and distribute
# linked combinations including the program with the OpenSSL library. You
# must comply with the Server Side Public License in all respects for
# all of the code used other than as permitted herein. If you modify file(s)
# with this exception, you may extend this exception to your version of the
# file(s), but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version. If you delete this
# exception statement from all source files in the program, then also delete
# it in the license file.
#
"""Validate that an Evergreen patch build link is included on the PR."""
import re
import sys
import requests
import structlog
import typer
from typing_extensions import Annotated
LOGGER = structlog.get_logger(__name__)
STATUS_OK = 0
STATUS_ERROR = 1
EVERGREEN_PATCH_URL_RE = re.compile(
r"https?://(?:spruce|evergreen)\.mongodb\.com/(?:version|patch)/[A-Za-z0-9_-]+",
re.IGNORECASE,
)
FAILURE_MESSAGE = """
No Evergreen patch build link found on this PR.
A required patch build is required before merging (https://wiki.corp.mongodb.com/spaces/KERNEL/pages/126668501/Required+Patch+Builds+Policy). To fix this:
1. Run an Evergreen patch for this PR:
evergreen patch -p mongodb-mongo-master -a required
2. Copy the patch link (looks like https://spruce.mongodb.com/version/<id>).
3. Post the link as a comment on this PR (do not put it in the description
Spruce/Evergreen URLs in the description are banned by validate_commit_message).
4. Restart this task in Spruce (click Restart on the failing task).
"""
def _gh_get(url: str, github_token: str) -> requests.Response:
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
resp = requests.get(url, headers=headers, timeout=60)
resp.raise_for_status()
return resp
def get_pr_comments(
github_org: str, github_repo: str, pr_number: int, github_token: str
) -> list[dict]:
"""Return all issue comments on the PR, following pagination."""
comments: list[dict] = []
url = (
f"https://api.github.com/repos/{github_org}/{github_repo}"
f"/issues/{pr_number}/comments?per_page=100"
)
while url:
resp = _gh_get(url, github_token)
comments.extend(resp.json())
url = resp.links.get("next", {}).get("url")
return comments
def has_patch_link(text: str) -> bool:
if not text:
return False
return bool(EVERGREEN_PATCH_URL_RE.search(text))
def find_patch_link(comments: list[dict]) -> bool:
"""Look for an Evergreen patch link in any PR comment.
Intentionally does NOT check the PR description: Spruce/Evergreen URLs are banned
there by validate_commit_message (they end up in the squashed commit message body).
"""
return any(has_patch_link(comment.get("body") or "") for comment in comments)
def main(
github_org: Annotated[
str, typer.Option(envvar="GITHUB_ORG", help="GitHub organization (e.g. 10gen)")
] = "",
github_repo: Annotated[
str, typer.Option(envvar="GITHUB_REPO", help="GitHub repo name (e.g. mongo)")
] = "",
pr_number: Annotated[int, typer.Option(envvar="PR_NUMBER", help="PR number")] = -1,
github_token: Annotated[
str,
typer.Option(
envvar="GITHUB_TOKEN",
help="GitHub token with pull_requests: read permission",
),
] = "",
requester: Annotated[str, typer.Option(envvar="REQUESTER", help="Evergreen requester")] = "",
):
"""Fail if the PR is missing an Evergreen patch build link."""
if requester != "github_pr":
LOGGER.info("Skipping: only runs for github_pr", requester=requester)
return
if not (github_org and github_repo and github_token and pr_number > 0):
LOGGER.error(
"Missing required inputs",
github_org=bool(github_org),
github_repo=bool(github_repo),
github_token=bool(github_token),
pr_number=pr_number,
)
raise typer.Exit(code=STATUS_ERROR)
comments = get_pr_comments(github_org, github_repo, pr_number, github_token)
if find_patch_link(comments):
LOGGER.info("Found Evergreen patch link on PR", pr_number=pr_number)
return
LOGGER.error(FAILURE_MESSAGE, pr_number=pr_number)
raise typer.Exit(code=STATUS_ERROR)
app = typer.Typer(pretty_exceptions_show_locals=False)
app.command()(main)
if __name__ == "__main__":
sys.exit(app())

View File

@ -134,7 +134,7 @@ commit_queue_aliases:
github_pr_aliases:
# TODO(SERVER-124155): add check_for_todos to the commit queue variant's task list once the codebase is free of existing violations
- variant: "^(commit-queue)$"
task: "^(bazel_.*|run_.*|unit_test.*|compile_.*|lint_.*|resmoke_tests|check_for_noexcept|version_gen_validation|validate_commit_message|resmoke_validation_tests|buildscripts_test)$"
task: "^(bazel_.*|run_.*|unit_test.*|compile_.*|lint_.*|resmoke_tests|check_for_noexcept|version_gen_validation|validate_commit_message|validate_evergreen_patch_link|resmoke_validation_tests|buildscripts_test)$"
variant_tags: []
task_tags: []
- variant: "^(amazon-linux2023-arm64-static-compile)$"