Repository :
http://git.fedorahosted.org/cgit/copr.git
On branch : master
---------------------------------------------------------------
commit 91d46e1e3b12109b65ca17b6ee9c56ad1148e8ea
Author: Adam Samalik <asamalik(a)redhat.com>
Date: Tue Nov 24 14:00:05 2015 +0100
[frontend] automatic builds from GitHub - initial implementation
Copr is now able to generate a unique URL for every project which can
be used to set up a GitGub project webhook.
Copr is also able to automatically rebuild packages with each commit, if:
1) Webhook URL is saved in the GitHub repository
2) A package exists in a Copr project, that:
a) has its 'Default Source' set to 'Git and Tito' or 'Mock
SCM'
b) uses the http(s) clone-URL of the given GitHub repository
c) has a 'Rebuild Automatically' flag set to True. However, setting
this flag *is not implemented* yet.
What needs to be done to achieve full functionality:
- User must be able to set/unset the 'Package.webhook_rebuild' option
Suggested cosmetic changes:
- Create a new 'Settings' view withing a project and move the
'Webhooks', 'Edit', 'Permissions' and 'Delete'
views under
that section.
---------------------------------------------------------------
.../alembic/versions/573044986ee9_.py | 30 +++++++++++++
frontend/coprs_frontend/coprs/__init__.py | 3 +
.../coprs_frontend/coprs/logic/builds_logic.py | 34 +++++++++++++++
.../coprs_frontend/coprs/logic/packages_logic.py | 14 ++++++-
frontend/coprs_frontend/coprs/models.py | 8 ++++
.../coprs/templates/coprs/detail.html | 5 ++
.../coprs/templates/coprs/detail/webhooks.html | 24 +++++++++++
.../coprs/views/coprs_ns/coprs_general.py | 31 ++++++++++++++
.../coprs/views/webhooks_ns/__init__.py | 3 +
.../coprs/views/webhooks_ns/webhooks_general.py | 44 ++++++++++++++++++++
10 files changed, 195 insertions(+), 1 deletions(-)
diff --git a/frontend/coprs_frontend/alembic/versions/573044986ee9_.py
b/frontend/coprs_frontend/alembic/versions/573044986ee9_.py
new file mode 100644
index 0000000..fc1f08b
--- /dev/null
+++ b/frontend/coprs_frontend/alembic/versions/573044986ee9_.py
@@ -0,0 +1,30 @@
+"""empty message
+
+Revision ID: 573044986ee9
+Revises: 4b57794e2b5
+Create Date: 2015-11-26 12:36:59.729053
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '573044986ee9'
+down_revision = '4b57794e2b5'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('copr', sa.Column('webhook_secret',
sa.String(length=100), nullable=True))
+ op.add_column('package', sa.Column('enable_net', sa.Boolean(),
server_default='0', nullable=False))
+ op.add_column('package', sa.Column('webhook_rebuild', sa.Boolean(),
nullable=True))
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('package', 'webhook_rebuild')
+ op.drop_column('package', 'enable_net')
+ op.drop_column('copr', 'webhook_secret')
+ ### end Alembic commands ###
diff --git a/frontend/coprs_frontend/coprs/__init__.py
b/frontend/coprs_frontend/coprs/__init__.py
index f763616..785f29a 100644
--- a/frontend/coprs_frontend/coprs/__init__.py
+++ b/frontend/coprs_frontend/coprs/__init__.py
@@ -63,6 +63,8 @@ from coprs.views import tmp_ns
from coprs.views.tmp_ns import tmp_general
from coprs.views.groups_ns import groups_ns
from coprs.views.groups_ns import groups_general
+from coprs.views.webhooks_ns import webhooks_ns
+from coprs.views.webhooks_ns import webhooks_general
from .context_processors import include_banner, inject_fedmenu
@@ -79,6 +81,7 @@ app.register_blueprint(recent_ns.recent_ns)
app.register_blueprint(stats_receiver.stats_rcv_ns)
app.register_blueprint(tmp_ns.tmp_ns)
app.register_blueprint(groups_ns)
+app.register_blueprint(webhooks_ns)
app.add_url_rule("/", "coprs_ns.coprs_show",
coprs_general.coprs_show)
diff --git a/frontend/coprs_frontend/coprs/logic/builds_logic.py
b/frontend/coprs_frontend/coprs/logic/builds_logic.py
index bda8aeb..02d2aed 100644
--- a/frontend/coprs_frontend/coprs/logic/builds_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/builds_logic.py
@@ -525,6 +525,40 @@ GROUP BY
return build
+ @classmethod
+ def rebuild_package(cls, package):
+ build = models.Build(
+ user=None,
+ pkgs=None,
+ package_id=package.id,
+ copr=package.copr,
+ repos=package.copr.repos,
+ source_type=package.source_type,
+ source_json=package.source_json,
+ submitted_on=int(time.time()),
+ enable_net=package.enable_net,
+ timeout=DEFAULT_BUILD_TIMEOUT
+ )
+
+ db.session.add(build)
+
+ chroots = package.copr.active_chroots
+
+ status = helpers.StatusEnum("importing")
+
+ for chroot in chroots:
+ buildchroot = models.BuildChroot(
+ build=build,
+ status=status,
+ mock_chroot=chroot,
+ git_hash=None
+ )
+
+ db.session.add(buildchroot)
+
+ return build
+
+
terminal_states = {StatusEnum("failed"), StatusEnum("succeeded"),
StatusEnum("canceled")}
@classmethod
diff --git a/frontend/coprs_frontend/coprs/logic/packages_logic.py
b/frontend/coprs_frontend/coprs/logic/packages_logic.py
index 4113d10..104c04c 100644
--- a/frontend/coprs_frontend/coprs/logic/packages_logic.py
+++ b/frontend/coprs_frontend/coprs/logic/packages_logic.py
@@ -1,7 +1,8 @@
import json
+import time
from sqlalchemy import or_
from sqlalchemy import and_
-from sqlalchemy.sql import false
+from sqlalchemy.sql import false, true
from coprs import app
from coprs import db
@@ -12,6 +13,8 @@ from coprs import helpers
from coprs.logic import coprs_logic
from coprs.logic import users_logic
+from coprs.constants import DEFAULT_BUILD_TIMEOUT
+
log = app.logger
@@ -27,6 +30,14 @@ class PackagesLogic(object):
models.Package.name == package_name)
@classmethod
+ def get_for_webhook_rebuild(cls, copr_id, webhook_secret, clone_url):
+ return (models.Package.query.join(models.Copr)
+ .filter(models.Copr.webhook_secret == webhook_secret)
+ .filter(models.Package.copr_id == copr_id)
+ .filter(models.Package.webhook_rebuild == true())
+ .filter(models.Package.source_json.contains(clone_url)))
+
+ @classmethod
def add(cls, user, copr, package_name):
users_logic.UsersLogic.raise_if_cant_build_in_copr(
user, copr,
@@ -56,3 +67,4 @@ class PackagesLogic(object):
return (models.Package.query
.filter(models.Package.copr_id == copr_id)
.filter(models.Package.name == package_name))
+
diff --git a/frontend/coprs_frontend/coprs/models.py
b/frontend/coprs_frontend/coprs/models.py
index 86ec82f..49f55c0 100644
--- a/frontend/coprs_frontend/coprs/models.py
+++ b/frontend/coprs_frontend/coprs/models.py
@@ -196,6 +196,9 @@ class Copr(db.Model, helpers.Serializer):
group = db.relationship("Group", backref=db.backref("groups"))
mock_chroots = association_proxy("copr_chroots", "mock_chroot")
+ # a secret to be used for webhooks authentication
+ webhook_secret = db.Column(db.String(100))
+
# enable networking for the builds by default
build_enable_net = db.Column(db.Boolean, default=True,
server_default="1", nullable=False)
@@ -352,6 +355,11 @@ class Package(db.Model, helpers.Serializer):
source_type = db.Column(db.Integer,
default=helpers.BuildSourceEnum("unset"))
# Source of the build: description in json, example: git link, srpm url, etc.
source_json = db.Column(db.Text)
+ # True if the package is built automatically via webhooks
+ webhook_rebuild = db.Column(db.Boolean, default=False)
+ # enable networking during a build process
+ enable_net = db.Column(db.Boolean, default=False,
+ server_default="0", nullable=False)
# relations
copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail.html
b/frontend/coprs_frontend/coprs/templates/coprs/detail.html
index d8866cb..0359501 100644
--- a/frontend/coprs_frontend/coprs/templates/coprs/detail.html
+++ b/frontend/coprs_frontend/coprs/templates/coprs/detail.html
@@ -15,6 +15,7 @@
{% set url_add_build = url_for('coprs_ns.copr_add_build', username =
copr.owner.name, coprname = copr.name) %}
{% set url_delete = url_for('coprs_ns.copr_delete', username =
copr.owner.name, coprname = copr.name) %}
{% set url_edit = url_for('coprs_ns.copr_edit', username = copr.owner.name,
coprname = copr.name) %}
+ {% set url_webhooks = url_for('coprs_ns.copr_webhooks', username =
copr.owner.name, coprname = copr.name) %}
{% endif %}
{% block title %}{{ copr.owner.name }}/{{ copr.name }} Copr{% endblock %}
@@ -77,6 +78,10 @@
{{ nav_element("edit", "Edit", url_edit) }}
{% endif %}
+ {% if g.user and g.user.can_edit(copr) %}
+ {{ nav_element("webhooks", "Webhooks", url_webhooks) }}
+ {% endif %}
+
{% if g.user and not copr.is_a_group_project %}
{{ nav_element("permissions", "Permissions", url_for(
'coprs_ns.copr_permissions', username = copr.owner.name, coprname =
copr.name))
diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/webhooks.html
b/frontend/coprs_frontend/coprs/templates/coprs/detail/webhooks.html
new file mode 100644
index 0000000..77b2e78
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/webhooks.html
@@ -0,0 +1,24 @@
+{% extends "coprs/detail.html" %}
+
+{% set selected_tab = "webhooks" %}
+
+{%block project_breadcrumb %}
+<li class="active">
+ Webhooks
+</li>
+{%endblock%}
+
+{% block detail_body %}
+<div class="row">
+ <div class="col-sm-8 col-md-9">
+ <h2> Webhooks </h2>
+ <p> You can set up an automatic builds with webhooks </p>
+
+ <h3> GitHub </h3>
+ <p> We might need to explain here what to do with that... </p>
+ <div class="well well-sm">
+ {{ github_url }}
+ </div>
+ </div>
+</div>
+{% endblock %}
diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
index 86dabcb..4a0712f 100644
--- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
+++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py
@@ -4,6 +4,7 @@ import os
import time
import os
import re
+import uuid
from six.moves.urllib.parse import urljoin
import flask
@@ -349,6 +350,36 @@ def copr_permissions(copr):
current_user_permissions=user_perm)
+def render_copr_webhooks(copr):
+ if not copr.webhook_secret:
+ copr.webhook_secret = uuid.uuid4()
+ db.session.add(copr)
+ db.session.commit()
+
+ github_url = "https://{}/webhooks/github/{}/{}/".format(
+ app.config["PUBLIC_COPR_HOSTNAME"],
+ copr.id,
+ copr.webhook_secret)
+
+ return flask.render_template(
+ "coprs/detail/webhooks.html",
+ copr=copr, github_url=github_url)
+
+
+@coprs_ns.route("/g/<group_name>/<coprname>/webhooks/")
+@login_required
+@req_with_copr
+def group_copr_webhooks(copr):
+ return render_copr_webhooks(copr)
+
+
+(a)coprs_ns.route("/<username>/<coprname>/webhooks/")
+@login_required
+@req_with_copr
+def copr_webhooks(copr):
+ return render_copr_webhooks(copr)
+
+
def render_copr_edit(copr, form, view):
if not form:
form = forms.CoprFormFactory.create_form_cls(
diff --git a/frontend/coprs_frontend/coprs/views/webhooks_ns/__init__.py
b/frontend/coprs_frontend/coprs/views/webhooks_ns/__init__.py
new file mode 100644
index 0000000..f7cd433
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/views/webhooks_ns/__init__.py
@@ -0,0 +1,3 @@
+import flask
+
+webhooks_ns = flask.Blueprint("webhooks_ns", __name__,
url_prefix="/webhooks")
diff --git a/frontend/coprs_frontend/coprs/views/webhooks_ns/webhooks_general.py
b/frontend/coprs_frontend/coprs/views/webhooks_ns/webhooks_general.py
new file mode 100644
index 0000000..6e57f5d
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/views/webhooks_ns/webhooks_general.py
@@ -0,0 +1,44 @@
+import flask
+
+from coprs import db, app
+from coprs import helpers
+
+from coprs.logic.builds_logic import BuildsLogic
+from coprs.logic.complex_logic import ComplexLogic
+from coprs.logic.coprs_logic import CoprsLogic
+from coprs.logic.packages_logic import PackagesLogic
+
+from coprs.exceptions import ObjectNotFound, AccessRestricted
+
+from coprs.views.webhooks_ns import webhooks_ns
+from coprs.views.misc import page_not_found, access_restricted
+
+import logging
+log = logging.getLogger(__name__)
+
+
+(a)webhooks_ns.route("/github/<copr_id>/<uuid>/",
methods=["POST"])
+def webhooks_hello(copr_id, uuid):
+ try:
+ copr = ComplexLogic.get_copr_by_id_safe(copr_id)
+ except ObjectNotFound:
+ return page_not_found("Project does not exist")
+
+ if copr.webhook_secret != uuid:
+ return access_restricted("This webhook is not valid")
+
+ try:
+ request_json = flask.request.json
+ clone_url = request_json["repository"]["clone_url"]
+ except KeyError:
+ return "Bad Request", 400
+
+ packages = PackagesLogic.get_for_webhook_rebuild(copr_id, uuid, clone_url)
+
+ for package in packages:
+ BuildsLogic.rebuild_package(package)
+
+ db.session.commit()
+
+ return "OK", 200
+