├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── README_ZH.md ├── setup.cfg ├── setup.py ├── tests ├── config.py └── webhookdata │ ├── github.json │ ├── gitlab.json │ ├── gitosc.json │ └── gogs.json └── webhookit ├── __init__.py ├── app.py ├── cli.py ├── parser.py ├── temp.py └── utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg, 4 | node_modules, data, docs, build, lib, 5 | webhookit/temp.py, tests/_* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | dist/ 14 | develop-eggs/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *,cover 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | .pydevproject 59 | .project 60 | .settings 61 | tests/_* 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | before_install: 8 | - pip install flake8 9 | - pip install hint 10 | - python setup.py install 11 | install: 12 | - sleep 3 13 | script: flake8 && hint . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Hust.cc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "COMMAND: runserver | help" 3 | 4 | runserver: 5 | @webhookit -p 18340 -c tests/config.py 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webhookit 2 | 3 | > A simple cli tool to create http server for git webhook, **GitHub**, **GitLab**, **GitOsc**, **Gogs**, **Coding** are all supported. 4 | 5 | [LIVE DEMO](http://webhookit.hust.cc) | [中文说明文档](README_ZH.md) 6 | 7 | [![Latest Stable Version](https://img.shields.io/pypi/v/webhookit.svg)](https://pypi.python.org/pypi/webhookit) [![Build Status](https://travis-ci.org/hustcc/webhookit.svg?branch=master)](https://travis-ci.org/hustcc/webhookit) ![GitHub](http://shields.hust.cc/Supported-GitHub-brightgreen.svg) ![GitLab](http://shields.hust.cc/Supported-GitLab-green.svg) ![GitOsc](http://shields.hust.cc/Supported-GitOsc-blue.svg) ![Gogs](http://shields.hust.cc/Supported-Gogs-yellowgreen.svg) ![Coding](http://shields.hust.cc/Supported-Coding-yellow.svg) 8 | 9 | 10 | # 1. Install 11 | 12 | > **pip install webhookit** 13 | 14 | Python 2 / 3 are all supported. After install, you can get two commands named `webhookit` and `webhookit_config` in your system. 15 | 16 | 17 | # 2. Usage 18 | 19 | Run `webhookit --help` to get help content of the command. Help content below: 20 | 21 | 22 | ```sh 23 | # webhookit --help 24 | Usage: webhookit [OPTIONS] 25 | 26 | Options: 27 | -c, --config PATH The web hook configure file path. 28 | -p, --port INTEGER The listening port of HTTP server. 29 | --help Show this message and exit. 30 | ``` 31 | 32 | Run **`webhookit_config`** to get the config template strings. 33 | 34 | Run **`webhookit -c config.py -p 18340`** to start the http server for git webhook. 35 | 36 | 37 | # 3. Example 38 | 39 | Here is an simple example to run the `webhookit` http server. 40 | 41 | ```sh 42 | # 1. install webhookit 43 | pip install webhookit 44 | 45 | # 2. initial a webhookit config file 46 | webhookit_config > /home/hustcc/webhook-configs/config4hustcc.py 47 | 48 | # 3. update config4hustcc.py with your own config and save 49 | vim config4hustcc.py 50 | 51 | # 4. run webhookit http server 52 | webhookit -c config4hustcc.py 53 | ``` 54 | 55 | Then open `http://host:18340` in your browser, can see: 56 | 57 | 1. The webhook status. 58 | 2. The webhook url. 59 | 3. The webhook server configures. 60 | 61 | 62 | # 4. configure file 63 | 64 | ```py 65 | # -*- coding: utf-8 -*- 66 | ''' 67 | Created on Mar-03-17 15:14:34 68 | @author: hustcc/webhookit 69 | ''' 70 | 71 | # This means: 72 | # When get a webhook request from `repo_name` on branch `branch_name`, 73 | # will exec SCRIPT on servers config in the array. 74 | WEBHOOKIT_CONFIGURE = { 75 | # a web hook request can trigger multiple servers. 76 | 'repo_name/branch_name': [{ 77 | # if exec shell on local server, keep empty. 78 | 'HOST': '', # will exec shell on which server. 79 | 'PORT': '', # ssh port, default is 22. 80 | 'USER': '', # linux user name 81 | 'PWD': '', # user password or private key. 82 | 83 | # The webhook shell script path. 84 | 'SCRIPT': '/home/hustcc/exec_hook_shell.sh' 85 | }, 86 | ...], 87 | ... 88 | } 89 | ``` 90 | 91 | The python var name `WEBHOOKIT_CONFIGURE` can not be modified. 92 | 93 | Each webhook has it's key with format of `'repo_name/branch_name'`, Each webhook can trigger a group of servers, which is the value of the key. 94 | 95 | Server can be remote and local, if local, keep `HOST`, `PORT`, `USER`, `PWD` be empty. 96 | 97 | 98 | # 5. License 99 | 100 | MIT@[hustcc](https://github.com/hustcc). 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # webhookit 2 | 3 | > 一个极简的命令行版本的 git webhok,部署容易,非常简单就可以部署一个 webhook server。支持**GitHub**, **GitLab**, **GitOsc**, **Gogs**, **Coding**。Python 2 / 3 都支持。 4 | 5 | [在线实例展示](http://webhookit.hust.cc) | [English README](README.md) 6 | 7 | [![Latest Stable Version](https://img.shields.io/pypi/v/webhookit.svg)](https://pypi.python.org/pypi/webhookit) [![Build Status](https://travis-ci.org/hustcc/webhookit.svg?branch=master)](https://travis-ci.org/hustcc/webhookit) ![GitHub](http://shields.hust.cc/Supported-GitHub-brightgreen.svg) ![GitLab](http://shields.hust.cc/Supported-GitLab-green.svg) ![GitOsc](http://shields.hust.cc/Supported-GitOsc-blue.svg) ![Gogs](http://shields.hust.cc/Supported-Gogs-yellowgreen.svg) ![Coding](http://shields.hust.cc/Supported-Coding-yellow.svg) 8 | 9 | 10 | # 1. 安装 11 | 12 | > **pip install webhookit** 13 | 14 | 支持 Python 2 / 3。安装之后,在系统中可以得到两个命令工具:`webhookit` and `webhookit_config`。 15 | 16 | 17 | # 2. 使用 18 | 19 | 运行 `webhookit --help` 可以得到命令的帮助信息,具体的信息如下: 20 | 21 | ```sh 22 | # webhookit --help 23 | Usage: webhookit [OPTIONS] 24 | 25 | Options: 26 | -c, --config PATH The web hook configure file path. 27 | -p, --port INTEGER The listening port of HTTP server. 28 | --help Show this message and exit. 29 | ``` 30 | 31 | 运行 **`webhookit_config`** 可以得到工具配置的模版内容。 32 | 33 | 运行 **`webhookit -c config.py -p 18340`** 开启一个 webhook 的 http 服务器。 34 | 35 | 36 | # 3. 一个示例 37 | 38 | 下面是一个简单的例子,用来展示如何使用本工具: 39 | 40 | ```sh 41 | # 1. 安装 webhookit 42 | pip install webhookit 43 | 44 | # 2. 初始化一个配置模版 45 | webhookit_config > /home/hustcc/webhook-configs/config4hustcc.py 46 | 47 | # 3. 更新 config4hustcc.py 配置内容 48 | vim config4hustcc.py 49 | 50 | # 4. 运行 http server 51 | webhookit -c config4hustcc.py 52 | ``` 53 | 54 | 然后在浏览器中打开 `http://host:18340` 就可以看到下面的一些信息了: 55 | 56 | 1. webhook 执行的状态; 57 | 2. webhook 的 URL 地址; 58 | 3. webhook 的配置信息(隐藏私密信息); 59 | 60 | 61 | # 4. 配置文件说明 62 | 63 | ```py 64 | # -*- coding: utf-8 -*- 65 | ''' 66 | Created on Mar-03-17 15:14:34 67 | @author: hustcc/webhookit 68 | ''' 69 | 70 | # This means: 71 | # When get a webhook request from `repo_name` on branch `branch_name`, 72 | # will exec SCRIPT on servers config in the array. 73 | WEBHOOKIT_CONFIGURE = { 74 | # a web hook request can trigger multiple servers. 75 | 'repo_name/branch_name': [{ 76 | # if exec shell on local server, keep empty. 77 | 'HOST': '', # will exec shell on which server. 78 | 'PORT': '', # ssh port, default is 22. 79 | 'USER': '', # linux user name 80 | 'PWD': '', # user password or private key. 81 | 82 | # The webhook shell script path. 83 | 'SCRIPT': '/home/hustcc/exec_hook_shell.sh' 84 | }, 85 | ...], 86 | ... 87 | } 88 | ``` 89 | 90 | Python 变量名 `WEBHOOKIT_CONFIGURE` 不要去修改。 91 | 92 | 每个 webhook 都用仓库的名字和分支名字 `'repo_name/branch_name'` 作为它的键值,每个 webhook 可以触发一组服务器,这些服务器的配置信息存储在一个数组中。 93 | 94 | 服务器可以是远程的服务器,也可以是本地机器,如果要触发本机的脚本运行,那么请保持 `HOST`, `PORT`, `USER`, `PWD` 这些配置为空,或者不存在这些键值。 95 | 96 | 97 | # 5. License 98 | 99 | MIT@[hustcc](https://github.com/hustcc). 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import setuptools # noqa 3 | from distutils.core import setup 4 | import io 5 | import re 6 | import os 7 | 8 | 9 | DOC = ''' 10 | ## 1. Install 11 | 12 | > **pip install webhookit** 13 | 14 | Then get cli command `webhookit_config` and `webhookit`. 15 | 16 | 17 | ## 2. Usage 18 | 19 | Simple, like below: 20 | 21 | > **webhookit_config > config.py** 22 | 23 | > **webhookit -c config.py -p 18340** 24 | 25 | Or you can `webhookit --help` to get the help content. 26 | ''' 27 | 28 | 29 | def read(*names, **kwargs): 30 | return io.open( 31 | os.path.join(os.path.dirname(__file__), *names), 32 | encoding=kwargs.get("encoding", "utf8") 33 | ).read() 34 | 35 | 36 | def find_version(*file_paths): 37 | version_file = read(*file_paths) 38 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 39 | version_file, re.M) 40 | if version_match: 41 | return version_match.group(1) 42 | raise RuntimeError("Unable to find version string.") 43 | 44 | 45 | setup(name='webhookit', 46 | version=find_version('webhookit/app.py'), 47 | description=('Bind git webhooks with actions. ' 48 | 'Simple git webhook cli tool for automation tasks.'), 49 | long_description=DOC, 50 | author='hustcc', 51 | author_email='i@hust.cc', 52 | url='https://github.com/hustcc', 53 | license='MIT', 54 | install_requires=[ 55 | 'click', 56 | 'tornado', 57 | ], 58 | classifiers=[ 59 | 'Intended Audience :: Developers', 60 | 'Operating System :: OS Independent', 61 | 'Natural Language :: Chinese (Simplified)', 62 | 'Programming Language :: Python', 63 | 'Programming Language :: Python :: 2', 64 | 'Programming Language :: Python :: 2.5', 65 | 'Programming Language :: Python :: 2.6', 66 | 'Programming Language :: Python :: 2.7', 67 | 'Programming Language :: Python :: 3', 68 | 'Programming Language :: Python :: 3.3', 69 | 'Programming Language :: Python :: 3.4', 70 | 'Programming Language :: Python :: 3.5', 71 | 'Programming Language :: Python :: 3.6', 72 | 'Topic :: Utilities' 73 | ], 74 | keywords='webhookit, webhook, github, gitlab, gogs, git-webhook', 75 | include_package_data=True, 76 | zip_safe=False, 77 | packages=['webhookit'], 78 | entry_points={ 79 | 'console_scripts': [ 80 | 'webhookit=webhookit.cli:runserver', 81 | 'webhookit_config=webhookit.cli:config' 82 | ] 83 | }) 84 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Mar-03-17 15:14:34 4 | 5 | @author: hustcc/webhookit 6 | ''' 7 | 8 | 9 | # This means: 10 | # When get a webhook request from `repo_name` on branch `branch_name`, 11 | # will exec SCRIPT on servers config in the array. 12 | WEBHOOKIT_CONFIGURE = { 13 | # a web hook request can trigger multiple servers. 14 | 'repo_name/branch_name': [{ 15 | # The webhook shell script path. 16 | 'SCRIPT': '/home/hustcc/exec_hook_shell.sh' 17 | }, { 18 | # if exec shell on local server, keep empty. 19 | 'HOST': '10.240.121.12', # will exec shell on which server. 20 | 'PORT': '21', # ssh port, default is 22. 21 | 'USER': 'hustcc', # linux user name 22 | 'PWD': 'hustcc_pwd', # user password or private key. 23 | 24 | # The webhook shell script path. 25 | 'SCRIPT': '/home/hustcc/exec_hook_shell.sh' 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /tests/webhookdata/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/master", 3 | "before": "a505ee7559ff0a64480d0a73e36f8d3ce3dc117a", 4 | "after": "f79c8445fdf5eab00ff33485e47474e0961072a7", 5 | "created": false, 6 | "deleted": false, 7 | "forced": false, 8 | "base_ref": null, 9 | "compare": "https://github.com/hustcc/placeholder.js/compare/a505ee7559ff...f79c8445fdf5", 10 | "commits": [{ 11 | "id": "f79c8445fdf5eab00ff33485e47474e0961072a7", 12 | "distinct": true, 13 | "message": "modify default font", 14 | "timestamp": "2015-12-30T10:22:05+08:00", 15 | "url": "https://github.com/hustcc/placeholder.js/commit/f79c8445fdf5eab00ff33485e47474e0961072a7", 16 | "author": { 17 | "name": "hustcc", 18 | "email": "i@hust.cc", 19 | "username": "hustcc" 20 | }, 21 | "committer": { 22 | "name": "hustcc", 23 | "email": "i@hust.cc", 24 | "username": "hustcc" 25 | }, 26 | "added": [], 27 | "removed": [], 28 | "modified": [ 29 | "server/placeholder.class.php" 30 | ] 31 | }], 32 | "head_commit": { 33 | "id": "f79c8445fdf5eab00ff33485e47474e0961072a7", 34 | "distinct": true, 35 | "message": "modify default font", 36 | "timestamp": "2015-12-30T10:22:05+08:00", 37 | "url": "https://github.com/hustcc/placeholder.js/commit/f79c8445fdf5eab00ff33485e47474e0961072a7", 38 | "author": { 39 | "name": "hustcc", 40 | "email": "i@hust.cc", 41 | "username": "hustcc" 42 | }, 43 | "committer": { 44 | "name": "hustcc", 45 | "email": "i@hust.cc", 46 | "username": "hustcc" 47 | }, 48 | "added": [], 49 | "removed": [], 50 | "modified": [ 51 | "server/placeholder.class.php" 52 | ] 53 | }, 54 | "repository": { 55 | "id": 48161277, 56 | "name": "git-webhook", 57 | "full_name": "hustcc/placeholder.js", 58 | "owner": { 59 | "name": "hustcc", 60 | "email": "i@atool.org" 61 | }, 62 | "private": false, 63 | "html_url": "https://github.com/hustcc/placeholder.js", 64 | "description": ":zap:<1kb. Client-side library generate image placeholders. Do not depends on jQuery or Other.", 65 | "fork": false, 66 | "url": "https://github.com/hustcc/placeholder.js", 67 | "forks_url": "https://api.github.com/repos/hustcc/placeholder.js/forks", 68 | "keys_url": "https://api.github.com/repos/hustcc/placeholder.js/keys{/key_id}", 69 | "collaborators_url": "https://api.github.com/repos/hustcc/placeholder.js/collaborators{/collaborator}", 70 | "teams_url": "https://api.github.com/repos/hustcc/placeholder.js/teams", 71 | "hooks_url": "https://api.github.com/repos/hustcc/placeholder.js/hooks", 72 | "issue_events_url": "https://api.github.com/repos/hustcc/placeholder.js/issues/events{/number}", 73 | "events_url": "https://api.github.com/repos/hustcc/placeholder.js/events", 74 | "assignees_url": "https://api.github.com/repos/hustcc/placeholder.js/assignees{/user}", 75 | "branches_url": "https://api.github.com/repos/hustcc/placeholder.js/branches{/branch}", 76 | "tags_url": "https://api.github.com/repos/hustcc/placeholder.js/tags", 77 | "blobs_url": "https://api.github.com/repos/hustcc/placeholder.js/git/blobs{/sha}", 78 | "git_tags_url": "https://api.github.com/repos/hustcc/placeholder.js/git/tags{/sha}", 79 | "git_refs_url": "https://api.github.com/repos/hustcc/placeholder.js/git/refs{/sha}", 80 | "trees_url": "https://api.github.com/repos/hustcc/placeholder.js/git/trees{/sha}", 81 | "statuses_url": "https://api.github.com/repos/hustcc/placeholder.js/statuses/{sha}", 82 | "languages_url": "https://api.github.com/repos/hustcc/placeholder.js/languages", 83 | "stargazers_url": "https://api.github.com/repos/hustcc/placeholder.js/stargazers", 84 | "contributors_url": "https://api.github.com/repos/hustcc/placeholder.js/contributors", 85 | "subscribers_url": "https://api.github.com/repos/hustcc/placeholder.js/subscribers", 86 | "subscription_url": "https://api.github.com/repos/hustcc/placeholder.js/subscription", 87 | "commits_url": "https://api.github.com/repos/hustcc/placeholder.js/commits{/sha}", 88 | "git_commits_url": "https://api.github.com/repos/hustcc/placeholder.js/git/commits{/sha}", 89 | "comments_url": "https://api.github.com/repos/hustcc/placeholder.js/comments{/number}", 90 | "issue_comment_url": "https://api.github.com/repos/hustcc/placeholder.js/issues/comments{/number}", 91 | "contents_url": "https://api.github.com/repos/hustcc/placeholder.js/contents/{+path}", 92 | "compare_url": "https://api.github.com/repos/hustcc/placeholder.js/compare/{base}...{head}", 93 | "merges_url": "https://api.github.com/repos/hustcc/placeholder.js/merges", 94 | "archive_url": "https://api.github.com/repos/hustcc/placeholder.js/{archive_format}{/ref}", 95 | "downloads_url": "https://api.github.com/repos/hustcc/placeholder.js/downloads", 96 | "issues_url": "https://api.github.com/repos/hustcc/placeholder.js/issues{/number}", 97 | "pulls_url": "https://api.github.com/repos/hustcc/placeholder.js/pulls{/number}", 98 | "milestones_url": "https://api.github.com/repos/hustcc/placeholder.js/milestones{/number}", 99 | "notifications_url": "https://api.github.com/repos/hustcc/placeholder.js/notifications{?since,all,participating}", 100 | "labels_url": "https://api.github.com/repos/hustcc/placeholder.js/labels{/name}", 101 | "releases_url": "https://api.github.com/repos/hustcc/placeholder.js/releases{/id}", 102 | "created_at": 1450339947, 103 | "updated_at": "2015-12-30T02:11:45Z", 104 | "pushed_at": 1451442132, 105 | "git_url": "git://github.com/hustcc/placeholder.js.git", 106 | "ssh_url": "git@github.com:hustcc/placeholder.js.git", 107 | "clone_url": "https://github.com/hustcc/placeholder.js.git", 108 | "svn_url": "https://github.com/hustcc/placeholder.js", 109 | "homepage": "http://placeholder.cn", 110 | "size": 376, 111 | "stargazers_count": 121, 112 | "watchers_count": 121, 113 | "language": "HTML", 114 | "has_issues": true, 115 | "has_downloads": true, 116 | "has_wiki": true, 117 | "has_pages": false, 118 | "forks_count": 16, 119 | "mirror_url": null, 120 | "open_issues_count": 1, 121 | "forks": 16, 122 | "open_issues": 1, 123 | "watchers": 121, 124 | "default_branch": "master", 125 | "stargazers": 121, 126 | "master_branch": "master" 127 | }, 128 | "pusher": { 129 | "name": "hustcc", 130 | "email": "i@atool.org" 131 | }, 132 | "sender": { 133 | "login": "hustcc", 134 | "id": 7856674, 135 | "avatar_url": "https://avatars.githubusercontent.com/u/7856674?v=3", 136 | "gravatar_id": "", 137 | "url": "https://api.github.com/users/hustcc", 138 | "html_url": "https://github.com/hustcc", 139 | "followers_url": "https://api.github.com/users/hustcc/followers", 140 | "following_url": "https://api.github.com/users/hustcc/following{/other_user}", 141 | "gists_url": "https://api.github.com/users/hustcc/gists{/gist_id}", 142 | "starred_url": "https://api.github.com/users/hustcc/starred{/owner}{/repo}", 143 | "subscriptions_url": "https://api.github.com/users/hustcc/subscriptions", 144 | "organizations_url": "https://api.github.com/users/hustcc/orgs", 145 | "repos_url": "https://api.github.com/users/hustcc/repos", 146 | "events_url": "https://api.github.com/users/hustcc/events{/privacy}", 147 | "received_events_url": "https://api.github.com/users/hustcc/received_events", 148 | "type": "User", 149 | "site_admin": false 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/webhookdata/gitlab.json: -------------------------------------------------------------------------------- 1 | { 2 | "object_kind": "push", 3 | "before": "2883a17f7a69c35b2a591c63d9581e634d7c3d0e", 4 | "after": "d5bb5e2fbafb75c37d201de8d0b7f50e2db6b04e", 5 | "ref": "refs/heads/master", 6 | "checkout_sha": "d5bb5e2fbafb75c37d201de8d0b7f50e2db6b04e", 7 | "message": null, 8 | "user_id": 41, 9 | "user_name": "王志伟", 10 | "user_email": "i@hust.cc", 11 | "project_id": 10, 12 | "repository": { 13 | "name": "git-webhook", 14 | "url": "ssh://git@hust.cc:32200/i/code-mess.git", 15 | "description": "一些杂碎简单的代码,自己写起来麻烦,搜索也能搜索到,收集起来,提高效率", 16 | "homepage": "http://hust.cc/i/code-mess", 17 | "git_http_url": "http://hust.cc/i/code-mess.git", 18 | "git_ssh_url": "ssh://git@hust.cc:32200/i/code-mess.git", 19 | "visibility_level": 20 20 | }, 21 | "commits": [{ 22 | "id": "d5bb5e2fbafb75c37d201de8d0b7f50e2db6b04e", 23 | "message": "update md", 24 | "timestamp": "2015-02-13T14:36:08+08:00", 25 | "url": "http://hust.cc/i/code-mess/commit/d5bb5e2fbafb75c37d201de8d0b7f50e2db6b04e", 26 | "author": { 27 | "name": "hustcc", 28 | "email": "i@hust.cc" 29 | } 30 | }, { 31 | "id": "689ffb6dd35ed9035264ed4155fd4ba8340fcf4d", 32 | "message": "增加之前做过微信易信公众号开发代码", 33 | "timestamp": "2015-02-13T14:31:19+08:00", 34 | "url": "http://hust.cc/i/code-mess/commit/689ffb6dd35ed9035264ed4155fd4ba8340fcf4d", 35 | "author": { 36 | "name": "hustcc", 37 | "email": "i@hust.cc" 38 | } 39 | }, { 40 | "id": "2883a17f7a69c35b2a591c63d9581e634d7c3d0e", 41 | "message": "加了最近的一些代码,也许大家有用", 42 | "timestamp": "2015-02-13T13:52:12+08:00", 43 | "url": "http://hust.cc/i/code-mess/commit/2883a17f7a69c35b2a591c63d9581e634d7c3d0e", 44 | "author": { 45 | "name": "hustcc", 46 | "email": "i@hust.cc" 47 | } 48 | }], 49 | "total_commits_count": 3 50 | } 51 | -------------------------------------------------------------------------------- /tests/webhookdata/gitosc.json: -------------------------------------------------------------------------------- 1 | { 2 | "password": "hod20x4hnkecy63", 3 | "hook_name": "push_hooks", 4 | "push_data": { 5 | "before": "26c9fb3533d1c63e3b822d58a8831b1caf6d9cd7", 6 | "after": "219aa13d45276a5cae7436ecc66053da67ec2a2d", 7 | "ref": "refs/heads/master", 8 | "user_id": 449815, 9 | "user_name": "gitlabu6d4bu8bd5u8d26u53f72", 10 | "user": { 11 | "id": 449815, 12 | "email": "gitlabtest_2@gitlab.com", 13 | "name": "gitlabu6d4bu8bd5u8d26u53f72", 14 | "time": "2015-11-06T14:51:55+08:00" 15 | }, 16 | "repository": { 17 | "name": "git-webhook", 18 | "url": "git@git.oschina.net:gitlab_test_1/git-webhook.git", 19 | "description": "git-webhook testting", 20 | "homepage": "http://git.oschina.net/gitlab_test_1/git-webhook" 21 | }, 22 | "commits": [{ 23 | "id": "219aa13d45276a5cae7436ecc66053da67ec2a2d", 24 | "message": "commit_access_test", 25 | "timestamp": "2015-11-06T14:50:47+08:00", 26 | "url": "http://git.oschina.net/gitlab_test_1/git-webhook/commit/219aa13d45276a5cae7436ecc66053da67ec2a2d", 27 | "author": { 28 | "name": "gitlab_test_2", 29 | "email": "gitlabtest_2@gitlab.com", 30 | "time": "2015-11-06T14:50:47+08:00" 31 | } 32 | }], 33 | "total_commits_count": 1, 34 | "commits_more_than_ten": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/webhookdata/gogs.json: -------------------------------------------------------------------------------- 1 | { 2 | "sender": { 3 | "username": "hustcc", 4 | "avatar_url": "https://secure.gravatar.com/avatar/89b00603405ed96c59206c65a4f2358e", 5 | "id": 1, 6 | "full_name": "", 7 | "email": "wzwahl36@qq.com" 8 | }, 9 | "repository": { 10 | "fork": false, 11 | "website": "", 12 | "open_issues_count": 0, 13 | "description": "git-webhook", 14 | "default_branch": "master", 15 | "created_at": "2016-10-28T15:11:35+08:00", 16 | "forks_count": 0, 17 | "html_url": "http://127.0.0.1:3000/hustcc/git-webhook", 18 | "private": false, 19 | "updated_at": "2016-10-28T15:11:36+08:00", 20 | "stars_count": 0, 21 | "clone_url": "http://127.0.0.1:3000/hustcc/git-webhook.git", 22 | "watchers_count": 1, 23 | "full_name": "hustcc/git-webhook", 24 | "ssh_url": "hzwangzhiwei@127.0.0.1:hustcc/git-webhook.git", 25 | "owner": { 26 | "username": "hustcc", 27 | "avatar_url": "https://secure.gravatar.com/avatar/89b00603405ed96c59206c65a4f2358e", 28 | "id": 1, 29 | "full_name": "", 30 | "email": "wzwahl36@qq.com" 31 | }, 32 | "id": 1, 33 | "name": "git-webhook" 34 | }, 35 | "pusher": { 36 | "username": "hustcc", 37 | "avatar_url": "https://secure.gravatar.com/avatar/89b00603405ed96c59206c65a4f2358e", 38 | "id": 1, 39 | "full_name": "", 40 | "email": "wzwahl36@qq.com" 41 | }, 42 | "commits": [{ 43 | "committer": { 44 | "username": "hustcc", 45 | "name": "hustcc", 46 | "email": "wzwahl36@qq.com" 47 | }, 48 | "author": { 49 | "username": "hustcc", 50 | "name": "hustcc", 51 | "email": "wzwahl36@qq.com" 52 | }, 53 | "url": "http://127.0.0.1:3000/hustcc/git-webhook/commit/331293f63684868be6b27da0104e35e3564059ad", 54 | "timestamp": "2016-10-28T15:15:21+08:00", 55 | "message": "test.txt", 56 | "id": "331293f63684868be6b27da0104e35e3564059ad" 57 | }], 58 | "after": "331293f63684868be6b27da0104e35e3564059ad", 59 | "compare_url": "http://127.0.0.1:3000/hustcc/git-webhook/compare/b51fdb16830e2206a528aa00e778fb0163506c05...331293f63684868be6b27da0104e35e3564059ad", 60 | "secret": "", 61 | "ref": "refs/heads/master", 62 | "before": "b51fdb16830e2206a528aa00e778fb0163506c05" 63 | } 64 | -------------------------------------------------------------------------------- /webhookit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2017-03-03 4 | 5 | @author: hustcc 6 | ''' 7 | -------------------------------------------------------------------------------- /webhookit/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Mar 3, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from webhookit import utils, temp, parser 9 | import json 10 | import tornado.ioloop 11 | import tornado.web 12 | import tornado.websocket 13 | import tornado.template 14 | 15 | 16 | __version__ = '0.1.1' 17 | 18 | webhook_cnt = 0 # webhook 计数,每次重启都清空 19 | webhook_last = '' 20 | webhook_repo = '' # webhook hook 哪一个 git 仓库 21 | WEBHOOKIT_CONFIGURE = None 22 | 23 | 24 | class IndexPageHandler(tornado.web.RequestHandler): 25 | def get(self): 26 | global webhook_cnt, webhook_last, webhook_repo, WEBHOOKIT_CONFIGURE 27 | config = WEBHOOKIT_CONFIGURE or {} 28 | config = utils.filter_sensitive(config) 29 | t = tornado.template.Template(temp.INDEX_HTML_TEMP) 30 | self.write(t.generate(version=__version__, 31 | count=webhook_cnt, 32 | date=webhook_last, 33 | repo=webhook_repo, 34 | logs=WSHandler.logs, 35 | config=json.dumps(config, 36 | indent=4))) 37 | 38 | 39 | class WebhookitHandler(tornado.web.RequestHandler): 40 | def post(self): 41 | global webhook_cnt, webhook_last, webhook_repo, WEBHOOKIT_CONFIGURE 42 | # gitosc: hook, github(application/x-www-form-urlencoded): payload 43 | data = self.get_argument('hook', self.get_argument('payload', None)) 44 | if data is None: 45 | # gitlab and github(application/json) 46 | data = self.request.body or None 47 | try: 48 | data = json.loads(data) # webhook data 49 | # webhook configs 50 | config = WEBHOOKIT_CONFIGURE or {} 51 | 52 | repo_name = parser.get_repo_name(data) or '' 53 | repo_branch = parser.get_repo_branch(data) or '' 54 | webhook_key = '%s/%s' % (repo_name, repo_branch) 55 | # 需要出发操作的服务器 server 数组 56 | servers = config.get(webhook_key, []) 57 | if servers and len(servers) > 0: 58 | # 存在 server,需要执行 shell 脚本 59 | cnt = 0 60 | for s in servers: 61 | # 遍历执行 62 | utils.log('Starting to execute %s' % s.get('SCRIPT', '')) 63 | utils.do_webhook_shell(s, data) 64 | webhook_cnt += 1 65 | cnt += 1 66 | # 更新最后执行的时间 67 | webhook_last = utils.current_date() 68 | # 更新最后执行的 git 仓库 69 | webhook_repo = webhook_key 70 | msg = [webhook_cnt, webhook_last, webhook_repo] 71 | WSHandler.push_msg({'type': 'stat', 'msg': msg}) 72 | t = 'Processed in thread, total %s threads.' % cnt 73 | self.write(utils.standard_response(True, t)) 74 | else: 75 | t = ('Not match the repo and branch ' 76 | 'or the webhook servers is not exist: %s') % webhook_key 77 | utils.log(t) 78 | self.write(utils.standard_response(False, t)) 79 | except Exception as e: 80 | t = ('Request trigger traceback: %s') % str(e) 81 | utils.log(t) 82 | utils.log('data is %s.' % (data or None)) 83 | self.write(utils.standard_response(False, t)) 84 | 85 | def get(self): 86 | return self.post() 87 | 88 | 89 | class WSHandler(tornado.websocket.WebSocketHandler): 90 | clients = set() 91 | logs = [] 92 | log_size = 10 # 初始仅仅显示最开始 10 条日志记录 93 | 94 | def open(self): 95 | WSHandler.clients.add(self) 96 | 97 | def on_close(self): 98 | WSHandler.clients.remove(self) 99 | 100 | @classmethod 101 | def update_logs(cls, msg): 102 | if msg.get('type') == 'log': 103 | cls.logs.append(msg) 104 | if len(cls.logs) > cls.log_size: 105 | cls.logs = cls.logs[-cls.log_size:] 106 | 107 | @classmethod 108 | def push_msg(cls, msg): 109 | ''' 110 | msg: 111 | { 112 | type: log|stat 113 | msg: msg 114 | } 115 | ''' 116 | WSHandler.update_logs(msg) 117 | msg = json.dumps(msg) 118 | for client in cls.clients: 119 | try: 120 | client.write_message(msg) 121 | except Exception as e: 122 | utils.log('Error sending message: %s' % str(e)) 123 | 124 | 125 | application = tornado.web.Application([ 126 | (r'/', IndexPageHandler), 127 | (r'/webhookit', WebhookitHandler), 128 | (r'/ws', WSHandler) 129 | ]) 130 | 131 | 132 | def runserver(port=18340): 133 | application.listen(port) 134 | tornado.ioloop.IOLoop.instance().start() 135 | 136 | 137 | if __name__ == '__main__': 138 | runserver() 139 | -------------------------------------------------------------------------------- /webhookit/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2017-03-03 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from tornado.options import define, options, parse_config_file 9 | from webhookit import temp, app 10 | import click 11 | import os 12 | 13 | 14 | @click.command() 15 | @click.option('-c', '--config', type=click.Path(exists=True), 16 | help='The web hook configure file path.') 17 | @click.option('-p', '--port', default=18340, 18 | type=click.INT, 19 | help='The listening port of HTTP server.') 20 | def webhookit_server_entry(config, port): 21 | if not config: 22 | click.echo('webhookit: `config` should not be empty.') 23 | return 24 | 25 | config_path = os.path.join(os.path.abspath(os.curdir), config) 26 | # load pyfile configure 27 | define('WEBHOOKIT_CONFIGURE', type=dict) 28 | parse_config_file(config_path) 29 | app.WEBHOOKIT_CONFIGURE = options.WEBHOOKIT_CONFIGURE 30 | 31 | click.echo('webhookit: HTTP Server started. Listening %s...' % port) 32 | app.runserver(port=port) 33 | 34 | 35 | def runserver(): 36 | webhookit_server_entry() 37 | 38 | 39 | @click.command() 40 | def webhookit_config_entry(): 41 | click.echo(temp.CONFIG_TEMP) 42 | 43 | 44 | def config(): 45 | webhookit_config_entry() 46 | 47 | 48 | if __name__ == '__main__': 49 | # config() 50 | runserver() 51 | -------------------------------------------------------------------------------- /webhookit/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Mar 3, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | 8 | 9 | # repo name 10 | def get_repo_name(hook_data): 11 | return hook_data.get('repository', {}).get('name', '') \ 12 | or hook_data.get('push_data', {}).get('repository', {}).get('name', '') 13 | 14 | 15 | # repo branch 16 | def get_repo_branch(hook_data): 17 | branch = hook_data.get('ref', '') # github, gitlib 18 | if not branch: 19 | branch = hook_data.get('push_data', {}).get('ref', '') 20 | if '/' in branch: 21 | return branch[branch.rfind("/") + 1:] 22 | return branch 23 | 24 | 25 | # push user name 26 | def get_push_name(hook_data): 27 | uid = hook_data.get('pusher', {}).get('name', None) # github的data格式 28 | if uid: 29 | return uid 30 | uid = hook_data.get('user_name', None) # gitlib 格式 31 | if uid: 32 | return uid 33 | uid = hook_data.get('pusher', {}).get('username', None) # gogs 格式 34 | if uid: 35 | return uid 36 | 37 | uid = hook_data.get('push_data', {}) \ 38 | .get('user', {}).get('name', None) # gitosc 的 data 格式 39 | if uid: 40 | return uid 41 | return '' 42 | 43 | 44 | # push user email 45 | def get_push_email(hook_data): 46 | uid = hook_data.get('pusher', {}).get('email', None) # github 的 data格式 47 | if uid: 48 | return uid 49 | uid = hook_data.get('user_email', None) # gitlab 格式 50 | if uid: 51 | return uid 52 | 53 | uid = hook_data\ 54 | .get('push_data', {})\ 55 | .get('user', {}).get('email', None) # gitosc 的data格式 56 | if uid: 57 | return uid 58 | return '' 59 | -------------------------------------------------------------------------------- /webhookit/temp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2017-03-03 4 | 5 | @author: hustcc 6 | ''' 7 | import datetime 8 | 9 | 10 | CONFIG_TEMP = """# -*- coding: utf-8 -*- 11 | ''' 12 | Created on %s 13 | 14 | @author: hustcc/webhookit 15 | ''' 16 | 17 | 18 | # This means: 19 | # When get a webhook request from `repo_name` on branch `branch_name`, 20 | # will exec SCRIPT on servers config in the array. 21 | WEBHOOKIT_CONFIGURE = { 22 | # a web hook request can trigger multiple servers. 23 | 'repo_name/branch_name': [{ 24 | # if exec shell on local server, keep empty. 25 | 'HOST': '', # will exec shell on which server. 26 | 'PORT': '', # ssh port, default is 22. 27 | 'USER': '', # linux user name 28 | 'PWD': '', # user password or private key. 29 | 30 | # The webhook shell script path. 31 | 'SCRIPT': '/home/hustcc/exec_hook_shell.sh' 32 | }] 33 | }""" % datetime.datetime.now().strftime('%b-%d-%y %H:%M:%S') 34 | 35 | 36 | INDEX_HTML_TEMP = ''' 37 | 38 | 39 | hustcc/webhookit: Simple git webhook cli tool for automation tasks. 40 | 41 | 42 | 43 | 44 | 63 | 64 | 65 |

webhookit

66 |

67 | 68 | hustcc/webhookit 69 | is a simple git webhook cli tool for automation tasks. 70 |

71 |

SERVER status

72 |
Trigger count {{count}}. Last trigger at {{date or 'None'}} on {{repo or 'None'}}.
73 |

WEBHOOK logs

74 |
{% for log in logs[::-1] %}{{log.get('msg', '')}}
{% end %}
75 |

WEBHOOK url

76 |
77 |

WEBHOOK configure

78 |
{{config}}
79 |
80 | 99 | 100 | 101 | 106 | 109 | 110 |

Code of hustcc/webhookit hosted on github. Authored by hustcc.

111 |

Current running version: v{{version}}

112 | 113 | 114 | 115 | 116 | 117 | 118 | ''' 119 | -------------------------------------------------------------------------------- /webhookit/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Mar 3, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | from functools import wraps 8 | from threading import Thread 9 | import json 10 | import click 11 | import datetime 12 | import copy 13 | from webhookit import app 14 | 15 | 16 | try: 17 | unicode # noqa 18 | except NameError: 19 | # py3 20 | the_unicode = str # noqa 21 | else: # noqa 22 | # py2 23 | the_unicode = unicode # noqa 24 | 25 | 26 | def standard_response(success, data): 27 | '''standard response 28 | ''' 29 | rst = {} 30 | rst['success'] = success 31 | rst['data'] = data 32 | return json.dumps(rst) 33 | 34 | 35 | def async(f): 36 | @wraps(f) 37 | def wrapper(*args, **kwargs): 38 | thr = Thread(target=f, args=args, kwargs=kwargs) 39 | thr.setDaemon(True) 40 | thr.start() 41 | return wrapper 42 | 43 | 44 | # log 45 | def log(t): 46 | msg = '%s: %s' % (current_date(), t) 47 | app.WSHandler.push_msg({'type': 'log', 'msg': msg}) 48 | click.echo(msg) 49 | 50 | 51 | def current_date(): 52 | return datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S.%f") 53 | 54 | 55 | def filter_server(server): 56 | server = copy.deepcopy(server) 57 | if is_remote_server(server): 58 | server['HOST'] = '***.**.**.**' 59 | server['PORT'] = '****' 60 | server['USER'] = '*******' 61 | server['PWD'] = '*******' 62 | return server 63 | 64 | 65 | # 过滤服务器配置信息的敏感信息 66 | def filter_sensitive(config): 67 | fconfig = {} 68 | for k, v in config.items(): 69 | fconfig[k] = [] 70 | for server in v: 71 | fconfig[k].append(filter_server(server)) 72 | return fconfig 73 | 74 | 75 | # if host port user pwd all is not empty, then it is a remote server. 76 | def is_remote_server(s): 77 | # all is not empty or zero, then remote server 78 | return all([s.get('HOST', None), 79 | s.get('PORT', None), 80 | s.get('USER', None), 81 | s.get('PWD', None)]) 82 | 83 | 84 | # ssh to exec cmd 85 | def do_ssh_cmd(ip, port, account, pkey, shell, push_data='', timeout=300): 86 | import paramiko 87 | import StringIO 88 | 89 | def is_msg_success(msg): 90 | for x in ['fatal', 'fail', 'error']: 91 | if msg.startswith(x) or msg.endswith(x): 92 | return False 93 | return True 94 | 95 | try: 96 | port = int(port) 97 | except: 98 | port = 22 99 | 100 | s = paramiko.SSHClient() 101 | s.load_system_host_keys() 102 | s.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 103 | 104 | try: 105 | # 首先以 ssh 密钥方式登陆 106 | pkey_file = StringIO.StringIO(pkey.strip() + '\n') # 注意最后有一个换行 107 | private_key = paramiko.RSAKey.from_private_key(pkey_file) 108 | s.connect(ip, port, account, pkey=private_key, timeout=10) 109 | pkey_file.close() 110 | except: 111 | # 如果出现异常,则使用 用户密码登陆的方式 112 | s.connect(ip, port, account, password=pkey, timeout=10) 113 | 114 | # if push_data: 115 | # shell = shell + (" '%s'" % push_data) 116 | shell = shell.split('\n') 117 | shell = [sh for sh in shell if sh.strip()] 118 | shell = ' && '.join(shell) 119 | 120 | stdin, stdout, stderr = s.exec_command(shell, timeout=timeout) 121 | 122 | msg = stdout.read() 123 | err = stderr.read() 124 | 125 | success = True 126 | if not msg and err: 127 | success = False 128 | msg = err 129 | 130 | s.close() 131 | 132 | if success: 133 | success = is_msg_success(msg) 134 | 135 | return (success, msg) 136 | 137 | 138 | # 使用线程来异步执行 139 | @async 140 | def do_webhook_shell(server, data): 141 | log('Start to process server: %s' % json.dumps(filter_server(server))) 142 | script = server.get('SCRIPT', '') 143 | if script: 144 | if is_remote_server(server): 145 | # ip, port, account, pkey, shell, push_data='', timeout=300 146 | log('Start to execute remote SSH command. %s' % script) 147 | (success, msg) = do_ssh_cmd(server.get('HOST', None), 148 | server.get('PORT', 0), 149 | server.get('USER', None), 150 | server.get('PWD', None), 151 | server.get('SCRIPT', ''), 152 | data) 153 | else: 154 | log('Start to execute local command. %s' % script) 155 | import commands 156 | # local 157 | (success, msg) = commands.getstatusoutput(server.get('SCRIPT', '')) 158 | success = success > 0 and False or True 159 | else: 160 | success = False 161 | msg = 'There is no SCRIPT configured.' 162 | # end exec, log data 163 | msg = the_unicode(msg, errors='ignore') or '' 164 | msg = msg.strip() 165 | msg = msg.replace('\n', ' ') 166 | log('Completed execute: (%s, %s)' % (success, msg)) 167 | return True 168 | --------------------------------------------------------------------------------