diff options
Diffstat (limited to 'app')
158 files changed, 6639 insertions, 9 deletions
diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg Binary files differnew file mode 100644 index 00000000000..0e05674e840 --- /dev/null +++ b/app/assets/images/ci/arch.jpg diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico Binary files differnew file mode 100644 index 00000000000..9663d4d00b9 --- /dev/null +++ b/app/assets/images/ci/favicon.ico diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif Binary files differnew file mode 100644 index 00000000000..2fcb8f2da0d --- /dev/null +++ b/app/assets/images/ci/loader.gif diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png Binary files differnew file mode 100644 index 00000000000..752d26adba7 --- /dev/null +++ b/app/assets/images/ci/no_avatar.png diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png Binary files differnew file mode 100644 index 00000000000..d5edc04e65f --- /dev/null +++ b/app/assets/images/ci/rails.png diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png Binary files differnew file mode 100644 index 00000000000..65d29e3fd89 --- /dev/null +++ b/app/assets/images/ci/service_sample.png diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js new file mode 100644 index 00000000000..ab635881087 --- /dev/null +++ b/app/assets/javascripts/ci/Chart.min.js @@ -0,0 +1,39 @@ +var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2* +Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h, +m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+ +1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(), +c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/ +a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&& +(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+ +1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath(); +b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1* +v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth= +c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&& +(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l= +0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily; +for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d], +0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth, +b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5), +e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a, +c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+ +d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)* +k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath(); +b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}};
\ No newline at end of file diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee new file mode 100644 index 00000000000..05aa0f366bb --- /dev/null +++ b/app/assets/javascripts/ci/application.js.coffee @@ -0,0 +1,40 @@ +# This is a manifest file that'll be compiled into application.js, which will include all the files +# listed below. +# +# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +# +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +# GO AFTER THE REQUIRES BELOW. +# +#= require pager +#= require jquery_nested_form +#= require_tree . +# +$(document).on 'click', '.edit-runner-link', (event) -> + event.preventDefault() + + descr = $(this).closest('.runner-description').first() + descr.addClass('hide') + form = descr.next('.runner-description-form') + descrInput = form.find('input.description') + originalValue = descrInput.val() + form.removeClass('hide') + form.find('.cancel').on 'click', (event) -> + event.preventDefault() + + form.addClass('hide') + descrInput.val(originalValue) + descr.removeClass('hide') + +$(document).on 'click', '.assign-all-runner', -> + $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..') + +window.unbindEvents = -> + $(document).unbind('scroll') + $(document).off('scroll') + +document.addEventListener("page:fetch", unbindEvents) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee new file mode 100644 index 00000000000..c30859b484b --- /dev/null +++ b/app/assets/javascripts/ci/build.coffee @@ -0,0 +1,41 @@ +class CiBuild + @interval: null + + constructor: (build_url, build_status) -> + clearInterval(CiBuild.interval) + + if build_status == "running" || build_status == "pending" + # + # Bind autoscroll button to follow build output + # + $("#autoscroll-button").bind "click", -> + state = $(this).data("state") + if "enabled" is state + $(this).data "state", "disabled" + $(this).text "enable autoscroll" + else + $(this).data "state", "enabled" + $(this).text "disable autoscroll" + + # + # Check for new build output if user still watching build page + # Only valid for runnig build when output changes during time + # + CiBuild.interval = setInterval => + if window.location.href is build_url + $.ajax + url: build_url + dataType: "json" + success: (build) => + if build.status == "running" + $('#build-trace code').html build.trace_html + $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>' + @checkAutoscroll() + else + Turbolinks.visit build_url + , 4000 + + checkAutoscroll: -> + $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") + +@CiBuild = CiBuild diff --git a/app/assets/javascripts/ci/pager.js.coffee b/app/assets/javascripts/ci/pager.js.coffee new file mode 100644 index 00000000000..226fbd654ab --- /dev/null +++ b/app/assets/javascripts/ci/pager.js.coffee @@ -0,0 +1,42 @@ +@CiPager = + init: (@url, @limit = 0, preload, @disable = false) -> + if preload + @offset = 0 + @getItems() + else + @offset = @limit + @initLoadMore() + + getItems: -> + $(".loading").show() + $.ajax + type: "GET" + url: @url + data: "limit=" + @limit + "&offset=" + @offset + complete: => + $(".loading").hide() + success: (data) => + CiPager.append(data.count, data.html) + dataType: "json" + + append: (count, html) -> + if count > 1 + $(".content-list").append html + if count == @limit + @offset += count + else + @disable = true + + initLoadMore: -> + $(document).unbind('scroll') + $(document).endlessScroll + bottomPixels: 400 + fireDelay: 1000 + fireOnce: true + ceaseFire: -> + CiPager.disable + + callback: (i) => + unless $(".loading").is(':visible') + $(".loading").show() + CiPager.getItems() diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee new file mode 100644 index 00000000000..7e028b4e115 --- /dev/null +++ b/app/assets/javascripts/ci/projects.js.coffee @@ -0,0 +1,6 @@ +$(document).on 'click', '.badge-codes-toggle', -> + $('.badge-codes-block').toggleClass("hide") + return false + +$(document).on 'click', '.sync-now', -> + $(this).find('i').addClass('fa-spin') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 46f7feddf8d..d9ede637944 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -61,3 +61,9 @@ * Styles for JS behaviors. */ @import "behaviors.scss"; + +/** + * CI specific styles: + */ +@import "ci/**/*"; + diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss new file mode 100644 index 00000000000..a11a935b54d --- /dev/null +++ b/app/assets/stylesheets/ci/builds.scss @@ -0,0 +1,70 @@ +.ci-body { + pre.trace { + background: #111111; + color: #fff; + font-family: $monospace_font; + white-space: pre; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + overflow: auto; + overflow-y: hidden; + font-size: 12px; + + .fa-refresh { + font-size: 24px; + margin-left: 20px; + } + } + + .autoscroll-container { + position: fixed; + bottom: 10px; + right: 20px; + z-index: 100; + } + + .scroll-controls { + position: fixed; + bottom: 10px; + left: 250px; + z-index: 100; + + a { + display: block; + margin-bottom: 5px; + } + } + + .page-sidebar-collapsed { + .scroll-controls { + left: 70px; + } + } + + .build-widget { + padding: 10px; + background: $background-color; + margin-bottom: 20px; + border-radius: 4px; + + .title { + margin-top: 0; + color: #666; + line-height: 1.5; + } + .attr-name { + color: #777; + } + } + + .alert-disabled { + background: $background-color; + + a { + color: #3084bb !important; + } + } +} diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/ci/lint.scss new file mode 100644 index 00000000000..6d2bd33b28b --- /dev/null +++ b/app/assets/stylesheets/ci/lint.scss @@ -0,0 +1,10 @@ +.ci-body { + .incorrect-syntax{ + font-size: 19px; + color: red; + } + .correct-syntax{ + font-size: 19px; + color: #47a447; + } +} diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/ci/projects.scss new file mode 100644 index 00000000000..b246fb9e07d --- /dev/null +++ b/app/assets/stylesheets/ci/projects.scss @@ -0,0 +1,56 @@ +.ci-body { + .project-title { + margin: 0; + color: #444; + font-size: 20px; + line-height: 1.5; + } + + .builds { + @extend .table; + + .build { + &.alert{ + margin-bottom: 6px; + } + } + } + + .projects-table { + td { + vertical-align: middle !important; + } + } + + .commit-info { + font-size: 14px; + + .attr-name { + font-weight: 300; + color: #666; + margin-right: 5px; + } + + pre.commit-message { + font-size: 14px; + background: none; + padding: 0; + margin: 0; + border: none; + margin: 20px 0; + border-bottom: 1px solid #EEE; + padding-bottom: 20px; + border-radius: 0; + } + } + + .loading{ + font-size: 20px; + } + + .ci-charts { + fieldset { + margin-bottom: 16px; + } + } +} diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/ci/runners.scss new file mode 100644 index 00000000000..2b15ab83129 --- /dev/null +++ b/app/assets/stylesheets/ci/runners.scss @@ -0,0 +1,36 @@ +.ci-body { + .runner-state { + padding: 6px 12px; + margin-right: 10px; + color: #FFF; + + &.runner-state-shared { + background: #32b186; + } + &.runner-state-specific { + background: #3498db; + } + } + + .runner-status-online { + color: green; + } + + .runner-status-offline { + color: gray; + } + + .runner-status-paused { + color: red; + } + + .runner { + .btn { + padding: 1px 6px; + } + + h4 { + font-weight: normal; + } + } +} diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/ci/xterm.scss new file mode 100644 index 00000000000..532dede0b23 --- /dev/null +++ b/app/assets/stylesheets/ci/xterm.scss @@ -0,0 +1,906 @@ +.ci-body { + // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg + // see also: https://gist.github.com/jasonm23/2868981 + + $black: #000000; + $red: #cd0000; + $green: #00cd00; + $yellow: #cdcd00; + $blue: #0000ee; // according to wikipedia, this is the xterm standard + //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) + $magenta: #cd00cd; + $cyan: #00cdcd; + $white: #e5e5e5; + $l-black: #7f7f7f; + $l-red: #ff0000; + $l-green: #00ff00; + $l-yellow: #ffff00; + $l-blue: #5c5cff; + $l-magenta: #ff00ff; + $l-cyan: #00ffff; + $l-white: #ffffff; + + .term-bold { + font-weight: bold; + } + .term-italic { + font-style: italic; + } + .term-conceal { + visibility: hidden; + } + .term-underline { + text-decoration: underline; + } + .term-cross { + text-decoration: line-through; + } + + .term-fg-black { + color: $black; + } + .term-fg-red { + color: $red; + } + .term-fg-green { + color: $green; + } + .term-fg-yellow { + color: $yellow; + } + .term-fg-blue { + color: $blue; + } + .term-fg-magenta { + color: $magenta; + } + .term-fg-cyan { + color: $cyan; + } + .term-fg-white { + color: $white; + } + .term-fg-l-black { + color: $l-black; + } + .term-fg-l-red { + color: $l-red; + } + .term-fg-l-green { + color: $l-green; + } + .term-fg-l-yellow { + color: $l-yellow; + } + .term-fg-l-blue { + color: $l-blue; + } + .term-fg-l-magenta { + color: $l-magenta; + } + .term-fg-l-cyan { + color: $l-cyan; + } + .term-fg-l-white { + color: $l-white; + } + + .term-bg-black { + background-color: $black; + } + .term-bg-red { + background-color: $red; + } + .term-bg-green { + background-color: $green; + } + .term-bg-yellow { + background-color: $yellow; + } + .term-bg-blue { + background-color: $blue; + } + .term-bg-magenta { + background-color: $magenta; + } + .term-bg-cyan { + background-color: $cyan; + } + .term-bg-white { + background-color: $white; + } + .term-bg-l-black { + background-color: $l-black; + } + .term-bg-l-red { + background-color: $l-red; + } + .term-bg-l-green { + background-color: $l-green; + } + .term-bg-l-yellow { + background-color: $l-yellow; + } + .term-bg-l-blue { + background-color: $l-blue; + } + .term-bg-l-magenta { + background-color: $l-magenta; + } + .term-bg-l-cyan { + background-color: $l-cyan; + } + .term-bg-l-white { + background-color: $l-white; + } + + + .xterm-fg-0 { + color: #000000; + } + .xterm-fg-1 { + color: #800000; + } + .xterm-fg-2 { + color: #008000; + } + .xterm-fg-3 { + color: #808000; + } + .xterm-fg-4 { + color: #000080; + } + .xterm-fg-5 { + color: #800080; + } + .xterm-fg-6 { + color: #008080; + } + .xterm-fg-7 { + color: #c0c0c0; + } + .xterm-fg-8 { + color: #808080; + } + .xterm-fg-9 { + color: #ff0000; + } + .xterm-fg-10 { + color: #00ff00; + } + .xterm-fg-11 { + color: #ffff00; + } + .xterm-fg-12 { + color: #0000ff; + } + .xterm-fg-13 { + color: #ff00ff; + } + .xterm-fg-14 { + color: #00ffff; + } + .xterm-fg-15 { + color: #ffffff; + } + .xterm-fg-16 { + color: #000000; + } + .xterm-fg-17 { + color: #00005f; + } + .xterm-fg-18 { + color: #000087; + } + .xterm-fg-19 { + color: #0000af; + } + .xterm-fg-20 { + color: #0000d7; + } + .xterm-fg-21 { + color: #0000ff; + } + .xterm-fg-22 { + color: #005f00; + } + .xterm-fg-23 { + color: #005f5f; + } + .xterm-fg-24 { + color: #005f87; + } + .xterm-fg-25 { + color: #005faf; + } + .xterm-fg-26 { + color: #005fd7; + } + .xterm-fg-27 { + color: #005fff; + } + .xterm-fg-28 { + color: #008700; + } + .xterm-fg-29 { + color: #00875f; + } + .xterm-fg-30 { + color: #008787; + } + .xterm-fg-31 { + color: #0087af; + } + .xterm-fg-32 { + color: #0087d7; + } + .xterm-fg-33 { + color: #0087ff; + } + .xterm-fg-34 { + color: #00af00; + } + .xterm-fg-35 { + color: #00af5f; + } + .xterm-fg-36 { + color: #00af87; + } + .xterm-fg-37 { + color: #00afaf; + } + .xterm-fg-38 { + color: #00afd7; + } + .xterm-fg-39 { + color: #00afff; + } + .xterm-fg-40 { + color: #00d700; + } + .xterm-fg-41 { + color: #00d75f; + } + .xterm-fg-42 { + color: #00d787; + } + .xterm-fg-43 { + color: #00d7af; + } + .xterm-fg-44 { + color: #00d7d7; + } + .xterm-fg-45 { + color: #00d7ff; + } + .xterm-fg-46 { + color: #00ff00; + } + .xterm-fg-47 { + color: #00ff5f; + } + .xterm-fg-48 { + color: #00ff87; + } + .xterm-fg-49 { + color: #00ffaf; + } + .xterm-fg-50 { + color: #00ffd7; + } + .xterm-fg-51 { + color: #00ffff; + } + .xterm-fg-52 { + color: #5f0000; + } + .xterm-fg-53 { + color: #5f005f; + } + .xterm-fg-54 { + color: #5f0087; + } + .xterm-fg-55 { + color: #5f00af; + } + .xterm-fg-56 { + color: #5f00d7; + } + .xterm-fg-57 { + color: #5f00ff; + } + .xterm-fg-58 { + color: #5f5f00; + } + .xterm-fg-59 { + color: #5f5f5f; + } + .xterm-fg-60 { + color: #5f5f87; + } + .xterm-fg-61 { + color: #5f5faf; + } + .xterm-fg-62 { + color: #5f5fd7; + } + .xterm-fg-63 { + color: #5f5fff; + } + .xterm-fg-64 { + color: #5f8700; + } + .xterm-fg-65 { + color: #5f875f; + } + .xterm-fg-66 { + color: #5f8787; + } + .xterm-fg-67 { + color: #5f87af; + } + .xterm-fg-68 { + color: #5f87d7; + } + .xterm-fg-69 { + color: #5f87ff; + } + .xterm-fg-70 { + color: #5faf00; + } + .xterm-fg-71 { + color: #5faf5f; + } + .xterm-fg-72 { + color: #5faf87; + } + .xterm-fg-73 { + color: #5fafaf; + } + .xterm-fg-74 { + color: #5fafd7; + } + .xterm-fg-75 { + color: #5fafff; + } + .xterm-fg-76 { + color: #5fd700; + } + .xterm-fg-77 { + color: #5fd75f; + } + .xterm-fg-78 { + color: #5fd787; + } + .xterm-fg-79 { + color: #5fd7af; + } + .xterm-fg-80 { + color: #5fd7d7; + } + .xterm-fg-81 { + color: #5fd7ff; + } + .xterm-fg-82 { + color: #5fff00; + } + .xterm-fg-83 { + color: #5fff5f; + } + .xterm-fg-84 { + color: #5fff87; + } + .xterm-fg-85 { + color: #5fffaf; + } + .xterm-fg-86 { + color: #5fffd7; + } + .xterm-fg-87 { + color: #5fffff; + } + .xterm-fg-88 { + color: #870000; + } + .xterm-fg-89 { + color: #87005f; + } + .xterm-fg-90 { + color: #870087; + } + .xterm-fg-91 { + color: #8700af; + } + .xterm-fg-92 { + color: #8700d7; + } + .xterm-fg-93 { + color: #8700ff; + } + .xterm-fg-94 { + color: #875f00; + } + .xterm-fg-95 { + color: #875f5f; + } + .xterm-fg-96 { + color: #875f87; + } + .xterm-fg-97 { + color: #875faf; + } + .xterm-fg-98 { + color: #875fd7; + } + .xterm-fg-99 { + color: #875fff; + } + .xterm-fg-100 { + color: #878700; + } + .xterm-fg-101 { + color: #87875f; + } + .xterm-fg-102 { + color: #878787; + } + .xterm-fg-103 { + color: #8787af; + } + .xterm-fg-104 { + color: #8787d7; + } + .xterm-fg-105 { + color: #8787ff; + } + .xterm-fg-106 { + color: #87af00; + } + .xterm-fg-107 { + color: #87af5f; + } + .xterm-fg-108 { + color: #87af87; + } + .xterm-fg-109 { + color: #87afaf; + } + .xterm-fg-110 { + color: #87afd7; + } + .xterm-fg-111 { + color: #87afff; + } + .xterm-fg-112 { + color: #87d700; + } + .xterm-fg-113 { + color: #87d75f; + } + .xterm-fg-114 { + color: #87d787; + } + .xterm-fg-115 { + color: #87d7af; + } + .xterm-fg-116 { + color: #87d7d7; + } + .xterm-fg-117 { + color: #87d7ff; + } + .xterm-fg-118 { + color: #87ff00; + } + .xterm-fg-119 { + color: #87ff5f; + } + .xterm-fg-120 { + color: #87ff87; + } + .xterm-fg-121 { + color: #87ffaf; + } + .xterm-fg-122 { + color: #87ffd7; + } + .xterm-fg-123 { + color: #87ffff; + } + .xterm-fg-124 { + color: #af0000; + } + .xterm-fg-125 { + color: #af005f; + } + .xterm-fg-126 { + color: #af0087; + } + .xterm-fg-127 { + color: #af00af; + } + .xterm-fg-128 { + color: #af00d7; + } + .xterm-fg-129 { + color: #af00ff; + } + .xterm-fg-130 { + color: #af5f00; + } + .xterm-fg-131 { + color: #af5f5f; + } + .xterm-fg-132 { + color: #af5f87; + } + .xterm-fg-133 { + color: #af5faf; + } + .xterm-fg-134 { + color: #af5fd7; + } + .xterm-fg-135 { + color: #af5fff; + } + .xterm-fg-136 { + color: #af8700; + } + .xterm-fg-137 { + color: #af875f; + } + .xterm-fg-138 { + color: #af8787; + } + .xterm-fg-139 { + color: #af87af; + } + .xterm-fg-140 { + color: #af87d7; + } + .xterm-fg-141 { + color: #af87ff; + } + .xterm-fg-142 { + color: #afaf00; + } + .xterm-fg-143 { + color: #afaf5f; + } + .xterm-fg-144 { + color: #afaf87; + } + .xterm-fg-145 { + color: #afafaf; + } + .xterm-fg-146 { + color: #afafd7; + } + .xterm-fg-147 { + color: #afafff; + } + .xterm-fg-148 { + color: #afd700; + } + .xterm-fg-149 { + color: #afd75f; + } + .xterm-fg-150 { + color: #afd787; + } + .xterm-fg-151 { + color: #afd7af; + } + .xterm-fg-152 { + color: #afd7d7; + } + .xterm-fg-153 { + color: #afd7ff; + } + .xterm-fg-154 { + color: #afff00; + } + .xterm-fg-155 { + color: #afff5f; + } + .xterm-fg-156 { + color: #afff87; + } + .xterm-fg-157 { + color: #afffaf; + } + .xterm-fg-158 { + color: #afffd7; + } + .xterm-fg-159 { + color: #afffff; + } + .xterm-fg-160 { + color: #d70000; + } + .xterm-fg-161 { + color: #d7005f; + } + .xterm-fg-162 { + color: #d70087; + } + .xterm-fg-163 { + color: #d700af; + } + .xterm-fg-164 { + color: #d700d7; + } + .xterm-fg-165 { + color: #d700ff; + } + .xterm-fg-166 { + color: #d75f00; + } + .xterm-fg-167 { + color: #d75f5f; + } + .xterm-fg-168 { + color: #d75f87; + } + .xterm-fg-169 { + color: #d75faf; + } + .xterm-fg-170 { + color: #d75fd7; + } + .xterm-fg-171 { + color: #d75fff; + } + .xterm-fg-172 { + color: #d78700; + } + .xterm-fg-173 { + color: #d7875f; + } + .xterm-fg-174 { + color: #d78787; + } + .xterm-fg-175 { + color: #d787af; + } + .xterm-fg-176 { + color: #d787d7; + } + .xterm-fg-177 { + color: #d787ff; + } + .xterm-fg-178 { + color: #d7af00; + } + .xterm-fg-179 { + color: #d7af5f; + } + .xterm-fg-180 { + color: #d7af87; + } + .xterm-fg-181 { + color: #d7afaf; + } + .xterm-fg-182 { + color: #d7afd7; + } + .xterm-fg-183 { + color: #d7afff; + } + .xterm-fg-184 { + color: #d7d700; + } + .xterm-fg-185 { + color: #d7d75f; + } + .xterm-fg-186 { + color: #d7d787; + } + .xterm-fg-187 { + color: #d7d7af; + } + .xterm-fg-188 { + color: #d7d7d7; + } + .xterm-fg-189 { + color: #d7d7ff; + } + .xterm-fg-190 { + color: #d7ff00; + } + .xterm-fg-191 { + color: #d7ff5f; + } + .xterm-fg-192 { + color: #d7ff87; + } + .xterm-fg-193 { + color: #d7ffaf; + } + .xterm-fg-194 { + color: #d7ffd7; + } + .xterm-fg-195 { + color: #d7ffff; + } + .xterm-fg-196 { + color: #ff0000; + } + .xterm-fg-197 { + color: #ff005f; + } + .xterm-fg-198 { + color: #ff0087; + } + .xterm-fg-199 { + color: #ff00af; + } + .xterm-fg-200 { + color: #ff00d7; + } + .xterm-fg-201 { + color: #ff00ff; + } + .xterm-fg-202 { + color: #ff5f00; + } + .xterm-fg-203 { + color: #ff5f5f; + } + .xterm-fg-204 { + color: #ff5f87; + } + .xterm-fg-205 { + color: #ff5faf; + } + .xterm-fg-206 { + color: #ff5fd7; + } + .xterm-fg-207 { + color: #ff5fff; + } + .xterm-fg-208 { + color: #ff8700; + } + .xterm-fg-209 { + color: #ff875f; + } + .xterm-fg-210 { + color: #ff8787; + } + .xterm-fg-211 { + color: #ff87af; + } + .xterm-fg-212 { + color: #ff87d7; + } + .xterm-fg-213 { + color: #ff87ff; + } + .xterm-fg-214 { + color: #ffaf00; + } + .xterm-fg-215 { + color: #ffaf5f; + } + .xterm-fg-216 { + color: #ffaf87; + } + .xterm-fg-217 { + color: #ffafaf; + } + .xterm-fg-218 { + color: #ffafd7; + } + .xterm-fg-219 { + color: #ffafff; + } + .xterm-fg-220 { + color: #ffd700; + } + .xterm-fg-221 { + color: #ffd75f; + } + .xterm-fg-222 { + color: #ffd787; + } + .xterm-fg-223 { + color: #ffd7af; + } + .xterm-fg-224 { + color: #ffd7d7; + } + .xterm-fg-225 { + color: #ffd7ff; + } + .xterm-fg-226 { + color: #ffff00; + } + .xterm-fg-227 { + color: #ffff5f; + } + .xterm-fg-228 { + color: #ffff87; + } + .xterm-fg-229 { + color: #ffffaf; + } + .xterm-fg-230 { + color: #ffffd7; + } + .xterm-fg-231 { + color: #ffffff; + } + .xterm-fg-232 { + color: #080808; + } + .xterm-fg-233 { + color: #121212; + } + .xterm-fg-234 { + color: #1c1c1c; + } + .xterm-fg-235 { + color: #262626; + } + .xterm-fg-236 { + color: #303030; + } + .xterm-fg-237 { + color: #3a3a3a; + } + .xterm-fg-238 { + color: #444444; + } + .xterm-fg-239 { + color: #4e4e4e; + } + .xterm-fg-240 { + color: #585858; + } + .xterm-fg-241 { + color: #626262; + } + .xterm-fg-242 { + color: #6c6c6c; + } + .xterm-fg-243 { + color: #767676; + } + .xterm-fg-244 { + color: #808080; + } + .xterm-fg-245 { + color: #8a8a8a; + } + .xterm-fg-246 { + color: #949494; + } + .xterm-fg-247 { + color: #9e9e9e; + } + .xterm-fg-248 { + color: #a8a8a8; + } + .xterm-fg-249 { + color: #b2b2b2; + } + .xterm-fg-250 { + color: #bcbcbc; + } + .xterm-fg-251 { + color: #c6c6c6; + } + .xterm-fg-252 { + color: #d0d0d0; + } + .xterm-fg-253 { + color: #dadada; + } + .xterm-fg-254 { + color: #e4e4e4; + } + .xterm-fg-255 { + color: #eeeeee; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4c112534ae6..9b6472a7b13 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -134,9 +134,6 @@ class ApplicationController < ActionController::Base def repository @repository ||= project.repository - rescue Grit::NoSuchPathError => e - log_exception(e) - nil end def authorize_project!(action) diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb new file mode 100644 index 00000000000..4ec2dc9c2cf --- /dev/null +++ b/app/controllers/ci/admin/application_controller.rb @@ -0,0 +1,10 @@ +module Ci + module Admin + class ApplicationController < Ci::ApplicationController + before_action :authenticate_user! + before_action :authenticate_admin! + + layout "ci/admin" + end + end +end diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb new file mode 100644 index 00000000000..71e253fac67 --- /dev/null +++ b/app/controllers/ci/admin/application_settings_controller.rb @@ -0,0 +1,31 @@ +module Ci + class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController + before_action :set_application_setting + + def show + end + + def update + if @application_setting.update_attributes(application_setting_params) + redirect_to ci_admin_application_settings_path, + notice: 'Application settings saved successfully' + else + render :show + end + end + + private + + def set_application_setting + @application_setting = Ci::ApplicationSetting.current + @application_setting ||= Ci::ApplicationSetting.create_from_defaults + end + + def application_setting_params + params.require(:application_setting).permit( + :all_broken_builds, + :add_pusher, + ) + end + end +end diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb new file mode 100644 index 00000000000..38abfdeafbf --- /dev/null +++ b/app/controllers/ci/admin/builds_controller.rb @@ -0,0 +1,18 @@ +module Ci + class Admin::BuildsController < Ci::Admin::ApplicationController + def index + @scope = params[:scope] + @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30) + + @builds = + case @scope + when "pending" + @builds.pending + when "running" + @builds.running + else + @builds + end + end + end +end diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb new file mode 100644 index 00000000000..5939efff980 --- /dev/null +++ b/app/controllers/ci/admin/events_controller.rb @@ -0,0 +1,9 @@ +module Ci + class Admin::EventsController < Ci::Admin::ApplicationController + EVENTS_PER_PAGE = 50 + + def index + @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE) + end + end +end diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb new file mode 100644 index 00000000000..5bbd0ce7396 --- /dev/null +++ b/app/controllers/ci/admin/projects_controller.rb @@ -0,0 +1,19 @@ +module Ci + class Admin::ProjectsController < Ci::Admin::ApplicationController + def index + @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30) + end + + def destroy + project.destroy + + redirect_to ci_projects_url + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + end +end diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb new file mode 100644 index 00000000000..e7de6eb12ca --- /dev/null +++ b/app/controllers/ci/admin/runner_projects_controller.rb @@ -0,0 +1,34 @@ +module Ci + class Admin::RunnerProjectsController < Ci::Admin::ApplicationController + layout 'ci/project' + + def index + @runner_projects = project.runner_projects.all + @runner_project = project.runner_projects.new + end + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + if @runner.assign_to(project, current_user) + redirect_to ci_admin_runner_path(@runner) + else + redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project' + end + end + + def destroy + rp = Ci::RunnerProject.find(params[:id]) + runner = rp.runner + rp.destroy + + redirect_to ci_admin_runner_path(runner) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb new file mode 100644 index 00000000000..dc3508b49dd --- /dev/null +++ b/app/controllers/ci/admin/runners_controller.rb @@ -0,0 +1,69 @@ +module Ci + class Admin::RunnersController < Ci::Admin::ApplicationController + before_action :runner, except: :index + + def index + @runners = Ci::Runner.order('id DESC') + @runners = @runners.search(params[:search]) if params[:search].present? + @runners = @runners.page(params[:page]).per(30) + @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count + end + + def show + @builds = @runner.builds.order('id DESC').first(30) + @projects = Ci::Project.all + @projects = @projects.search(params[:search]) if params[:search].present? + @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end + + def update + @runner.update_attributes(runner_params) + + respond_to do |format| + format.js + format.html { redirect_to ci_admin_runner_path(@runner) } + end + end + + def destroy + @runner.destroy + + redirect_to ci_admin_runners_path + end + + def resume + if @runner.update_attributes(active: true) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def assign_all + Ci::Project.unassigned(@runner).all.each do |project| + @runner.assign_to(project, current_user) + end + + redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects" + end + + private + + def runner + @runner ||= Ci::Runner.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active) + end + end +end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb new file mode 100644 index 00000000000..a5868da377f --- /dev/null +++ b/app/controllers/ci/application_controller.rb @@ -0,0 +1,76 @@ +module Ci + class ApplicationController < ::ApplicationController + def self.railtie_helpers_paths + "app/helpers/ci" + end + + helper_method :gl_project + + private + + def authenticate_public_page! + unless project.public + unless current_user + redirect_to(new_user_sessions_path) and return + end + + return access_denied! unless can?(current_user, :read_project, gl_project) + end + end + + def authenticate_token! + unless project.valid_token?(params[:token]) + return head(403) + end + end + + def authorize_access_project! + unless can?(current_user, :read_project, gl_project) + return page_404 + end + end + + def authorize_manage_builds! + unless can?(current_user, :admin_project, gl_project) + return page_404 + end + end + + def authenticate_admin! + return render_404 unless current_user.is_admin? + end + + def authorize_manage_project! + unless can?(current_user, :admin_project, gl_project) + return page_404 + end + end + + def page_404 + render file: "#{Rails.root}/public/404.html", status: 404, layout: false + end + + def default_headers + headers['X-Frame-Options'] = 'DENY' + headers['X-XSS-Protection'] = '1; mode=block' + end + + # JSON for infinite scroll via Pager object + def pager_json(partial, count) + html = render_to_string( + partial, + layout: false, + formats: [:html] + ) + + render json: { + html: html, + count: count + } + end + + def gl_project + ::Project.find(@project.gitlab_id) + end + end +end diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb new file mode 100644 index 00000000000..80ee8666792 --- /dev/null +++ b/app/controllers/ci/builds_controller.rb @@ -0,0 +1,78 @@ +module Ci + class BuildsController < Ci::ApplicationController + before_action :authenticate_user!, except: [:status, :show] + before_action :authenticate_public_page!, only: :show + before_action :project + before_action :authorize_access_project!, except: [:status, :show] + before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel] + before_action :authorize_manage_builds!, only: [:retry, :cancel] + before_action :build, except: [:show] + layout 'ci/build' + + def show + if params[:id] =~ /\A\d+\Z/ + @build = build + else + # try to find commit by sha + commit = commit_by_sha + + if commit + # Redirect to commit page + redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha) + return + end + end + + raise ActiveRecord::RecordNotFound unless @build + + @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) + @commit = @build.commit + + respond_to do |format| + format.html + format.json do + render json: @build.to_json(methods: :trace_html) + end + end + end + + def retry + if @build.commands.blank? + return page_404 + end + + build = Ci::Build.retry(@build) + + if params[:return_to] + redirect_to URI.parse(params[:return_to]).path + else + redirect_to ci_project_build_path(project, build) + end + end + + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + + def cancel + @build.cancel + + redirect_to ci_project_build_path(@project, @build) + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def build + @build ||= project.builds.unscoped.find_by(id: params[:id]) + end + + def commit_by_sha + @project.commits.find_by(sha: params[:id]) + end + end +end diff --git a/app/controllers/ci/charts_controller.rb b/app/controllers/ci/charts_controller.rb new file mode 100644 index 00000000000..aa875e70987 --- /dev/null +++ b/app/controllers/ci/charts_controller.rb @@ -0,0 +1,24 @@ +module Ci + class ChartsController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def show + @charts = {} + @charts[:week] = Ci::Charts::WeekChart.new(@project) + @charts[:month] = Ci::Charts::MonthChart.new(@project) + @charts[:year] = Ci::Charts::YearChart.new(@project) + @charts[:build_times] = Ci::Charts::BuildTime.new(@project) + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb new file mode 100644 index 00000000000..7a0a500fbe6 --- /dev/null +++ b/app/controllers/ci/commits_controller.rb @@ -0,0 +1,38 @@ +module Ci + class CommitsController < Ci::ApplicationController + before_action :authenticate_user!, except: [:status, :show] + before_action :authenticate_public_page!, only: :show + before_action :project + before_action :authorize_access_project!, except: [:status, :show, :cancel] + before_action :authorize_manage_builds!, only: [:cancel] + before_action :commit, only: :show + layout 'ci/commit' + + def show + @builds = @commit.builds + end + + def status + commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) + rescue ActiveRecord::RecordNotFound + render json: { status: "not_found" } + end + + def cancel + commit.builds.running_or_pending.each(&:cancel) + + redirect_to ci_project_ref_commits_path(project, commit.ref, commit.sha) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + + def commit + @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + end + end +end diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb new file mode 100644 index 00000000000..89b784a1e89 --- /dev/null +++ b/app/controllers/ci/events_controller.rb @@ -0,0 +1,21 @@ +module Ci + class EventsController < Ci::ApplicationController + EVENTS_PER_PAGE = 50 + + before_action :authenticate_user! + before_action :project + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/helps_controller.rb b/app/controllers/ci/helps_controller.rb new file mode 100644 index 00000000000..a1ee4111614 --- /dev/null +++ b/app/controllers/ci/helps_controller.rb @@ -0,0 +1,16 @@ +module Ci + class HelpsController < Ci::ApplicationController + skip_filter :check_config + + def show + end + + def oauth2 + if valid_config? + redirect_to ci_root_path + else + render layout: 'ci/empty' + end + end + end +end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb new file mode 100644 index 00000000000..a81e4e319ff --- /dev/null +++ b/app/controllers/ci/lints_controller.rb @@ -0,0 +1,26 @@ +module Ci + class LintsController < Ci::ApplicationController + before_action :authenticate_user! + + def show + end + + def create + if params[:content].blank? + @status = false + @error = "Please provide content of .gitlab-ci.yml" + else + @config_processor = Ci::GitlabCiYamlProcessor.new params[:content] + @stages = @config_processor.stages + @builds = @config_processor.builds + @status = true + end + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + @error = e.message + @status = false + rescue Exception => e + @error = "Undefined error" + @status = false + end + end +end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb new file mode 100644 index 00000000000..6483a84ee91 --- /dev/null +++ b/app/controllers/ci/projects_controller.rb @@ -0,0 +1,137 @@ +module Ci + class ProjectsController < Ci::ApplicationController + PROJECTS_BATCH = 100 + + before_action :authenticate_user!, except: [:build, :badge, :index, :show] + before_action :authenticate_public_page!, only: :show + before_action :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_action :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create] + before_action :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_action :authenticate_token!, only: [:build] + before_action :no_cache, only: [:badge] + protect_from_forgery except: :build + + layout 'ci/project', except: [:index, :gitlab] + + def index + @projects = Ci::Project.ordered_by_last_commit_date.public_only.page(params[:page]) unless current_user + end + + def gitlab + @limit, @offset = (params[:limit] || PROJECTS_BATCH).to_i, (params[:offset] || 0).to_i + @page = @offset == 0 ? 1 : (@offset / @limit + 1) + + @gl_projects = current_user.authorized_projects + @gl_projects = @gl_projects.where("name LIKE ?", "%#{params[:search]}%") if params[:search] + @gl_projects = @gl_projects.page(@page).per(@limit) + + @projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date + @total_count = @gl_projects.size + + @gl_projects = @gl_projects.where.not(id: @projects.map(&:gitlab_id)) + + respond_to do |format| + format.json do + pager_json("ci/projects/gitlab", @total_count) + end + end + rescue + @error = 'Failed to fetch GitLab projects' + end + + def show + @ref = params[:ref] + + @commits = @project.commits.reverse_order + @commits = @commits.where(ref: @ref) if @ref + @commits = @commits.page(params[:page]).per(20) + end + + def integration + end + + def create + project_data = OpenStruct.new(JSON.parse(params["project"])) + + unless can?(current_user, :admin_project, ::Project.find(project_data.id)) + return redirect_to ci_root_path, alert: 'You have to have at least master role to enable CI for this project' + end + + @project = Ci::CreateProjectService.new.execute(current_user, project_data, ci_project_url(":project_id")) + + if @project.persisted? + redirect_to ci_project_path(@project, show_guide: true), notice: 'Project was successfully created.' + else + redirect_to :back, alert: 'Cannot save project' + end + end + + def edit + end + + def update + if project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, project) + + redirect_to :back, notice: 'Project was successfully updated.' + else + render action: "edit" + end + end + + def destroy + project.gl_project.gitlab_ci_service.update_attributes(active: false) + project.destroy + + Ci::EventService.new.remove_project(current_user, project) + + redirect_to ci_projects_url + end + + def build + @commit = Ci::CreateCommitService.new.execute(@project, params.dup) + + if @commit && @commit.valid? + head 201 + else + head 400 + end + end + + # Project status badge + # Image with build status for sha or ref + def badge + image = Ci::ImageForBuildService.new.execute(@project, params) + + send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" + end + + def toggle_shared_runners + project.toggle!(:shared_runners_enabled) + redirect_to :back + end + + def dumped_yaml + send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml' + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + + def no_cache + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + end + + def project_params + params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, + :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, + :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, + { variables_attributes: [:id, :key, :value, :_destroy] }) + end + end +end diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb new file mode 100644 index 00000000000..a8bdd5bb362 --- /dev/null +++ b/app/controllers/ci/runner_projects_controller.rb @@ -0,0 +1,34 @@ +module Ci + class RunnerProjectsController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_manage_project! + + layout 'ci/project' + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + return head(403) unless current_user.ci_authorized_runners.include?(@runner) + + if @runner.assign_to(project, current_user) + redirect_to ci_project_runners_path(project) + else + redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project' + end + end + + def destroy + runner_project = project.runner_projects.find(params[:id]) + runner_project.destroy + + redirect_to ci_project_runners_path(project) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb new file mode 100644 index 00000000000..a672370302b --- /dev/null +++ b/app/controllers/ci/runners_controller.rb @@ -0,0 +1,73 @@ +module Ci + class RunnersController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @runners = @project.runners.order('id DESC') + @specific_runners = + Ci::Runner.specific.includes(:runner_projects). + where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ). + where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20) + @shared_runners = Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + end + + def edit + end + + def update + if @runner.update_attributes(runner_params) + redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def destroy + if @runner.only_for?(@project) + @runner.destroy + end + + redirect_to ci_project_runners_path(@project) + end + + def resume + if @runner.update_attributes(active: true) + redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def show + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def set_runner + @runner ||= @project.runners.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) + end + end +end diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb new file mode 100644 index 00000000000..52c96a34ce8 --- /dev/null +++ b/app/controllers/ci/services_controller.rb @@ -0,0 +1,59 @@ +module Ci + class ServicesController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + before_action :service, only: [:edit, :update, :test] + + respond_to :html + + layout 'ci/project' + + def index + @project.build_missing_services + @services = @project.services.reload + end + + def edit + end + + def update + if @service.update_attributes(service_params) + redirect_to edit_ci_project_service_path(@project, @service.to_param) + else + render 'edit' + end + end + + def test + last_build = @project.builds.last + + if @service.execute(last_build) + message = { notice: 'We successfully tested the service' } + else + message = { alert: 'We tried to test the service but error occurred' } + end + + redirect_to :back, message + end + + private + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def service + @service ||= @project.services.find { |service| service.to_param == params[:id] } + end + + def service_params + params.require(:service).permit( + :type, :active, :webhook, :notify_only_broken_builds, + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server + ) + end + end +end diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb new file mode 100644 index 00000000000..a39cc5d3a56 --- /dev/null +++ b/app/controllers/ci/triggers_controller.rb @@ -0,0 +1,43 @@ +module Ci + class TriggersController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @triggers = @project.triggers + @trigger = Ci::Trigger.new + end + + def create + @trigger = @project.triggers.new + @trigger.save + + if @trigger.valid? + redirect_to ci_project_triggers_path(@project) + else + @triggers = @project.triggers.select(&:persisted?) + render :index + end + end + + def destroy + trigger.destroy + + redirect_to ci_project_triggers_path(@project) + end + + private + + def trigger + @trigger ||= @project.triggers.find(params[:id]) + end + + def project + @project = Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb new file mode 100644 index 00000000000..9c6c775fde8 --- /dev/null +++ b/app/controllers/ci/variables_controller.rb @@ -0,0 +1,33 @@ +module Ci + class VariablesController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def show + end + + def update + if project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, project) + + redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.' + else + render action: 'show' + end + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + + def project_params + params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + end + end +end diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb new file mode 100644 index 00000000000..24074a6d9ac --- /dev/null +++ b/app/controllers/ci/web_hooks_controller.rb @@ -0,0 +1,53 @@ +module Ci + class WebHooksController < Ci::ApplicationController + before_action :authenticate_user! + before_action :project + before_action :authorize_access_project! + before_action :authorize_manage_project! + + layout 'ci/project' + + def index + @web_hooks = @project.web_hooks + @web_hook = Ci::WebHook.new + end + + def create + @web_hook = @project.web_hooks.new(web_hook_params) + @web_hook.save + + if @web_hook.valid? + redirect_to ci_project_web_hooks_path(@project) + else + @web_hooks = @project.web_hooks.select(&:persisted?) + render :index + end + end + + def test + Ci::TestHookService.new.execute(hook, current_user) + + redirect_to :back + end + + def destroy + hook.destroy + + redirect_to ci_project_web_hooks_path(@project) + end + + private + + def hook + @web_hook ||= @project.web_hooks.find(params[:id]) + end + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def web_hook_params + params.require(:web_hook).permit(:url) + end + end +end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index fc31118124b..dc22101cd5e 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,7 +1,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings include PageLayoutHelper - + before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb new file mode 100644 index 00000000000..3198fe55f91 --- /dev/null +++ b/app/helpers/ci/application_helper.rb @@ -0,0 +1,140 @@ +module Ci + module ApplicationHelper + def loader_html + image_tag 'ci/loader.gif', alt: 'Loading' + end + + # Navigation link helper + # + # Returns an `li` element with an 'active' class if the supplied + # controller(s) and/or action(s) are currently active. The content of the + # element is the value passed to the block. + # + # options - The options hash used to determine if the element is "active" (default: {}) + # :controller - One or more controller names to check (optional). + # :action - One or more action names to check (optional). + # :path - A shorthand path, such as 'dashboard#index', to check (optional). + # :html_options - Extra options to be passed to the list element (optional). + # block - An optional block that will become the contents of the returned + # `li` element. + # + # When both :controller and :action are specified, BOTH must match in order + # to be marked as active. When only one is given, either can match. + # + # Examples + # + # # Assuming we're on TreeController#show + # + # # Controller matches, but action doesn't + # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" } + # # => '<li>Hello</li>' + # + # # Controller matches + # nav_link(controller: [:tree, :refs]) { "Hello" } + # # => '<li class="active">Hello</li>' + # + # # Shorthand path + # nav_link(path: 'tree#show') { "Hello" } + # # => '<li class="active">Hello</li>' + # + # # Supplying custom options for the list element + # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } + # # => '<li class="home active">Hello</li>' + # + # Returns a list item element String + def nav_link(options = {}, &block) + if path = options.delete(:path) + if path.respond_to?(:each) + c = path.map { |p| p.split('#').first } + a = path.map { |p| p.split('#').last } + else + c, a, _ = path.split('#') + end + else + c = options.delete(:controller) + a = options.delete(:action) + end + + if c && a + # When given both options, make sure BOTH are active + klass = current_controller?(*c) && current_action?(*a) ? 'active' : '' + else + # Otherwise check EITHER option + klass = current_controller?(*c) || current_action?(*a) ? 'active' : '' + end + + # Add our custom class into the html_options, which may or may not exist + # and which may or may not already have a :class key + o = options.delete(:html_options) || {} + o[:class] ||= '' + o[:class] += ' ' + klass + o[:class].strip! + + if block_given? + content_tag(:li, capture(&block), o) + else + content_tag(:li, nil, o) + end + end + + # Check if a particular controller is the current one + # + # args - One or more controller names to check + # + # Examples + # + # # On TreeController + # current_controller?(:tree) # => true + # current_controller?(:commits) # => false + # current_controller?(:commits, :tree) # => true + def current_controller?(*args) + args.any? { |v| v.to_s.downcase == controller.controller_name } + end + + # Check if a particular action is the current one + # + # args - One or more action names to check + # + # Examples + # + # # On Projects#new + # current_action?(:new) # => true + # current_action?(:create) # => false + # current_action?(:new, :create) # => true + def current_action?(*args) + args.any? { |v| v.to_s.downcase == action_name } + end + + def date_from_to(from, to) + "#{from.to_s(:short)} - #{to.to_s(:short)}" + end + + def body_data_page + path = controller.controller_path.split('/') + namespace = path.first if path.second + + [namespace, controller.controller_name, controller.action_name].compact.join(":") + end + + def duration_in_words(finished_at, started_at) + if finished_at && started_at + interval_in_seconds = finished_at.to_i - started_at.to_i + elsif started_at + interval_in_seconds = Time.now.to_i - started_at.to_i + end + + time_interval_in_words(interval_in_seconds) + end + + def time_interval_in_words(interval_in_seconds) + minutes = interval_in_seconds / 60 + seconds = interval_in_seconds - minutes * 60 + + if minutes >= 1 + "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + else + "#{pluralize(seconds, "second")}" + end + end + end +end diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb new file mode 100644 index 00000000000..cdabdad17d2 --- /dev/null +++ b/app/helpers/ci/builds_helper.rb @@ -0,0 +1,41 @@ +module Ci + module BuildsHelper + def build_ref_link build + gitlab_ref_link build.project, build.ref + end + + def build_compare_link build + gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha + end + + def build_commit_link build + gitlab_commit_link build.project, build.short_sha + end + + def build_url(build) + ci_project_build_url(build.project, build) + end + + def build_status_alert_class(build) + if build.success? + 'alert-success' + elsif build.failed? + 'alert-danger' + elsif build.canceled? + 'alert-disabled' + else + 'alert-warning' + end + end + + def build_icon_css_class(build) + if build.success? + 'fa-circle cgreen' + elsif build.failed? + 'fa-circle cred' + else + 'fa-circle light' + end + end + end +end diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb new file mode 100644 index 00000000000..74de30e006e --- /dev/null +++ b/app/helpers/ci/commits_helper.rb @@ -0,0 +1,39 @@ +module Ci + module CommitsHelper + def commit_status_alert_class(commit) + return 'alert-info' unless commit + + case commit.status + when 'success' + 'alert-success' + when 'failed', 'canceled' + 'alert-danger' + when 'skipped' + 'alert-disabled' + else + 'alert-warning' + end + end + + def ci_commit_path(commit) + ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) + end + + def commit_link(commit) + link_to(commit.short_sha, ci_commit_path(commit)) + end + + def truncate_first_line(message, length = 50) + truncate(message.each_line.first.chomp, length: length) if message + end + + def ci_commit_title(commit) + content_tag :span do + link_to( + simple_sanitize(commit.project.name), ci_project_path(commit.project) + ) + ' @ ' + + gitlab_commit_link(@project, @commit.sha) + end + end + end +end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb new file mode 100644 index 00000000000..2b89a0ce93e --- /dev/null +++ b/app/helpers/ci/gitlab_helper.rb @@ -0,0 +1,36 @@ +module Ci + module GitlabHelper + def no_turbolink + { :"data-no-turbolink" => "data-no-turbolink" } + end + + def gitlab_ref_link project, ref + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commits/#{ref}" + link_to ref, gitlab_url, no_turbolink + end + + def gitlab_compare_link project, before, after + gitlab_url = project.gitlab_url.dup + gitlab_url << "/compare/#{before}...#{after}" + + link_to "#{before}...#{after}", gitlab_url, no_turbolink + end + + def gitlab_commit_link project, sha + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commit/#{sha}" + link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink + end + + def yaml_web_editor_link(project) + commits = project.commits + + if commits.any? && commits.last.push_data[:ci_yaml_file] + "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml" + else + "#{@project.gitlab_url}/new/master" + end + end + end +end diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb new file mode 100644 index 00000000000..be40f79e880 --- /dev/null +++ b/app/helpers/ci/icons_helper.rb @@ -0,0 +1,11 @@ +module Ci + module IconsHelper + def boolean_to_icon(value) + if value.to_s == "true" + content_tag :i, nil, class: 'fa fa-circle cgreen' + else + content_tag :i, nil, class: 'fa fa-power-off clgray' + end + end + end +end diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb new file mode 100644 index 00000000000..fd991a4165a --- /dev/null +++ b/app/helpers/ci/projects_helper.rb @@ -0,0 +1,36 @@ +module Ci + module ProjectsHelper + def ref_tab_class ref = nil + 'active' if ref == @ref + end + + def success_ratio(success_builds, failed_builds) + failed_builds = failed_builds.count(:all) + success_builds = success_builds.count(:all) + + return 100 if failed_builds.zero? + + ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio.to_i + end + + def markdown_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" + end + + def html_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>" + end + + def project_uses_specific_runner?(project) + project.runners.any? + end + + def no_runners_for_project?(project) + project.runners.blank? && + Ci::Runner.shared.blank? + end + end +end diff --git a/app/helpers/ci/routes_helper.rb b/app/helpers/ci/routes_helper.rb new file mode 100644 index 00000000000..42cd54b064f --- /dev/null +++ b/app/helpers/ci/routes_helper.rb @@ -0,0 +1,29 @@ +module Ci + module RoutesHelper + class Base + include Gitlab::Application.routes.url_helpers + + def default_url_options + { + host: Settings.gitlab['host'], + protocol: Settings.gitlab['https'] ? "https" : "http", + port: Settings.gitlab['port'] + } + end + end + + def url_helpers + @url_helpers ||= Base.new + end + + def self.method_missing(method, *args, &block) + @url_helpers ||= Base.new + + if @url_helpers.respond_to?(method) + @url_helpers.send(method, *args, &block) + else + super method, *args, &block + end + end + end +end diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb new file mode 100644 index 00000000000..03c9914641e --- /dev/null +++ b/app/helpers/ci/runners_helper.rb @@ -0,0 +1,22 @@ +module Ci + module RunnersHelper + def runner_status_icon(runner) + unless runner.contacted_at + return content_tag :i, nil, + class: "fa fa-warning-sign", + title: "New runner. Has not connected yet" + end + + status = + if runner.active? + runner.contacted_at > 3.hour.ago ? :online : :offline + else + :paused + end + + content_tag :i, nil, + class: "fa fa-circle runner-status-#{status}", + title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" + end + end +end diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb new file mode 100644 index 00000000000..0d2438928ce --- /dev/null +++ b/app/helpers/ci/triggers_helper.rb @@ -0,0 +1,7 @@ +module Ci + module TriggersHelper + def ci_build_trigger_url(project_id, ref_name) + "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + end + end +end diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb new file mode 100644 index 00000000000..c332d6ed9cf --- /dev/null +++ b/app/helpers/ci/user_helper.rb @@ -0,0 +1,15 @@ +module Ci + module UserHelper + def user_avatar_url(user = nil, size = nil, default = 'identicon') + size = 40 if size.nil? || size <= 0 + + if user.blank? || user.avatar_url.blank? + 'ci/no_avatar.png' + elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url + Regexp.last_match[0] + "?s=#{size}&d=#{default}" + else + user.avatar_url + end + end + end +end diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb new file mode 100644 index 00000000000..6fb4fba85e5 --- /dev/null +++ b/app/mailers/ci/emails/builds.rb @@ -0,0 +1,17 @@ +module Ci + module Emails + module Builds + def build_fail_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) + end + + def build_success_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) + end + end + end +end diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb new file mode 100644 index 00000000000..4462da0d7d2 --- /dev/null +++ b/app/mailers/ci/notify.rb @@ -0,0 +1,47 @@ +module Ci + class Notify < ActionMailer::Base + include Ci::Emails::Builds + + add_template_helper Ci::ApplicationHelper + add_template_helper Ci::GitlabHelper + + default_url_options[:host] = Gitlab.config.gitlab.host + default_url_options[:protocol] = Gitlab.config.gitlab.protocol + default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root + + default from: Gitlab.config.gitlab.email_from + + # Just send email with 3 seconds delay + def self.delay + delay_for(2.seconds) + end + + private + + # Formats arguments into a String suitable for use as an email subject + # + # extra - Extra Strings to be inserted into the subject + # + # Examples + # + # >> subject('Lorem ipsum') + # => "GitLab-CI | Lorem ipsum" + # + # # Automatically inserts Project name when @project is set + # >> @project = Project.last + # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> + # >> subject('Lorem ipsum') + # => "GitLab-CI | Ruby on Rails | Lorem ipsum " + # + # # Accepts multiple arguments + # >> subject('Lorem ipsum', 'Dolor sit amet') + # => "GitLab-CI | Lorem ipsum | Dolor sit amet" + def subject(*extra) + subject = "GitLab-CI" + subject << (@project ? " | #{@project.name}" : "") + subject << " | " + extra.join(' | ') if extra.present? + subject + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 5717c89e61d..f196ffd53f3 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -100,7 +100,7 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) if @project - headers['X-GitLab-Project'] = @project.name + headers['X-GitLab-Project'] = @project.name headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Path'] = @project.path_with_namespace end diff --git a/app/models/ability.rb b/app/models/ability.rb index f8e5afa9b01..a020b24a550 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -149,6 +149,7 @@ class Ability :admin_merge_request, :create_merge_request, :create_wiki, + :manage_builds, :push_code ] end diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb new file mode 100644 index 00000000000..0cf496f7d81 --- /dev/null +++ b/app/models/ci/application_setting.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# all_broken_builds :boolean +# add_pusher :boolean +# created_at :datetime +# updated_at :datetime +# + +module Ci + class ApplicationSetting < ActiveRecord::Base + extend Ci::Model + + def self.current + Ci::ApplicationSetting.last + end + + def self.create_from_defaults + create( + all_broken_builds: Settings.gitlab_ci['all_broken_builds'], + add_pusher: Settings.gitlab_ci['add_pusher'], + ) + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb new file mode 100644 index 00000000000..8096d4fa5ae --- /dev/null +++ b/app/models/ci/build.rb @@ -0,0 +1,285 @@ +# == Schema Information +# +# Table name: builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# commit_id :integer +# coverage :float +# commands :text +# job_id :integer +# name :string(255) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# deploy :boolean default(FALSE) +# trigger_request_id :integer +# + +module Ci + class Build < ActiveRecord::Base + extend Ci::Model + + LAZY_ATTRIBUTES = ['trace'] + + belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :project, class_name: 'Ci::Project' + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + + serialize :options + + validates :commit, presence: true + validates :status, presence: true + validates :coverage, numericality: true, allow_blank: true + + scope :running, ->() { where(status: "running") } + scope :pending, ->() { where(status: "pending") } + scope :success, ->() { where(status: "success") } + scope :failed, ->() { where(status: "failed") } + scope :unstarted, ->() { where(runner_id: nil) } + scope :running_or_pending, ->() { where(status:[:running, :pending]) } + + acts_as_taggable + + # To prevent db load megabytes of data from trace + default_scope -> { select(Ci::Build.columns_without_lazy) } + + class << self + def columns_without_lazy + (column_names - LAZY_ATTRIBUTES).map do |column_name| + "#{table_name}.#{column_name}" + end + end + + def last_month + where('created_at > ?', Date.today - 1.month) + end + + def first_pending + pending.unstarted.order('created_at ASC').first + end + + def create_from(build) + new_build = build.dup + new_build.status = :pending + new_build.runner_id = nil + new_build.save + end + + def retry(build) + new_build = Ci::Build.new(status: :pending) + new_build.options = build.options + new_build.commands = build.commands + new_build.tag_list = build.tag_list + new_build.commit_id = build.commit_id + new_build.project_id = build.project_id + new_build.name = build.name + new_build.allow_failure = build.allow_failure + new_build.stage = build.stage + new_build.trigger_request = build.trigger_request + new_build.save + new_build + end + end + + state_machine :status, initial: :pending do + event :run do + transition pending: :running + end + + event :drop do + transition running: :failed + end + + event :success do + transition running: :success + end + + event :cancel do + transition [:pending, :running] => :canceled + end + + after_transition pending: :running do |build, transition| + build.update_attributes started_at: Time.now + end + + after_transition any => [:success, :failed, :canceled] do |build, transition| + build.update_attributes finished_at: Time.now + project = build.project + + if project.web_hooks? + Ci::WebHookService.new.build_end(build) + end + + if build.commit.success? + build.commit.create_next_builds(build.trigger_request) + end + + project.execute_services(build) + + if project.coverage_enabled? + build.update_coverage + end + end + + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + end + + delegate :sha, :short_sha, :before_sha, :ref, + to: :commit, prefix: false + + def trace_html + html = Ci::Ansi2html::convert(trace) if trace.present? + html ||= '' + end + + def trace + if project && read_attribute(:trace).present? + read_attribute(:trace).gsub(project.token, 'xxxxxx') + end + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + + def ignored? + failed? && allow_failure? + end + + def timeout + project.timeout + end + + def variables + yaml_variables + project_variables + trigger_variables + end + + def duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end + + def project + commit.project + end + + def project_id + commit.project_id + end + + def project_name + project.name + end + + def repo_url + project.repo_url_with_auth + end + + def allow_git_fetch + project.allow_git_fetch + end + + def update_coverage + coverage = extract_coverage(trace, project.coverage_regex) + + if coverage.is_a? Numeric + update_attributes(coverage: coverage) + end + end + + def extract_coverage(text, regex) + begin + matches = text.gsub(Regexp.new(regex)).to_a.last + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f + end + rescue => ex + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now + end + end + + def trace + if File.exist?(path_to_trace) + File.read(path_to_trace) + else + # backward compatibility + read_attribute :trace + end + end + + def trace=(trace) + unless Dir.exists? dir_to_trace + FileUtils.mkdir_p dir_to_trace + end + + File.write(path_to_trace, trace) + end + + def dir_to_trace + File.join( + Settings.gitlab_ci.builds_path, + created_at.utc.strftime("%Y_%m"), + project.id.to_s + ) + end + + def path_to_trace + "#{dir_to_trace}/#{id}.log" + end + + private + + def yaml_variables + if commit.config_processor + commit.config_processor.variables.map do |key, value| + { key: key, value: value, public: true } + end + else + [] + end + end + + def project_variables + project.variables.map do |variable| + { key: variable.key, value: variable.value, public: false } + end + end + + def trigger_variables + if trigger_request && trigger_request.variables + trigger_request.variables.map do |key, value| + { key: key, value: value, public: false } + end + else + [] + end + end + end +end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb new file mode 100644 index 00000000000..23cd47dfe37 --- /dev/null +++ b/app/models/ci/commit.rb @@ -0,0 +1,267 @@ +# == Schema Information +# +# Table name: commits +# +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# + +module Ci + class Commit < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + has_many :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + serialize :push_data + + validates_presence_of :ref, :sha, :before_sha, :push_data + validate :valid_commit_sha + + def self.truncate_sha(sha) + sha[0...8] + end + + def to_param + sha + end + + def last_build + builds.order(:id).last + end + + def retry + builds_without_retry.each do |build| + Ci::Build.retry(build) + end + end + + def valid_commit_sha + if self.sha == Ci::Git::BLANK_SHA + self.errors.add(:sha, " cant be 00000000 (branch removal)") + end + end + + def new_branch? + before_sha == Ci::Git::BLANK_SHA + end + + def compare? + !new_branch? + end + + def git_author_name + commit_data[:author][:name] if commit_data && commit_data[:author] + end + + def git_author_email + commit_data[:author][:email] if commit_data && commit_data[:author] + end + + def git_commit_message + commit_data[:message] if commit_data && commit_data[:message] + end + + def short_before_sha + Ci::Commit.truncate_sha(before_sha) + end + + def short_sha + Ci::Commit.truncate_sha(sha) + end + + def commit_data + push_data[:commits].find do |commit| + commit[:id] == sha + end + rescue + nil + end + + def project_recipients + recipients = project.email_recipients.split(' ') + + if project.email_add_pusher? && push_data[:user_email].present? + recipients << push_data[:user_email] + end + + recipients.uniq + end + + def stage + return unless config_processor + stages = builds_without_retry.select(&:active?).map(&:stage) + config_processor.stages.find { |stage| stages.include? stage } + end + + def create_builds_for_stage(stage, trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) + builds_attrs.map do |build_attrs| + builds.create!({ + project: project, + name: build_attrs[:name], + commands: build_attrs[:script], + tag_list: build_attrs[:tags], + options: build_attrs[:options], + allow_failure: build_attrs[:allow_failure], + stage: build_attrs[:stage], + trigger_request: trigger_request, + }) + end + end + + def create_next_builds(trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + stages = builds.where(trigger_request: trigger_request).group_by(&:stage) + + config_processor.stages.any? do |stage| + !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present? + end + end + + def create_builds(trigger_request = nil) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + config_processor.stages.any? do |stage| + create_builds_for_stage(stage, trigger_request).present? + end + end + + def builds_without_retry + @builds_without_retry ||= + begin + grouped_builds = builds.group_by(&:name) + grouped_builds.map do |name, builds| + builds.sort_by(&:id).last + end + end + end + + def builds_without_retry_sorted + return builds_without_retry unless config_processor + + stages = config_processor.stages + builds_without_retry.sort_by do |build| + [stages.index(build.stage) || -1, build.name || ""] + end + end + + def retried_builds + @retried_builds ||= (builds.order(id: :desc) - builds_without_retry) + end + + def status + if skip_ci? + return 'skipped' + elsif yaml_errors.present? + return 'failed' + elsif builds.none? + return 'skipped' + elsif success? + 'success' + elsif pending? + 'pending' + elsif running? + 'running' + elsif canceled? + 'canceled' + else + 'failed' + end + end + + def pending? + builds_without_retry.all? do |build| + build.pending? + end + end + + def running? + builds_without_retry.any? do |build| + build.running? || build.pending? + end + end + + def success? + builds_without_retry.all? do |build| + build.success? || build.ignored? + end + end + + def failed? + status == 'failed' + end + + def canceled? + builds_without_retry.all? do |build| + build.canceled? + end + end + + def duration + @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i + end + + def finished_at + @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) + end + + def coverage + if project.coverage_enabled? + coverage_array = builds_without_retry.map(&:coverage).compact + if coverage_array.size >= 1 + '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) + end + end + end + + def matrix? + builds_without_retry.size > 1 + end + + def config_processor + @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config) + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + save_yaml_error(e.message) + nil + rescue Exception => e + logger.error e.message + "\n" + e.backtrace.join("\n") + save_yaml_error("Undefined yaml error") + nil + end + + def skip_ci? + return false if builds.any? + commits = push_data[:commits] + commits.present? && commits.last[:message] =~ /(\[ci skip\])/ + end + + def update_committed! + update!(committed_at: DateTime.now) + end + + private + + def save_yaml_error(error) + return if self.yaml_errors? + self.yaml_errors = error + save + end + end +end diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb new file mode 100644 index 00000000000..cac3a7a49c1 --- /dev/null +++ b/app/models/ci/event.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: events +# +# id :integer not null, primary key +# project_id :integer +# user_id :integer +# is_admin :integer +# description :text +# created_at :datetime +# updated_at :datetime +# + +module Ci + class Event < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates :description, + presence: true, + length: { in: 5..200 } + + scope :admin, ->(){ where(is_admin: true) } + scope :project_wide, ->(){ where(is_admin: false) } + end +end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb new file mode 100644 index 00000000000..2cf1783616f --- /dev/null +++ b/app/models/ci/project.rb @@ -0,0 +1,225 @@ +# == Schema Information +# +# Table name: projects +# +# id :integer not null, primary key +# name :string(255) not null +# timeout :integer default(3600), not null +# created_at :datetime +# updated_at :datetime +# token :string(255) +# default_ref :string(255) +# path :string(255) +# always_build :boolean default(FALSE), not null +# polling_interval :integer +# public :boolean default(FALSE), not null +# ssh_url_to_repo :string(255) +# gitlab_id :integer +# allow_git_fetch :boolean default(TRUE), not null +# email_recipients :string(255) default(""), not null +# email_add_pusher :boolean default(TRUE), not null +# email_only_broken_builds :boolean default(TRUE), not null +# skip_refs :string(255) +# coverage_regex :string(255) +# shared_runners_enabled :boolean default(FALSE) +# generated_yaml_config :text +# + +module Ci + class Project < ActiveRecord::Base + extend Ci::Model + + include Ci::ProjectStatus + + belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id + + has_many :commits, ->() { order(:committed_at) }, dependent: :destroy, class_name: 'Ci::Commit' + has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' + has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' + has_many :events, dependent: :destroy, class_name: 'Ci::Event' + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' + + # Project services + has_many :services, dependent: :destroy, class_name: 'Ci::Service' + has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService' + has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService' + has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService' + + accepts_nested_attributes_for :variables, allow_destroy: true + + # + # Validations + # + validates_presence_of :name, :timeout, :token, :default_ref, + :path, :ssh_url_to_repo, :gitlab_id + + validates_uniqueness_of :gitlab_id + + validates :polling_interval, + presence: true, + if: ->(project) { project.always_build.present? } + + scope :public_only, ->() { where(public: true) } + + before_validation :set_default_values + + class << self + include Ci::CurrentSettings + + def base_build_script + <<-eos + git submodule update --init + ls -la + eos + end + + def parse(project) + params = { + name: project.name_with_namespace, + gitlab_id: project.id, + path: project.path_with_namespace, + default_ref: project.default_branch || 'master', + ssh_url_to_repo: project.ssh_url_to_repo, + email_add_pusher: current_application_settings.add_pusher, + email_only_broken_builds: current_application_settings.all_broken_builds, + } + + project = Ci::Project.new(params) + project.build_missing_services + project + end + + # TODO: remove + def from_gitlab(user, scope = :owned, options) + opts = user.authenticate_options + opts.merge! options + + raise 'Implement me of fix' + #projects = Ci::Network.new.projects(opts.compact, scope) + + if projects + projects.map { |pr| OpenStruct.new(pr) } + else + [] + end + end + + def already_added?(project) + where(gitlab_id: project.id).any? + end + + def unassigned(runner) + joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ + "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). + where('#{Ci::RunnerProject.table_name}.project_id' => nil) + end + + def ordered_by_last_commit_date + last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)" + joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id"). + order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") + end + + def search(query) + where("LOWER(#{Ci::Project.table_name}.name) LIKE :query", + query: "%#{query.try(:downcase)}%") + end + end + + def any_runners? + if runners.active.any? + return true + end + + shared_runners_enabled && Ci::Runner.shared.active.any? + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def tracked_refs + @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip} + end + + def valid_token? token + self.token && self.token == token + end + + def no_running_builds? + # Get running builds not later than 3 days ago to ignore hangs + builds.running.where("updated_at > ?", 3.days.ago).empty? + end + + def email_notification? + email_add_pusher || email_recipients.present? + end + + def web_hooks? + web_hooks.any? + end + + def services? + services.any? + end + + def timeout_in_minutes + timeout / 60 + end + + def timeout_in_minutes=(value) + self.timeout = value.to_i * 60 + end + + def coverage_enabled? + coverage_regex.present? + end + + # Build a clone-able repo url + # using http and basic auth + def repo_url_with_auth + auth = "gitlab-ci-token:#{token}@" + url = gitlab_url + ".git" + url.sub(/^https?:\/\//) do |prefix| + prefix + auth + end + end + + def available_services_names + %w(slack mail hip_chat) + end + + def build_missing_services + available_services_names.each do |service_name| + service = services.find { |service| service.to_param == service_name } + + # If service is available but missing in db + # we should create an instance. Ex `create_gitlab_ci_service` + service = self.send :"create_#{service_name}_service" if service.nil? + end + end + + def execute_services(data) + services.each do |service| + + # Call service hook only if it is active + begin + service.execute(data) if service.active && service.can_execute?(data) + rescue => e + logger.error(e) + end + end + end + + def gitlab_url + File.join(Gitlab.config.gitlab.url, path) + end + + def setup_finished? + commits.any? + end + end +end diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb new file mode 100644 index 00000000000..6d5cafe81a2 --- /dev/null +++ b/app/models/ci/project_status.rb @@ -0,0 +1,47 @@ +module Ci + module ProjectStatus + def status + last_commit.status if last_commit + end + + def broken? + last_commit.failed? if last_commit + end + + def success? + last_commit.success? if last_commit + end + + def broken_or_success? + broken? || success? + end + + def last_commit + @last_commit ||= commits.last if commits.any? + end + + def last_commit_date + last_commit.try(:created_at) + end + + def human_status + status + end + + # only check for toggling build status within same ref. + def last_commit_changed_status? + ref = last_commit.ref + last_commits = commits.where(ref: ref).last(2) + + if last_commits.size < 2 + false + else + last_commits[0].status != last_commits[1].status + end + end + + def last_commit_for_ref(ref) + commits.where(ref: ref).last + end + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb new file mode 100644 index 00000000000..1e9f78a3748 --- /dev/null +++ b/app/models/ci/runner.rb @@ -0,0 +1,80 @@ +# == Schema Information +# +# Table name: runners +# +# id :integer not null, primary key +# token :string(255) +# created_at :datetime +# updated_at :datetime +# description :string(255) +# contacted_at :datetime +# active :boolean default(TRUE), not null +# is_shared :boolean default(FALSE) +# name :string(255) +# version :string(255) +# revision :string(255) +# platform :string(255) +# architecture :string(255) +# + +module Ci + class Runner < ActiveRecord::Base + extend Ci::Model + + has_many :builds, class_name: 'Ci::Build' + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :projects, through: :runner_projects, class_name: 'Ci::Project' + + has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' + + before_validation :set_default_values + + scope :specific, ->() { where(is_shared: false) } + scope :shared, ->() { where(is_shared: true) } + scope :active, ->() { where(active: true) } + scope :paused, ->() { where(active: false) } + + acts_as_taggable + + def self.search(query) + where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', + query: "%#{query.try(:downcase)}%") + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def assign_to(project, current_user = nil) + self.is_shared = false if shared? + self.save + project.runner_projects.create!(runner_id: self.id) + end + + def display_name + return token unless !description.blank? + + description + end + + def shared? + is_shared + end + + def belongs_to_one_project? + runner_projects.count == 1 + end + + def specific? + !shared? + end + + def only_for?(project) + projects == [project] + end + + def short_sha + token[0...10] + end + end +end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb new file mode 100644 index 00000000000..44453ee4b41 --- /dev/null +++ b/app/models/ci/runner_project.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: runner_projects +# +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +module Ci + class RunnerProject < ActiveRecord::Base + extend Ci::Model + + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :project, class_name: 'Ci::Project' + + validates_uniqueness_of :runner_id, scope: :project_id + end +end diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb new file mode 100644 index 00000000000..ed5e3f940b6 --- /dev/null +++ b/app/models/ci/service.rb @@ -0,0 +1,105 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +# To add new service you should build a class inherited from Service +# and implement a set of methods +module Ci + class Service < ActiveRecord::Base + extend Ci::Model + + serialize :properties, JSON + + default_value_for :active, false + + after_initialize :initialize_properties + + belongs_to :project, class_name: 'Ci::Project' + + validates :project_id, presence: true + + def activated? + active + end + + def category + :common + end + + def initialize_properties + self.properties = {} if properties.nil? + end + + def title + # implement inside child + end + + def description + # implement inside child + end + + def help + # implement inside child + end + + def to_param + # implement inside child + end + + def fields + # implement inside child + [] + end + + def can_test? + project.builds.any? + end + + def can_execute?(build) + true + end + + def execute(build) + # implement inside child + end + + # Provide convenient accessor methods + # for each serialized property. + def self.prop_accessor(*args) + args.each do |arg| + class_eval %{ + def #{arg} + (properties || {})['#{arg}'] + end + + def #{arg}=(value) + self.properties ||= {} + self.properties['#{arg}'] = value + end + } + end + end + + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval %{ + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } + end + end + end +end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb new file mode 100644 index 00000000000..fe224b7dc70 --- /dev/null +++ b/app/models/ci/trigger.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: triggers +# +# id :integer not null, primary key +# token :string(255) +# project_id :integer not null +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# + +module Ci + class Trigger < ActiveRecord::Base + extend Ci::Model + + acts_as_paranoid + + belongs_to :project, class_name: 'Ci::Project' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + validates_presence_of :token + validates_uniqueness_of :token + + before_validation :set_default_values + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def last_trigger_request + trigger_requests.last + end + + def short_token + token[0...10] + end + end +end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb new file mode 100644 index 00000000000..29cd9553394 --- /dev/null +++ b/app/models/ci/trigger_request.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: trigger_requests +# +# id :integer not null, primary key +# trigger_id :integer not null +# variables :text +# created_at :datetime +# updated_at :datetime +# commit_id :integer +# + +module Ci + class TriggerRequest < ActiveRecord::Base + extend Ci::Model + + belongs_to :trigger, class_name: 'Ci::Trigger' + belongs_to :commit, class_name: 'Ci::Commit' + has_many :builds, class_name: 'Ci::Build' + + serialize :variables + end +end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb new file mode 100644 index 00000000000..7a542802fa6 --- /dev/null +++ b/app/models/ci/variable.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# + +module Ci + class Variable < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates_presence_of :key + validates_uniqueness_of :key, scope: :project_id + + attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + end +end diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb new file mode 100644 index 00000000000..8f03b0625da --- /dev/null +++ b/app/models/ci/web_hook.rb @@ -0,0 +1,44 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +module Ci + class WebHook < ActiveRecord::Base + extend Ci::Model + + include HTTParty + + belongs_to :project, class_name: 'Ci::Project' + + # HTTParty timeout + default_timeout 10 + + validates :url, presence: true, + format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + + def execute(data) + parsed_url = URI.parse(url) + if parsed_url.userinfo.blank? + Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) + else + post_url = url.gsub("#{parsed_url.userinfo}@", "") + auth = { + username: URI.decode(parsed_url.user), + password: URI.decode(parsed_url.password), + } + Ci::WebHook.post(post_url, + body: data.to_json, + headers: { "Content-Type" => "application/json" }, + verify: false, + basic_auth: auth) + end + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 49525eb9227..81951467d41 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -329,7 +329,7 @@ class Project < ActiveRecord::Base end def web_url - Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -455,7 +455,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb new file mode 100644 index 00000000000..58825fe066c --- /dev/null +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -0,0 +1,78 @@ +module Ci + class HipChatMessage + attr_reader :build + + def initialize(build) + @build = build + end + + def to_s + lines = Array.new + lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_url(project)}\">#{project.name}</a> - ") + + if commit.matrix? + lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_ref_commits_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>") + else + first_build = commit.builds_without_retry.first + lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_build_url(project, first_build)}\">Build '#{first_build.name}' ##{first_build.id}</a></br>") + end + + lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>") + lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") + lines.join('') + end + + def status_color(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :success + 'green' + when :failed, :canceled + 'red' + else # :pending, :running or unknown + 'yellow' + end + end + + def notify? + [:failed, :canceled].include?(commit_status) + end + + + private + + def commit + build.commit + end + + def project + commit.project + end + + def build_status + build.status.to_sym + end + + def commit_status + commit.status.to_sym + end + + def humanized_status(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :pending + "Pending" + when :running + "Running" + when :failed + "Failed" + when :success + "Successful" + when :canceled + "Canceled" + else + "Unknown" + end + end + end +end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb new file mode 100644 index 00000000000..0e6e97394bc --- /dev/null +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -0,0 +1,93 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class HipChatService < Ci::Service + prop_accessor :hipchat_token, :hipchat_room, :hipchat_server + boolean_accessor :notify_only_broken_builds + validates :hipchat_token, presence: true, if: :activated? + validates :hipchat_room, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + + def title + "HipChat" + end + + def description + "Private group chat, video chat, instant messaging for teams" + end + + def help + end + + def to_param + 'hip_chat' + end + + def fields + [ + { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' }, + { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' }, + { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include? build + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + msg = Ci::HipChatMessage.new(build) + opts = default_options.merge( + token: hipchat_token, + room: hipchat_room, + server: server_url, + color: msg.status_color, + notify: msg.notify? + ) + Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts) + end + + private + + def default_options + { + service_name: 'GitLab CI', + message_format: 'html' + } + end + + def server_url + if hipchat_server.blank? + 'https://api.hipchat.com' + else + hipchat_server + end + end + end +end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb new file mode 100644 index 00000000000..1bd2f33612b --- /dev/null +++ b/app/models/project_services/ci/mail_service.rb @@ -0,0 +1,84 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class MailService < Ci::Service + delegate :email_recipients, :email_recipients=, + :email_add_pusher, :email_add_pusher=, + :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false + + before_save :update_project + + default_value_for :active, true + + def title + 'Mail' + end + + def description + 'Email notification' + end + + def to_param + 'mail' + end + + def fields + [ + { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' }, + { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' }, + { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + # it doesn't make sense to send emails for retried builds + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case build.status.to_sym + when :failed + true + when :success + true unless email_only_broken_builds + else + false + end + end + + def execute(build) + build.commit.project_recipients.each do |recipient| + case build.status.to_sym + when :success + mailer.build_success_email(build.id, recipient) + when :failed + mailer.build_fail_email(build.id, recipient) + end + end + end + + private + + def update_project + project.save! + end + + def mailer + Ci::Notify.delay + end + end +end diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb new file mode 100644 index 00000000000..491ace50111 --- /dev/null +++ b/app/models/project_services/ci/slack_message.rb @@ -0,0 +1,97 @@ +require 'slack-notifier' + +module Ci + class SlackMessage + def initialize(commit) + @commit = commit + end + + def pretext + '' + end + + def color + attachment_color + end + + def fallback + format(attachment_message) + end + + def attachments + fields = [] + + if commit.matrix? + commit.builds_without_retry.each do |build| + next if build.allow_failure? + next unless build.failed? + fields << { + title: build.name, + value: "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." + } + end + end + + [{ + text: attachment_message, + color: attachment_color, + fields: fields + }] + end + + private + + attr_reader :commit + + def attachment_message + out = "<#{Ci::RoutesHelper.ci_project_url(project)}|#{project_name}>: " + if commit.matrix? + out << "Commit <#{Ci::RoutesHelper.ci_project_ref_commits_url(project, commit.ref, commit.sha)}|\##{commit.id}> " + else + build = commit.builds_without_retry.first + out << "Build <#{Ci::RoutesHelper.ci_project_build_path(project, build)}|\##{build.id}> " + end + out << "(<#{commit_sha_link}|#{commit.short_sha}>) " + out << "of <#{commit_ref_link}|#{commit.ref}> " + out << "by #{commit.git_author_name} " if commit.git_author_name + out << "#{commit_status} in " + out << "#{commit.duration} second(s)" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def project + commit.project + end + + def project_name + project.name + end + + def commit_sha_link + "#{project.gitlab_url}/commit/#{commit.sha}" + end + + def commit_ref_link + "#{project.gitlab_url}/commits/#{commit.ref}" + end + + def attachment_color + if commit.success? + 'good' + else + 'danger' + end + end + + def commit_status + if commit.success? + 'succeeded' + else + 'failed' + end + end + end +end diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb new file mode 100644 index 00000000000..76db573dc17 --- /dev/null +++ b/app/models/project_services/ci/slack_service.rb @@ -0,0 +1,81 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class SlackService < Ci::Service + prop_accessor :webhook + boolean_accessor :notify_only_broken_builds + validates :webhook, presence: true, if: :activated? + + default_value_for :notify_only_broken_builds, true + + def title + 'Slack' + end + + def description + 'A team communication tool for the 21st century' + end + + def to_param + 'slack' + end + + def help + 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present? + end + + def fields + [ + { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + message = Ci::SlackMessage.new(build.commit) + options = default_options.merge( + color: message.color, + fallback: message.fallback, + attachments: message.attachments + ) + Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options) + end + + private + + def default_options + { + username: 'GitLab CI' + } + end + end +end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 0ebc0a3ba1a..9558292fea3 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -19,7 +19,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index bfa8fc7b860..35e30b1cb0b 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,7 +19,7 @@ # class JiraService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/user.rb b/app/models/user.rb index bff8eeed96d..25371f9138a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -753,4 +753,13 @@ class User < ActiveRecord::Base def can_be_removed? !solo_owned_groups.present? end + + def ci_authorized_projects + @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects) + end + + def ci_authorized_runners + Ci::Runner.specific.includes(:runner_projects). + where(ci_runner_projects: { project_id: ci_authorized_projects } ) + end end diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb new file mode 100644 index 00000000000..0a1abf89a95 --- /dev/null +++ b/app/services/ci/create_commit_service.rb @@ -0,0 +1,50 @@ +module Ci + class CreateCommitService + def execute(project, params) + before_sha = params[:before] + sha = params[:checkout_sha] || params[:after] + origin_ref = params[:ref] + + unless origin_ref && sha.present? + return false + end + + ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + + # Skip branch removal + if sha == Ci::Git::BLANK_SHA + return false + end + + commit = project.commits.find_by_sha_and_ref(sha, ref) + + # Create commit if not exists yet + unless commit + data = { + ref: ref, + sha: sha, + tag: origin_ref.start_with?('refs/tags/'), + before_sha: before_sha, + push_data: { + before: before_sha, + after: sha, + ref: ref, + user_name: params[:user_name], + user_email: params[:user_email], + repository: params[:repository], + commits: params[:commits], + total_commits_count: params[:total_commits_count], + ci_yaml_file: params[:ci_yaml_file] + } + } + + commit = project.commits.create(data) + end + + commit.update_committed! + commit.create_builds unless commit.builds.any? + + commit + end + end +end diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb new file mode 100644 index 00000000000..0419612d521 --- /dev/null +++ b/app/services/ci/create_project_service.rb @@ -0,0 +1,35 @@ +module Ci + class CreateProjectService + include Gitlab::Application.routes.url_helpers + + def execute(current_user, params, project_route, forked_project = nil) + @project = Ci::Project.parse(params) + + Ci::Project.transaction do + @project.save! + + data = { + token: @project.token, + project_url: project_route.gsub(":project_id", @project.id.to_s), + } + + gl_project = ::Project.find(@project.gitlab_id) + gl_project.build_missing_services + gl_project.gitlab_ci_service.update_attributes(data.merge(active: true)) + end + + if forked_project + # Copy settings + settings = forked_project.attributes.select do |attr_name, value| + ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name + end + + @project.update(settings) + end + + Ci::EventService.new.create_project(current_user, @project) + + @project + end + end +end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb new file mode 100644 index 00000000000..9bad09f2f54 --- /dev/null +++ b/app/services/ci/create_trigger_request_service.rb @@ -0,0 +1,17 @@ +module Ci + class CreateTriggerRequestService + def execute(project, trigger, ref, variables = nil) + commit = project.commits.where(ref: ref).last + return unless commit + + trigger_request = trigger.trigger_requests.create!( + commit: commit, + variables: variables + ) + + if commit.create_builds(trigger_request) + trigger_request + end + end + end +end diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb new file mode 100644 index 00000000000..3f4e02dd26c --- /dev/null +++ b/app/services/ci/event_service.rb @@ -0,0 +1,31 @@ +module Ci + class EventService + def remove_project(user, project) + create( + description: "Project \"#{project.name}\" has been removed by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def create_project(user, project) + create( + description: "Project \"#{project.name}\" has been created by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def change_project_settings(user, project) + create( + project_id: project.id, + user_id: user.id, + description: "User \"#{user.username}\" updated projects settings" + ) + end + + def create(*args) + Ci::Event.create!(*args) + end + end +end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb new file mode 100644 index 00000000000..b95835ba093 --- /dev/null +++ b/app/services/ci/image_for_build_service.rb @@ -0,0 +1,31 @@ +module Ci + class ImageForBuildService + def execute(project, params) + image_name = + if params[:sha] + commit = project.commits.find_by(sha: params[:sha]) + image_for_commit(commit) + elsif params[:ref] + commit = project.last_commit_for_ref(params[:ref]) + image_for_commit(commit) + else + 'build-unknown.svg' + end + + image_path = Rails.root.join('public/ci', image_name) + + OpenStruct.new( + path: image_path, + name: image_name + ) + end + + private + + def image_for_commit(commit) + return 'build-unknown.svg' unless commit + + 'build-' + commit.status + ".svg" + end + end +end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb new file mode 100644 index 00000000000..33f1c1e918d --- /dev/null +++ b/app/services/ci/register_build_service.rb @@ -0,0 +1,40 @@ +module Ci + # This class responsible for assigning + # proper pending build to runner on runner API request + class RegisterBuildService + def execute(current_runner) + builds = Ci::Build.pending.unstarted + + builds = + if current_runner.shared? + # don't run projects which have not enables shared runners + builds.includes(:project).where(ci_projects: { shared_runners_enabled: true }) + else + # do run projects which are only assigned to this runner + builds.where(project_id: current_runner.projects) + end + + builds = builds.order('created_at ASC') + + build = builds.find do |build| + (build.tag_list - current_runner.tag_list).empty? + end + + + if build + # In case when 2 runners try to assign the same build, second runner will be declined + # with StateMachine::InvalidTransition in run! method. + build.with_lock do + build.runner_id = current_runner.id + build.save! + build.run! + end + end + + build + + rescue StateMachine::InvalidTransition + nil + end + end +end diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb new file mode 100644 index 00000000000..3a17596aaeb --- /dev/null +++ b/app/services/ci/test_hook_service.rb @@ -0,0 +1,7 @@ +module Ci + class TestHookService + def execute(hook, current_user) + Ci::WebHookService.new.build_end(hook.project.commits.last.last_build) + end + end +end diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb new file mode 100644 index 00000000000..87984b20fa1 --- /dev/null +++ b/app/services/ci/web_hook_service.rb @@ -0,0 +1,36 @@ +module Ci + class WebHookService + def build_end(build) + execute_hooks(build.project, build_data(build)) + end + + def execute_hooks(project, data) + project.web_hooks.each do |web_hook| + async_execute_hook(web_hook, data) + end + end + + def async_execute_hook(hook, data) + Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) + end + + def build_data(build) + project = build.project + data = {} + data.merge!({ + build_id: build.id, + build_name: build.name, + build_status: build.status, + build_started_at: build.started_at, + build_finished_at: build.finished_at, + project_id: project.id, + project_name: project.name, + gitlab_url: project.gitlab_url, + ref: build.ref, + sha: build.sha, + before_sha: build.before_sha, + push_data: build.commit.push_data + }) + end + end +end diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml new file mode 100644 index 00000000000..634c9daa477 --- /dev/null +++ b/app/views/ci/admin/application_settings/_form.html.haml @@ -0,0 +1,24 @@ += form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + - if @application_setting.errors.any? + #error_explanation + .alert.alert-danger + - @application_setting.errors.full_messages.each do |msg| + %p= msg + + %fieldset + %legend Default Project Settings + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :all_broken_builds do + = f.check_box :all_broken_builds + Send emails only on broken builds + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :add_pusher do + = f.check_box :add_pusher + Add pusher to recipients list + + .form-actions + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml new file mode 100644 index 00000000000..7ef0aa89ed6 --- /dev/null +++ b/app/views/ci/admin/application_settings/show.html.haml @@ -0,0 +1,3 @@ +%h3.page-title Settings +%hr += render 'form' diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml new file mode 100644 index 00000000000..47f8df8f98e --- /dev/null +++ b/app/views/ci/admin/builds/_build.html.haml @@ -0,0 +1,32 @@ +- if build.commit && build.project + %tr.build.alert{class: build_status_alert_class(build)} + %td.build-link + = link_to ci_project_build_url(build.project, build) do + %strong #{build.id} + + %td.status + = build.status + + %td.commit-link + = commit_link(build.commit) + + %td.runner + - if build.runner + = link_to build.runner.id, ci_admin_runner_path(build.runner) + + %td.build-project + = truncate build.project.name, length: 30 + + %td.build-message + %span= truncate(build.commit.git_commit_message, length: 30) + + %td.build-branch + %span= truncate(build.ref, length: 25) + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml new file mode 100644 index 00000000000..d23119162cc --- /dev/null +++ b/app/views/ci/admin/builds/index.html.haml @@ -0,0 +1,28 @@ +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ("active" if @scope.nil?)} + = link_to 'All builds', ci_admin_builds_path + + %li{class: ("active" if @scope == "pending")} + = link_to "Pending", ci_admin_builds_path(scope: :pending) + + %li{class: ("active" if @scope == "running")} + = link_to "Running", ci_admin_builds_path(scope: :running) + + +%table.builds + %thead + %tr + %th Build + %th Status + %th Commit + %th Runner + %th Project + %th Message + %th Branch + %th Duration + %th Finished at + + - @builds.each do |build| + = render "ci/admin/builds/build", build: build + += paginate @builds diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml new file mode 100644 index 00000000000..f9ab0994304 --- /dev/null +++ b/app/views/ci/admin/events/index.html.haml @@ -0,0 +1,17 @@ +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events
\ No newline at end of file diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml new file mode 100644 index 00000000000..505dd4b3fdc --- /dev/null +++ b/app/views/ci/admin/projects/_project.html.haml @@ -0,0 +1,28 @@ +- last_commit = project.last_commit +%tr.alert{class: commit_status_alert_class(last_commit) } + %td + = project.id + %td + = link_to [:ci, project] do + %strong= project.name + %td + - if last_commit + #{last_commit.status} (#{commit_link(last_commit)}) + - if project.last_commit_date + = time_ago_in_words project.last_commit_date + ago + - else + No builds yet + %td + - if project.public + %i.fa.fa-globe + Public + - else + %i.fa.fa-lock + Private + %td + = project.commits.count + %td + = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do + %i.fa.fa-remove + Remove diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml new file mode 100644 index 00000000000..dc7b041473b --- /dev/null +++ b/app/views/ci/admin/projects/index.html.haml @@ -0,0 +1,15 @@ +%table.table + %thead + %tr + %th ID + %th Name + %th Last build + %th Access + %th Builds + %th + + - @projects.each do |project| + = render "ci/admin/projects/project", project: project + += paginate @projects + diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml new file mode 100644 index 00000000000..f049b4f4c4e --- /dev/null +++ b/app/views/ci/admin/runner_projects/index.html.haml @@ -0,0 +1,57 @@ +%p.lead + To register new runner visit #{link_to 'this page ', ci_runners_path} + +.row + .col-md-8 + %h5 Activated: + %table.table + %tr + %th Runner ID + %th Runner Description + %th Last build + %th Builds Stats + %th Registered + %th + + - @runner_projects.each do |runner_project| + - runner = runner_project.runner + - builds = runner.builds.where(project_id: @project.id) + %tr + %td + %span.badge.badge-info= runner.id + %td + = runner.display_name + %td + - last_build = builds.last + - if last_build + = link_to last_build.short_sha, [last_build.project, last_build] + - else + unknown + %td + %span.badge.badge-success + #{builds.success.count} + %span / + %span.badge.badge-important + #{builds.failed.count} + %td + #{time_ago_in_words(runner_project.created_at)} ago + %td + = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right' + .col-md-4 + %h5 Available + %table.table + %tr + %th ID + %th Token + %th + + - (Ci::Runner.all - @project.runners).each do |runner| + %tr + %td + = runner.id + %td + = runner.token + %td + = form_for [:ci, @project, @runner_project] do |f| + = f.hidden_field :runner_id, value: runner.id + = f.submit 'Add', class: 'btn btn-sm' diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/ci/admin/runners/_runner.html.haml new file mode 100644 index 00000000000..701782d26bb --- /dev/null +++ b/app/views/ci/admin/runners/_runner.html.haml @@ -0,0 +1,48 @@ +%tr{id: dom_id(runner)} + %td + - if runner.shared? + %span.label.label-success shared + - else + %span.label.label-info specific + - unless runner.active? + %span.label.label-danger paused + + %td + = link_to ci_admin_runner_path(runner) do + = runner.short_sha + %td + .runner-description + = runner.description + %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) + .runner-description-form.hide + = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f| + .form-group + = f.text_field :description, class: 'form-control' + = f.submit 'Save', class: 'btn' + %span (#{link_to 'cancel', '#', class: 'cancel'}) + %td + - if runner.shared? + \- + - else + = runner.projects.count(:all) + %td + #{runner.builds.count(:all)} + %td + - runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %td + - if runner.contacted_at + #{time_ago_in_words(runner.contacted_at)} ago + - else + Never + %td + .pull-right + = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm' + + - if runner.active? + = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + - else + = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm' + = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml new file mode 100644 index 00000000000..b9d6703ff41 --- /dev/null +++ b/app/views/ci/admin/runners/index.html.haml @@ -0,0 +1,52 @@ +%p.lead + %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. + %code #{GitlabCi::REGISTRATION_TOKEN} + +.bs-callout + %p + A 'runner' is a process which runs a build. + You can setup as many runners as you need. + %br + Runners can be placed on separate users, servers, and even on your local machine. + %br + + %div + %span Each runner can be in one of the following states: + %ul + %li + %span.label.label-success shared + \- run builds from all unassigned projects + %li + %span.label.label-info specific + \- run builds from assigned projects + %li + %span.label.label-danger paused + \- runner will not receive any new build + +.append-bottom-20.clearfix + .pull-left + = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token' + = submit_tag 'Search', class: 'btn' + + .pull-right.light + Runners with last contact less than a minute ago: #{@active_runners_cnt} + +%br + +%table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Projects + %th Builds + %th Tags + %th Last contact + %th + + - @runners.each do |runner| + = render "ci/admin/runners/runner", runner: runner += paginate @runners diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml new file mode 100644 index 00000000000..24e0ad3b070 --- /dev/null +++ b/app/views/ci/admin/runners/show.html.haml @@ -0,0 +1,118 @@ += content_for :title do + %h3.project-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific + + + +- if @runner.shared? + .bs-callout.bs-callout-success + %h4 This runner will process build from ALL UNASSIGNED projects + %p + If you want runners to build only specific projects, enable them in the table below. + Keep in mind that this is a one way transition. +- else + .bs-callout.bs-callout-info + %h4 This runner will process build only from ASSIGNED projects + %p You can't make this a shared runner. +%hr += form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, class: 'form-control' + .help-block You can setup builds to only use runners with specific tags + .form-actions + = f.submit 'Save', class: 'btn btn-save' + +.row + .col-md-6 + %h4 Restrict projects for this runner + - if @runner.projects.any? + %table.table + %thead + %tr + %th Assigned projects + %th + - @runner.runner_projects.each do |runner_project| + - project = runner_project.project + %tr.alert-info + %td + %strong + = project.name + %td + .pull-right + = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + + %table.table + %thead + %tr + %th Project + %th + .pull-right + = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner), + class: 'btn btn-sm assign-all-runner', + title: 'Assign runner to all projects', + method: :put + + %tr + %td + = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control' + = submit_tag 'Search', class: 'btn' + + %td + - @projects.each do |project| + %tr + %td + = project.name + %td + .pull-right + = form_for [:ci, :admin, project, project.runner_projects.new] do |f| + = f.hidden_field :runner_id, value: @runner.id + = f.submit 'Enable', class: 'btn btn-xs' + = paginate @projects + + .col-md-6 + %h4 Recent builds served by this runner + %table.builds.runner-builds + %thead + %tr + %th Status + %th Project + %th Commit + %th Finished at + + - @builds.each do |build| + %tr.build.alert{class: build_status_alert_class(build)} + %td.status + = build.status + + %td.status + = build.project.name + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong #{build.short_sha} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/ci/admin/runners/update.js.haml new file mode 100644 index 00000000000..2b7d3067e20 --- /dev/null +++ b/app/views/ci/admin/runners/update.js.haml @@ -0,0 +1,2 @@ +:plain + $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}") diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml new file mode 100644 index 00000000000..da306c9f020 --- /dev/null +++ b/app/views/ci/builds/_build.html.haml @@ -0,0 +1,45 @@ +%tr.build.alert{class: build_status_alert_class(build)} + %td.status + = build.status + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong Build ##{build.id} + + %td + = build.stage + + %td + = build.name + .pull-right + - if build.tags.any? + - build.tag_list.each do |tag| + %span.label.label-primary + = tag + - if build.trigger_request + %span.label.label-info triggered + - if build.allow_failure + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago + + - if build.project.coverage_enabled? + %td.coverage + - if build.coverage + #{build.coverage}% + + %td + - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if build.active? + = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do + %i.fa.fa-remove.cred + - elsif build.commands.present? + = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do + %i.fa.fa-repeat diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml new file mode 100644 index 00000000000..d1e955b5012 --- /dev/null +++ b/app/views/ci/builds/show.html.haml @@ -0,0 +1,167 @@ +#up-build-trace +- if @commit.matrix? + %ul.nav.nav-tabs.append-bottom-10 + - @commit.builds_without_retry_sorted.each do |build| + %li{class: ('active' if build == @build) } + = link_to ci_project_build_url(@project, build) do + %i{class: build_icon_css_class(build)} + %span + Build ##{build.id} + - if build.name + · + = build.name + + - unless @commit.builds_without_retry.include?(@build) + %li.active + %a + Build ##{@build.id} + · + %i.fa.fa-warning-sign + This build was retried. + +.row + .col-md-9 + .build-head.alert{class: build_status_alert_class(@build)} + %h4 + - if @build.commit.tag? + Build for tag + %code #{@build.ref} + - else + Build for commit + %code #{@build.short_sha} + from + + = link_to ci_project_path(@build.project, ref: @build.ref) do + %span.label.label-primary= "#{@build.ref}" + + - if @build.duration + .pull-right + %span + %i.fa.fa-time + #{duration_in_words(@build.finished_at, @build.started_at)} + + .clearfix + = @build.status + .pull-right + = @build.updated_at.stamp('19:00 Aug 27') + + + + .clearfix + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + .clearfix + .scroll-controls + = link_to '#up-build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + + %pre.trace#build-trace + %code.bash + = preserve do + = raw @build.trace_html + %div#down-build-trace + + .col-md-3 + - if @build.coverage + .build-widget + %h4.title + Test coverage + %h1 #{@build.coverage}% + + + .build-widget + %h4.title + Build + - if current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if @build.active? + = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger' + - elsif @build.commands.present? + = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post + + - if @build.duration + %p + %span.attr-name Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + %p + %span.attr-name Created: + #{time_ago_in_words(@build.created_at)} ago + - if @build.finished_at + %p + %span.attr-name Finished: + #{time_ago_in_words(@build.finished_at)} ago + %p + %span.attr-name Runner: + - if @build.runner && current_user && current_user.admin + \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)} + - elsif @build.runner + \##{@build.runner.id} + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.attr-name Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.attr-name Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .build-widget + %h4.title + Commit + .pull-right + %small #{build_commit_link @build} + + - if @build.commit.compare? + %p + %span.attr-name Compare: + #{build_compare_link @build} + %p + %span.attr-name Branch: + #{build_ref_link @build} + %p + %span.attr-name Author: + #{@build.commit.git_author_name} + %p + %span.attr-name Message: + #{@build.commit.git_commit_message} + + - if @build.tags.any? + .build-widget + %h4.title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag + + - if @builds.present? + .build-widget + %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %table.builds + - @builds.each_with_index do |build, i| + %tr.build.alert{class: build_status_alert_class(build)} + %td + = link_to ci_project_build_url(@project, build) do + %span ##{build.id} + %td + - if build.name + = build.name + %td.status= build.status + + + = paginate @builds + + +:javascript + new CiBuild("#{ci_project_build_url(@project, @build)}", "#{@build.status}") diff --git a/app/views/ci/charts/_build_times.haml b/app/views/ci/charts/_build_times.haml new file mode 100644 index 00000000000..c3c2f572414 --- /dev/null +++ b/app/views/ci/charts/_build_times.haml @@ -0,0 +1,21 @@ +%fieldset + %legend + Commit duration in minutes for last 30 commits + + %canvas#build_timesChart.padded{width: 800, height: 300} + +:javascript + var data = { + labels : #{@charts[:build_times].labels.to_json}, + datasets : [ + { + fillColor : "#4A3", + strokeColor : "rgba(151,187,205,1)", + pointColor : "rgba(151,187,205,1)", + pointStrokeColor : "#fff", + data : #{@charts[:build_times].build_times.to_json} + } + ] + } + var ctx = $("#build_timesChart").get(0).getContext("2d"); + new Chart(ctx).Line(data,{"scaleOverlay": true}); diff --git a/app/views/ci/charts/_builds.haml b/app/views/ci/charts/_builds.haml new file mode 100644 index 00000000000..1b0039fb834 --- /dev/null +++ b/app/views/ci/charts/_builds.haml @@ -0,0 +1,41 @@ +%fieldset + %legend + Builds chart for last week + (#{date_from_to(Date.today - 7.days, Date.today)}) + + %canvas#weekChart.padded{width: 800, height: 200} + +%fieldset + %legend + Builds chart for last month + (#{date_from_to(Date.today - 30.days, Date.today)}) + + %canvas#monthChart.padded{width: 800, height: 300} + +%fieldset + %legend Builds chart for last year + %canvas#yearChart.padded{width: 800, height: 400} + +- [:week, :month, :year].each do |scope| + :javascript + var data = { + labels : #{@charts[scope].labels.to_json}, + datasets : [ + { + fillColor : "rgba(220,220,220,0.5)", + strokeColor : "rgba(220,220,220,1)", + pointColor : "rgba(220,220,220,1)", + pointStrokeColor : "#EEE", + data : #{@charts[scope].total.to_json} + }, + { + fillColor : "#4A3", + strokeColor : "rgba(151,187,205,1)", + pointColor : "rgba(151,187,205,1)", + pointStrokeColor : "#fff", + data : #{@charts[scope].success.to_json} + } + ] + } + var ctx = $("##{scope}Chart").get(0).getContext("2d"); + new Chart(ctx).Line(data,{"scaleOverlay": true}); diff --git a/app/views/ci/charts/_overall.haml b/app/views/ci/charts/_overall.haml new file mode 100644 index 00000000000..f522f35a629 --- /dev/null +++ b/app/views/ci/charts/_overall.haml @@ -0,0 +1,21 @@ +%fieldset + %legend Overall + %p + Total: + %strong= pluralize @project.builds.count(:all), 'build' + %p + Successful: + %strong= pluralize @project.builds.success.count(:all), 'build' + %p + Failed: + %strong= pluralize @project.builds.failed.count(:all), 'build' + + %p + Success ratio: + %strong + #{success_ratio(@project.builds.success, @project.builds.failed)}% + + %p + Commits covered: + %strong + = @project.commits.count(:all) diff --git a/app/views/ci/charts/show.html.haml b/app/views/ci/charts/show.html.haml new file mode 100644 index 00000000000..0497f037721 --- /dev/null +++ b/app/views/ci/charts/show.html.haml @@ -0,0 +1,4 @@ +#charts.ci-charts + = render 'builds' + = render 'build_times' += render 'overall' diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml new file mode 100644 index 00000000000..c1b1988d147 --- /dev/null +++ b/app/views/ci/commits/_commit.html.haml @@ -0,0 +1,32 @@ +%tr.build.alert{class: commit_status_alert_class(commit)} + %td.status + = commit.status + - if commit.running? + · + = commit.stage + + + %td.build-link + = link_to ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) do + %strong #{commit.short_sha} + + %td.build-message + %span= truncate_first_line(commit.git_commit_message) + + %td.build-branch + - unless @ref + %span + = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref) + + %td.duration + - if commit.duration > 0 + #{time_interval_in_words commit.duration} + + %td.timestamp + - if commit.finished_at + %span #{time_ago_in_words commit.finished_at} ago + + - if commit.project.coverage_enabled? + %td.coverage + - if commit.coverage + #{commit.coverage}% diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml new file mode 100644 index 00000000000..1aeb557314a --- /dev/null +++ b/app/views/ci/commits/show.html.haml @@ -0,0 +1,88 @@ +.commit-info + %pre.commit-message + #{@commit.git_commit_message} + + .row + .col-sm-6 + - if @commit.compare? + %p + %span.attr-name Compare: + #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)} + - else + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} + + %p + %span.attr-name Branch: + #{gitlab_ref_link(@project, @commit.ref)} + .col-sm-6 + %p + %span.attr-name Author: + #{@commit.git_author_name} (#{@commit.git_author_email}) + - if @commit.created_at + %p + %span.attr-name Created at: + #{@commit.created_at.to_s(:short)} + +- if current_user && can?(current_user, :manage_builds, gl_project) + .pull-right + - if @commit.builds.running_or_pending.any? + = link_to "Cancel", cancel_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger' + + +- if @commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @commit.yaml_errors.split(",").each do |error| + %li= error + +- unless @commit.push_data[:ci_yaml_file] + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +%h3 Status + +.build.alert{class: commit_status_alert_class(@commit)} + .status + = @commit.status.titleize + +%h3 + Builds + - if @commit.duration > 0 + %small.pull-right + %i.fa.fa-time + #{time_interval_in_words @commit.duration} + +%table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.builds_without_retry_sorted, controls: true + +- if @commit.retried_builds.any? + %h3 + Retried builds + + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.retried_builds diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml new file mode 100644 index 00000000000..2788112c835 --- /dev/null +++ b/app/views/ci/errors/show.haml @@ -0,0 +1,2 @@ +%h3.error Error += @error diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml new file mode 100644 index 00000000000..779f49b3d3a --- /dev/null +++ b/app/views/ci/events/index.html.haml @@ -0,0 +1,19 @@ +%h3.page-title Events + +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events
\ No newline at end of file diff --git a/app/views/ci/helps/oauth2.html.haml b/app/views/ci/helps/oauth2.html.haml new file mode 100644 index 00000000000..2031b7340d4 --- /dev/null +++ b/app/views/ci/helps/oauth2.html.haml @@ -0,0 +1,20 @@ +.welcome-block + %h1 + Welcome to GitLab CI + %p + GitLab CI integrates with your GitLab installation and runs tests for your projects. + + %h3 You need only 2 steps to set it up + + %ol + %li + In the GitLab admin area under OAuth applications create a new entry. The redirect url should be + %code= callback_ci_user_sessions_url + %li + Update the GitLab CI config with the application id and the application secret from GitLab. + %li + Restart your GitLab CI instance + %li + Refresh this page when GitLab CI has started again + + diff --git a/app/views/ci/helps/show.html.haml b/app/views/ci/helps/show.html.haml new file mode 100644 index 00000000000..9b32d529c60 --- /dev/null +++ b/app/views/ci/helps/show.html.haml @@ -0,0 +1,40 @@ +.jumbotron + %h2 + GitLab CI + %span= GitlabCi::VERSION + %small= GitlabCi::REVISION + %p + GitLab CI integrates with your GitLab installation and run tests for your projects. + %br + Login with your GitLab account, add a project with one click and enjoy running your tests. + %br + Read more about GitLab CI at #{link_to "about.gitlab.com/gitlab-ci", "https://about.gitlab.com/gitlab-ci/", target: "_blank"}. + + +.bs-callout.bs-callout-success + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/api' do + %i.fa.fa-cogs + API + %p Explore how you can access GitLab CI via the API. + +.bs-callout.bs-callout-info + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/tree/master/doc/examples' do + %i.fa.fa-info-sign + Build script examples + %p This includes the build script we use to test GitLab CE. + +.bs-callout.bs-callout-danger + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/issues' do + %i.fa.fa-bug + Issue tracker + %p Reports about recent bugs and problems.. + +.bs-callout.bs-callout-warning + %h4 + = link_to 'http://feedback.gitlab.com/forums/176466-general/category/64310-gitlab-ci' do + %i.fa.fa-thumbs-up + Feedback forum + %p Suggest improvements or new features for GitLab CI. diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml new file mode 100644 index 00000000000..e2179e60f3e --- /dev/null +++ b/app/views/ci/lints/_create.html.haml @@ -0,0 +1,39 @@ +- if @status + %p + %b Status: + syntax is correct + %i.fa.fa-ok.correct-syntax + + %table.table.table-bordered + %thead + %tr + %th Parameter + %th Value + %tbody + - @stages.each do |stage| + - @builds.select { |build| build[:stage] == stage }.each do |build| + %tr + %td #{stage.capitalize} Job - #{build[:name]} + %td + %pre + = simple_format build[:script] + + %br + %b Tag list: + = build[:tags] + %br + %b Refs only: + = build[:only] && build[:only].join(", ") + %br + %b Refs except: + = build[:except] && build[:except].join(", ") + +-else + %p + %b Status: + syntax is incorrect + %i.fa.fa-remove.incorrect-syntax + %b Error: + = @error + + diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml new file mode 100644 index 00000000000..a96c0b11b6e --- /dev/null +++ b/app/views/ci/lints/create.js.haml @@ -0,0 +1,2 @@ +:plain + $(".results").html("#{escape_javascript(render "create")}")
\ No newline at end of file diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml new file mode 100644 index 00000000000..a9b954771c5 --- /dev/null +++ b/app/views/ci/lints/show.html.haml @@ -0,0 +1,25 @@ +%h2 Check your .gitlab-ci.yml +%hr + += form_tag ci_lint_path, method: :post, remote: true do + .control-group + = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label' + .controls + = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + + .control-group.clearfix + .controls.pull-left.prepend-top-10 + = submit_tag "Validate", class: 'btn btn-success submit-yml' + + +%p.text-center.loading + %i.fa.fa-refresh.fa-spin + +.results.prepend-top-20 + +:coffeescript + $(".loading").hide() + $('form').bind 'ajax:beforeSend', -> + $(".loading").show() + $('form').bind 'ajax:complete', -> + $(".loading").hide() diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml new file mode 100644 index 00000000000..d818e8b6756 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -0,0 +1,19 @@ +- content_for :header do + %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build failed) +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb new file mode 100644 index 00000000000..1add215a1c8 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -0,0 +1,9 @@ +Build failed for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml new file mode 100644 index 00000000000..a20dcaee24e --- /dev/null +++ b/app/views/ci/notify/build_success_email.html.haml @@ -0,0 +1,20 @@ +- content_for :header do + %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build successful) + +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb new file mode 100644 index 00000000000..7ebd17e7270 --- /dev/null +++ b/app/views/ci/notify/build_success_email.text.erb @@ -0,0 +1,9 @@ +Build successful for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/projects/_form.html.haml b/app/views/ci/projects/_form.html.haml new file mode 100644 index 00000000000..d50e1a83b06 --- /dev/null +++ b/app/views/ci/projects/_form.html.haml @@ -0,0 +1,101 @@ +.bs-callout.help-callout + %p + If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} + %p + Edit your + #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)} + += nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f| + - if @project.errors.any? + #error_explanation + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + .alert.alert-error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + %fieldset + %legend Build settings + .form-group + = label_tag nil, class: 'control-label' do + Get code + .col-sm-10 + %p Get recent application code using the following command: + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'false' + %strong git clone + .light Slower but makes sure you have a clean dir before every build + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'true' + %strong git fetch + .light Faster + .form-group + = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :timeout_in_minutes, class: 'form-control', min: '0' + .light per build in minutes + + + %fieldset + %legend Build Schedule + .form-group + = f.label :always_build, 'Schedule build', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :always_build do + = f.check_box :always_build + %span.light Repeat last build after X hours if no builds + .form-group + = f.label :polling_interval, "Build interval", class: 'control-label' + .col-sm-10 + = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control' + .light In hours + + %fieldset + %legend Project settings + .form-group + = f.label :default_ref, "Make tabs for the following branches", class: 'control-label' + .col-sm-10 + = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable' + .light You will be able to filter builds by the following branches + .form-group + = f.label :public, 'Public mode', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :public do + = f.check_box :public + %span.light Anyone can see project and builds + .form-group + = f.label :coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%$ + + + + %fieldset + %legend Advanced settings + .form-group + = f.label :token, "CI token", class: 'control-label' + .col-sm-10 + = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' + + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' + = link_to 'Cancel', projects_path, class: 'btn' + - unless @project.new_record? + = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/ci/projects/_gl_projects.html.haml b/app/views/ci/projects/_gl_projects.html.haml new file mode 100644 index 00000000000..7bd30b37caf --- /dev/null +++ b/app/views/ci/projects/_gl_projects.html.haml @@ -0,0 +1,15 @@ +- @gl_projects.sort_by(&:name_with_namespace).each do |project| + %tr.light + %td + = project.name_with_namespace + %td + %small Not added to CI + %td + %td + - if Ci::Project.already_added?(project) + %strong.cgreen + Added + - else + = form_tag ci_projects_path do + = hidden_field_tag :project, project.to_json(methods: [:name_with_namespace, :path_with_namespace, :ssh_url_to_repo]) + = submit_tag 'Add project to CI', class: 'btn btn-default btn-sm' diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml new file mode 100644 index 00000000000..1888e1bde93 --- /dev/null +++ b/app/views/ci/projects/_info.html.haml @@ -0,0 +1,2 @@ +- if no_runners_for_project?(@project) + = render 'no_runners' diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/ci/projects/_no_runners.html.haml new file mode 100644 index 00000000000..c0a296fb17d --- /dev/null +++ b/app/views/ci/projects/_no_runners.html.haml @@ -0,0 +1,8 @@ +.alert.alert-danger + %p + There are NO runners to build this project. + %br + You can add Specific runner for this project on Runners page + + - if current_user.is_admin + or add Shared runner for whole application in admin are. diff --git a/app/views/ci/projects/_project.html.haml b/app/views/ci/projects/_project.html.haml new file mode 100644 index 00000000000..b3ad47ce432 --- /dev/null +++ b/app/views/ci/projects/_project.html.haml @@ -0,0 +1,22 @@ +- last_commit = project.last_commit +%tr.alert{class: commit_status_alert_class(last_commit) } + %td + = link_to [:ci, project] do + %strong= project.name + %td + - if last_commit + #{last_commit.status} (#{commit_link(last_commit)}) + - if project.last_commit_date + = time_ago_in_words project.last_commit_date + ago + - else + No builds yet + %td + - if project.public + %i.fa.fa-globe + Public + - else + %i.fa.fa-lock + Private + %td + = project.commits.count diff --git a/app/views/ci/projects/_public.html.haml b/app/views/ci/projects/_public.html.haml new file mode 100644 index 00000000000..c2157ab741a --- /dev/null +++ b/app/views/ci/projects/_public.html.haml @@ -0,0 +1,21 @@ += content_for :title do + %h3.project-title + Public projects + +.bs-callout + = link_to new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath)) do + %strong Login with GitLab + to see your private projects + +- if @projects.present? + .projects + %table.table + %tr + %th Name + %th Last commit + %th Access + %th Commits + = render @projects + = paginate @projects +- else + %h4 No public projects yet diff --git a/app/views/ci/projects/_search.html.haml b/app/views/ci/projects/_search.html.haml new file mode 100644 index 00000000000..6d84b25a6af --- /dev/null +++ b/app/views/ci/projects/_search.html.haml @@ -0,0 +1,17 @@ +.search + = form_tag "#", method: :get, class: 'ci-search-form' do |f| + .input-group + = search_field_tag "search", params[:search], placeholder: "Search", class: "search-input form-control" + .input-group-addon + %i.fa.fa-search + + +:coffeescript + $('.ci-search-form').submit -> + NProgress.start() + query = $('.ci-search-form .search-input').val() + $.get '#{gitlab_ci_projects_path}', { search: query }, (data) -> + $(".projects").html data.html + NProgress.done() + CiPager.init "#{gitlab_ci_projects_path}" + "?search=" + query, #{Ci::ProjectsController::PROJECTS_BATCH}, false + false diff --git a/app/views/ci/projects/edit.html.haml b/app/views/ci/projects/edit.html.haml new file mode 100644 index 00000000000..298007a6565 --- /dev/null +++ b/app/views/ci/projects/edit.html.haml @@ -0,0 +1,21 @@ +- if @project.generated_yaml_config + %p.alert.alert-danger + CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_project_path(@project)} + or + %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview + yaml file which is based on your old jobs. + Put this file to the root of your project and name it .gitlab-ci.yml + += render 'form' + +- if @project.generated_yaml_config + #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} + .modal-dialog + .modal-content + .modal-header + %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × + %h4.modal-title Content of .gitlab-ci.yml + .modal-body + = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control" + .modal-footer + %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/app/views/ci/projects/gitlab.html.haml b/app/views/ci/projects/gitlab.html.haml new file mode 100644 index 00000000000..f57dfcb0790 --- /dev/null +++ b/app/views/ci/projects/gitlab.html.haml @@ -0,0 +1,27 @@ +- if @offset == 0 + .clearfix.light + .pull-left.fetch-status + - if params[:search].present? + by keyword: "#{params[:search]}", + #{@total_count} projects, #{@projects.size} of them added to CI + %br + + %table.table.projects-table.content-list + %thead + %tr + %th Project Name + %th Last commit + %th Access + %th Commits + + = render @projects + + = render "gl_projects" + + %p.text-center.hide.loading + %i.fa.fa-refresh.fa-spin + +- else + = render @projects + + = render "gl_projects" diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml new file mode 100644 index 00000000000..085a70811ae --- /dev/null +++ b/app/views/ci/projects/index.html.haml @@ -0,0 +1,13 @@ +- if current_user + .gray-content-block.top-block + = render "search" + .projects.prepend-top-default + %p.fetch-status.light + %i.fa.fa-refresh.fa-spin + :coffeescript + $.get '#{gitlab_ci_projects_path}', (data) -> + $(".projects").html data.html + CiPager.init "#{gitlab_ci_projects_path}", #{Ci::ProjectsController::PROJECTS_BATCH}, false + +- else + = render 'public' diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml new file mode 100644 index 00000000000..6443378af99 --- /dev/null +++ b/app/views/ci/projects/show.html.haml @@ -0,0 +1,60 @@ += render 'ci/shared/guide' unless @project.setup_finished? + +- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners? + .alert.alert-danger + Builds for this project wont be served unless you configure runners on + = link_to "Runners page", ci_project_runners_path(@project) + +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ref_tab_class} + = link_to 'All commits', ci_project_path(@project) + - @project.tracked_refs.each do |ref| + %li{class: ref_tab_class(ref)} + = link_to ref, ci_project_path(@project, ref: ref) + + - if @ref && !@project.tracked_refs.include?(@ref) + %li{class: 'active'} + = link_to @ref, ci_project_path(@project, ref: @ref) + + %li.pull-right + = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' ) + +- if @ref + %p + Paste build status image for #{@ref} with next link + = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do + Status Badge + .badge-codes-block.bs-callout.bs-callout-info.hide + %p + Status badge for + %span.label.label-info #{@ref} + branch + %div + %label Markdown: + = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control' + %label Html: + = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control' + + + + +%table.table.builds + %thead + %tr + %th Status + %th Commit + %th Message + %th Branch + %th Total duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + + = render @commits + += paginate @commits + +- if @commits.empty? + .bs-callout + %h4 No commits yet + diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/ci/runners/_runner.html.haml new file mode 100644 index 00000000000..ef8622e2807 --- /dev/null +++ b/app/views/ci/runners/_runner.html.haml @@ -0,0 +1,35 @@ +%li.runner{id: dom_id(runner)} + %h4 + = runner_status_icon(runner) + %span.monospace + - if @runners.include?(runner) + = link_to runner.short_sha, ci_project_runner_path(@project, runner) + %small + =link_to edit_ci_project_runner_path(@project, runner) do + %i.fa.fa-edit.btn + - else + = runner.short_sha + + .pull-right + - if @runners.include?(runner) + - if runner.belongs_to_one_project? + = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - else + - runner_project = @project.runner_projects.find_by(runner_id: runner) + = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - elsif runner.specific? + = form_for [:ci, @project, @project.runner_projects.new] do |f| + = f.hidden_field :runner_id, value: runner.id + = f.submit 'Enable for this project', class: 'btn btn-sm' + .pull-right + %small.light + \##{runner.id} + - if runner.description.present? + %p.runner-description + = runner.description + - if runner.tag_list.present? + %p + - runner.tag_list.each do |tag| + %span.label.label-primary + = tag + diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/ci/runners/_shared_runners.html.haml new file mode 100644 index 00000000000..944b3fd930d --- /dev/null +++ b/app/views/ci/runners/_shared_runners.html.haml @@ -0,0 +1,23 @@ +%h3 Shared runners + +.bs-callout.bs-callout-warning + GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. + %hr + - if @project.shared_runners_enabled + = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do + Disable shared runners + - else + = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do + Enable shared runners + for this project + +- if @shared_runners_count.zero? + This application has no shared runners yet. + Please use specific runners or ask administrator to create one +- else + %h4.underlined-title Available shared runners - #{@shared_runners_count} + %ul.bordered-list.available-shared-runners + = render @shared_runners.first(10) + - if @shared_runners_count > 10 + .light + and #{@shared_runners_count - 10} more... diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/ci/runners/_specific_runners.html.haml new file mode 100644 index 00000000000..0604e7a46c5 --- /dev/null +++ b/app/views/ci/runners/_specific_runners.html.haml @@ -0,0 +1,29 @@ +%h3 Specific runners + +.bs-callout.help-callout + %h4 How to setup a new project specific runner + + %ol + %li + Install GitLab Runner software. + Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + %li + Specify following URL during runner setup: + %code #{ci_root_url(only_path: false)} + %li + Use the following registration token during setup: + %code #{@project.token} + %li + Start runner! + + +- if @runners.any? + %h4.underlined-title Runners activated for this project + %ul.bordered-list.activated-specific-runners + = render @runners + +- if @specific_runners.any? + %h4.underlined-title Available specific runners + %ul.bordered-list.available-specific-runners + = render @specific_runners + = paginate @specific_runners diff --git a/app/views/ci/runners/edit.html.haml b/app/views/ci/runners/edit.html.haml new file mode 100644 index 00000000000..81c8e58ae2b --- /dev/null +++ b/app/views/ci/runners/edit.html.haml @@ -0,0 +1,27 @@ +%h4 Runner ##{@runner.id} +%hr += form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f| + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/ci/runners/index.html.haml b/app/views/ci/runners/index.html.haml new file mode 100644 index 00000000000..529fb9c296d --- /dev/null +++ b/app/views/ci/runners/index.html.haml @@ -0,0 +1,25 @@ +.light + %p + A 'runner' is a process which runs a build. + You can setup as many runners as you need. + %br + Runners can be placed on separate users, servers, and even on your local machine. + + %p Each runner can be in one of the following states: + %div + %ul + %li + %span.label.label-success active + \- runner is active and can process any new build + %li + %span.label.label-danger paused + \- runner is paused and will not receive any new build + +%hr + +%p.lead To start serving your builds you can either add specific runners to your project or use shared runners +.row + .col-sm-6 + = render 'specific_runners' + .col-sm-6 + = render 'shared_runners' diff --git a/app/views/ci/runners/show.html.haml b/app/views/ci/runners/show.html.haml new file mode 100644 index 00000000000..ffec495f85a --- /dev/null +++ b/app/views/ci/runners/show.html.haml @@ -0,0 +1,64 @@ += content_for :title do + %h3.project-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific + +%table.table + %thead + %tr + %th Property Name + %th Value + %tr + %td + Tags + %td + - @runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %tr + %td + Name + %td + = @runner.name + %tr + %td + Version + %td + = @runner.version + %tr + %td + Revision + %td + = @runner.revision + %tr + %td + Platform + %td + = @runner.platform + %tr + %td + Architecture + %td + = @runner.architecture + %tr + %td + Description + %td + = @runner.description + %tr + %td + Last contact + %td + - if @runner.contacted_at + #{time_ago_in_words(@runner.contacted_at)} ago + - else + Never + + + diff --git a/app/views/ci/services/_form.html.haml b/app/views/ci/services/_form.html.haml new file mode 100644 index 00000000000..9110aaa0528 --- /dev/null +++ b/app/views/ci/services/_form.html.haml @@ -0,0 +1,57 @@ +%h3.page-title + = @service.title + = boolean_to_icon @service.activated? + +%p= @service.description + +.back-link + = link_to ci_project_services_path(@project) do + ← to services + +%hr + += form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| + - if @service.errors.any? + .alert.alert-danger + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + + - if @service.help.present? + .bs-callout + = @service.help + + .form-group + = f.label :active, "Active", class: "control-label" + .col-sm-10 + = f.check_box :active + + - @service.fields.each do |field| + - name = field[:name] + - label = field[:label] || name + - value = @service.send(name) + - type = field[:type] + - placeholder = field[:placeholder] + - choices = field[:choices] + - default_choice = field[:default_choice] + - help = field[:help] + + .form-group + = f.label label, class: "control-label" + .col-sm-10 + - if type == 'text' + = f.text_field name, class: "form-control", placeholder: placeholder + - elsif type == 'textarea' + = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder + - elsif type == 'checkbox' + = f.check_box name + - elsif type == 'select' + = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - if help + .light #{help} + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + + - if @service.valid? && @service.activated? && @service.can_test? + = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn' diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml new file mode 100644 index 00000000000..bcc5832792f --- /dev/null +++ b/app/views/ci/services/edit.html.haml @@ -0,0 +1 @@ += render 'form' diff --git a/app/views/ci/services/index.html.haml b/app/views/ci/services/index.html.haml new file mode 100644 index 00000000000..37e5723b541 --- /dev/null +++ b/app/views/ci/services/index.html.haml @@ -0,0 +1,22 @@ +%h3.page-title Project services +%p.light Project services allow you to integrate GitLab CI with other applications + +%table.table + %thead + %tr + %th + %th Service + %th Desription + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_ci_project_service_path(@project, service.to_param) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml new file mode 100644 index 00000000000..8a42f29b77c --- /dev/null +++ b/app/views/ci/shared/_guide.html.haml @@ -0,0 +1,15 @@ +.bs-callout.help-callout + %h4 How to setup CI for this project + + %ol + %li + Add at least one runner to the project. + Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions. + %li + Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + %li + Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank} + and press the "Test settings" button. + %li + Return to this page and refresh it, it should show a new build. diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml new file mode 100644 index 00000000000..f56c37d9b37 --- /dev/null +++ b/app/views/ci/shared/_no_runners.html.haml @@ -0,0 +1,7 @@ +.alert.alert-danger + %p + Now you need Runners to process your builds. + %span + Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + + diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/ci/triggers/_trigger.html.haml new file mode 100644 index 00000000000..addfbfcb0d4 --- /dev/null +++ b/app/views/ci/triggers/_trigger.html.haml @@ -0,0 +1,14 @@ +%tr + %td + .clearfix + %span.monospace= trigger.token + + %td + - if trigger.last_trigger_request + #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago + - else + Never + + %td + .pull-right + = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped" diff --git a/app/views/ci/triggers/index.html.haml b/app/views/ci/triggers/index.html.haml new file mode 100644 index 00000000000..44374a1a4d5 --- /dev/null +++ b/app/views/ci/triggers/index.html.haml @@ -0,0 +1,67 @@ +%h3.page-title + Triggers + +%p.light + Triggers can be used to force a rebuild of a specific branch or tag with an API call. + +%hr.clearfix + +-if @triggers.any? + %table.table + %thead + %th Token + %th Last used + %th + = render @triggers +- else + %h4 No triggers + += form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f| + .clearfix + = f.submit "Add Trigger", class: 'btn btn-success pull-right' + +%hr.clearfix + +-if @triggers.any? + %h3 + Use CURL + + %p.light + Copy the token above and set your branch or tag name. This is the reference that will be rebuild. + + + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + #{ci_build_trigger_url(@project.id, 'REF_NAME')} + %h3 + Use .gitlab-ci.yml + + %p.light + Copy the snippet to + %i .gitlab-ci.yml + of dependent project. + At the end of your build it will trigger this project to rebuilt. + + %pre + :plain + trigger: + type: deploy + script: + - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}" + %h3 + Pass build variables + + %p.light + Add + %strong variables[VARIABLE]=VALUE + to API request. + The value of variable could then be used to distinguish triggered build from normal one. + + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + -F "variables[RUN_NIGHTLY_BUILD]=true" \ + #{ci_build_trigger_url(@project.id, 'REF_NAME')} diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml new file mode 100644 index 00000000000..308b217ea78 --- /dev/null +++ b/app/views/ci/user_sessions/new.html.haml @@ -0,0 +1,8 @@ +.login-block + %h2 Login using GitLab account + %p.light + Make sure you have account on GitLab server + = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink + %hr + = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) + diff --git a/app/views/ci/variables/show.html.haml b/app/views/ci/variables/show.html.haml new file mode 100644 index 00000000000..ebf68341e08 --- /dev/null +++ b/app/views/ci/variables/show.html.haml @@ -0,0 +1,39 @@ +%h3.page-title + Secret Variables + +%p.light + These variables will be set to environment by the runner and will be hidden in the build log. + %br + So you can use them for passwords, secret keys or whatever you want. + +%hr + + += nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| + - if @project.errors.any? + #error_explanation + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + .alert.alert-error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + = f.fields_for :variables do |variable_form| + .form-group + = variable_form.label :key, 'Key', class: 'control-label' + .col-sm-10 + = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" + + .form-group + = variable_form.label :value, 'Value', class: 'control-label' + .col-sm-10 + = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" + + = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10' + %hr + %p + .clearfix + = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' + + .form-actions + = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/ci/web_hooks/index.html.haml new file mode 100644 index 00000000000..78e8203b25e --- /dev/null +++ b/app/views/ci/web_hooks/index.html.haml @@ -0,0 +1,92 @@ +%h3.page-title + Web hooks + +%p.light + Web Hooks can be used for binding events when build completed. + +%hr.clearfix + += form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f| + -if @web_hook.errors.any? + .alert.alert-danger + - @web_hook.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :url, "URL", class: 'control-label' + .col-sm-10 + = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' + .form-actions + = f.submit "Add Web Hook", class: "btn btn-create" + +-if @web_hooks.any? + %h4 Activated web hooks (#{@web_hooks.count}) + %table.table + - @web_hooks.each do |hook| + %tr + %td + .clearfix + %span.monospace= hook.url + %td + .pull-right + - if @project.commits.any? + = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + +%h4 Web Hook data example + +:erb + <pre> + <code> + { + "build_id": 2, + "build_name":"rspec_linux" + "build_status": "failed", + "build_started_at": "2014-05-05T18:01:02.563Z", + "build_finished_at": "2014-05-05T18:01:07.611Z", + "project_id": 1, + "project_name": "Brightbox \/ Brightbox Cli", + "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli", + "ref": "master", + "sha": "a26cf5de9ed9827746d4970872376b10d9325f40", + "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", + "push_data": { + "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", + "after": "a26cf5de9ed9827746d4970872376b10d9325f40", + "ref": "refs\/heads\/master", + "user_id": 1, + "user_name": "Administrator", + "project_id": 5, + "repository": { + "name": "Brightbox Cli", + "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git", + "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.", + "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli" + }, + "commits": [ + { + "id": "a26cf5de9ed9827746d4970872376b10d9325f40", + "message": "Release v1.2.2", + "timestamp": "2014-04-22T16:46:42+03:00", + "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40", + "author": { + "name": "Paul Thornthwaite", + "email": "tokengeek@gmail.com" + } + }, + { + "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c", + "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.", + "timestamp": "2014-04-11T18:17:26+03:00", + "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c", + "author": { + "name": "Paul Thornthwaite", + "email": "tokengeek@gmail.com" + } + } + ], + "total_commits_count": 2, + "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n" + } + } + </code> + </pre> diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml new file mode 100644 index 00000000000..24c68a6dbf5 --- /dev/null +++ b/app/views/layouts/ci/_info.html.haml @@ -0,0 +1,2 @@ +- if current_user && current_user.is_admin? && Ci::Runner.count.zero? + = render 'ci/shared/no_runners' diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml new file mode 100644 index 00000000000..c987ab876a3 --- /dev/null +++ b/app/views/layouts/ci/_nav_admin.html.haml @@ -0,0 +1,33 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to ci_root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to Dashboard + + %li.separate-item + = nav_link path: 'projects#index' do + = link_to ci_admin_projects_path do + %i.fa.fa-list-alt + Projects + = nav_link path: 'events#index' do + = link_to ci_admin_events_path do + %i.fa.fa-book + Events + = nav_link path: ['runners#index', 'runners#show'] do + = link_to ci_admin_runners_path do + %i.fa.fa-cog + Runners + %small.pull-right + = Ci::Runner.count(:all) + = nav_link path: 'builds#index' do + = link_to ci_admin_builds_path do + %i.fa.fa-link + Builds + %small.pull-right + = Ci::Build.count(:all) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = link_to ci_admin_application_settings_path do + %i.fa.fa-cogs + %span + Settings diff --git a/app/views/layouts/ci/_nav_build.html.haml b/app/views/layouts/ci/_nav_build.html.haml new file mode 100644 index 00000000000..732882726e7 --- /dev/null +++ b/app/views/layouts/ci/_nav_build.html.haml @@ -0,0 +1,3 @@ += render 'layouts/ci/nav_project', + back_title: 'Back to project commit', + back_url: ci_project_ref_commits_path(@project, @commit.ref, @commit.sha) diff --git a/app/views/layouts/ci/_nav_commit.haml b/app/views/layouts/ci/_nav_commit.haml new file mode 100644 index 00000000000..19c526678d0 --- /dev/null +++ b/app/views/layouts/ci/_nav_commit.haml @@ -0,0 +1,3 @@ += render 'layouts/ci/nav_project', + back_title: 'Back to project commits', + back_url: ci_project_path(@project) diff --git a/app/views/layouts/ci/_nav_dashboard.html.haml b/app/views/layouts/ci/_nav_dashboard.html.haml new file mode 100644 index 00000000000..fcff405d19d --- /dev/null +++ b/app/views/layouts/ci/_nav_dashboard.html.haml @@ -0,0 +1,24 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span + Back to GitLab + %li.separate-item + = nav_link path: 'projects#index' do + = link_to ci_root_path do + %i.fa.fa-home + %span + Projects + - if current_user && current_user.is_admin? + %li + = link_to ci_admin_projects_path do + %i.fa.fa-cogs + %span + Admin + %li + = link_to ci_help_path do + %i.fa.fa-info + %span + Help + diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml new file mode 100644 index 00000000000..10b87e3a2b1 --- /dev/null +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -0,0 +1,53 @@ +%ul.nav.nav-sidebar + = nav_link do + = link_to defined?(back_url) ? back_url : ci_root_path, title: defined?(back_title) ? back_title : 'Back to Dashboard', data: {placement: 'right'}, class: 'back-link' do + = icon('caret-square-o-left fw') + %span= defined?(back_title) ? back_title : 'Back to Dashboard' + %li.separate-item + = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do + = link_to ci_project_path(@project) do + %i.fa.fa-list-alt + %span + Commits + %small.pull-right= @project.commits.count + = nav_link path: 'charts#show' do + = link_to ci_project_charts_path(@project) do + %i.fa.fa-bar-chart + %span + Charts + = nav_link path: ['runners#index', 'runners#show', 'runners#edit'] do + = link_to ci_project_runners_path(@project) do + %i.fa.fa-cog + %span + Runners + = nav_link path: 'variables#show' do + = link_to ci_project_variables_path(@project) do + %i.fa.fa-code + %span + Variables + = nav_link path: 'web_hooks#index' do + = link_to ci_project_web_hooks_path(@project) do + %i.fa.fa-link + %span + Web Hooks + = nav_link path: 'triggers#index' do + = link_to ci_project_triggers_path(@project) do + %i.fa.fa-retweet + %span + Triggers + = nav_link path: ['services#index', 'services#edit'] do + = link_to ci_project_services_path(@project) do + %i.fa.fa-share + %span + Services + = nav_link path: 'events#index' do + = link_to ci_project_events_path(@project) do + %i.fa.fa-book + %span + Events + %li.separate-item + = nav_link path: 'projects#edit' do + = link_to edit_ci_project_path(@project) do + %i.fa.fa-cogs + %span + Settings diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml new file mode 100644 index 00000000000..c598f63c4c8 --- /dev/null +++ b/app/views/layouts/ci/_page.html.haml @@ -0,0 +1,26 @@ +.page-with-sidebar{ class: nav_sidebar_class } + = render "layouts/broadcast" + .sidebar-wrapper.nicescroll + .header-logo + = link_to ci_root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do + = brand_header_logo + .gitlab-text-container + %h3 GitLab CI + - if defined?(sidebar) && sidebar + = render "layouts/ci/#{sidebar}" + - elsif current_user + = render 'layouts/nav/dashboard' + .collapse-nav + = render partial: 'layouts/collapse_button' + - if current_user + = link_to current_user, class: 'sidebar-user' do + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36' + .username + = current_user.username + .content-wrapper + = render "layouts/flash" + = render 'layouts/ci/info' + %div{ class: container_class } + .content + .clearfix + = yield diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml new file mode 100644 index 00000000000..c8cb185d28c --- /dev/null +++ b/app/views/layouts/ci/admin.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title = "Admin area" + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_admin' diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml new file mode 100644 index 00000000000..b9f871d5447 --- /dev/null +++ b/app/views/layouts/ci/application.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title = "CI Projects" + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_dashboard' diff --git a/app/views/layouts/ci/build.html.haml b/app/views/layouts/ci/build.html.haml new file mode 100644 index 00000000000..d404ecb894a --- /dev/null +++ b/app/views/layouts/ci/build.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title ci_commit_title(@commit) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_build' diff --git a/app/views/layouts/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml new file mode 100644 index 00000000000..5727f1b8e3e --- /dev/null +++ b/app/views/layouts/ci/commit.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title ci_commit_title(@commit) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_commit' diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml new file mode 100644 index 00000000000..270b206df5e --- /dev/null +++ b/app/views/layouts/ci/notify.html.haml @@ -0,0 +1,19 @@ +%html{lang: "en"} + %head + %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %title + GitLab CI + + %body + = yield :header + + %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"} + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + = yield + %br + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + %p{style: "font-size:small;color:#777"} + - if @project + You're receiving this notification because you are the one who triggered a build on the #{@project.name} project. diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml new file mode 100644 index 00000000000..15478c3f5bc --- /dev/null +++ b/app/views/layouts/ci/project.html.haml @@ -0,0 +1,11 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/head' + %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page} + - header_title @project.name, ci_project_path(@project) + - if current_user + = render "layouts/header/default", title: header_title + - else + = render "layouts/header/public", title: header_title + + = render 'layouts/ci/page', sidebar: 'nav_project' diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb new file mode 100644 index 00000000000..ebb43570e2a --- /dev/null +++ b/app/workers/ci/hip_chat_notifier_worker.rb @@ -0,0 +1,19 @@ +module Ci + class HipChatNotifierWorker + include Sidekiq::Worker + + def perform(message, options={}) + room = options.delete('room') + token = options.delete('token') + server = options.delete('server') + name = options.delete('service_name') + client_opts = { + api_version: 'v2', + server_url: server + } + + client = HipChat::Client.new(token, client_opts) + client[room].send(name, message, options.symbolize_keys) + end + end +end diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb new file mode 100644 index 00000000000..3bbb9b4bec7 --- /dev/null +++ b/app/workers/ci/slack_notifier_worker.rb @@ -0,0 +1,10 @@ +module Ci + class SlackNotifierWorker + include Sidekiq::Worker + + def perform(webhook_url, message, options={}) + notifier = Slack::Notifier.new(webhook_url) + notifier.ping(message, options) + end + end +end diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb new file mode 100644 index 00000000000..0bb83845572 --- /dev/null +++ b/app/workers/ci/web_hook_worker.rb @@ -0,0 +1,9 @@ +module Ci + class WebHookWorker + include Sidekiq::Worker + + def perform(hook_id, data) + Ci::WebHook.find(hook_id).execute data + end + end +end |