Reorganize Servo's WPT Python scripts

This change moves all of Servo's WPT Python support scripts into one
directory as they were previously scattered throughout the directory
structure. This should allow more code reuse and make it easier to
understand how everything fits together.

The changes:

- `tests/wpt/update` → `python/wpt/importer`
- `etc/ci/upstream-wpt-changes/wptupstreamer` → `python/wpt/exporter`
- `etc/ci/upstream-wpt-changes/test.py` → `python/wpt/test.py`
- `etc/ci/upstream-wpt-changes/tests` → `python/wpt/tests`
- `tests/wpt/servowpt.py` →
    - `python/wpt/update.py`
    - `python/wpt/run.py`
- `tests/wpt/manifestupdate.py` → `python/wpt/manifestupdate.py`

This change also removes
 - The ability to run the `update-wpt` and `test-wpt` commands without
   using `mach`. These didn't work very well, because it was difficult
   to get all of the wptrunner and mach dependencies installed outside
   of the Python virtualenv. It's simpler if they are always run through
   `mach`.
- The old WPT change upstreaming script that was no longer used.
This commit is contained in:
Martin Robinson 2023-04-16 11:33:02 +02:00
parent 9acb9cc5cf
commit e2cf3e8d1a
52 changed files with 237 additions and 888 deletions

View file

@ -0,0 +1,285 @@
# Copyright 2023 The Servo Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# pylint: disable=broad-except
# pylint: disable=dangerous-default-value
# pylint: disable=fixme
# pylint: disable=missing-docstring
# This allows using types that are defined later in the file.
from __future__ import annotations
import dataclasses
import json
import logging
import re
import subprocess
from typing import Callable, Optional
from .common import \
CLOSING_EXISTING_UPSTREAM_PR, \
NO_SYNC_SIGNAL, \
NO_UPSTREAMBLE_CHANGES_COMMENT, \
OPENED_NEW_UPSTREAM_PR, \
UPDATED_EXISTING_UPSTREAM_PR, \
UPDATED_TITLE_IN_EXISTING_UPSTREAM_PR, \
UPSTREAMABLE_PATH, \
wpt_branch_name_from_servo_pr_number
from .github import GithubRepository, PullRequest
from .step import \
AsyncValue, \
ChangePRStep, \
CommentStep, \
CreateOrUpdateBranchForPRStep, \
MergePRStep, \
OpenPRStep, \
RemoveBranchForPRStep, \
Step
class LocalGitRepo:
def __init__(self, path: str, sync: WPTSync):
self.path = path
self.sync = sync
def run_without_encoding(self, *args, env: dict = {}):
command_line = ["git"] + list(args)
logging.info(" → Execution (cwd='%s'): %s",
self.path, " ".join(command_line))
env.setdefault("GIT_AUTHOR_EMAIL", self.sync.github_email)
env.setdefault("GIT_COMMITTER_EMAIL", self.sync.github_email)
env.setdefault("GIT_AUTHOR_NAME", self.sync.github_name)
env.setdefault("GIT_COMMITTER_NAME", self.sync.github_name)
try:
return subprocess.check_output(
command_line, cwd=self.path, env=env, stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError as exception:
logging.warning("Process execution failed with output:\n%s",
exception.output.decode("utf-8", errors="surrogateescape"))
raise exception
def run(self, *args, env: dict = {}):
return (
self
.run_without_encoding(*args, env=env)
.decode("utf-8", errors="surrogateescape")
)
@dataclasses.dataclass()
class SyncRun:
sync: WPTSync
servo_pr: PullRequest
upstream_pr: AsyncValue[PullRequest]
step_callback: Optional[Callable[[Step], None]]
steps: list[Step] = dataclasses.field(default_factory=list)
def make_comment(self, template: str) -> str:
upstream_pr = self.upstream_pr.value() if self.upstream_pr.has_value() else ""
return template.format(
upstream_pr=upstream_pr,
servo_pr=self.servo_pr,
)
def add_step(self, step) -> Optional[AsyncValue]:
self.steps.append(step)
return step.provides()
def run(self):
# This loop always removes the first step and runs it, because
# individual steps can modify the list of steps. For instance, if a
# step fails, it might clear the remaining steps and replace them with
# steps that report the error to GitHub.
while self.steps:
step = self.steps.pop(0)
step.run(self)
if self.step_callback:
self.step_callback(step)
@staticmethod
def clean_up_body_text(body: str) -> str:
# Turn all bare or relative issue references into unlinked ones, so that
# the PR doesn't inadvertently close or link to issues in the upstream
# repository.
return (
re.sub(
r"(^|\s)(\w*)#([1-9]\d*)",
r"\g<1>\g<2>#<!-- nolink -->\g<3>",
body,
flags=re.MULTILINE,
)
.split("\n---")[0]
.split("<!-- Thank you for")[0]
)
def prepare_body_text(self, body: str) -> str:
return SyncRun.clean_up_body_text(body) + f"\nReviewed in {self.servo_pr}"
@dataclasses.dataclass(kw_only=True)
class WPTSync:
servo_repo: str
wpt_repo: str
downstream_wpt_repo: str
servo_path: str
wpt_path: str
github_api_token: str
github_api_url: str
github_username: str
github_email: str
github_name: str
suppress_force_push: bool = False
def __post_init__(self):
self.servo = GithubRepository(self, self.servo_repo)
self.wpt = GithubRepository(self, self.wpt_repo)
self.downstream_wpt = GithubRepository(self, self.downstream_wpt_repo)
self.local_servo_repo = LocalGitRepo(self.servo_path, self)
self.local_wpt_repo = LocalGitRepo(self.wpt_path, self)
def run(self, payload: dict, step_callback=None) -> bool:
if "pull_request" not in payload:
return True
pull_data = payload["pull_request"]
if NO_SYNC_SIGNAL in pull_data.get("body", ""):
return True
# Only look for an existing remote PR if the action is appropriate.
logging.info("Processing '%s' action...", payload["action"])
action = payload["action"]
if action not in ["opened", "synchronize", "reopened", "edited", "closed"]:
return True
if (
action == "edited"
and "title" not in payload["changes"]
and "body" not in payload["changes"]
):
return True
try:
servo_pr = self.servo.get_pull_request(pull_data["number"])
downstream_wpt_branch = self.downstream_wpt.get_branch(
wpt_branch_name_from_servo_pr_number(servo_pr.number)
)
upstream_pr = self.wpt.get_open_pull_request_for_branch(
downstream_wpt_branch
)
if upstream_pr:
logging.info(
" → Detected existing upstream PR %s", upstream_pr)
run = SyncRun(self, servo_pr, AsyncValue(
upstream_pr), step_callback)
pull_data = payload["pull_request"]
if payload["action"] in ["opened", "synchronize", "reopened"]:
self.handle_new_pull_request_contents(run, pull_data)
elif payload["action"] == "edited":
self.handle_edited_pull_request(run, pull_data)
elif payload["action"] == "closed":
self.handle_closed_pull_request(run, pull_data)
run.run()
return True
except Exception as exception:
if isinstance(exception, subprocess.CalledProcessError):
logging.error(exception.output)
logging.error(json.dumps(payload))
logging.error(exception, exc_info=True)
return False
def handle_new_pull_request_contents(self, run: SyncRun, pull_data: dict):
num_commits = pull_data["commits"]
head_sha = pull_data["head"]["sha"]
is_upstreamable = (
len(
self.local_servo_repo.run(
"diff", head_sha, f"{head_sha}~{num_commits}", "--", UPSTREAMABLE_PATH
)
)
> 0
)
logging.info(" → PR is upstreamable: '%s'", is_upstreamable)
title = pull_data['title']
body = pull_data['body']
if run.upstream_pr.has_value():
if is_upstreamable:
# In case this is adding new upstreamable changes to a PR that was closed
# due to a lack of upstreamable changes, force it to be reopened.
# Github refuses to reopen a PR that had a branch force pushed, so be sure
# to do this first.
run.add_step(ChangePRStep(
run.upstream_pr.value(), "opened", title, body))
# Push the relevant changes to the upstream branch.
run.add_step(CreateOrUpdateBranchForPRStep(
pull_data, run.servo_pr))
run.add_step(CommentStep(
run.servo_pr, UPDATED_EXISTING_UPSTREAM_PR))
else:
# Close the upstream PR, since would contain no changes otherwise.
run.add_step(CommentStep(run.upstream_pr.value(),
NO_UPSTREAMBLE_CHANGES_COMMENT))
run.add_step(ChangePRStep(run.upstream_pr.value(), "closed"))
run.add_step(RemoveBranchForPRStep(pull_data))
run.add_step(CommentStep(
run.servo_pr, CLOSING_EXISTING_UPSTREAM_PR))
elif is_upstreamable:
# Push the relevant changes to a new upstream branch.
branch = run.add_step(
CreateOrUpdateBranchForPRStep(pull_data, run.servo_pr))
# Create a pull request against the upstream repository for the new branch.
assert branch
upstream_pr = run.add_step(OpenPRStep(
branch, self.wpt, title, body,
["servo-export", "do not merge yet"],
))
assert upstream_pr
run.upstream_pr = upstream_pr
# Leave a comment to the new pull request in the original pull request.
run.add_step(CommentStep(run.servo_pr, OPENED_NEW_UPSTREAM_PR))
def handle_edited_pull_request(self, run: SyncRun, pull_data: dict):
logging.info("Changing upstream PR title")
if run.upstream_pr.has_value():
run.add_step(ChangePRStep(
run.upstream_pr.value(
), "open", pull_data["title"], pull_data["body"]
))
run.add_step(CommentStep(
run.servo_pr, UPDATED_TITLE_IN_EXISTING_UPSTREAM_PR))
def handle_closed_pull_request(self, run: SyncRun, pull_data: dict):
logging.info("Processing closed PR")
if not run.upstream_pr.has_value():
# If we don't recognize this PR, it never contained upstreamable changes.
return
if pull_data["merged"]:
# Since the upstreamable changes have now been merged locally, merge the
# corresponding upstream PR.
run.add_step(MergePRStep(
run.upstream_pr.value(), ["do not merge yet"]))
else:
# If a PR with upstreamable changes is closed without being merged, we
# don't want to merge the changes upstream either.
run.add_step(ChangePRStep(run.upstream_pr.value(), "closed"))
# Always clean up our remote branch.
run.add_step(RemoveBranchForPRStep(pull_data))

View file

@ -0,0 +1,53 @@
# Copyright 2023 The Servo Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# pylint: disable=missing-docstring
UPSTREAMABLE_PATH = "tests/wpt/web-platform-tests/"
NO_SYNC_SIGNAL = "[no-wpt-sync]"
OPENED_NEW_UPSTREAM_PR = (
"🤖 Opened new upstream WPT pull request ({upstream_pr}) "
"with upstreamable changes."
)
UPDATED_EXISTING_UPSTREAM_PR = (
"📝 Transplanted new upstreamable changes to existing "
"upstream WPT pull request ({upstream_pr})."
)
UPDATED_TITLE_IN_EXISTING_UPSTREAM_PR = (
"✍ Updated existing upstream WPT pull request ({upstream_pr}) title and body."
)
CLOSING_EXISTING_UPSTREAM_PR = (
"🤖 This change no longer contains upstreamable changes to WPT; closed existing "
"upstream pull request ({upstream_pr})."
)
NO_UPSTREAMBLE_CHANGES_COMMENT = (
"👋 Downstream pull request ({servo_pr}) no longer contains any upstreamable "
"changes. Closing pull request without merging."
)
COULD_NOT_APPLY_CHANGES_DOWNSTREAM_COMMENT = (
"🛠 These changes could not be applied onto the latest upstream WPT. "
"Servo's copy of the Web Platform Tests may be out of sync."
)
COULD_NOT_APPLY_CHANGES_UPSTREAM_COMMENT = (
"🛠 Changes from the source pull request ({servo_pr}) can no longer be "
"cleanly applied. Waiting for a new version of these changes downstream."
)
COULD_NOT_MERGE_CHANGES_DOWNSTREAM_COMMENT = (
"⛔ Failed to properly merge the upstream pull request ({upstream_pr}). "
"Please address any CI issues and try to merge manually."
)
COULD_NOT_MERGE_CHANGES_UPSTREAM_COMMENT = (
"⛔ The downstream PR has merged ({servo_pr}), but these changes could not "
"be merged properly. Please address any CI issues and try to merge manually."
)
def wpt_branch_name_from_servo_pr_number(servo_pr_number):
return f"servo_export_{servo_pr_number}"

View file

@ -0,0 +1,178 @@
# Copyright 2023 The Servo Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# pylint: disable=missing-docstring
"""This modules contains some abstractions of GitHub repositories. It could one
day be entirely replaced with something like PyGithub."""
# This allows using types that are defined later in the file.
from __future__ import annotations
import logging
import urllib
from typing import Optional, TYPE_CHECKING
import requests
if TYPE_CHECKING:
from . import WPTSync
USER_AGENT = "Servo web-platform-test sync service"
TIMEOUT = 30 # 30 seconds
def authenticated(sync: WPTSync, method, url, json=None) -> requests.Response:
logging.info(" → Request: %s %s", method, url)
if json:
logging.info(" → Request JSON: %s", json)
headers = {
"Authorization": f"Bearer {sync.github_api_token}",
"User-Agent": USER_AGENT,
}
url = urllib.parse.urljoin(sync.github_api_url, url)
response = requests.request(
method, url, headers=headers, json=json, timeout=TIMEOUT
)
if int(response.status_code / 100) != 2:
raise ValueError(
f"Got unexpected {response.status_code} response: {response.text}"
)
return response
class GithubRepository:
"""
This class allows interacting with a single GitHub repository.
"""
def __init__(self, sync: WPTSync, repo: str):
self.sync = sync
self.repo = repo
self.org = repo.split("/")[0]
self.pulls_url = f"repos/{self.repo}/pulls"
def __str__(self):
return self.repo
def get_pull_request(self, number: int) -> PullRequest:
return PullRequest(self, number)
def get_branch(self, name: str) -> GithubBranch:
return GithubBranch(self, name)
def get_open_pull_request_for_branch(
self, branch: GithubBranch
) -> Optional[PullRequest]:
"""If this repository has an open pull request with the
given source head reference targeting the master branch,
return the first matching pull request, otherwise return None."""
params = "+".join([
"is:pr",
"state:open",
f"repo:{self.repo}",
f"author:{branch.repo.org}",
f"head:{branch.name}",
])
response = authenticated(self.sync, "GET", f"search/issues?q={params}")
if int(response.status_code / 100) != 2:
return None
json = response.json()
if not isinstance(json, dict) or \
"total_count" not in json or \
"items" not in json:
raise ValueError(
f"Got unexpected response from GitHub search: {response.text}"
)
if json["total_count"] < 1:
return None
return self.get_pull_request(json["items"][0]["number"])
def open_pull_request(self, branch: GithubBranch, title: str, body: str):
data = {
"title": title,
"head": branch.get_pr_head_reference_for_repo(self),
"base": "master",
"body": body,
"maintainer_can_modify": False,
}
response = authenticated(self.sync, "POST", self.pulls_url, json=data)
return self.get_pull_request(response.json()["number"])
class GithubBranch:
def __init__(self, repo: GithubRepository, branch_name: str):
self.repo = repo
self.name = branch_name
def __str__(self):
return f"{self.repo}/{self.name}"
def get_pr_head_reference_for_repo(self, other_repo: GithubRepository) -> str:
"""Get the head reference to use in pull requests for the given repository.
If the organization is the same this is just `<branch>` otherwise
it will be `<org>:<branch>`."""
if self.repo.org == other_repo.org:
return self.name
return f"{self.repo.org}:{self.name}"
class PullRequest:
"""
This class allows interacting with a single pull request on GitHub.
"""
def __init__(self, repo: GithubRepository, number: int):
self.repo = repo
self.context = repo.sync
self.number = number
self.base_url = f"repos/{self.repo.repo}/pulls/{self.number}"
self.base_issues_url = f"repos/{self.repo.repo}/issues/{self.number}"
def __str__(self):
return f"{self.repo}#{self.number}"
def api(self, *args, **kwargs) -> requests.Response:
return authenticated(self.context, *args, **kwargs)
def leave_comment(self, comment: str):
return self.api(
"POST", f"{self.base_issues_url}/comments", json={"body": comment}
)
def change(
self,
state: Optional[str] = None,
title: Optional[str] = None,
body: Optional[str] = None,
):
data = {}
if title:
data["title"] = title
if body:
data["body"] = body
if state:
data["state"] = state
return self.api("PATCH", self.base_url, json=data)
def remove_label(self, label: str):
self.api("DELETE", f"{self.base_issues_url}/labels/{label}")
def add_labels(self, labels: list[str]):
self.api("POST", f"{self.base_issues_url}/labels", json=labels)
def merge(self):
self.api("PUT", f"{self.base_url}/merge", json={"merge_method": "rebase"})

313
python/wpt/exporter/step.py Normal file
View file

@ -0,0 +1,313 @@
# Copyright 2023 The Servo Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
# pylint: disable=broad-except
# pylint: disable=dangerous-default-value
# pylint: disable=fixme
# pylint: disable=missing-docstring
# This allows using types that are defined later in the file.
from __future__ import annotations
import logging
import os
import textwrap
from typing import TYPE_CHECKING, Generic, Optional, TypeVar
from .common import COULD_NOT_APPLY_CHANGES_DOWNSTREAM_COMMENT
from .common import COULD_NOT_APPLY_CHANGES_UPSTREAM_COMMENT
from .common import COULD_NOT_MERGE_CHANGES_DOWNSTREAM_COMMENT
from .common import COULD_NOT_MERGE_CHANGES_UPSTREAM_COMMENT
from .common import UPSTREAMABLE_PATH
from .common import wpt_branch_name_from_servo_pr_number
from .github import GithubBranch, GithubRepository, PullRequest
if TYPE_CHECKING:
from . import SyncRun, WPTSync
PATCH_FILE_NAME = "tmp.patch"
class Step:
def __init__(self, name):
self.name = name
def provides(self) -> Optional[AsyncValue]:
return None
def run(self, _: SyncRun):
return
T = TypeVar('T')
class AsyncValue(Generic[T]):
def __init__(self, value: Optional[T] = None):
self._value = value
def resolve(self, value: T):
self._value = value
def value(self) -> T:
assert self._value is not None
return self._value
def has_value(self):
return self._value is not None
class CreateOrUpdateBranchForPRStep(Step):
def __init__(self, pull_data: dict, pull_request: PullRequest):
Step.__init__(self, "CreateOrUpdateBranchForPRStep")
self.pull_data = pull_data
self.pull_request = pull_request
self.branch: AsyncValue[GithubBranch] = AsyncValue()
def provides(self):
return self.branch
def run(self, run: SyncRun):
try:
commits = self._get_upstreamable_commits_from_local_servo_repo(
run.sync)
branch_name = self._create_or_update_branch_for_pr(run, commits)
branch = run.sync.downstream_wpt.get_branch(branch_name)
self.branch.resolve(branch)
self.name += f":{len(commits)}:{branch}"
except Exception as exception:
logging.info("Could not apply changes to upstream WPT repository.")
logging.info(exception, exc_info=True)
run.steps = []
run.add_step(CommentStep(
self.pull_request, COULD_NOT_APPLY_CHANGES_DOWNSTREAM_COMMENT
))
if run.upstream_pr.has_value():
run.add_step(CommentStep(
run.upstream_pr.value(), COULD_NOT_APPLY_CHANGES_UPSTREAM_COMMENT
))
def _get_upstreamable_commits_from_local_servo_repo(self, sync: WPTSync):
local_servo_repo = sync.local_servo_repo
number_of_commits = self.pull_data["commits"]
pr_head = self.pull_data["head"]["sha"]
commit_shas = local_servo_repo.run(
"log", "--pretty=%H", pr_head, f"-{number_of_commits}"
).splitlines()
filtered_commits = []
for sha in commit_shas:
# Specifying the path here does a few things. First, it excludes any
# changes that do not touch WPT files at all. Secondly, when a file is
# moved in or out of the WPT directory the filename which is outside the
# directory becomes /dev/null and the change becomes an addition or
# deletion. This makes the patch usable on the WPT repository itself.
# TODO: If we could cleverly parse and manipulate the full commit diff
# we could avoid cloning the servo repository altogether and only
# have to fetch the commit diffs from GitHub.
# NB: The output of git show might include binary files or non-UTF8 text,
# so store the content of the diff as a `bytes`.
diff = local_servo_repo.run_without_encoding(
"show", "--binary", "--format=%b", sha, "--", UPSTREAMABLE_PATH
)
# Retrieve the diff of any changes to files that are relevant
if diff:
# Create an object that contains everything necessary to transplant this
# commit to another repository.
filtered_commits += [
{
"author": local_servo_repo.run(
"show", "-s", "--pretty=%an <%ae>", sha
),
"message": local_servo_repo.run(
"show", "-s", "--pretty=%B", sha
),
"diff": diff,
}
]
return filtered_commits
def _apply_filtered_servo_commit_to_wpt(self, run: SyncRun, commit: dict):
patch_path = os.path.join(run.sync.wpt_path, PATCH_FILE_NAME)
strip_count = UPSTREAMABLE_PATH.count("/") + 1
try:
with open(patch_path, "wb") as file:
file.write(commit["diff"])
run.sync.local_wpt_repo.run(
"apply", PATCH_FILE_NAME, "-p", str(strip_count)
)
finally:
# Ensure the patch file is not added with the other changes.
os.remove(patch_path)
run.sync.local_wpt_repo.run("add", "--all")
run.sync.local_wpt_repo.run(
"commit", "--message", commit["message"], "--author", commit["author"]
)
def _create_or_update_branch_for_pr(
self, run: SyncRun, commits: list[dict], pre_commit_callback=None
):
branch_name = wpt_branch_name_from_servo_pr_number(
self.pull_data["number"])
try:
# Create a new branch with a unique name that is consistent between
# updates of the same PR.
run.sync.local_wpt_repo.run("checkout", "-b", branch_name)
for commit in commits:
self._apply_filtered_servo_commit_to_wpt(run, commit)
if pre_commit_callback:
pre_commit_callback()
# Push the branch upstream (forcing to overwrite any existing changes).
if not run.sync.suppress_force_push:
# In order to push to our downstream branch we need to ensure that
# the local repository isn't a shallow clone. Shallow clones are
# commonly created by GitHub actions.
run.sync.local_wpt_repo.run("fetch", "--unshallow", "origin")
user = run.sync.github_username
token = run.sync.github_api_token
repo = run.sync.downstream_wpt_repo
remote_url = f"https://{user}:{token}@github.com/{repo}.git"
run.sync.local_wpt_repo.run(
"push", "-f", remote_url, branch_name)
return branch_name
finally:
try:
run.sync.local_wpt_repo.run("checkout", "master")
run.sync.local_wpt_repo.run("branch", "-D", branch_name)
except Exception:
pass
class RemoveBranchForPRStep(Step):
def __init__(self, pull_request):
Step.__init__(self, "RemoveBranchForPRStep")
self.branch_name = wpt_branch_name_from_servo_pr_number(
pull_request["number"])
def run(self, run: SyncRun):
self.name += f":{run.sync.downstream_wpt.get_branch(self.branch_name)}"
logging.info(" -> Removing branch used for upstream PR")
if not run.sync.suppress_force_push:
user = run.sync.github_username
token = run.sync.github_api_token
repo = run.sync.downstream_wpt_repo
remote_url = f"https://{user}:{token}@github.com/{repo}.git"
run.sync.local_wpt_repo.run("push", remote_url, "--delete",
self.branch_name)
class ChangePRStep(Step):
def __init__(
self,
pull_request: PullRequest,
state: str,
title: Optional[str] = None,
body: Optional[str] = None,
):
name = f"ChangePRStep:{pull_request}:{state}"
if title:
name += f":{title}"
Step.__init__(self, name)
self.pull_request = pull_request
self.state = state
self.title = title
self.body = body
def run(self, run: SyncRun):
body = self.body
if body:
body = run.prepare_body_text(body)
self.name += (
f':{textwrap.shorten(body, width=20, placeholder="...")}[{len(body)}]'
)
self.pull_request.change(state=self.state, title=self.title, body=body)
class MergePRStep(Step):
def __init__(self, pull_request: PullRequest, labels_to_remove: list[str] = []):
Step.__init__(self, f"MergePRStep:{pull_request}")
self.pull_request = pull_request
self.labels_to_remove = labels_to_remove
def run(self, run: SyncRun):
for label in self.labels_to_remove:
self.pull_request.remove_label(label)
try:
self.pull_request.merge()
except Exception as exception:
logging.warning("Could not merge PR (%s).", self.pull_request)
logging.warning(exception, exc_info=True)
run.steps = []
run.add_step(CommentStep(
self.pull_request, COULD_NOT_MERGE_CHANGES_UPSTREAM_COMMENT
))
run.add_step(CommentStep(
run.servo_pr, COULD_NOT_MERGE_CHANGES_DOWNSTREAM_COMMENT
))
self.pull_request.add_labels(["stale-servo-export"])
class OpenPRStep(Step):
def __init__(
self,
source_branch: AsyncValue[GithubBranch],
target_repo: GithubRepository,
title: str,
body: str,
labels: list[str],
):
Step.__init__(self, "OpenPRStep")
self.title = title
self.body = body
self.source_branch = source_branch
self.target_repo = target_repo
self.new_pr: AsyncValue[PullRequest] = AsyncValue()
self.labels = labels
def provides(self):
return self.new_pr
def run(self, run: SyncRun):
pull_request = self.target_repo.open_pull_request(
self.source_branch.value(), self.title, run.prepare_body_text(self.body)
)
if self.labels:
pull_request.add_labels(self.labels)
self.new_pr.resolve(pull_request)
self.name += f":{self.source_branch.value()}{self.new_pr.value()}"
class CommentStep(Step):
def __init__(self, pull_request: PullRequest, comment_template: str):
Step.__init__(self, "CommentStep")
self.pull_request = pull_request
self.comment_template = comment_template
def run(self, run: SyncRun):
comment = run.make_comment(self.comment_template)
self.name += f":{self.pull_request}:{comment}"
self.pull_request.leave_comment(comment)