diff --git a/release_dashboard/static/release_dashboard/js/hotfix_release.js b/release_dashboard/static/release_dashboard/js/hotfix_release.js
new file mode 100644
index 0000000..630ce15
--- /dev/null
+++ b/release_dashboard/static/release_dashboard/js/hotfix_release.js
@@ -0,0 +1,70 @@
+/**
+ *
+ *
+ */
+$( "button.hotfix" ).click( function( e ) {
+
+ // don't send the form
+ e.preventDefault();
+ var button = $( this );
+ var id = button.attr( "id" ).replace( "hotfix_", "" );
+ var repo = id;
+ var span = $( "span#hotfix_error_" + id );
+ var links = $( "#links_" + id );
+ var push = $( "select#push_" + id + " option:selected" ).val();
+ var empty = $( "input#empty_" + id ).prop( "checked" );
+
+ $.ajax( {
+ url: repo + "/",
+ type: "POST",
+ data: JSON.stringify( { push: push, empty: empty } ),
+ contentType: "application/json; charset=utf-8",
+ dataType: "json",
+ success: successFunc,
+ error: errorFunc,
+ /* eslint-disable-next-line no-undef */ // at csrf.js
+ beforeSend: csrftokenFunc
+ } );
+
+ button.attr( "disabled", "disabled" );
+ span.html( "processing" );
+ span.show();
+ links.addClass( "hidden" );
+
+ function successFunc( data, _status ) {
+ span.html( "" );
+ $( "#link_done_" + id ).attr( "href", data.urls[ 0 ] );
+ $( "#link_latest_" + id ).attr( "href", data.urls[ 1 ] );
+ links.removeClass( "hidden" );
+ button.removeAttr( "disabled" );
+ }
+
+ function errorFunc( _jqXHR, _status, error ) {
+ span.html( error );
+ button.removeAttr( "disabled" );
+ }
+} );
+
+$( "td.version > select" ).change( function() {
+ var id = $( this ).attr( "id" ).replace( "version_", "" );
+ var version = $( this ).val();
+ var button = $( "button#hotfix_" + id );
+ var span = $( "span#hotfix_error_" + id );
+ var links = $( "#links_" + id );
+
+ if ( version.match( /^branch\/mr[0-9]+\.[0-9]+\.[0-9]+$/ ) ) {
+ button.html( "Release hotfix" );
+ button.removeAttr( "disabled" );
+ } else {
+ button.html( "Select branch to hotfix" );
+ button.attr( "disabled", "disabled" );
+ }
+ span.html( "" );
+ links.addClass( "hidden" );
+} );
+
+$( document ).ready( function() {
+ $( "td.version > select option[value^=\"branch/mr\"]" ).each( function() {
+ $( this ).change();
+} );
+} );
diff --git a/release_dashboard/templates/release_dashboard/build_release.html b/release_dashboard/templates/release_dashboard/build_release.html
index 0c7cab5..9719fe5 100644
--- a/release_dashboard/templates/release_dashboard/build_release.html
+++ b/release_dashboard/templates/release_dashboard/build_release.html
@@ -61,13 +61,19 @@
{% for prj in config.projects %}
-
{{ prj }}
+ {% if prj in build_deps %}
dependency
{% endif %}
{% endfor %}
@@ -84,6 +90,7 @@ $( document ).ready(function() {
if (builds > 0 && release.match(/^release-mr[0-9]+\.[0-9]+\.[0-9]+$/)) {
$( "#build_button" ).attr("disabled", "disabled");
console.debug("mrX.X.X release, can't be built twice");
+ $( "#hotfix" ).toggleClass("hidden");
}
});
@@ -98,6 +105,7 @@ function delete_build_release( id ) {
$("#br_" + id).remove();
if ( $(".build_release").length === 0 ) {
$( "#build_button" ).removeAttr("disabled");
+ $( "#hotfix" ).toggleClass("hidden");
}
}
diff --git a/release_dashboard/templates/release_dashboard/hotfix_release.html b/release_dashboard/templates/release_dashboard/hotfix_release.html
new file mode 100644
index 0000000..11b5153
--- /dev/null
+++ b/release_dashboard/templates/release_dashboard/hotfix_release.html
@@ -0,0 +1,66 @@
+{% extends "release_dashboard/base.html" %}
+{% load static %}
+{% block title %}Release Dashboard{% endblock %}
+{% block navlist %}
+
Release Dashboard
+
Build Release
+
{{ config.release }}
+{% endblock %}
+{% block content %}
+
+
+{% endblock %}
+{% block extrajs %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/release_dashboard/test/test_views.py b/release_dashboard/test/test_views.py
index 5b94a01..080510f 100644
--- a/release_dashboard/test/test_views.py
+++ b/release_dashboard/test/test_views.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2020 The Sipwise Team - http://sipwise.com
+# Copyright (C) 2020-2022 The Sipwise Team - http://sipwise.com
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
@@ -68,6 +68,68 @@ class TestHotfix(TestCase):
self.assertEqual(res.context["projects"], expected)
+class TestHotfixRelease(TestCase):
+ def test_no_login(self):
+ res = self.client.get(
+ reverse(
+ "release_dashboard:hotfix_release", args=["release-mr7.5.2"]
+ )
+ )
+ self.assertNotEqual(res.status_code, 200)
+
+ def test_login_ok(self):
+ user = User.objects.create_user(username="test")
+ self.client.force_login(user)
+ res = self.client.get(
+ reverse(
+ "release_dashboard:hotfix_release", args=["release-mr7.5.2"]
+ )
+ )
+ self.assertEqual(res.status_code, 200)
+
+ def test_no_mrXXX(self):
+ user = User.objects.create_user(username="test")
+ self.client.force_login(user)
+ res = self.client.get(
+ reverse("release_dashboard:hotfix_release", args=["release-mr7.5"])
+ )
+ self.assertNotEqual(res.status_code, 200)
+
+ res = self.client.get(
+ reverse(
+ "release_dashboard:hotfix_release",
+ args=["release-trunk-buster"],
+ )
+ )
+ self.assertNotEqual(res.status_code, 200)
+
+ def test_project_ok(self):
+ user = User.objects.create_user(username="test")
+ self.client.force_login(user)
+ res = self.client.post(
+ reverse(
+ "release_dashboard:hotfix_release_build",
+ args=["release-mr7.5.2", "data-hal"],
+ ),
+ {"push": "no"},
+ content_type="application/json",
+ )
+ self.assertEqual(res.status_code, 200)
+
+ def test_project_wrong(self):
+ user = User.objects.create_user(username="test")
+ self.client.force_login(user)
+ res = self.client.post(
+ reverse(
+ "release_dashboard:hotfix_release_build",
+ args=["release-mr7.5.2", "fake-project"],
+ ),
+ {"push": "no"},
+ content_type="application/json",
+ )
+ self.assertNotEqual(res.status_code, 200)
+
+
class TestDocker(TestCase):
def test_no_login(self):
res = self.client.get(reverse("release_dashboard:docker_images"))
@@ -81,17 +143,17 @@ class TestDocker(TestCase):
class TestBuildRelease(BaseTest):
- fixtures = ['test_build_release']
+ fixtures = ["test_build_release"]
def test_no_login(self):
- url = reverse("release_dashboard:build_release", args=['trunk'])
+ url = reverse("release_dashboard:build_release", args=["trunk"])
res = self.client.get(url)
self.assertNotEqual(res.status_code, 200)
def test_login_ok(self):
user = User.objects.create_user(username="test")
self.client.force_login(user)
- url = reverse("release_dashboard:build_release", args=['trunk'])
+ url = reverse("release_dashboard:build_release", args=["trunk"])
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
@@ -99,10 +161,10 @@ class TestBuildRelease(BaseTest):
user = User.objects.create_user(username="test")
self.client.force_login(user)
# no build yet
- url = reverse("release_dashboard:build_release", args=['mr8.1'])
+ url = reverse("release_dashboard:build_release", args=["mr8.1"])
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
- self.assertTrue(res.context['done'])
+ self.assertTrue(res.context["done"])
def test_context_not_done(self):
user = User.objects.create_user(username="test")
@@ -112,7 +174,7 @@ class TestBuildRelease(BaseTest):
)
br.built_projects = None
br.save()
- url = reverse("release_dashboard:build_release", args=['trunk'])
+ url = reverse("release_dashboard:build_release", args=["trunk"])
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
- self.assertFalse(res.context['done'])
+ self.assertFalse(res.context["done"])
diff --git a/release_dashboard/urls.py b/release_dashboard/urls.py
index 0d97d98..a148746 100644
--- a/release_dashboard/urls.py
+++ b/release_dashboard/urls.py
@@ -27,6 +27,16 @@ urlpatterns = [
build.build_release,
name="build_release",
),
+ re_path(
+ r"^hotfix_release/(?P
[^/]+)/$",
+ build.hotfix_release,
+ name="hotfix_release",
+ ),
+ re_path(
+ r"^hotfix_release/(?P[^/]+)/(?P[^/]+)/$",
+ build.hotfix_release_build,
+ name="hotfix_release_build",
+ ),
re_path(r"^hotfix/$", build.hotfix, name="hotfix"),
re_path(
r"^hotfix/(?P[^/]+)/(?P[^/]+)/$", build.hotfix_build
diff --git a/release_dashboard/views/__init__.py b/release_dashboard/views/__init__.py
index e346beb..d1b7b76 100644
--- a/release_dashboard/views/__init__.py
+++ b/release_dashboard/views/__init__.py
@@ -17,10 +17,11 @@ import re
from django.views.generic.base import TemplateView
from natsort import humansorted
+from ..conf import settings
from ..utils import get_branches
from ..utils import get_tags
-regex_hotfix = re.compile(r"^mr[0-9]+\.[0-9]+\.[0-9]+$")
+regex_hotfix = re.compile(settings.RELEASE_DASHBOARD_FILTER_MRXXX)
regex_mr = re.compile(r"^mr.+$")
diff --git a/release_dashboard/views/build.py b/release_dashboard/views/build.py
index 57bc79c..31c80aa 100644
--- a/release_dashboard/views/build.py
+++ b/release_dashboard/views/build.py
@@ -112,6 +112,44 @@ def hotfix(request):
return render(request, "release_dashboard/hotfix.html", context)
+@login_required
+@require_http_methods(["POST"])
+def hotfix_release_build(request, release, project):
+ release_config = ReleaseConfig(release)
+ if project not in release_config.projects:
+ error = f"project:{project} not in {release_config.release}"
+ logger.error(error)
+ return HttpResponseNotFound(error)
+
+ if not regex_hotfix.match(release_config.branch):
+ error = f"branch:{release_config.branch} not valid. Not mrX.X.X format"
+ logger.error(error)
+ return HttpResponseNotFound(error)
+ logger.debug(body=request.body)
+ json_data = json.loads(request.body.decode("utf-8"))
+ push = json_data.get("push", "no")
+ empty = json_data.get("empty", False)
+ if push == "no":
+ logger.warn(f"dry-run for {project}:{release_config.branch}")
+ urls = build.trigger_hotfix(
+ project, release_config.branch, request.user, push, empty
+ )
+ return JsonResponse({"urls": urls})
+
+
+@login_required
+def hotfix_release(request, release):
+ release_config = ReleaseConfig(release)
+ if not regex_hotfix.match(release_config.branch):
+ error = (
+ "branch:%s not valid. Not mrX.X.X format" % release_config.branch
+ )
+ logger.error(error)
+ return HttpResponseNotFound(error)
+ context = {"config": release_config}
+ return render(request, "release_dashboard/hotfix_release.html", context)
+
+
@login_required
def refresh_all(request):
if request.method == "POST":