SERVER-126353: Added required patch on github PR (#53563)
GitOrigin-RevId: b5663bb0c96bdeb9d98046aa52622d353a1cfb83
This commit is contained in:
parent
145524fa5e
commit
5316914cc7
@ -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"],
|
||||
|
||||
@ -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 = [
|
||||
|
||||
111
buildscripts/tests/test_validate_evergreen_patch_link.py
Normal file
111
buildscripts/tests/test_validate_evergreen_patch_link.py
Normal 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()
|
||||
151
buildscripts/validate_evergreen_patch_link.py
Normal file
151
buildscripts/validate_evergreen_patch_link.py
Normal 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())
|
||||
@ -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)$"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user