Add automatic GitHub webhook creation

This commit is contained in:
rubenwardy 2020-01-24 23:19:06 +00:00
parent d4936e18ee
commit e12aec4ccd
6 changed files with 183 additions and 9 deletions

@ -18,19 +18,22 @@ from flask import Blueprint
bp = Blueprint("github", __name__)
from flask import redirect, url_for, request, flash, abort
from flask_user import current_user
from flask import redirect, url_for, request, flash, abort, render_template, jsonify
from flask_user import current_user, login_required
from sqlalchemy import func
from flask_github import GitHub
from app import github, csrf
from app.models import db, User, APIToken, Package
from app.utils import loginUser
from app.utils import loginUser, randomString
from app.blueprints.api.support import error, handleCreateRelease
import hmac
import hmac, requests, json
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField
@bp.route("/github/start/")
def start():
return github.authorize("")
return github.authorize("", redirect_uri=url_for("github.callback"))
@bp.route("/github/callback/")
@github.authorized_handler
@ -40,8 +43,6 @@ def callback(oauth_token):
flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
return redirect(url_for("user.login"))
import requests
# Get Github username
url = "https://api.github.com/user"
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
@ -121,11 +122,100 @@ def webhook():
if event == "push":
title = json["head_commit"]["message"].partition("\n")[0]
ref = json["after"]
elif event == "ping":
return jsonify({ "success": True, "message": "Ping successful" })
else:
return error(400, "Unknown event, expected 'push'")
return error(400, "Unsupported event. Only 'push' and 'ping' are supported.")
#
# Perform release
#
return handleCreateRelease(actual_token, package, title, ref)
class SetupWebhookForm(FlaskForm):
event = SelectField("Event Type", choices=[('push', 'Push'), ('tag', 'New tag')])
submit = SubmitField("Save")
@bp.route("/github/callback/webhook/")
@github.authorized_handler
def callback_webhook(oauth_token=None):
pid = request.args.get("pid")
if pid is None:
abort(404)
current_user.github_access_token = oauth_token
db.session.commit()
return redirect(url_for("github.setup_webhook", pid=pid))
@bp.route("/github/webhook/new/", methods=["GET", "POST"])
@login_required
def setup_webhook():
pid = request.args.get("pid")
if pid is None:
abort(404)
package = Package.query.get(pid)
if package is None:
abort(404)
gh_user, gh_repo = package.getGitHubFullName()
if gh_user is None or gh_repo is None:
flash("Unable to get Github full name from repo address", "danger")
return redirect(package.getDetailsURL())
if current_user.github_access_token is None:
return github.authorize("write:repo_hook", \
redirect_uri=url_for("github.callback_webhook", pid=pid, _external=True))
form = SetupWebhookForm(formdata=request.form)
if request.method == "POST" and form.validate():
token = APIToken()
token.name = "Github Webhook for " + package.title
token.owner = current_user
token.access_token = randomString(32)
token.package = package
event = form.event.data
if event != "push" and event != "tag":
abort(500)
# Create webhook
url = "https://api.github.com/repos/{}/{}/hooks".format(gh_user, gh_repo)
data = {
"name": "web",
"active": True,
"events": [event],
"config": {
"url": url_for("github.webhook", _external=True),
"content_type": "json",
"secret": token.access_token
},
}
headers = {
"Authorization": "token " + current_user.github_access_token
}
r = requests.post(url, headers=headers, data=json.dumps(data))
if r.status_code == 201:
db.session.add(token)
db.session.commit()
return redirect(package.getDetailsURL())
elif r.status_code == 403:
current_user.github_access_token = None
db.session.commit()
return github.authorize("write:repo_hook", \
redirect_uri=url_for("github.callback_webhook", pid=pid, _external=True))
else:
flash("Failed to create webhook, received response from Github: " +
str(r.json().get("message") or r.status_code), "danger")
return render_template("github/setup_webhook.html", \
form=form, package=package)

@ -126,6 +126,9 @@ class User(db.Model, UserMixin):
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
# Access token for webhook setup
github_access_token = db.Column(db.String(50), nullable=True, server_default=None)
# User email information
email = db.Column(db.String(255), nullable=True, unique=True)
email_confirmed_at = db.Column(db.DateTime())
@ -461,6 +464,31 @@ class Package(db.Model):
def getIsFOSS(self):
return self.license.is_foss and self.media_license.is_foss
def getIsOnGitHub(self):
if self.repo is None:
return False
url = urlparse(self.repo)
return url.netloc == "github.com"
def getGitHubFullName(self):
if self.repo is None:
return None
url = urlparse(self.repo)
if url.netloc != "github.com":
return None
import re
m = re.search(r"^\/([^\/]+)\/([^\/]+)\/?$", url.path)
if m is None:
return
user = m.group(1)
repo = m.group(2).replace(".git", "")
return (user,repo)
def getSortedDependencies(self, is_hard=None):
query = self.dependencies
if is_hard is not None:

@ -111,7 +111,7 @@
<li class="alert alert-{{category}} container">
<span class="icon_message"></span>
{{ message|safe }}
{{ message }}
<div style="clear: both;"></div>
</li>

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}
{{ _("Setup GitHub webhook") }}
{% endblock %}
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %}
{% block content %}
<h1 class="mt-0">{{ self.title() }}</h1>
<div class="alert alert-info">
{{ _("You can delete the webhook at any time by going into Settings > Webhooks on the repository.") }}
</div>
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ render_field(form.event) }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

@ -364,6 +364,15 @@
</ul>
</div>
{% if package.getIsOnGitHub() %}
<p class="small text-centered">
<a href="{{ url_for('github.setup_webhook', pid=package.id) }}">
Set up a webhook
</a>
to create releases automatically.
</p>
{% endif %}
<div class="card my-4">
<div class="card-header">
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}

@ -0,0 +1,24 @@
"""empty message
Revision ID: 7a48dbd05780
Revises: df66c78e6791
Create Date: 2020-01-24 21:52:49.744404
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '7a48dbd05780'
down_revision = 'df66c78e6791'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('user', sa.Column('github_access_token', sa.String(length=50), nullable=True, server_default=None))
def downgrade():
op.drop_column('user', 'github_access_token')