* Add Github http link to SDK/Rest parameters

* Cast folder as str (can be Path)

* Basic JSON-RPC server

* Allow all hosts

* Gitattributes for EOL

* Add Github handler basic

* Fix getting token

* Plug generate from README

* Fix indentation

* Fix input path

* Add exception handler

* Add exists clause

* Fix base branch

* Error does not have a required message

* Add SDK subfolder

* Conf user + fix reponse

* Handle creation of issue

* Improve comments

* Improve error message

* Add help

* Improve message

* Fix return type of build_sdk

* Add rest endpoint

* Take SDKID as query parameter

* Add PR hook

* Handle PR from a fork

* Improve already existing PR

* Improve do pr to return already existing one if there is

* Fix conflict PR code

* Add rebuild endpoint

* Fix endpoint

* Fix rebuild command

* Add local log

* Fix build sdk for Windows

* Switch current to master

* Fix build_libraries API after rebase against master

* Adapt webhook code to skip_callback

* Enabling SwaggerToSdk logs

* Add PR close action

* Add synchronize/pr event

* Add Consume thread

* Fix thread loop

* Fix comment on close

* Add Delivery in log

* Plug auto-conf detection

* Add labels to SDK PRs

* Add sdkbase and make branch creation more robust
This commit is contained in:
Laurent Mazuel 2018-01-23 11:42:09 -08:00 коммит произвёл GitHub
Родитель bcd65625d4
Коммит 784a5d3d70
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 1015 добавлений и 48 удалений

1
.gitattributes поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
*.py text

Просмотреть файл

@ -41,7 +41,7 @@ ENV LC_ALL en_US.UTF-8
COPY setup.py /tmp
COPY swaggertosdk /tmp/swaggertosdk/
WORKDIR /tmp
RUN pip3.6 install .
RUN pip3.6 install .[rest]
WORKDIR /git-restapi
ENTRYPOINT ["python3.6", "-m", "swaggertosdk"]

Просмотреть файл

@ -1,4 +1,4 @@
-e .
-e .[rest]
pytest-cov
pytest>=3.2.0
pylint

Просмотреть файл

@ -24,5 +24,13 @@ setup(
"requests",
"mistune",
"pyyaml",
]
"cookiecutter",
"wheel"
],
extras_require={
'rest': [
'flask',
'json-rpc'
]
}
)

Просмотреть файл

@ -1,13 +1,14 @@
import os
import logging
from contextlib import contextmanager
import json
import logging
import os
import re
import shutil
import stat
import subprocess
import tempfile
from pathlib import Path
from contextlib import contextmanager
from urllib.parse import urlparse
from git import Repo
from github import Github, GithubException, UnknownObjectException
@ -116,9 +117,9 @@ def do_commit(repo, message_template, branch_name, hexsha):
checkout_and_create_branch(repo, branch_name)
msg = message_template.format(hexsha=hexsha)
repo.index.commit(msg)
commit = repo.index.commit(msg)
_LOGGER.info("Commit done: %s", msg)
return True
return commit.hexsha
def sync_fork(gh_token, github_repo_id, repo):
@ -242,7 +243,7 @@ def configure_user(gh_token, repo):
git config --global user.name "Your Name"
"""
user = user_from_token(gh_token)
repo.git.config('user.email', user.email or 'autorestci@microsoft.com')
repo.git.config('user.email', user.email or 'aspysdk2@microsoft.com')
repo.git.config('user.name', user.name or 'SwaggerToSDK Automation')
@ -258,7 +259,7 @@ def compute_branch_name(branch_name, gh_token=None):
return DEFAULT_TRAVIS_BRANCH_NAME.format(branch=os.environ['TRAVIS_BRANCH'])
return DEFAULT_TRAVIS_PR_BRANCH_NAME.format(number=pr_object.number)
def do_pr(gh_token, sdk_git_id, sdk_pr_target_repo_id, branch_name, base_branch):
def do_pr(gh_token, sdk_git_id, sdk_pr_target_repo_id, branch_name, base_branch, pr_body=""):
"Do the PR"
if not gh_token:
_LOGGER.info('Skipping the PR, no token found')
@ -276,31 +277,30 @@ def do_pr(gh_token, sdk_git_id, sdk_pr_target_repo_id, branch_name, base_branch)
head_name = "{}:{}".format(sdk_git_owner, branch_name)
else:
head_name = branch_name
sdk_git_repo = github_con.get_repo(sdk_git_id)
sdk_git_owner = sdk_git_repo.owner.login
body = ''
rest_api_pr = get_initial_pr(gh_token)
if rest_api_pr:
body += "Generated from RestAPI PR: {}".format(rest_api_pr.html_url)
try:
github_pr = sdk_pr_target_repo.create_pull(
title='Automatic PR from {}'.format(branch_name),
body=body,
body=pr_body,
head=head_name,
base=base_branch
)
except GithubException as err:
if err.status == 422 and err.data['errors'][0]['message'].startswith('A pull request already exists'):
_LOGGER.info('PR already exists, it was a commit on an open PR')
return
if err.status == 422 and err.data['errors'][0].get('message', '').startswith('A pull request already exists'):
matching_pulls = sdk_pr_target_repo.get_pulls(base=base_branch, head=sdk_git_owner+":"+head_name)
matching_pull = matching_pulls[0]
_LOGGER.info('PR already exists: '+matching_pull.html_url)
return matching_pull
raise
_LOGGER.info("Made PR %s", github_pr.html_url)
comment = compute_pr_comment_with_sdk_pr(github_pr.html_url, sdk_git_id, branch_name)
add_comment_to_initial_pr(gh_token, comment)
return github_pr
def get_swagger_hexsha(restapi_git_folder):
"""Get the SHA1 of the current repo"""
repo = Repo(restapi_git_folder)
repo = Repo(str(restapi_git_folder))
if repo.bare:
not_git_hexsha = "notgitrepo"
_LOGGER.warning("Not a git repo, SHA1 used will be: %s", not_git_hexsha)
@ -330,9 +330,16 @@ def add_comment_to_initial_pr(gh_token, comment):
return True
def clone_to_path(gh_token, temp_dir, sdk_git_id):
"""Clone the given repo_id to the 'sdk' folder in given temp_dir"""
def clone_to_path(gh_token, temp_dir, sdk_git_id, branch=None):
"""Clone the given repo_id to the temp_dir folder.
:param str branch: If specified, switch to this branch. Branch must exist.
"""
_LOGGER.info("Clone SDK repository %s", sdk_git_id)
url_parsing = urlparse(sdk_git_id)
sdk_git_id = url_parsing.path
if sdk_git_id.startswith("/"):
sdk_git_id = sdk_git_id[1:]
credentials_part = ''
if gh_token:
@ -348,11 +355,14 @@ def clone_to_path(gh_token, temp_dir, sdk_git_id):
credentials=credentials_part,
sdk_git_id=sdk_git_id
)
sdk_path = os.path.join(temp_dir, 'sdk')
Repo.clone_from(https_authenticated_url, sdk_path)
_LOGGER.info("Clone success")
repo = Repo.clone_from(https_authenticated_url, str(temp_dir))
# Do NOT clone and set branch at the same time, since we allow branch to be a SHA1
# And you can't clone a SHA1
if branch:
_LOGGER.info("Checkout branch %s", branch)
repo.git.checkout(branch)
return sdk_path
_LOGGER.info("Clone success")
def remove_readonly(func, path, _):
"Clear the readonly bit and reattempt the removal"
@ -360,17 +370,23 @@ def remove_readonly(func, path, _):
func(path)
@contextmanager
def manage_sdk_folder(gh_token, temp_dir, sdk_git_id):
def manage_git_folder(gh_token, temp_dir, git_id):
"""Context manager to avoid readonly problem while cleanup the temp dir"""
sdk_path = clone_to_path(gh_token, temp_dir, sdk_git_id)
_LOGGER.debug("SDK path %s", sdk_path)
_LOGGER.debug("Git ID %s", git_id)
if Path(git_id).exists():
yield git_id
return None # Do not erase a local folder, just skip here
# Clone the specific branch
split_git_id = git_id.split("@")
branch = split_git_id[1] if len(split_git_id) > 1 else None
clone_to_path(gh_token, temp_dir, split_git_id[0], branch=branch)
try:
yield sdk_path
yield temp_dir
# Pre-cleanup for Windows http://bugs.python.org/issue26660
finally:
_LOGGER.debug("Preclean SDK folder")
shutil.rmtree(sdk_path, onerror=remove_readonly)
_LOGGER.debug("Preclean Rest folder")
shutil.rmtree(temp_dir, onerror=remove_readonly)
def get_full_sdk_id(gh_token, sdk_git_id):
"""If the SDK git id is incomplete, try to complete it with user login"""

Просмотреть файл

@ -3,6 +3,8 @@ import os
import logging
import tempfile
from git import Repo, GitCommandError
from pathlib import Path
import sys
from .SwaggerToSdkCore import (
IS_TRAVIS,
@ -12,7 +14,7 @@ from .SwaggerToSdkCore import (
DEFAULT_TRAVIS_BRANCH_NAME,
DEFAULT_TRAVIS_PR_BRANCH_NAME,
get_full_sdk_id,
manage_sdk_folder,
manage_git_folder,
compute_branch_name,
configure_user,
sync_fork,
@ -22,6 +24,7 @@ from .SwaggerToSdkCore import (
do_commit,
do_pr,
add_comment_to_initial_pr,
compute_pr_comment_with_sdk_pr,
get_swagger_project_files_in_pr,
get_commit_object_from_travis,
extract_conf_from_readmes,
@ -34,16 +37,17 @@ from .autorest_tools import (
_LOGGER = logging.getLogger(__name__)
def generate_sdk(gh_token, config_path, project_pattern, restapi_git_folder,
def generate_sdk(gh_token, config_path, project_pattern, restapi_git_id,
sdk_git_id, pr_repo_id, message_template, base_branch_name, branch_name,
autorest_bin=None):
"""Main method of the the file"""
sdk_git_id = get_full_sdk_id(gh_token, sdk_git_id)
with tempfile.TemporaryDirectory() as temp_dir, \
manage_sdk_folder(gh_token, temp_dir, sdk_git_id) as sdk_folder:
manage_git_folder(gh_token, Path(temp_dir) / Path("rest"), restapi_git_id) as restapi_git_folder, \
manage_git_folder(gh_token, Path(temp_dir) / Path("sdk"), sdk_git_id) as sdk_folder:
sdk_repo = Repo(sdk_folder)
sdk_repo = Repo(str(sdk_folder))
if gh_token:
branch_name = compute_branch_name(branch_name, gh_token)
@ -104,7 +108,10 @@ def generate_sdk(gh_token, config_path, project_pattern, restapi_git_folder,
if do_commit(sdk_repo, message_template, branch_name, hexsha):
sdk_repo.git.push('origin', branch_name, set_upstream=True)
if pr_repo_id:
do_pr(gh_token, sdk_git_id, pr_repo_id, branch_name, base_branch_name)
pr_body = "Generated from PR: {}".format(initial_pr.html_url)
github_pr = do_pr(gh_token, sdk_git_id, pr_repo_id, branch_name, base_branch_name, pr_body)
comment = compute_pr_comment_with_sdk_pr(github_pr.html_url, sdk_git_id, branch_name)
add_comment_to_initial_pr(gh_token, comment)
else:
add_comment_to_initial_pr(gh_token, "No modification for {}".format(sdk_git_id))
else:
@ -113,8 +120,29 @@ def generate_sdk(gh_token, config_path, project_pattern, restapi_git_folder,
_LOGGER.info("Build SDK finished and cleaned")
def main():
def main(argv):
"""Main method"""
if 'GH_TOKEN' not in os.environ:
gh_token = None
else:
gh_token = os.environ['GH_TOKEN']
if "--rest-server" in argv:
from .restapi import app
log_level = logging.WARNING
if "-v" in argv or "--verbose" in argv:
log_level = logging.INFO
if "--debug" in argv:
log_level = logging.DEBUG
main_logger = logging.getLogger()
logging.basicConfig()
main_logger.setLevel(log_level)
app.run(debug=log_level == logging.DEBUG, host='0.0.0.0')
sys.exit(0)
epilog = "\n".join([
'The script activates this additional behaviour if Travis is detected:',
' --branch is setted by default to "{}" if triggered by a PR, "{}" otherwise'.format(
@ -165,12 +193,7 @@ def main():
'Otherwise, you can use the syntax username/repoid')
args = parser.parse_args()
if 'GH_TOKEN' not in os.environ:
gh_token = None
else:
gh_token = os.environ['GH_TOKEN']
main_logger = logging.getLogger()
if args.verbose or args.debug:
logging.basicConfig()

Просмотреть файл

@ -1,2 +1,3 @@
from .SwaggerToSdkMain import main
main()
import sys
from swaggertosdk.SwaggerToSdkMain import main
main(sys.argv)

173
swaggertosdk/build_sdk.py Normal file
Просмотреть файл

@ -0,0 +1,173 @@
#!/usr/bin/env python
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import json
import logging
import os.path
from pathlib import Path
import sys
import requests
from cookiecutter.main import cookiecutter
from swaggertosdk.SwaggerToSdkNewCLI import generate_code
_LOGGER = logging.getLogger(__name__)
def guess_service_info_from_path(spec_path):
"""Guess Python Autorest options based on the spec path.
Expected path:
specification/compute/resource-manager/readme.md
"""
spec_path = spec_path.lower()
spec_path = spec_path[spec_path.index("specification"):] # Might raise and it's ok
split_spec_path = spec_path.split("/") if "/" in spec_path else spec_path.split("\\")
rp_name = split_spec_path[1]
is_arm = split_spec_path[2] == "resource-manager"
return {
"rp_name": rp_name,
"is_arm": is_arm
}
def get_data(spec_path):
if spec_path.startswith("http"):
response = requests.get(spec_path)
response.raise_for_status()
return response.text
else:
with open(spec_path, "r") as fd:
return fd.read()
def guess_service_info_from_content(spec_path):
data = get_data(spec_path)
lines = data.splitlines()
while lines:
line = lines.pop(0)
if line.startswith("# "):
pretty_name = line[2:]
break
else:
raise ValueError("Unable to find main title in this README")
return {
'pretty_name': pretty_name
}
def guess_service_info(spec_path):
result = guess_service_info_from_content(spec_path)
result.update(guess_service_info_from_path(spec_path))
return result
def create_swagger_to_sdk_conf(spec_path, service_info, autorest_options):
type_str = ".mgmt" if service_info["is_arm"] else ".data"
return {
"{}{}".format(service_info["rp_name"], type_str): {
"markdown": spec_path[spec_path.index("specification"):],
"autorest_options": {
"namespace": autorest_options["namespace"],
"package-version": autorest_options["package-version"],
"package-name": autorest_options["package-name"]
},
"output_dir": "{}/{}".format(autorest_options["package-name"], autorest_options["package-name"].replace("-","/")),
"build_dir": autorest_options["package-name"]
}
}
def create_package_service_mapping(spec_path, service_info, autorest_options):
type_str = "Management" if service_info["is_arm"] else "Client"
return {
autorest_options["package-name"]: {
"service_name": service_info["pretty_name"],
"category": type_str,
"namespaces": [
autorest_options["namespace"]
]
}
}
def generate(spec_path, cwd='.'):
service_info = guess_service_info(spec_path)
autorest_options = {}
if service_info["is_arm"]:
autorest_options["azure-arm"] = True
namespace = "azure.mgmt." + service_info["rp_name"]
package_name = "azure-mgmt-" + service_info["rp_name"]
else:
autorest_options["add-credentials"] = True
namespace = "azure." + service_info["rp_name"]
package_name = "azure-" + service_info["rp_name"]
# Create output folder
output_dir = Path(cwd).resolve() / Path(package_name)
output_dir.mkdir(exist_ok=True)
_LOGGER.info("Output folder: %s", output_dir)
_LOGGER.info("Calling Autorest")
autorest_options.update({
"use": "@microsoft.azure/autorest.python@preview",
"license-header": "MICROSOFT_MIT_NO_VERSION",
"payload-flattening-threshold": 2,
"python": "",
"package-version": "0.1.0",
"python.output-folder": output_dir
})
autorest_options["namespace"] = namespace
autorest_options["package-name"] = package_name
generate_code(
input_file=Path(spec_path) if not spec_path.startswith("http") else spec_path,
output_dir=output_dir,
global_conf={"autorest_options": autorest_options},
local_conf={}
)
_LOGGER.info("Rebuilding packaging with Cookiecutter")
pretty_name = service_info["pretty_name"]
cookiecutter('gh:Azure/cookiecutter-azuresdk-pypackage',
no_input=True,
extra_context={
'package_name': package_name,
'package_pprint_name': pretty_name
},
overwrite_if_exists=True,
output_dir=cwd
)
entry = create_swagger_to_sdk_conf(spec_path, service_info, autorest_options)
swagger_to_sdk = Path(cwd) / Path("swagger_to_sdk_config.json")
if swagger_to_sdk.exists():
_LOGGER.info("Updating swagger_to_sdk_config.json")
with swagger_to_sdk.open() as fd:
data_conf = json.load(fd)
data_conf["projects"].update(entry)
with swagger_to_sdk.open("w") as fd:
json.dump(data_conf, fd, indent=2, sort_keys=True)
package_service_mapping = Path(cwd) / Path("package_service_mapping.json")
if package_service_mapping.exists():
_LOGGER.info("Updating package_service_mapping.json")
package_service_mapping_entry = create_package_service_mapping(spec_path, service_info, autorest_options)
with package_service_mapping.open() as fd:
data_conf = json.load(fd)
data_conf.update(package_service_mapping_entry)
with package_service_mapping.open("w") as fd:
json.dump(data_conf, fd, indent=2, sort_keys=True)
_LOGGER.info("Done! Enjoy your Python SDK!!")
return entry
def main(spec_path):
generate(spec_path)
if __name__ == "__main__":
logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)
main(sys.argv[1])

Просмотреть файл

@ -0,0 +1,33 @@
from flask import Flask
from jsonrpc.backend.flask import api
from ..SwaggerToSdkMain import generate_sdk
from ..SwaggerToSdkCore import CONFIG_FILE, DEFAULT_COMMIT_MESSAGE
app = Flask(__name__)
app.add_url_rule('/', 'api', api.as_view(), methods=['POST'])
@api.dispatcher.add_method
def ping(*args, **kwargs):
return "Pong!"
@api.dispatcher.add_method
def generate_project(*args, **kwargs):
# Get required parameter
rest_api_id = kwargs['rest_api_id']
sdk_id = kwargs['sdk_id']
project = kwargs['project']
generate_sdk(
os.environ['GH_TOKEN'],
CONFIG_FILE,
project,
rest_api_id,
sdk_id,
None, # No PR repo id
DEFAULT_COMMIT_MESSAGE,
'master',
None # Destination branch
)
from .github import *

Просмотреть файл

@ -0,0 +1,2 @@
from swaggertosdk.restapi import app
app.run(debug=True)

Просмотреть файл

@ -0,0 +1,350 @@
from datetime import datetime
from enum import Enum
from functools import wraps, lru_cache
import logging
from queue import Queue
import re
import traceback
from threading import Thread
from flask import request, jsonify
from github import Github, GithubException, UnknownObjectException
import hmac, hashlib
import os
from .github_handler import (
build_from_issue_comment,
build_from_issues,
GithubHandler as LocalHandler,
generate_sdk_from_commit
)
from . import app
_LOGGER = logging.getLogger(__name__)
_QUEUE = Queue(64)
# Webhook secreet to authenticate message (bytes)
SECRET = b'mydeepsecret'
_HMAC_CHECK = False
def check_hmac(request, secret):
data = request.get_data()
hmac_tester = hmac.HMAC(b'mydeepsecret', data, hashlib.sha1)
if not 'X-Hub-Signature' in request.headers:
raise ValueError('X-Hub-Signature is mandatory on this WebService')
if request.headers['X-Hub-Signature'] == 'sha1='+hmac_tester.hexdigest():
return True
raise ValueError('Bad X-Hub-Signature signature')
@app.route('/github', methods=['POST'])
def notify():
"""Github main endpoint."""
github_index = {
'ping': ping,
'issue_comment': issue_comment,
'issues': issues
}
return handle_github_webhook(
github_index,
request.headers['X-GitHub-Event'],
request.get_json()
)
@app.route('/github/rest', methods=['POST'])
def rest_notify():
"""Github rest endpoint."""
github_index = {
'ping': ping,
'push': push,
'pull_request': rest_pull_request
}
return handle_github_webhook(
github_index,
request.headers['X-GitHub-Event'],
request.get_json()
)
def handle_github_webhook(github_index, gh_event_type, json_body):
if _HMAC_CHECK:
check_hmac(request, SECRET)
_LOGGER.info("Received Webhook %s", request.headers.get("X-GitHub-Delivery"))
json_answer = notify_github(github_index, gh_event_type, json_body)
return jsonify(json_answer)
@lru_cache()
def robot_name():
github_con = Github(os.environ["GH_TOKEN"])
return github_con.get_user().login
def notify_github(github_index, event_type, json_body):
if json_body['sender']['login'].lower() == robot_name().lower():
return {'message': 'I don\'t talk to myself, I\'m not schizo'}
if event_type in github_index:
return github_index[event_type](json_body)
return {'message': 'Not handled currently'}
def ping(body):
return {'message': 'Moi aussi zen beaucoup'}
def issue_comment(body):
if body["action"] in ["created", "edited"]:
webhook_data = build_from_issue_comment(body)
response = manage_comment(webhook_data)
if response:
return response
return {'message': 'Nothing for me'}
def issues(body):
if body["action"] in ["opened"]:
webhook_data = build_from_issues(body)
response = manage_comment(webhook_data)
if response:
return response
return {'message': 'Nothing for me'}
def manage_comment(webhook_data):
handler = LocalHandler()
# Is someone talking to me:
message = re.search("@{} (.*)".format(robot_name()), webhook_data.text, re.I)
if message:
command = message.group(1)
try:
response = handler.act_and_response(webhook_data, command)
except Exception as err:
response = traceback.format_exc()
if response:
return {'message': response}
def push(body):
sdkid = request.args.get("sdkid")
if not sdkid:
return {'message': 'sdkid is a required query parameter'}
sdkbase = request.args.get("sdkbase", "master")
rest_api_branch_name = body["ref"][len("refs/heads/"):]
if rest_api_branch_name == "master":
return {'message': 'Webhook disabled for RestAPI master'}
gh_token = os.environ["GH_TOKEN"]
github_con = Github(gh_token)
restapi_git_id = body['repository']['full_name']
repo = github_con.get_repo(restapi_git_id)
commit_obj = repo.get_commit(body["after"])
generate_sdk_from_commit(
commit_obj,
"restapi_auto_"+rest_api_branch_name,
restapi_git_id,
sdkid,
None, # I don't know if the origin branch comes from "master", assume it.
sdkbase
)
return {'message': 'No return for this endpoint'}
def rest_pull_request(body):
sdkid = request.args.get("sdkid")
if not sdkid:
return {'message': 'sdkid is a required query parameter'}
sdkbase = request.args.get("sdkbase", "master")
_LOGGER.info("Received PR action %s", body["action"])
_QUEUE.put((body, sdkid, sdkbase))
_LOGGER.info("Received action has been queued. Queue size: %d", _QUEUE.qsize())
return {'message': 'Current queue size: {}'.format(_QUEUE.qsize())}
def rest_handle_action(body, sdkid, sdkbase):
"""First method in the thread.
"""
_LOGGER.info("Rest handle action")
gh_token = os.environ["GH_TOKEN"]
github_con = Github(gh_token)
sdk_pr_target_repo = github_con.get_repo(sdkid)
restapi_git_id = body['repository']['full_name']
restapi_repo = github_con.get_repo(restapi_git_id)
_LOGGER.info("Received PR action %s", body["action"])
if body["action"] in ["opened", "reopened"]:
return rest_pull_open(body, github_con, restapi_repo, sdk_pr_target_repo, sdkbase)
if body["action"] == "closed":
return rest_pull_close(body, github_con, restapi_repo, sdk_pr_target_repo, sdkbase)
if body["action"] == "synchronize": # push to a PR from a fork
return rest_pull_sync(body, github_con, restapi_repo, sdk_pr_target_repo, sdkbase)
def rest_pull_open(body, github_con, restapi_repo, sdk_pr_target_repo, sdk_default_base="master"):
rest_basebranch = body["pull_request"]["base"]["ref"]
dest_branch = body["pull_request"]["head"]["ref"]
origin_repo = body["pull_request"]["head"]["repo"]["full_name"]
if rest_basebranch == "master":
sdk_base = sdk_default_base
sdk_checkout_base = None
else:
sdk_base = "restapi_auto_" + rest_basebranch
sdk_checkout_base = sdk_base
rest_pr = restapi_repo.get_pull(body["number"])
if origin_repo != restapi_repo.full_name:
_LOGGER.info("This comes from a fork, I need generation first, since targetted branch does not exist")
fork_repo = github_con.get_repo(origin_repo)
fork_owner = fork_repo.owner.login
commit_obj = fork_repo.get_commit(body["pull_request"]["head"]["sha"])
subbranch_name_part = fork_owner+"_"+dest_branch
sdk_dest_branch = "restapi_auto_" + subbranch_name_part
generate_sdk_from_commit(
commit_obj,
sdk_dest_branch,
origin_repo,
sdk_pr_target_repo.full_name,
sdk_checkout_base,
sdk_default_base
)
else:
sdk_dest_branch = "restapi_auto_" + dest_branch
try:
github_pr = sdk_pr_target_repo.create_pull(
title='Automatic PR of {} into {}'.format(sdk_dest_branch, sdk_base),
body="Created to sync {}".format(rest_pr.html_url),
head=sdk_dest_branch,
base=sdk_base
)
sdk_pr_as_issue = sdk_pr_target_repo.get_issue(github_pr.number)
sdk_pr_as_issue.add_to_labels(get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.in_progress))
except GithubException as err:
if err.status == 422 and err.data['errors'][0].get('message', '').startswith('A pull request already exists'):
_LOGGER.info('PR already exists, it was a commit on an open PR')
sdk_pr = list(sdk_pr_target_repo.get_pulls(
head=sdk_pr_target_repo.owner.login+":"+sdk_dest_branch,
base=sdk_base
))[0]
sdk_pr_as_issue = sdk_pr_target_repo.get_issue(sdk_pr.number)
sdk_pr_as_issue.add_to_labels(get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.in_progress))
safe_remove_label(sdk_pr_as_issue, get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.refused))
safe_remove_label(sdk_pr_as_issue, get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.merged))
return {'message': 'PR already exists'}
else:
return {'message': err.data}
except Exception as err:
response = traceback.format_exc()
return {'message': response}
class SwaggerToSdkLabels(Enum):
merged = "RestPRMerged", "0e8a16"
refused = "RestPRRefused", "b60205"
in_progress = "RestPRInProgress", "fbca04"
def get_or_create_label(sdk_pr_target_repo, label_enum):
try:
return sdk_pr_target_repo.get_label(label_enum.value[0])
except UnknownObjectException:
return sdk_pr_target_repo.create_label(*label_enum.value)
def safe_remove_label(issue, label):
"""Remove a label, does not fail if label was not there.
"""
try:
issue.remove_from_labels(label)
except GithubException:
pass
def rest_pull_close(body, github_con, restapi_repo, sdk_pr_target_repo, sdk_default_base="master"):
_LOGGER.info("Received a PR closed event")
sdkid = sdk_pr_target_repo.full_name
rest_pr = restapi_repo.get_pull(body["number"])
# What was "head" name
origin_repo = body["pull_request"]["head"]["repo"]["full_name"]
dest_branch = body["pull_request"]["head"]["ref"]
if origin_repo != restapi_repo.full_name: # This PR comes from a fork
fork_repo = github_con.get_repo(origin_repo)
fork_owner = fork_repo.owner.login
subbranch_name_part = fork_owner+"_"+dest_branch
sdk_dest_branch = "restapi_auto_" + subbranch_name_part
else:
sdk_dest_branch = "restapi_auto_" + dest_branch
_LOGGER.info("SDK head branch should be %s", sdk_dest_branch)
full_head = sdk_pr_target_repo.owner.login+":"+sdk_dest_branch
_LOGGER.info("Will filter with %s", full_head)
# What was "base"
rest_basebranch = body["pull_request"]["base"]["ref"]
sdk_base = sdk_default_base if rest_basebranch == "master" else "restapi_auto_" + rest_basebranch
_LOGGER.info("SDK base branch should be %s", sdk_base)
sdk_prs = list(sdk_pr_target_repo.get_pulls(
head=full_head,
base=sdk_base
))
if not sdk_prs:
rest_pr.create_issue_comment("Was unable to find SDK {} PR for this closed PR.".format(sdkid))
elif len(sdk_prs) == 1:
sdk_pr = sdk_prs[0]
sdk_pr_as_issue = sdk_pr_target_repo.get_issue(sdk_pr.number)
safe_remove_label(sdk_pr_as_issue, get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.in_progress))
try:
if body["pull_request"]["merged"]:
sdk_pr_as_issue.add_to_labels(get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.merged))
else:
sdk_pr_as_issue.add_to_labels(get_or_create_label(sdk_pr_target_repo, SwaggerToSdkLabels.refused))
except GithubException:
sdk_pr.create_issue_comment("Cannot set labels. Initial PR has been closed with merged status: {}".format(body["pull_request"]["merged"]))
else:
# Should be impossible, create_pull would have sent a 422
pr_list = "\n".join(["- {}".format(pr.html_url) for pr in sdk_prs])
rest_pr.create_issue_comment("We found several SDK {} PRs and didn't notify closing event.\n{}".format(sdkid, pr_list))
def rest_pull_sync(body, github_con, restapi_repo, sdk_pr_target_repo, sdk_default_base="master"):
if body["before"] == body["after"]:
return {'message': 'No commit id change'}
# What was "head" name
origin_repo = body["pull_request"]["head"]["repo"]["full_name"]
if origin_repo == restapi_repo.full_name:
_LOGGER.info("This will be handled by 'push' event on the branch")
dest_branch = body["pull_request"]["head"]["ref"]
fork_repo = github_con.get_repo(origin_repo)
fork_owner = fork_repo.owner.login
commit_obj = fork_repo.get_commit(body["pull_request"]["head"]["sha"])
subbranch_name_part = fork_owner+"_"+dest_branch
generate_sdk_from_commit(
commit_obj,
"restapi_auto_"+subbranch_name_part,
origin_repo,
sdk_pr_target_repo.full_name,
None, # I don't know if the origin branch comes from "master", assume it.
sdk_default_base
)
return {'message': 'No return for this endpoint'}
def consume():
"""Consume action and block if there is not.
"""
while True:
body, sdkid, sdkbase = _QUEUE.get()
try:
rest_handle_action(body, sdkid, sdkbase)
except Exception as err:
_LOGGER.critical("Worked thread issue:\n%s", traceback.format_exc())
_LOGGER.info("End of WorkerThread")
_WORKER_THREAD = Thread(
target=consume,
name="WorkerThread"
)
_WORKER_THREAD.start()

Просмотреть файл

@ -0,0 +1,276 @@
from collections import namedtuple
import logging
import os
import re
from pathlib import Path
import tempfile
import traceback
from github import Github
from git import Repo, GitCommandError
from swaggertosdk.build_sdk import generate as build_sdk
from swaggertosdk.SwaggerToSdkCore import (
manage_git_folder,
checkout_and_create_branch,
do_commit,
do_pr,
configure_user,
CONFIG_FILE,
read_config,
DEFAULT_COMMIT_MESSAGE,
get_input_paths,
extract_conf_from_readmes,
checkout_and_create_branch
)
_LOGGER = logging.getLogger(__name__)
WebhookMetadata = namedtuple(
'WebhookMetadata',
['repo', 'issue', 'text']
)
def build_from_issue_comment(body):
gh_token = os.environ["GH_TOKEN"]
github_con = Github(gh_token)
repo = github_con.get_repo(body['repository']['full_name'])
issue = repo.get_issue(body['issue']['number'])
text = body['comment']['body']
return WebhookMetadata(repo, issue, text)
def build_from_issues(body):
gh_token = os.environ["GH_TOKEN"]
github_con = Github(gh_token)
repo = github_con.get_repo(body['repository']['full_name'])
issue = repo.get_issue(body['issue']['number'])
text = body['issue']['body']
return WebhookMetadata(repo, issue, text)
class GithubHandler:
def __init__(self):
self.gh_token = os.environ["GH_TOKEN"]
def act_and_response(self, webhook_data, command):
issue = webhook_data.issue
try:
response = self.comment_command(issue, command)
except Exception as err:
response = "Something's wrong:\n```python\n{}\n```\n".format(traceback.format_exc())
new_comment = issue.create_comment(response)
return 'Posted: {}'.format(new_comment.html_url)
def comment_command(self, issue, text):
split_text = text.lower().split()
if split_text[0] == "generate":
return self.generate(issue, split_text[1])
elif split_text[0] == "rebuild":
return self.rebuild(issue, split_text[1])
elif split_text[0] == "help":
return self.help(issue)
else:
return "I didn't understand your command:\n```bash\n{}\n```\nin this context, sorry :(".format(text)
def help(self, issue):
message = """This is what I can do:
- `help` : this help message
- `generate <raw github path to a readme>` : create a PR for this README
"""
new_comment = issue.create_comment(message)
def generate(self, issue, readme_parameter):
# Do a start comment
new_comment = issue.create_comment("Working on generating this for you!!!")
# Clone SDK repo
sdk_git_id = issue.repository.full_name
pr_repo_id = sdk_git_id
base_branch_name = "master"
with tempfile.TemporaryDirectory() as temp_dir, \
manage_git_folder(self.gh_token, temp_dir + "/sdk", sdk_git_id) as sdk_folder:
sdk_conf = build_sdk(readme_parameter, sdk_folder)
branch_name = list(sdk_conf.keys()).pop()
new_comment.edit("Generated! Let's see if there is something to PR.")
sdk_repo = Repo(str(sdk_folder))
configure_user(self.gh_token, sdk_repo)
modification = do_commit(
sdk_repo,
"Generated from {}".format(issue.html_url),
branch_name,
""
)
new_comment.delete()
if modification:
sdk_repo.git.push('origin', branch_name, set_upstream=True)
pip_command = 'pip install "git+{}@{}#egg={}&subdirectory={}"'.format(
issue.repository.html_url,
branch_name,
sdk_conf[branch_name]["autorest_options"]["package-name"],
sdk_conf[branch_name]["autorest_options"]["package-name"]
)
local_command = 'pip install -e ./{}'.format(sdk_conf[branch_name]["autorest_options"]["package-name"])
pr_body = """Generated from Issue: {}
You can install the new package of this PR for testing using the following command:
`{}`
If you have a local clone of this repo in the folder /home/git/repo, please checkout this branch and do:
`{}`
""".format(issue.html_url, pip_command, local_command)
pr = do_pr(self.gh_token, sdk_git_id, pr_repo_id, branch_name, base_branch_name, pr_body)
answer = """
Done! I created this branch and this PR:
- {}
- {}
""".format(branch_name, pr.html_url, pip_command)
return answer
else:
return "Sorry, there is nothing to PR"
def rebuild(self, issue, project_pattern):
if not issue.pull_request:
return "Rebuild is just supported in PR for now"
pr = issue.repository.get_pull(issue.number)
new_comment = issue.create_comment("Working on generating {} for you!!!".format(project_pattern))
config_path = CONFIG_FILE
message = "Rebuild by "+issue.html_url
initial_pr = None # There is no initial PR to test for files
autorest_bin = None
branch_name = pr.head.ref
rest_api_id = "Azure/azure-rest-api-specs" # current
branched_sdk_id = pr.head.repo.full_name+'@'+branch_name
with tempfile.TemporaryDirectory() as temp_dir, \
manage_git_folder(self.gh_token, Path(temp_dir) / Path("rest"), rest_api_id) as restapi_git_folder, \
manage_git_folder(self.gh_token, Path(temp_dir) / Path("sdk"), branched_sdk_id) as sdk_folder:
sdk_repo = Repo(str(sdk_folder))
configure_user(self.gh_token, sdk_repo)
config = read_config(sdk_repo.working_tree_dir, config_path)
def skip_callback(project, local_conf):
if not project.startswith(project_pattern):
return True
return False
from swaggertosdk import SwaggerToSdkNewCLI
SwaggerToSdkNewCLI.build_libraries(config, skip_callback, restapi_git_folder,
sdk_repo, temp_dir, autorest_bin)
new_comment.edit("End of generation, doing commit")
commit_sha = do_commit(sdk_repo, message, branch_name, "")
if commit_sha:
new_comment.edit("Pushing")
sdk_repo.git.push('origin', branch_name, set_upstream=True)
new_comment.delete()
else:
new_comment.delete()
return "Nothing to rebuild, this PR is up to date"
_LOGGER.info("Build SDK finished and cleaned")
return "Build SDK finished and cleaned"
def generate_sdk_from_commit_safe(commit_obj, branch_name, restapi_git_id, sdkid, base_branch_name, fallback_base_branch_name="master"):
try:
response = generate_sdk_from_commit(commit_obj, branch_name, restapi_git_id, sdkid, base_branch_name, fallback_base_branch_name)
except Exception as err:
response = "Something's wrong:\n```python\n{}\n```\n".format(traceback.format_exc())
new_comment = commit_obj.create_comment(response)
return 'Posted: {}'.format(new_comment.html_url)
def generate_sdk_from_commit(commit_obj, branch_name, restapi_git_id, sdk_git_id, base_branch_name, fallback_base_branch_name="master"):
"""Generate SDK from a commit.
commit_obj is the initial commit_obj from the RestAPI repo. restapi_git_id explains where to clone the repo.
sdk_git_id explains where to push the commit.
branch_name is the expected branch name in the SDK repo.
- If this branch exists, use it.
- If not, use the base branch to create that branch (base branch is where I intend to do my PR)
- If base_branch is not provided, use fallback_base_branch_name as base
- If this base branch is provided and does not exists, create this base branch first using fallback_base_branch_name (this one is required to exist)
WARNING:
This method might push to "branch_name" and "base_branch_name". No push will be made to "fallback_base_branch_name"
"""
gh_token = os.environ["GH_TOKEN"]
config_path = CONFIG_FILE
message_template = DEFAULT_COMMIT_MESSAGE
autorest_bin = None
branched_rest_api_id = restapi_git_id+'@'+commit_obj.sha
branched_sdk_git_id = sdk_git_id+'@'+fallback_base_branch_name
with tempfile.TemporaryDirectory() as temp_dir, \
manage_git_folder(gh_token, Path(temp_dir) / Path("rest"), branched_rest_api_id) as restapi_git_folder, \
manage_git_folder(gh_token, Path(temp_dir) / Path("sdk"), branched_sdk_git_id) as sdk_folder:
sdk_repo = Repo(str(sdk_folder))
_LOGGER.info('Destination branch for generated code is %s', branch_name)
try:
_LOGGER.info('Try to checkout the destination branch if it already exists')
sdk_repo.git.checkout(branch_name)
except GitCommandError:
_LOGGER.info('Destination branch does not exists')
if base_branch_name is not None:
_LOGGER.info('Try to checkout base branch {} '.format(base_branch_name))
try:
sdk_repo.git.checkout(base_branch_name)
except GitCommandError:
_LOGGER.info('Base branch does not exists, create it from {}'.format(fallback_base_branch_name))
checkout_and_create_branch(sdk_repo, base_branch_name)
sdk_repo.git.push('origin', base_branch_name, set_upstream=True)
configure_user(gh_token, sdk_repo)
config = read_config(sdk_repo.working_tree_dir, config_path)
global_conf = config["meta"]
from swaggertosdk import SwaggerToSdkNewCLI
from swaggertosdk import SwaggerToSdkCore
swagger_files_in_commit = SwaggerToSdkCore.get_swagger_project_files_in_pr(commit_obj, restapi_git_folder)
_LOGGER.info("Files in PR: %s ", swagger_files_in_commit)
# Look for configuration in Readme
extract_conf_from_readmes(gh_token, swagger_files_in_commit, restapi_git_folder, sdk_git_id, config)
def skip_callback(project, local_conf):
if not swagger_files_in_commit:
return True
markdown_relative_path, optional_relative_paths = get_input_paths(global_conf, local_conf)
if not (
markdown_relative_path in swagger_files_in_commit or
any(input_file in swagger_files_in_commit for input_file in optional_relative_paths)):
_LOGGER.info(f"In project {project} no files involved in this commit")
return True
return False
SwaggerToSdkNewCLI.build_libraries(config, skip_callback, restapi_git_folder,
sdk_repo, temp_dir, autorest_bin)
message = message_template + "\n\n" + commit_obj.commit.message
commit_sha = do_commit(sdk_repo, message, branch_name, commit_obj.sha)
if commit_sha:
sdk_repo.git.push('origin', branch_name, set_upstream=True)
commit_url = "https://github.com/{}/commit/{}".format(sdk_git_id, commit_sha)
commit_obj.create_comment("Did a commit to SDK for Python:\n{}".format(commit_url))
else:
commit_obj.create_comment("This commit was treated and no generation was made for Python")
_LOGGER.info("Build SDK finished and cleaned")
return "Build SDK finished and cleaned"

Просмотреть файл

@ -0,0 +1,6 @@
from . import app
from jsonrpc.backend.flask import api
@app.route("/")
def hello():
return "Hello World!"

5
tests/test_build_sdk.py Normal file
Просмотреть файл

@ -0,0 +1,5 @@
from swaggertosdk.build_sdk import *
def test_guess_autorest_options():
assert guess_service_info_from_path("specification/compute/resource-manager/readme.md") == {"rp_name": "compute", "is_arm": True}
assert guess_service_info_from_path("specification/servicefabric/data-plane/readme.md") == {"rp_name": "servicefabric", "is_arm": False}

Просмотреть файл

@ -5,6 +5,8 @@ import tempfile
from pathlib import Path
logging.basicConfig(level=logging.INFO)
from git import GitCommandError
# Fake Travis before importing the Script
os.environ['TRAVIS'] = 'true'
@ -113,6 +115,77 @@ class TestSwaggerToSDK(unittest.TestCase):
)
def test_manage_git_folder(self):
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir, \
manage_git_folder(GH_TOKEN, temp_dir, "lmazuel/TestingRepo") as rest_repo:
self.assertTrue((Path(rest_repo) / Path("README.md")).exists())
finished = True
except (PermissionError, FileNotFoundError):
if not finished:
raise
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir, \
manage_git_folder(GH_TOKEN, temp_dir, "lmazuel/TestingRepo@lmazuel-patch-1") as rest_repo:
self.assertTrue((Path(rest_repo) / Path("README.md")).exists())
self.assertTrue(Repo(rest_repo).active_branch, "lmazuel-patch-1")
finished = True
except (PermissionError, FileNotFoundError):
if not finished:
raise
def test_clone_path(self):
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir:
clone_to_path(GH_TOKEN, temp_dir, "lmazuel/TestingRepo")
self.assertTrue((Path(temp_dir) / Path("README.md")).exists())
finished = True
except PermissionError:
if not finished:
raise
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir:
clone_to_path(GH_TOKEN, temp_dir, "https://github.com/lmazuel/TestingRepo")
self.assertTrue((Path(temp_dir) / Path("README.md")).exists())
finished = True
except PermissionError:
if not finished:
raise
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir:
clone_to_path(GH_TOKEN, temp_dir, "lmazuel/TestingRepo", "lmazuel-patch-1")
self.assertTrue((Path(temp_dir) / Path("README.md")).exists())
finished = True
except PermissionError:
if not finished:
raise
finished = False # Authorize PermissionError on cleanup
try:
with tempfile.TemporaryDirectory() as temp_dir:
with self.assertRaises(GitCommandError):
clone_to_path(GH_TOKEN, temp_dir, "lmazuel/TestingRepo", "fakebranch")
finished = True
except (PermissionError, FileNotFoundError):
if not finished:
raise
def test_do_commit(self):
finished = False # Authorize PermissionError on cleanup
try: