2021-01-21 13:51:16 +03:00
|
|
|
import argparse
|
|
|
|
import ghlib
|
|
|
|
import jiralib
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import json
|
|
|
|
import util
|
2021-02-22 21:35:22 +03:00
|
|
|
from sync import Sync, DIRECTION_G2J, DIRECTION_J2G, DIRECTION_BOTH
|
2021-01-28 00:23:13 +03:00
|
|
|
import logging
|
2021-02-08 16:20:12 +03:00
|
|
|
import server
|
2021-12-02 02:53:01 +03:00
|
|
|
import anticrlf
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-01-28 00:23:13 +03:00
|
|
|
root = logging.getLogger()
|
|
|
|
root.setLevel(logging.DEBUG)
|
|
|
|
|
|
|
|
handler = logging.StreamHandler(sys.stdout)
|
2021-12-02 02:53:01 +03:00
|
|
|
handler.setFormatter(anticrlf.LogFormatter("%(levelname)s:%(name)s:%(message)s"))
|
2021-01-28 00:23:13 +03:00
|
|
|
handler.setLevel(logging.DEBUG)
|
|
|
|
root.addHandler(handler)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-02 00:17:35 +03:00
|
|
|
|
2021-01-21 13:51:16 +03:00
|
|
|
def fail(msg):
|
|
|
|
print(msg)
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
2021-02-02 00:17:35 +03:00
|
|
|
def direction_str_to_num(dstr):
|
2021-08-17 00:48:57 +03:00
|
|
|
if dstr == "gh2jira":
|
2021-02-22 21:35:22 +03:00
|
|
|
return DIRECTION_G2J
|
2021-08-17 00:48:57 +03:00
|
|
|
elif dstr == "jira2gh":
|
2021-02-22 21:35:22 +03:00
|
|
|
return DIRECTION_J2G
|
2021-08-17 00:48:57 +03:00
|
|
|
elif dstr == "both":
|
2021-02-22 21:35:22 +03:00
|
|
|
return DIRECTION_BOTH
|
2021-02-02 00:17:35 +03:00
|
|
|
else:
|
|
|
|
fail('Unknown direction argument "{direction}"!'.format(direction=dstr))
|
|
|
|
|
|
|
|
|
2021-01-21 13:51:16 +03:00
|
|
|
def serve(args):
|
2021-02-02 00:17:35 +03:00
|
|
|
if not args.gh_url or not args.jira_url:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("Both GitHub and JIRA URL have to be specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
if not args.gh_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub token specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
|
|
|
if not args.jira_user or not args.jira_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA credentials specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
|
|
|
if not args.jira_project:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA project specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
|
|
|
if not args.secret:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No Webhook secret specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
github = ghlib.GitHub(args.gh_url, args.gh_token)
|
2021-08-13 20:38:59 +03:00
|
|
|
jira = jiralib.Jira(args.jira_url, args.jira_user, args.jira_token)
|
2021-02-22 21:35:22 +03:00
|
|
|
s = Sync(
|
2021-02-02 00:17:35 +03:00
|
|
|
github,
|
2021-09-03 03:10:48 +03:00
|
|
|
jira.getProject(args.jira_project, args.jira_labels),
|
2021-08-18 08:04:27 +03:00
|
|
|
direction=direction_str_to_num(args.direction),
|
2021-02-02 00:17:35 +03:00
|
|
|
)
|
2021-02-22 21:35:22 +03:00
|
|
|
server.run_server(s, args.secret, port=args.port)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
|
|
|
|
def sync(args):
|
|
|
|
if not args.gh_url or not args.jira_url:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("Both GitHub and JIRA URL have to be specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
if not args.gh_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub credentials specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if not args.jira_user or not args.jira_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA credentials specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-02 00:17:35 +03:00
|
|
|
if not args.jira_project:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA project specified!")
|
2021-02-02 00:17:35 +03:00
|
|
|
|
2021-01-21 13:51:16 +03:00
|
|
|
if not args.gh_org:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub organization specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if not args.gh_repo:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub repository specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
github = ghlib.GitHub(args.gh_url, args.gh_token)
|
2021-01-21 13:51:16 +03:00
|
|
|
jira = jiralib.Jira(args.jira_url, args.jira_user, args.jira_token)
|
2021-08-17 00:48:57 +03:00
|
|
|
jira_project = jira.getProject(
|
2021-08-18 08:04:27 +03:00
|
|
|
args.jira_project,
|
|
|
|
args.issue_end_state,
|
|
|
|
args.issue_reopen_state,
|
2021-09-03 03:10:48 +03:00
|
|
|
args.jira_labels,
|
2021-08-17 00:48:57 +03:00
|
|
|
)
|
|
|
|
repo_id = args.gh_org + "/" + args.gh_repo
|
2021-02-12 19:41:07 +03:00
|
|
|
|
2021-02-22 21:35:22 +03:00
|
|
|
if args.state_file:
|
|
|
|
if args.state_issue:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("--state-file and --state-issue are mutually exclusive!")
|
2021-02-22 21:35:22 +03:00
|
|
|
|
|
|
|
state = util.state_from_file(args.state_file)
|
|
|
|
elif args.state_issue:
|
|
|
|
state = jira_project.fetch_repo_state(repo_id, args.state_issue)
|
2021-02-12 19:41:07 +03:00
|
|
|
else:
|
2021-02-22 21:35:22 +03:00
|
|
|
state = {}
|
2021-02-12 19:41:07 +03:00
|
|
|
|
2021-08-17 00:48:57 +03:00
|
|
|
s = Sync(github, jira_project, direction=direction_str_to_num(args.direction))
|
2021-02-22 21:35:22 +03:00
|
|
|
s.sync_repo(repo_id, states=state)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-22 21:35:22 +03:00
|
|
|
if args.state_file:
|
|
|
|
util.state_to_file(args.state_file, state)
|
|
|
|
elif args.state_issue:
|
|
|
|
jira_project.save_repo_state(repo_id, state, args.state_issue)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
|
|
|
|
def check_hooks(args):
|
2021-02-24 13:10:39 +03:00
|
|
|
pass
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
|
|
|
|
def install_hooks(args):
|
|
|
|
if not args.hook_url:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No hook URL specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-02 00:17:35 +03:00
|
|
|
if not args.secret:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No hook secret specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if not args.gh_url and not args.jira_url:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("Neither GitHub nor JIRA URL specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
# user wants to install a github hook
|
|
|
|
if args.gh_url:
|
2021-02-08 16:15:28 +03:00
|
|
|
if not args.gh_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub token specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if not args.gh_org:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub organization specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
github = ghlib.GitHub(args.gh_url, args.gh_token)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if args.gh_repo:
|
2021-08-17 00:48:57 +03:00
|
|
|
ghrepo = github.getRepository(args.gh_org + "/" + args.gh_repo)
|
2021-02-02 00:17:35 +03:00
|
|
|
ghrepo.create_hook(url=args.hook_url, secret=args.secret)
|
2021-01-21 13:51:16 +03:00
|
|
|
else:
|
2021-02-02 00:17:35 +03:00
|
|
|
github.create_org_hook(url=args.hook_url, secret=args.secret)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
# user wants to install a JIRA hook
|
|
|
|
if args.jira_url:
|
|
|
|
if not args.jira_user or not args.jira_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA credentials specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
jira = jiralib.Jira(args.jira_url, args.jira_user, args.jira_token)
|
2021-08-17 00:48:57 +03:00
|
|
|
jira.create_hook("github_jira_synchronization_hook", args.hook_url, args.secret)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
|
|
|
|
def list_hooks(args):
|
|
|
|
if not args.gh_url and not args.jira_url:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("Neither GitHub nor JIRA URL specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
# user wants to list github hooks
|
|
|
|
if args.gh_url:
|
2021-02-08 16:15:28 +03:00
|
|
|
if not args.gh_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub token specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if not args.gh_org:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No GitHub organization specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-02-08 16:15:28 +03:00
|
|
|
github = ghlib.GitHub(args.gh_url, args.gh_token)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
if args.gh_repo:
|
2021-08-17 00:48:57 +03:00
|
|
|
for h in github.getRepository(
|
|
|
|
args.gh_org + "/" + args.gh_repo
|
|
|
|
).list_hooks():
|
2021-01-21 13:51:16 +03:00
|
|
|
print(json.dumps(h, indent=4))
|
|
|
|
else:
|
|
|
|
for h in github.list_org_hooks(args.gh_org):
|
|
|
|
print(json.dumps(h, indent=4))
|
|
|
|
|
|
|
|
# user wants to list JIRA hooks
|
|
|
|
if args.jira_url:
|
|
|
|
if not args.jira_user or not args.jira_token:
|
2021-08-17 00:48:57 +03:00
|
|
|
fail("No JIRA credentials specified!")
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
jira = jiralib.Jira(args.jira_url, args.jira_user, args.jira_token)
|
|
|
|
|
|
|
|
for h in jira.list_hooks():
|
|
|
|
print(json.dumps(h, indent=4))
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
credential_base = argparse.ArgumentParser(add_help=False)
|
2021-08-17 00:48:57 +03:00
|
|
|
credential_base.add_argument("--gh-org", help="GitHub organization")
|
|
|
|
credential_base.add_argument("--gh-repo", help="GitHub repository")
|
2021-01-21 13:51:16 +03:00
|
|
|
credential_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--gh-url",
|
|
|
|
help="API URL of GitHub instance",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
|
|
|
credential_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--gh-token",
|
|
|
|
help="GitHub API token. Alternatively, the GH2JIRA_GH_TOKEN may be set.",
|
|
|
|
default=os.getenv("GH2JIRA_GH_TOKEN"),
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
2021-08-17 00:48:57 +03:00
|
|
|
credential_base.add_argument("--jira-url", help="URL of JIRA instance")
|
|
|
|
credential_base.add_argument("--jira-user", help="JIRA user name")
|
2021-01-21 13:51:16 +03:00
|
|
|
credential_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--jira-token",
|
|
|
|
help="JIRA password. Alternatively, the GH2JIRA_JIRA_TOKEN may be set.",
|
|
|
|
default=os.getenv("GH2JIRA_JIRA_TOKEN"),
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
2021-08-17 00:48:57 +03:00
|
|
|
credential_base.add_argument("--jira-project", help="JIRA project key")
|
2021-09-03 03:10:48 +03:00
|
|
|
credential_base.add_argument("--jira-labels", help="JIRA bug label(s)")
|
2021-08-12 23:16:23 +03:00
|
|
|
credential_base.add_argument(
|
2021-08-18 08:04:27 +03:00
|
|
|
"--secret",
|
|
|
|
help="Webhook secret. Alternatively, the GH2JIRA_SECRET may be set.",
|
|
|
|
default=os.getenv("GH2JIRA_SECRET"),
|
2021-02-02 00:17:35 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
direction_base = argparse.ArgumentParser(add_help=False)
|
|
|
|
direction_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--direction",
|
2021-02-02 00:17:35 +03:00
|
|
|
help='Sync direction. Possible values are "gh2jira" (alert states have higher priority than issue states),'
|
2021-08-17 00:48:57 +03:00
|
|
|
+ '"jira2gh" (issue states have higher priority than alert states) and "both" (adjust in both directions)',
|
|
|
|
default="both",
|
2021-02-02 00:17:35 +03:00
|
|
|
)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-06-25 03:00:08 +03:00
|
|
|
issue_state_base = argparse.ArgumentParser(add_help=False)
|
|
|
|
issue_state_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--issue-end-state",
|
|
|
|
help="Custom end state (e.g. Closed) Done by default",
|
|
|
|
default="Done",
|
2021-06-25 03:00:08 +03:00
|
|
|
)
|
|
|
|
issue_state_base.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--issue-reopen-state",
|
|
|
|
help="Custom reopen state (e.g. In Progress) To Do by default",
|
|
|
|
default="To Do",
|
2021-02-02 00:17:35 +03:00
|
|
|
)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
2021-08-17 00:48:57 +03:00
|
|
|
parser = argparse.ArgumentParser(prog="gh2jira")
|
2021-01-21 13:51:16 +03:00
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
|
|
|
|
# serve
|
2021-01-27 23:22:46 +03:00
|
|
|
serve_parser = subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"serve",
|
2021-06-25 03:00:08 +03:00
|
|
|
parents=[credential_base, direction_base, issue_state_base],
|
2021-08-17 00:48:57 +03:00
|
|
|
help="Spawn a webserver which keeps GitHub alerts and JIRA tickets in sync",
|
|
|
|
description="Spawn a webserver which keeps GitHub alerts and JIRA tickets in sync",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
2021-02-10 18:38:51 +03:00
|
|
|
serve_parser.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--port", help="The port the server will listen on", default=5000
|
2021-02-10 18:38:51 +03:00
|
|
|
)
|
2021-01-27 23:22:46 +03:00
|
|
|
serve_parser.set_defaults(func=serve)
|
2021-01-21 13:51:16 +03:00
|
|
|
|
|
|
|
# sync
|
|
|
|
sync_parser = subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"sync",
|
2021-06-25 03:00:08 +03:00
|
|
|
parents=[credential_base, direction_base, issue_state_base],
|
2021-08-17 00:48:57 +03:00
|
|
|
help="Synchronize GitHub alerts and JIRA tickets for a given repository",
|
|
|
|
description="Synchronize GitHub alerts and JIRA tickets for a given repository",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
2021-02-12 19:41:07 +03:00
|
|
|
sync_parser.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--state-file",
|
|
|
|
help="File holding the current states of all alerts. The program will create the"
|
|
|
|
+ " file if it doesn't exist and update it after each run.",
|
|
|
|
default=None,
|
2021-02-12 19:41:07 +03:00
|
|
|
)
|
2021-02-22 21:35:22 +03:00
|
|
|
sync_parser.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--state-issue",
|
|
|
|
help="The key of the issue holding the current states of all alerts. The program "
|
|
|
|
+ 'will create the issue if "-" is given as the argument. The issue will be '
|
|
|
|
+ "updated after each run.",
|
|
|
|
default=None,
|
2021-02-22 21:35:22 +03:00
|
|
|
)
|
2021-01-21 13:51:16 +03:00
|
|
|
sync_parser.set_defaults(func=sync)
|
|
|
|
|
|
|
|
# hooks
|
|
|
|
hooks = subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"hooks",
|
|
|
|
help="Manage JIRA and GitHub webhooks",
|
|
|
|
description="Manage JIRA and GitHub webhooks",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
hooks_subparsers = hooks.add_subparsers()
|
|
|
|
|
|
|
|
# list hooks
|
|
|
|
hooks_list = hooks_subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"list",
|
2021-01-21 13:51:16 +03:00
|
|
|
parents=[credential_base],
|
2021-08-17 00:48:57 +03:00
|
|
|
help="List existing GitHub or JIRA webhooks",
|
|
|
|
description="List existing GitHub or JIRA webhooks",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
|
|
|
hooks_list.set_defaults(func=list_hooks)
|
|
|
|
|
|
|
|
# install hooks
|
|
|
|
hooks_install = hooks_subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"install",
|
2021-01-21 13:51:16 +03:00
|
|
|
parents=[credential_base],
|
2021-08-17 00:48:57 +03:00
|
|
|
help="Install existing GitHub or JIRA webhooks",
|
|
|
|
description="Install GitHub or JIRA webhooks",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
2021-08-17 00:48:57 +03:00
|
|
|
hooks_install.add_argument("--hook-url", help="Webhook target url")
|
2021-01-21 13:51:16 +03:00
|
|
|
hooks_install.add_argument(
|
2021-08-17 00:48:57 +03:00
|
|
|
"--insecure-ssl",
|
|
|
|
action="store_true",
|
|
|
|
help="Install GitHub hook without SSL check",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
|
|
|
hooks_install.set_defaults(func=install_hooks)
|
|
|
|
|
|
|
|
# check hooks
|
|
|
|
hooks_check = hooks_subparsers.add_parser(
|
2021-08-17 00:48:57 +03:00
|
|
|
"check",
|
2021-01-21 13:51:16 +03:00
|
|
|
parents=[credential_base],
|
2021-08-17 00:48:57 +03:00
|
|
|
help="Check that hooks are installed properly",
|
|
|
|
description="Check that hooks are installed properly",
|
2021-01-21 13:51:16 +03:00
|
|
|
)
|
|
|
|
hooks_check.set_defaults(func=check_hooks)
|
|
|
|
|
|
|
|
def print_usage(args):
|
|
|
|
print(parser.format_usage())
|
|
|
|
|
|
|
|
parser.set_defaults(func=print_usage)
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
# run the given action
|
|
|
|
args.func(args)
|
|
|
|
|
2021-08-17 00:48:57 +03:00
|
|
|
|
2021-01-21 13:51:16 +03:00
|
|
|
main()
|