mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1698758 [wpt PR 28055] - [ci] Remove deployment of PRs to wptpr.live, a=testonly
Automatic update from web-platform-tests [ci] Remove deployment of PRs to wptpr.live (#28055) -- wpt-commits: 82c69ff8075f5c9b46c16a07fbbf317f555c9746 wpt-pr: 28055
This commit is contained in:
parent
e8d3eb601b
commit
ab68c66a43
@ -1,35 +0,0 @@
|
||||
# Create previews for pull requests on https://wptpr.live
|
||||
#
|
||||
# Mirroring pull requests to wptpr.live requires write access to the WPT GitHub
|
||||
# repository. This means that we cannot run any code from the pull request in
|
||||
# doing so, to defend against attacks from malicious pull requests. As such,
|
||||
# this workflow uses the 'pull_request_target' event which runs in the context
|
||||
# of the base repository and thus doesn't run code from the pull request. Any
|
||||
# code run in this workflow should NOT checkout or trust code from the pull
|
||||
# request.
|
||||
#
|
||||
# Note that in pull_request_target the GITHUB token is read/write.
|
||||
name: create-pr-preview
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, closed, labeled, unlabeled]
|
||||
jobs:
|
||||
update-pr-preview:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependency
|
||||
run: pip install requests
|
||||
- name: Deploy PR
|
||||
# Use a conditional step instead of a conditional job to work around #20700.
|
||||
if: github.repository == 'web-platform-tests/wpt'
|
||||
run:
|
||||
./tools/ci/pr_preview.py
|
||||
--host https://api.github.com
|
||||
--github-project web-platform-tests/wpt
|
||||
--target https://wptpr.live
|
||||
--timeout 600
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@ -1,378 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# The service provided by this script is not critical, but it shares a GitHub
|
||||
# API request quota with critical services. For this reason, all requests to
|
||||
# the GitHub API are preceded by a "guard" which verifies that the subsequent
|
||||
# request will not deplete the shared quota.
|
||||
#
|
||||
# In effect, this script will fail rather than interfere with the operation of
|
||||
# critical services.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
# The ratio of "requests remaining" to "total request quota" below which this
|
||||
# script should refuse to interact with the GitHub.com API
|
||||
API_RATE_LIMIT_THRESHOLD = 0.2
|
||||
# The GitHub Pull Request label which indicates that a Pull Request is expected
|
||||
# to be actively mirrored by the preview server
|
||||
LABEL = 'safe for preview'
|
||||
# The number of seconds to wait between attempts to verify that a submission
|
||||
# preview is available on the Pull Request preview server
|
||||
POLLING_PERIOD = 15
|
||||
# Pull Requests from authors with the following associations to the project
|
||||
# should automatically receive previews
|
||||
#
|
||||
# https://developer.github.com/v4/enum/commentauthorassociation/ (equivalent
|
||||
# documentation for the REST API was not available at the time of writing)
|
||||
TRUSTED_AUTHOR_ASSOCIATIONS = ('COLLABORATOR', 'MEMBER', 'OWNER')
|
||||
# These GitHub accounts are not associated with individuals, and the Pull
|
||||
# Requests they submit rarely require a preview.
|
||||
AUTOMATION_GITHUB_USERS = (
|
||||
'autofoolip', 'chromium-wpt-export-bot', 'moz-wptsync-bot',
|
||||
'servo-wpt-sync'
|
||||
)
|
||||
DEPLOYMENT_PREFIX = 'wpt-preview-'
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def gh_request(method_name, url, body=None, media_type=None):
|
||||
github_token = os.environ['GITHUB_TOKEN']
|
||||
|
||||
kwargs = {
|
||||
'headers': {
|
||||
'Authorization': 'token {}'.format(github_token),
|
||||
'Accept': media_type or 'application/vnd.github.v3+json'
|
||||
}
|
||||
}
|
||||
method = getattr(requests, method_name.lower())
|
||||
|
||||
if body is not None:
|
||||
kwargs['json'] = body
|
||||
|
||||
logger.info('Issuing request: %s %s', method_name.upper(), url)
|
||||
|
||||
resp = method(url, **kwargs)
|
||||
|
||||
logger.info('Response status code: %s', resp.status_code)
|
||||
|
||||
# If GitHub thinks the fields are invalid, it will send a 422 back and
|
||||
# include debugging information in the body. See
|
||||
# https://developer.github.com/v3/#client-errors
|
||||
if resp.status_code == 422:
|
||||
logger.error(resp.json())
|
||||
|
||||
resp.raise_for_status()
|
||||
|
||||
if resp.status_code == 204:
|
||||
return None
|
||||
return resp.json()
|
||||
|
||||
class GitHubRateLimitException(Exception):
|
||||
pass
|
||||
|
||||
def guard(resource):
|
||||
'''Decorate a `Project` instance method which interacts with the GitHub
|
||||
API, ensuring that the subsequent request will not deplete the relevant
|
||||
allowance. This verification does not itself influence rate limiting:
|
||||
|
||||
> Accessing this endpoint does not count against your REST API rate limit.
|
||||
|
||||
https://developer.github.com/v3/rate_limit/
|
||||
'''
|
||||
def guard_decorator(func):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
limits = gh_request('GET', '{}/rate_limit'.format(self._host))
|
||||
|
||||
values = limits['resources'].get(resource)
|
||||
|
||||
remaining = values['remaining']
|
||||
limit = values['limit']
|
||||
|
||||
logger.info(
|
||||
'Limit for "%s" resource: %s/%s', resource, remaining, limit
|
||||
)
|
||||
|
||||
if limit and float(remaining) / limit < API_RATE_LIMIT_THRESHOLD:
|
||||
raise GitHubRateLimitException(
|
||||
'Exiting to avoid GitHub.com API request throttling.'
|
||||
)
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
return guard_decorator
|
||||
|
||||
class Project(object):
|
||||
def __init__(self, host, github_project):
|
||||
self._host = host
|
||||
self._github_project = github_project
|
||||
|
||||
@guard('core')
|
||||
def create_ref(self, refspec, revision):
|
||||
url = '{}/repos/{}/git/refs'.format(self._host, self._github_project)
|
||||
|
||||
logger.info('Creating ref "%s" (%s)', refspec, revision)
|
||||
|
||||
gh_request('POST', url, {
|
||||
'ref': 'refs/{}'.format(refspec),
|
||||
'sha': revision
|
||||
})
|
||||
|
||||
@guard('core')
|
||||
def get_ref_revision(self, refspec):
|
||||
url = '{}/repos/{}/git/refs/{}'.format(
|
||||
self._host, self._github_project, refspec
|
||||
)
|
||||
|
||||
logger.info('Fetching ref "%s"', refspec)
|
||||
|
||||
try:
|
||||
body = gh_request('GET', url)
|
||||
logger.info('Ref data: %s', json.dumps(body, indent=2))
|
||||
return body['object']['sha']
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
return None
|
||||
raise e
|
||||
|
||||
@guard('core')
|
||||
def update_ref(self, refspec, revision):
|
||||
url = '{}/repos/{}/git/refs/{}'.format(
|
||||
self._host, self._github_project, refspec
|
||||
)
|
||||
|
||||
logger.info('Updating ref "%s" (%s)', refspec, revision)
|
||||
|
||||
gh_request('PATCH', url, {'sha': revision})
|
||||
|
||||
@guard('core')
|
||||
def delete_ref(self, refspec):
|
||||
url = '{}/repos/{}/git/refs/{}'.format(
|
||||
self._host, self._github_project, refspec
|
||||
)
|
||||
|
||||
logger.info('Deleting ref "%s"', refspec)
|
||||
|
||||
gh_request('DELETE', url)
|
||||
|
||||
@guard('core')
|
||||
def create_deployment(self, pull_request, revision):
|
||||
url = '{}/repos/{}/deployments'.format(
|
||||
self._host, self._github_project
|
||||
)
|
||||
# The Pull Request preview system only exposes one Deployment for a
|
||||
# given Pull Request. Identifying the Deployment by the Pull Request
|
||||
# number ensures that GitHub.com automatically responds to new
|
||||
# Deployments by designating prior Deployments as "inactive"
|
||||
environment = DEPLOYMENT_PREFIX + str(pull_request['number'])
|
||||
|
||||
logger.info('Creating Deployment "%s" for "%s"', environment, revision)
|
||||
|
||||
return gh_request('POST', url, {
|
||||
'ref': revision,
|
||||
'environment': environment,
|
||||
'auto_merge': False,
|
||||
# Pull Request previews are created regardless of GitHub Commit
|
||||
# Status Checks, so Status Checks should be ignored when creating
|
||||
# GitHub Deployments.
|
||||
'required_contexts': []
|
||||
}, 'application/vnd.github.ant-man-preview+json')
|
||||
|
||||
@guard('core')
|
||||
def get_deployment(self, revision):
|
||||
url = '{}/repos/{}/deployments?sha={}'.format(
|
||||
self._host, self._github_project, revision
|
||||
)
|
||||
|
||||
deployments = gh_request('GET', url)
|
||||
|
||||
return deployments.pop() if len(deployments) else None
|
||||
|
||||
@guard('core')
|
||||
def add_deployment_status(self, target, deployment, state, description=''):
|
||||
if state in ('pending', 'success'):
|
||||
pr_number = deployment['environment'][len(DEPLOYMENT_PREFIX):]
|
||||
environment_url = '{}/{}'.format(target, pr_number)
|
||||
else:
|
||||
environment_url = None
|
||||
url = '{}/repos/{}/deployments/{}/statuses'.format(
|
||||
self._host, self._github_project, deployment['id']
|
||||
)
|
||||
|
||||
gh_request('POST', url, {
|
||||
'state': state,
|
||||
'description': description,
|
||||
'environment_url': environment_url
|
||||
}, 'application/vnd.github.ant-man-preview+json')
|
||||
|
||||
def is_open(pull_request):
|
||||
return not pull_request['closed_at']
|
||||
|
||||
def has_mirroring_label(pull_request):
|
||||
for label in pull_request['labels']:
|
||||
if label['name'] == LABEL:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def should_be_mirrored(project, pull_request):
|
||||
return (
|
||||
is_open(pull_request) and (
|
||||
has_mirroring_label(pull_request) or (
|
||||
pull_request['user']['login'] not in AUTOMATION_GITHUB_USERS and
|
||||
pull_request['author_association'] in TRUSTED_AUTHOR_ASSOCIATIONS
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def is_deployed(host, deployment):
|
||||
worktree_name = deployment['environment'][len(DEPLOYMENT_PREFIX):]
|
||||
url = '{}/.git/worktrees/{}/HEAD'.format(host, worktree_name)
|
||||
logger.info('Issuing request: GET %s', url)
|
||||
response = requests.get(url)
|
||||
|
||||
logger.info('Response status code: %s', response.status_code)
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
|
||||
logger.info('Response text: %s', response.text.strip())
|
||||
return response.text.strip() == deployment['sha']
|
||||
|
||||
def update_mirror_refs(project, pull_request):
|
||||
'''Update the WPT refs that control mirroring of this pull request.
|
||||
|
||||
Two sets of refs are used to control wptpr.live's mirroring of pull
|
||||
requests:
|
||||
|
||||
1. refs/prs-trusted-for-preview/{number}
|
||||
2. refs/prs-open/{number}
|
||||
|
||||
wptpr.live will only mirror a pull request if both exist for the given pull
|
||||
request number; otherwise the pull request is either not open or is not
|
||||
trustworthy (e.g. came from someone who doesn't have push access anyway.)
|
||||
|
||||
This method returns the revision that is being mirrored, or None if the
|
||||
pull request should not be mirrored.
|
||||
'''
|
||||
|
||||
refspec_trusted = 'prs-trusted-for-preview/{number}'.format(
|
||||
**pull_request
|
||||
)
|
||||
refspec_open = 'prs-open/{number}'.format(**pull_request)
|
||||
|
||||
revision_latest = pull_request['head']['sha']
|
||||
revision_trusted = project.get_ref_revision(refspec_trusted)
|
||||
revision_open = project.get_ref_revision(refspec_open)
|
||||
|
||||
if should_be_mirrored(project, pull_request):
|
||||
logger.info('Pull Request should be mirrored')
|
||||
|
||||
if revision_trusted is None:
|
||||
project.create_ref(refspec_trusted, revision_latest)
|
||||
elif revision_trusted != revision_latest:
|
||||
project.update_ref(refspec_trusted, revision_latest)
|
||||
|
||||
if revision_open is None:
|
||||
project.create_ref(refspec_open, revision_latest)
|
||||
elif revision_open != revision_latest:
|
||||
project.update_ref(refspec_open, revision_latest)
|
||||
|
||||
return revision_latest
|
||||
|
||||
logger.info('Pull Request should not be mirrored')
|
||||
|
||||
if not has_mirroring_label(pull_request) and revision_trusted is not None:
|
||||
project.delete_ref(refspec_trusted)
|
||||
|
||||
if revision_open is not None and not is_open(pull_request):
|
||||
project.delete_ref(refspec_open)
|
||||
|
||||
# No revision to be deployed to wptpr.live
|
||||
return None
|
||||
|
||||
class DeploymentFailedException(Exception):
|
||||
pass
|
||||
|
||||
def deploy(project, target, pull_request, revision, timeout):
|
||||
'''Create a GitHub deployment for the given pull request and revision.
|
||||
|
||||
This method creates a pending GitHub deployment, waits for the
|
||||
corresponding revision to be available on wptpr.live and marks the
|
||||
deployment as successful. If the revision does not appear in the given
|
||||
timeout, the deployment is marked as errored instead.'''
|
||||
if project.get_deployment(revision) is not None:
|
||||
return
|
||||
|
||||
deployment = project.create_deployment(pull_request, revision)
|
||||
|
||||
message = 'Waiting up to {} seconds for Deployment {} to be available on {}'.format(
|
||||
timeout, deployment['environment'], target
|
||||
)
|
||||
logger.info(message)
|
||||
project.add_deployment_status(target, deployment, 'pending', message)
|
||||
|
||||
start = time.time()
|
||||
while not is_deployed(target, deployment):
|
||||
if time.time() - start > timeout:
|
||||
message = 'Deployment did not become available after {} seconds'.format(timeout)
|
||||
project.add_deployment_status(target, deployment, 'error', message)
|
||||
raise DeploymentFailedException(message)
|
||||
|
||||
time.sleep(POLLING_PERIOD)
|
||||
|
||||
result = project.add_deployment_status(target, deployment, 'success')
|
||||
logger.info(json.dumps(result, indent=2))
|
||||
|
||||
def main(host, github_project, target, timeout):
|
||||
project = Project(host, github_project)
|
||||
|
||||
with open(os.environ['GITHUB_EVENT_PATH']) as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
logger.info('Event data: %s', json.dumps(data, indent=2))
|
||||
|
||||
pull_request = data['pull_request']
|
||||
|
||||
logger.info('Processing Pull Request #%(number)d', pull_request)
|
||||
|
||||
revision_to_mirror = update_mirror_refs(project, pull_request)
|
||||
if revision_to_mirror:
|
||||
deploy(project, target, pull_request, revision_to_mirror, timeout)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='''Mirror a pull request to an externally-hosted preview
|
||||
system, and create a GitHub Deployment associated with the pull
|
||||
request pointing at the preview.'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'--host', required=True, help='the location of the GitHub API server'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--github-project',
|
||||
required=True,
|
||||
help='''the GitHub organization and GitHub project name, separated by
|
||||
a forward slash (e.g. "web-platform-tests/wpt")'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'--target',
|
||||
required=True,
|
||||
help='''the URL of the website to which submission previews are
|
||||
expected to become available'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timeout',
|
||||
type=int,
|
||||
required=True,
|
||||
help='''the number of seconds to wait for a submission preview to
|
||||
become available before reporting a GitHub Deployment failure'''
|
||||
)
|
||||
|
||||
values = dict(vars(parser.parse_args()))
|
||||
main(**values)
|
@ -1,683 +0,0 @@
|
||||
try:
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
except ImportError:
|
||||
# Python 3 case
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import contextlib
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
import pr_preview
|
||||
|
||||
|
||||
TEST_HOST = 'localhost'
|
||||
|
||||
|
||||
def same_members(a, b):
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
a_copy = list(a)
|
||||
for elem in b:
|
||||
try:
|
||||
a_copy.remove(elem)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return len(a_copy) == 0
|
||||
|
||||
|
||||
# When these tests are executed in Windows, files in the temporary git
|
||||
# repositories may be marked as "read only" at the moment they are intended to
|
||||
# be deleted. The following handler for `shutil.rmtree` accounts for this by
|
||||
# making the files writable and attempting to delete them a second time.
|
||||
#
|
||||
# Source:
|
||||
# https://stackoverflow.com/questions/1213706/what-user-do-python-scripts-run-as-in-windows
|
||||
def handle_remove_readonly(func, path, exc):
|
||||
excvalue = exc[1]
|
||||
candidates = (os.rmdir, os.remove, os.unlink)
|
||||
if func in candidates and excvalue.errno == errno.EACCES:
|
||||
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
|
||||
func(path)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class MockHandler(BaseHTTPRequestHandler, object):
|
||||
def do_all(self):
|
||||
path = self.path.split('?')[0]
|
||||
request_body = None
|
||||
|
||||
if 'Content-Length' in self.headers:
|
||||
request_body = self.rfile.read(
|
||||
int(self.headers['Content-Length'])
|
||||
).decode('utf-8')
|
||||
|
||||
if self.headers.get('Content-Type') == 'application/json':
|
||||
request_body = json.loads(request_body)
|
||||
|
||||
for request, response in self.server.expected_traffic:
|
||||
if request[0] != self.command:
|
||||
continue
|
||||
if request[1] != path:
|
||||
continue
|
||||
body_matches = True
|
||||
for key in request[2]:
|
||||
body_matches &= request[2][key] == request_body.get(key)
|
||||
if not body_matches:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
request = (self.command, path, request_body)
|
||||
response = (400, {})
|
||||
|
||||
self.server.actual_traffic.append((request, response))
|
||||
self.send_response(response[0])
|
||||
self.end_headers()
|
||||
if self.server.reponse_body_is_json:
|
||||
self.wfile.write(json.dumps(response[1]).encode('utf-8'))
|
||||
else:
|
||||
self.wfile.write(response[1].encode('utf-8'))
|
||||
|
||||
def do_DELETE(self):
|
||||
return self.do_all()
|
||||
|
||||
def do_GET(self):
|
||||
return self.do_all()
|
||||
|
||||
def do_PATCH(self):
|
||||
return self.do_all()
|
||||
|
||||
def do_POST(self):
|
||||
return self.do_all()
|
||||
|
||||
|
||||
class MockServer(HTTPServer, object):
|
||||
'''HTTP server that responds to all requests with status code 200 and body
|
||||
'{}' unless an alternative status code and body are specified for the given
|
||||
method and path in the `responses` parameter.'''
|
||||
def __init__(self, address, expected_traffic, reponse_body_is_json=True):
|
||||
super(MockServer, self).__init__(address, MockHandler)
|
||||
self.expected_traffic = expected_traffic
|
||||
self.actual_traffic = []
|
||||
self.reponse_body_is_json = reponse_body_is_json
|
||||
|
||||
def __enter__(self):
|
||||
threading.Thread(target=lambda: self.serve_forever()).start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.shutdown()
|
||||
|
||||
|
||||
class Requests(object):
|
||||
get_rate = ('GET', '/rate_limit', {})
|
||||
ref_create_open = (
|
||||
'POST', '/repos/test-org/test-repo/git/refs', {'ref':'refs/prs-open/45'}
|
||||
)
|
||||
ref_create_trusted = (
|
||||
'POST',
|
||||
'/repos/test-org/test-repo/git/refs',
|
||||
{'ref':'refs/prs-trusted-for-preview/45'}
|
||||
)
|
||||
ref_get_open = (
|
||||
'GET', '/repos/test-org/test-repo/git/refs/prs-open/45', {}
|
||||
)
|
||||
ref_get_trusted = (
|
||||
'GET', '/repos/test-org/test-repo/git/refs/prs-trusted-for-preview/45', {}
|
||||
)
|
||||
ref_update_open = (
|
||||
'PATCH', '/repos/test-org/test-repo/git/refs/prs-open/45', {}
|
||||
)
|
||||
ref_update_trusted = (
|
||||
'PATCH', '/repos/test-org/test-repo/git/refs/prs-trusted-for-preview/45', {}
|
||||
)
|
||||
ref_delete_open = (
|
||||
'DELETE', '/repos/test-org/test-repo/git/refs/prs-open/45', {}
|
||||
)
|
||||
ref_delete_trusted = (
|
||||
'DELETE', '/repos/test-org/test-repo/git/refs/prs-trusted-for-preview/45', {}
|
||||
)
|
||||
deployment_get = ('GET', '/repos/test-org/test-repo/deployments', {})
|
||||
deployment_create = ('POST', '/repos/test-org/test-repo/deployments', {})
|
||||
deployment_status_create_pending = (
|
||||
'POST',
|
||||
'/repos/test-org/test-repo/deployments/24601/statuses',
|
||||
{'state':'pending'}
|
||||
)
|
||||
deployment_status_create_error = (
|
||||
'POST',
|
||||
'/repos/test-org/test-repo/deployments/24601/statuses',
|
||||
{'state':'error'}
|
||||
)
|
||||
deployment_status_create_success = (
|
||||
'POST',
|
||||
'/repos/test-org/test-repo/deployments/24601/statuses',
|
||||
{'state':'success'}
|
||||
)
|
||||
preview = ('GET', '/.git/worktrees/45/HEAD', {})
|
||||
|
||||
|
||||
class Responses(object):
|
||||
no_limit = (200, {
|
||||
'resources': {
|
||||
'search': {
|
||||
'remaining': 100,
|
||||
'limit': 100
|
||||
},
|
||||
'core': {
|
||||
'remaining': 100,
|
||||
'limit': 100
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def temp_repo():
|
||||
original_dir = os.getcwd()
|
||||
directory = tempfile.mkdtemp()
|
||||
os.chdir(directory)
|
||||
|
||||
try:
|
||||
subprocess.check_call(['git', 'init'], cwd=directory)
|
||||
# Explicitly create the default branch.
|
||||
subprocess.check_call(
|
||||
['git', 'checkout', '-b', 'master'],
|
||||
cwd=directory
|
||||
)
|
||||
subprocess.check_call(
|
||||
['git', 'config', 'user.name', 'example'],
|
||||
cwd=directory
|
||||
)
|
||||
subprocess.check_call(
|
||||
['git', 'config', 'user.email', 'example@example.com'],
|
||||
cwd=directory
|
||||
)
|
||||
subprocess.check_call(
|
||||
['git', 'commit', '--allow-empty', '-m', 'first'],
|
||||
cwd=directory
|
||||
)
|
||||
|
||||
yield directory
|
||||
finally:
|
||||
os.chdir(original_dir)
|
||||
shutil.rmtree(
|
||||
directory, ignore_errors=False, onerror=handle_remove_readonly
|
||||
)
|
||||
|
||||
def update_mirror_refs(pull_request, expected_traffic):
|
||||
os.environ['GITHUB_TOKEN'] = 'c0ffee'
|
||||
|
||||
github_server = MockServer((TEST_HOST, 0), expected_traffic)
|
||||
github_port = github_server.server_address[1]
|
||||
|
||||
method_threw = False
|
||||
with temp_repo(), github_server:
|
||||
project = pr_preview.Project(
|
||||
'http://{}:{}'.format(TEST_HOST, github_port),
|
||||
'test-org/test-repo',
|
||||
)
|
||||
try:
|
||||
pr_preview.update_mirror_refs(project, pull_request)
|
||||
except pr_preview.GitHubRateLimitException:
|
||||
method_threw = True
|
||||
|
||||
return (
|
||||
method_threw,
|
||||
github_server.actual_traffic,
|
||||
)
|
||||
|
||||
|
||||
def deploy(pr_num, revision, expected_github_traffic, expected_preview_traffic):
|
||||
os.environ['GITHUB_TOKEN'] = 'c0ffee'
|
||||
|
||||
github_server = MockServer((TEST_HOST, 0), expected_github_traffic)
|
||||
github_port = github_server.server_address[1]
|
||||
preview_server = MockServer((TEST_HOST, 0), expected_preview_traffic, reponse_body_is_json=False)
|
||||
preview_port = preview_server.server_address[1]
|
||||
|
||||
method_threw = False
|
||||
with github_server, preview_server:
|
||||
project = pr_preview.Project(
|
||||
'http://{}:{}'.format(TEST_HOST, github_port),
|
||||
'test-org/test-repo',
|
||||
)
|
||||
target = 'http://{}:{}'.format(TEST_HOST, preview_port)
|
||||
pull_request = {'number': pr_num}
|
||||
timeout = 1
|
||||
try:
|
||||
pr_preview.deploy(project, target, pull_request, revision, timeout)
|
||||
except (pr_preview.GitHubRateLimitException, pr_preview.DeploymentFailedException):
|
||||
method_threw = True
|
||||
|
||||
return (
|
||||
method_threw,
|
||||
github_server.actual_traffic,
|
||||
preview_server.actual_traffic
|
||||
)
|
||||
|
||||
def test_update_mirror_refs_fail_rate_limited():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, (
|
||||
200,
|
||||
{
|
||||
'resources': {
|
||||
'core': {
|
||||
'remaining': 1,
|
||||
'limit': 10
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_synchronize_ignore_closed():
|
||||
# No existing refs, but a closed PR event comes in. Nothing should happen.
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': '2019-10-28',
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_collaborator():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_open, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_trusted, (200, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic, = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_ignore_collaborator_bot():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'chromium-wpt-export-bot'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_ignore_untrusted_contributor():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'CONTRIBUTOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_trusted_contributor():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
# user here is a contributor (untrusted), but the issue
|
||||
# has been labelled as safe.
|
||||
'labels': [{'name': 'safe for preview'}],
|
||||
'user': {'login': 'Hexcles'},
|
||||
'author_association': 'CONTRIBUTOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_open, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_trusted, (200, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_synchronize_sync_bot_with_label():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
# user here is a bot which is normally not mirrored,
|
||||
# but the issue has been labelled as safe.
|
||||
'labels': [{'name': 'safe for preview'}],
|
||||
'user': {'login': 'chromium-wpt-export-bot'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (404, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_open, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_create_trusted, (200, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_update_collaborator():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_update_open, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_update_trusted, (200, {})),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_synchronize_update_member():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'jgraham'},
|
||||
'author_association': 'MEMBER',
|
||||
'closed_at': None,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_update_open, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_update_trusted, (200, {}))
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_update_mirror_refs_delete_collaborator():
|
||||
pull_request = {
|
||||
'number': 45,
|
||||
'head': {'sha': 'abc123'},
|
||||
'labels': [],
|
||||
'user': {'login': 'stephenmcgruer'},
|
||||
'author_association': 'COLLABORATOR',
|
||||
'closed_at': 2019-10-30,
|
||||
}
|
||||
expected_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_trusted, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_get_open, (
|
||||
200,
|
||||
{
|
||||
'object': {'sha': 'def234'},
|
||||
}
|
||||
)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_delete_trusted, (204, None)),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.ref_delete_open, (204, None)),
|
||||
]
|
||||
|
||||
method_threw, actual_traffic = update_mirror_refs(
|
||||
pull_request, expected_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert same_members(expected_traffic, actual_traffic)
|
||||
|
||||
def test_deploy_fail_rate_limited():
|
||||
expected_github_traffic = [
|
||||
(Requests.get_rate, (
|
||||
200,
|
||||
{
|
||||
'resources': {
|
||||
'core': {
|
||||
'remaining': 1,
|
||||
'limit': 10
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
]
|
||||
expected_preview_traffic = []
|
||||
|
||||
pr_num = 45
|
||||
revision = "abcdef123"
|
||||
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
|
||||
pr_num, revision, expected_github_traffic, expected_preview_traffic
|
||||
)
|
||||
|
||||
assert method_threw
|
||||
assert actual_github_traffic == expected_github_traffic
|
||||
assert actual_preview_traffic == expected_preview_traffic
|
||||
|
||||
def test_deploy_success():
|
||||
pr_num = 45
|
||||
revision = 'abcdef123'
|
||||
|
||||
expected_github_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_get, (200, [])),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_create, (200, {
|
||||
'id': 24601,
|
||||
'sha': revision,
|
||||
'environment': 'wpt-preview-45',
|
||||
})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_pending, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_success, (200, {}))
|
||||
]
|
||||
expected_preview_traffic = [
|
||||
(Requests.preview, (200, revision))
|
||||
]
|
||||
|
||||
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
|
||||
pr_num, revision, expected_github_traffic, expected_preview_traffic
|
||||
)
|
||||
|
||||
assert not method_threw
|
||||
assert actual_github_traffic == expected_github_traffic
|
||||
assert actual_preview_traffic == expected_preview_traffic
|
||||
|
||||
def test_deploy_timeout_missing():
|
||||
pr_num = 45
|
||||
revision = 'abcdef123'
|
||||
|
||||
expected_github_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_get, (200, [])),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_create, (200, {
|
||||
'id': 24601,
|
||||
'sha': revision,
|
||||
'environment': 'wpt-preview-45',
|
||||
})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_pending, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_error, (200, {}))
|
||||
]
|
||||
expected_preview_traffic = [
|
||||
(Requests.preview, (404, ""))
|
||||
]
|
||||
|
||||
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
|
||||
pr_num, revision, expected_github_traffic, expected_preview_traffic
|
||||
)
|
||||
|
||||
assert method_threw
|
||||
assert expected_github_traffic == actual_github_traffic
|
||||
ping_count = len(actual_preview_traffic)
|
||||
assert ping_count > 0
|
||||
assert actual_preview_traffic == expected_preview_traffic * ping_count
|
||||
|
||||
def test_deploy_timeout_wrong_revision():
|
||||
pr_num = 45
|
||||
revision = 'abcdef123'
|
||||
|
||||
expected_github_traffic = [
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_get, (200, [])),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_create, (200, {
|
||||
'id': 24601,
|
||||
'sha': revision,
|
||||
'environment': 'wpt-preview-45',
|
||||
})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_pending, (200, {})),
|
||||
(Requests.get_rate, Responses.no_limit),
|
||||
(Requests.deployment_status_create_error, (200, {}))
|
||||
]
|
||||
expected_preview_traffic = [
|
||||
# wptpr.live has the wrong revision deployed
|
||||
(Requests.preview, (200, 'ghijkl456'))
|
||||
]
|
||||
|
||||
method_threw, actual_github_traffic, actual_preview_traffic = deploy(
|
||||
pr_num, revision, expected_github_traffic, expected_preview_traffic
|
||||
)
|
||||
|
||||
assert method_threw
|
||||
assert expected_github_traffic == actual_github_traffic
|
||||
ping_count = len(actual_preview_traffic)
|
||||
assert ping_count > 0
|
||||
assert actual_preview_traffic == expected_preview_traffic * ping_count
|
Loading…
Reference in New Issue
Block a user