integration/run.py (127 lines of code) (raw):
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import argparse
import json
import os
import re
import shutil
import subprocess
import structlog
import yaml
from taskcluster.helper import TaskclusterConfig
taskcluster = TaskclusterConfig("https://community-tc.services.mozilla.com")
logger = structlog.getLogger(__name__)
ROOT = os.path.realpath(os.path.dirname(__file__))
def configure():
"""
Load configuration from CLI args and Taskcluster secrets
"""
parser = argparse.ArgumentParser(description="Run code-review integration tests")
parser.add_argument(
"-c",
"--configuration",
help="Local configuration file replacing Taskcluster secrets",
type=open,
)
parser.add_argument(
"--clone-dir",
help="Directory where to clone repositories",
default=os.environ.get("CLONE_DIR", os.path.join(ROOT, "clone")),
)
parser.add_argument(
"--taskcluster-secret",
help="Taskcluster Secret path",
default=os.environ.get("TASKCLUSTER_SECRET"),
)
args = parser.parse_args()
taskcluster.auth()
taskcluster.load_secrets(
args.taskcluster_secret,
required=("phabricator", "admins"),
existing={"admins": ["babadie@mozilla.com"]},
local_secrets=yaml.safe_load(args.configuration)
if args.configuration
else None,
)
# Make sure the clone dir is available
os.makedirs(args.clone_dir, exist_ok=True)
# Check the url is correctly formatted
assert taskcluster.secrets["phabricator"]["url"].endswith(
"/api/"
), "Phabricator url must end in /api/"
return args
def clone(url, directory, branch="tip"):
"""
Mercurial clone with robustcheckout
"""
logger.info("Cloning repository", url=url, dir=directory)
# Parent should exist, not current dir
assert os.path.exists(os.path.dirname(directory)), "Missing parent of clone dir"
# Cleanup existing target
if os.path.exists(directory):
logger.info("Removing previous clone")
shutil.rmtree(directory)
# Now let's clone
cmd = [
"hg",
"robustcheckout",
"--purge",
f"--sharebase={directory}-shared",
f"--branch={branch}",
url,
directory,
]
subprocess.check_output(cmd)
def tip(repo_dir):
"""
Get the tip of the repo
"""
cmd = ["hg", "tip", "--template={rev}"]
rev = subprocess.check_output(cmd, cwd=repo_dir)
return int(rev)
def patch(filename, repo_dir, message):
"""
Apply a locally stored patch on the repository
and commit the difference
"""
assert os.path.isdir(repo_dir), f"Not a directory {repo_dir}"
path = os.path.join(ROOT, "patches", filename)
assert os.path.exists(path), f"Missing patch {path}"
logger.info("Applying patch", name=filename, dir=repo_dir)
cmd = [
"hg",
"import",
"--user=code-review-integration",
f"--message={message}",
path,
]
subprocess.check_output(cmd, cwd=repo_dir)
# Load revision created
rev = tip(repo_dir)
logger.info("Committed a new revision", id=rev)
return rev
def publish(repo_dir, repo_callsign, revision):
"""
Publish diff on Phabricator
from the base of the repository
"""
def _dump(path, payload):
if os.path.exists(path):
logger.info("Skip overriding arc config", path=path)
return
with open(path, "w") as f:
json.dump(payload, f, indent=4, sort_keys=True)
logger.info("Setup arc configuration", path=path)
# Write arcrc config files
phab_url = taskcluster.secrets["phabricator"]["url"]
base_url = phab_url.replace("/api/", "/")
phab_token = taskcluster.secrets["phabricator"]["token"]
_dump(os.path.expanduser("~/.arcrc"), {"hosts": {phab_url: {"token": phab_token}}})
_dump(
os.path.join(repo_dir, ".hg", ".arcconfig"),
{"repository.callsign": repo_callsign, "phabricator.uri": base_url},
)
logger.info(
"Publishing a revision on phabricator", url=phab_url, local_revision=revision
)
cmd = ["moz-phab", "submit", "--yes", "--no-lint", "--no-bug", f"{revision}"]
output = subprocess.check_output(cmd, cwd=repo_dir)
# Parse output to get the revision url on the last line
last_line = output.splitlines()[-1]
match = re.search(rf"^-> ({base_url}D\d+)$", last_line.decode("utf-8"))
assert match is not None, f"No revision found in moz-phab output:\n{output}"
return match.group(1)
def notify(message):
"""
Notify admins through email
"""
notify = taskcluster.get_service("notify")
for email in taskcluster.secrets["admins"]:
logger.info("Sending email", to=email)
notify.email(
{
"address": email,
"subject": "Code review integration test",
"content": message,
}
)
if __name__ == "__main__":
logger.info("Running integration test")
args = configure()
# Clone NSS for a shorter time than MC
nss = os.path.join(args.clone_dir, "nss")
clone("https://hg.mozilla.org/projects/nss", nss)
base = tip(nss)
# Apply a specific patch on the NSS clone
revision = patch("nss.diff", nss, "Bug XXYYZZ - Code review integration test")
# Submit commit on Phabricator instance
url = publish(nss, "NSS", revision)
# Send notification to admins
notify(f"New code-review integration test: {url}")
logger.info("All done !")