From 3e5162fc5b7b59cf69ba905d84a862b5b8eedf30 Mon Sep 17 00:00:00 2001
From: GitLab Bot
~X+b3L_AgT)sh25*B&HQ4d?EuN}OIcyk_rXjr f8?g)ZDVSs^rNN72OQJu&YIO@@!$%MH%j@s6@>)akRQy+ce@y}DO9<2Hf zUJ@HP=FSY0Fti1GPT5K=uOUV)Dxvd>K+3^EeLD)rQo>J3bt?%C4G9g2BW*{6@V18$ zW5GB?=>DCt=$&+fld zZa=6L0)mq&2_%(w8{zKZn06==E)n9kUDm!%s1xoEFvSuhwCWzuo`p#+5#Z^##*$Y6 zY4;dgj`Mbe{3Rt<4Z|!-%rJ+rHQewiSii-Nj7F#^pfybGCiHw4oWoypwd}9T`jM5D z^~20eR%yt72?Gfq5z&ET{J=i7&6^81VnfgG{&a!CPYvNjOMeiT^InCC;9LNLQ2Tj? z+#GXdP?CxwSZ?|PqkaY{LN|^ oe_jOPY@S9&WT8=`ny@5S&&XCR>5y3iQafx3#tr`a&4?Z7-gDOi9 z$_m>GoQ900$&z@bU?MmH%*7o`eJMyHA~VM?w?|V~e4i34Fd;YT(iFpDB<)l|3RY2L z73JlzM10Btb@r~{WFqRC;U8g7BoG+UUYc2CJJa!@u(0vO 2h`nzEU4 zeT0JIxD9A=sUrTQ(AA$|olZY3Xk;LtMkzSlbGHSGi{+uA0}y4t01~mNuqw%>pn&4K z;>NugA+)oVKk1%Im1<$klEgG@vMxnUO*Bg|5g7}{tJ_%s0nmZuuE{t+&Y>S9*IOSA zqrOMY2NdJ24h Ok~PMc9wGq(Nm>RaM=>Ik&qz=h!Rat2BFPG}j8j z>cG;51rrNc7McSlrE7G^9Vprx%wPVH43cPM!3i<+QUhl@W>pQ^ 4fjdjvoQbe1V-9;7F*^CZ&UMy49@=zWtG)} &)8plQ_4$;& Bf2+x@N}WMI0N8dt63l2|#}a0x?BV;+*}YV=J_J<}=atwB5CIo!|Y z1_rhOg*U-CDpG22iDsxUPO=D+h;LuN5CE31i-A@x!VhTYr17Gy4u_i@8q3!QQLmzn zq1X=94rXd9cM}K?=M``t-X{v--H4Oq%f+uhIdQ!-<@W2Qemf2LncI)7zO?u`FI#&? znK&m>RFg=3k<4#8nuwlqt~awxE@Ylb4-QUcj4d?Z1rMjFtE2V0$Arh57Hci+x8x1T zAiy+WvI<7ppiwnvuOt0t9q40F7S{w`=}Q1~db}NX3Ha$%P!`B}gu3e)=p D ?pI-^m0^h$%mZaXs1~)P3hgo zSM+Uqdn9*ZRⅈA0N+F)0zE!d)#>n(Mvi|&(?3JLY$w(y>F1Jf3Ks8oUc ;?f--M<2U?Gm_Ee_ zpZt%CJ#$32z+bTuo=~)3`S-}Lv2Okys9NfA6n4`693`Z7RDB;KKIm#RYJtrBJyUE* zLOKSJzJujw4_d!dvugEzD~|xWG>Ej~B2no-Vm(~@j2YQ*!Gkt7cg@JaxSc0KZqPS$+)fA@uMkkC%T(AX7w tTm#}{P{c(lJ z*4EYn*XqRAlsN-&Xq32r8qImY*4@04Belbto6jkjnwinwc>4`yQN}f4l=B9R?s=L} z%Z_;-|14%(Qi+Rmc6N51nSFX`iDTMre{~uXyQXc6oW(Uj)=O{PQOP7qhXyreUfYEtNed84^4I zH2EEkXuo@RsYXLOIgK6w|2RJ~Rz>yxy2?M1P@18*IKQ;kvFbugY+(?I$cf?cW;1cI z) nPJ7QcsqeuN`;saOGYQO~NN4(WPoaYNr8JSU) DpJs5u6Th5 zn1(>{vzyPQy!h~$FmAv$b;9`qO7m-$X(_XLHH){T5GZDBa16sUGfptcT#EG|6GXe& zw!S|7L!sotgeRflv3y&RNY?aAhzr3a!qEVp1cR~x_@ra%J?rIZ%sSJy&Yb>U1;%+L zX*Kfb3avo_i|Y9=hx4y)@Y1|W**=<6j6&jMl4OjBamJRG<%g~nGn0(46NG~ktQ%3+ zxTE}{iJHoU(t@DU0W=Eh6xlS)MBk9o-&*H{PJCX`^o-MGTCA9}i*oab@I<4UXZvT|AF&&Ep>EL4W+RoP=2ozy=% zBL`(`g3b+dL-(hFIOS5y^6fdw&`L97(5&RiM6I$S(#ON~kk<8~s aYoM?NqLfA{yUF-{FxEe=nit{KR11O-iPk#5R2{Me~NciH)y8Hmy**fGeMKkH^t_ zLamB#jiTm_NW&oTfCC8v4PwlV=qg*e{61C%i7_}-O87lK=B+t0c Qw51+qsgzT8Is?>0;%V{Pn>D5K&0&=0elz|p9&<)z zsA{+Rz|xTyvCioUAyB}A$Vey$J1i0q6Mz9vPQZJpQBL?Hq07nX+_BMN3o_E@M%p(0 zhI$en4m5Z@N3t547QrlaF%|%=MiZR^;+mRwX7V{*F|3F0-- 1 z3QdYW^A4?z(tpo#e(cwC*w=1jC&RS-!jQ&K6OPvh4+vGx3xNPwr(o7X71EXclGud$ zA_8DkDZphsFzKrdBG ~KK+F3DdVDZc6!(vZE zCFlLQz~hnlr?wZprXDB-=3JdWGSCjKys$DR_z=CJjje^O6TAZ-Z)8VMxc+{=$}|}* zUtR;P0gg0YTFK4QK6-!mz{^V2HzN*iW;1o~4~+?H1+VD5nz)U}A9`dC{J8y7QY|`& zt-9cWLSwXqN|EYLMa>3L0$`T;Z z6 zTFd?x$Hga5MgHz#l_=JX5F&F+`?P-YW)nE3{N_fahCK`+sWCQe2SP}tWj8bwqPT`a zDS?zNlzNiUO*UDcCgyb@Yy~68ClPHrU<{yf^@8b--w3SO@`evDIg1Y;!-Wjou*%u& z`Fn+5) jwf(2@LXUa6B1*ZmT7$!4B4;y(mD~H6LvwaW=e|twpxBd+mnkj$RfM+MU zG!TRw5;}8!Pnn9%tc-y}u{BlY`67a# YIQ=E4^OVhg;NGlFva>!$IWSi32IHU`xz;-`nBfRNBRZ zK-=e*Nv;}^89_NF6p#2?My1TLT)KxrLq!W8B~G)PJFXnT^qXosloNfXI $bNCKpqqD*Uocs+drC=dC)U-Zao53dsb?3zWr_ zk{6J>Za#=xyx3<)#E@6bd2hCjBWLk9czR2dA7p&f6*;f=yKt^2_W5>_bCQ#mHi|!& z`8k%N(5+XW{n^$1kO4hrG8Wj+*TsG0tlP$X vG6m4 zJ^W#J?VW{v;It|s8s82Wd7!so$1x>m)_D5I89EI|`Oi0!A}j{#0%O6XFiW%obxaL7 zL3I&AT7N7;3rkE$!zPAcEP`*gYJ%}tQ#5e_dc8vCax7SL*4isvrahLj;8y}THnwUG z4m2Nmf}pmn5T;T+CZx-6JRjFz_sO<9#mHbVv&onnmP_j8^g2DC>rF;%?`eJ5E*-N9 z#(MR5vg8rjFP|wgHWnYfS$Gb =UTV>^-h{QcXzh3h7j()i z^K$%Jt0-xTWrkH}Z1Gafp-n52j3B{BOxMO2>k!u8FEs1ecXBhn`g;DI#N+)-)?=gN z=$oVZh~}Y0a{T+u_*n+zb_pK)5Ynrustl8=^ jF8K38gS}C3P3n>%B_j&!-$zTx6bO@|xBK}T@Z#2i^=&vsaC(4RCR_R;K z`En}*EY%{Xo_St4p%opFv9gGp8RjsY&rGZW&nVOVE*m^KG6iv!hH0ahIj(e^SjFAW zZCSPz_ofB+hi0~THsaGMh%u`+?60>=$zG7o`yV|Y@2`*N5#iAoGC*MieS1w!`&?Bt zsk2$ZYA7QBf>JO!#~*t!xGMM}G$voEHkQ6tN Mi#^X2jRUcQw1lnvT(&*S18P z%!xi+4ET1vJwu+ Krj`2QsS5H@0S5FPQ zFS-`E`QGn_!Y =4n;QV&R%t{jXg9G-sN;v^=1hQ{ zotdw#sMk>kF0U>e*_ZYHu=)l$!XTI8D9TosH#k@+JBfx-RZRubl{9ixgW2ZRW>@U7 z)6?^krJwCsIyvM(`J=a1qh665&hO%xHHRm~g yA@b@;vEcpm2E8HGeD^WLVf$(bvc7X9cg*LCktgyCJ`l+A4gdx$#a zhe-o>OzeczYg|BD(Jq~$@b&lo%hhq4v8UF(;{_BxC)@V%BS)@7rq#wbS6^Gp6?X1+ zu083AUtMy-?~;MiBShPz@sd ^%g9*#>6PCFL1ip#zi3vwY7u&*Dz{8)Wl zPi}^bHFarl_90eoUEid9%zj;e{B>pFr9g{o-9+cqB>kKu1jnv-l%Q@HDwD|%XBA4D zmpPzbKGKuX0&M-&(3~F`sqNzF&%2oXWmfu3w=G!#hNfIXwGoQ4hA*A6ibcrH=#&Ks zX`!^l?kIWlHNU%2)UN3i3B9P@!M<>9R#!s>^8fU8RzYz9T^7ayOd!bMZi5C7PH>yS z-5r9vySp>EySoRM06~JgYtW#|PY&gFOvL%0K zX7)1hWq@0anBXUMEeA_vekx4DcjZ6tE1 GK-GqRbHu8Yc3YN zR~==6);|%Tos00qq|qRcNO8)jV2?(6S+GhHxLh}6fVFmgr%;9`t=n!pfE5cU+@#J- zTfi j CMB>~V5K4abzv_Gn}G=s)D zWtTl9wzZVz+mG^BI4@mSn>Qnj8d2*OH^|Gx7`w4uKrVv;?@=lA;f5(aH^c1t>FMKv ze5_n@x&Qm&6w7+I_vxZXn$dvMLOE})ABGdOqfFTx4?l%KQjjSk+3P&{3=1p_45!ZG z`4W=wYu3dr6hvVSfs3;NN`4OoB@kO#P8Qgb=X| l;XdMmuV#fgZ x>>=WOj*Z8s&9mDb8nVgxa~lV3xE^ND0hLEvbB>`p*{2Xdz* zlhB1kkb@Rfm>Jrq4wnUD^SYP}`yVZU$Y}?_Xe%mDHJ&b6t@wLelXd~?p-|v?m`)f5 zEVNnu-fdBkSJF=qofksA53-e0yt*7ce&pg~klyc3+fV`W?CD(S>UwZx!9q>b-R5Ib zQniO^P}Qpn%*WVo0}s-o7oUQzgMDiy1jTk$6zrlEpg|0^& +lAeXgVtTLS@oDTwm~Y-rJ49Bpyt5S zARj i83E_L!Q*CZ5t^@^#iDI5-xRbs?rtY0xjH!} zD7k3me! F@AyO;W(`>Gkz|^i^2At5FdA zv7s0!_`Tqxp!Ksb#a1kO4#n=@CU2F-K4?t!jaRgMU0x|v_EwW~BRcx2x6WyB5%G?{ zw};sO{O~6i`N&fn=xdcQ!-O;hLjPUU!x KyyKa)wW0Ykw~L8E ~18{gE z7zqDYcv8Q3B41PffnIWxk3{#8NB!?f?2*;&oUX()z;0vASzxt#$n#K9Q}m_gAt6G; zow|1J6>z+S*}OkzWU(2*$z9gdjZFC+2qs>@=9 zp}w3xbv0oAJxVCisAxmw62bySKo$j_+UvJy->PDwiOEF2xs#w67qnGl>Kxm;heRfE ze1tRofm<|F&MliP8$i+CPEFl*%cS>o5c7R>wD?kEN=EaS;Tsv|AooYYP;W-_a;h+( z9wd@kBTa^LhmqG2fpMi{49baU`l?aqRbr&$gKoHi1jTnO62^e~%y|qW=g38(1qcs7 zoV>j{#+B2=nXWt+G=aBye}>%cX4L;&clEI-zb|ASNi?`^3>$_GM*TntiWr7B!2t6> zoSa>6(zJ7As4W|~{C!NXBk0cC(;l%&wt3AU2L~e*)dmS )#dLNMBmG)# zTgRQMF;910L~v*vf+Oe1Fau?S&}8!4+68Lz&NK1jkRUYFf1;LbCw<}%v36u4G gijjzIY n(@_NNMe z6?a!QTWXDmBR-8kZ20;ZBU#pe&FdZd*hYl#-$#b=U*J=P)d{nMV31@*WrL(erMUuT znP#|L8I6-CB(;eXX_RpahZDF)?ZlI-3+Ys~C~T{WBek2Y)GDgFZffEMoNdW%I+=j0 zV>w9YIT`%-n;DM>&y&2)p)38CW7(#97c!{gpl=`NP97Gjxaa67A|eDYo6PYbc6R#h zhBQT{;CJQ}46rT*YZEMya8WT1CP^dAs$(9S?Y5J0e|=1>Fd>LIohkwrzS+ -w_H}CuJDg>0%TIptr<;9=RCdp$k5|V8EW0iC19>VTb8#E$-IZ zgyV0U-y=hhsa#5!E#;2z^K-lxwgJ~Z4P)tIi#-WQk_6+i%lTn;^yB91lzUAjd!Y%m zz)0x@Kl5y*Q3Zo92@#T`Nc(UGFDKW(t2B1Es=qL-G)j-yEW?c=k5zksU6HeHNo}g^ zg^BR`>}1eq;PYSsNVi9`It^Gh>FF+p0ocRej>YCHFr_*%Xepv<%vdt1t1Hk^0;VJr z5NgO_T<%1q$TZ6aECL+pI2!Xnoj>i|g=Gc^<{Lhvg% @};O zxP{3`Pj$V|(IR5O`RufII`OonW z JAw9a_y$~LD5l2&Yj zd9uTY%6RrY;3#sfB?j(a3Jc}H`CPe#eeg>}q#!4E&=4h9m-*At%1xOF
y<7qr0nHB9x-q_{@6G{oug-?fTC4`pt>qHmmH5{Z>y-282e1n`N?% zjsV PG2LvQwHAV+)^DBLvrF>;|>$Bvr3leXG1(I0L1;8t}(1OV+%*$vgtfyEZ zR4tTZL(1`jysF!lEnmuMrcGJEAh>j|{3-NSdEVqz0oZt~Pe~iZc~k3H_BA6-@U}4C zoge5R2|)J_IA#s~B^ar=JWLKZ5|3)~@SQkg7q73djB2-stg&!Mq@mglF~S-F0wzB( z@p>-35g~Dc!ye)##Cn}x$D3s58!|vuC4V8xCGEjEu78J4!nh=xQ~(<(xi}0?Z|s z+cq{wQBwiB7$KH?hm>UpQhiCP4e-)q(+DaUT~jp~R9f0?8qJ#EfTdX__N>otcPlCW zd8OIg2u^MR-U92Zo7`Pchz$@8B!U$J0O>dCV&Phb7RF@V%$`cG8y;he(-4qze=|Tw z#{sl?7BoJzNk~k)5ek!mXeg?N KZ;H63GjJ;f zVv7`q>>R-k;Um>EDsz8s?5Y!JN#wx=bvTTI0A#@vlf*4ONy?9JteU#e=5>rDY?pT! zY0(ox^>4=kt@KbL4K$ASo9)|5h%(V57%!#8Y2gkPT$o5mXS@@j9>4TG7;}63>2YPn znU+iU$|WZB^~HUaaeI!MxK}1&u$fH~6txcUem2n1I2=Lq>~ZSfR47c!2HZ^zs*IcD zt=3S27eDc4u{F#})?|gr8BZ@st38rW{bM+8(a?WV)(Rzzib8PEI5%24r&pnqjxI?k zv!74a2yIpr5&Kgo-diqs_ zulnN--@0{(u+3eh(AvVH9IL#O+O^)`=M#%9!9tU?uMP@PHiHh1x33ru6O}K8Mdn|a zN+}d<7`6RRha6)fv+3}B6$LecApq%n*WablLn{kQ$;}zo$9Kw3wT~oS(^o47GgtFu zb^bnzFr)obFr{Qy{}P4@`sVg*zWHf({I91{NFf`c5a|=uUo?^Qa=8`!RpWK)yH^j_ zN$q5%2Y7jc{$Bpazpt00QT%(K<2k@wZ9yn=s>5(Jl zD_ R_cUPy=evyci<2efxbNna4&aPIcy|hAf8xQUS?eF8UJOw?ZZ(BeXWKpv=Adivt zn)27EBl(NbdUnSG 1Y;)?a*-!EoY!xJ8`Cj$K^Bp3knxi-?sCB0_^Gll-0 zUV$*Fwq8Gv(}(Bv7H5auJC4yAdLDlpFL#WceAz0XNFjc&HHrW0_T&bYl;HQy>zVB) zqR%E$$`w>5!3KKzL_T*#x3*4A?aKrbBY(N)7X9hp)zxK#(%EmFaE+4e>&BkyQat>g zuMBv|oBCgKKDj4fN1O6zsFQlx`!WUXUwQI4e CJ2 zZ**^Ev(+;v7V1D_7y>;zu_3Gk7*O$ _70LJtH~W^0toP1!zC$~Y^X6)soyCBZKy z?1?C&qD68`3&PNotHlx+ikT@6S1ATW#8BcEk!u&sin~0{+GjPRd*?dW*-C3Ok*E$F zztS$>Kjof!`<}-iKIZK?qqm0HBFRVA0thT;w~iif^X$;4meA19 +8=Kq&SI9*oomzpI8S(;IP57Mru@v zJ$F{q(@mSFTJ?D>+F~K2}38|74N)8Fa zbRu9UIuKBs@-y0Ktz^jheIWpbrTzqW$Nkg1A}gDY_F?zwne7M_XSZ)i({XydV>(X5 z%E%;oiE)JGf;zXUu5IuTVSP s^Lzd$GzL3A*6|KU-krUqPJJ0`~#^U;e;dAG-iGdiC2a$uB8}&honCOO7V(8 zGI^-TFqG+b`Y|AST~MSb9KVe`cPBCMK8#@0q1HqEsu_cbXaBDHHpQ+vv7$ipR+0tX z9Mum&T6a;p3d^Bnr+8savai}0 9!~Z8Lq2=zlwkhN4ao#r3G@6c*d-5CgdG3}twQa} zts&T8?G1f*G76PiTm>2ewzbv85mh2xU? Bbf}*To@07gOa#vn!v;K;8rqJ9G`Hq%yeYA>ZoyDD-V2iWaryiNqB2Sb z1#fKZtQZ>7@F;Q$2^ri{*sl3twt!H1A6h{d`|a~ZZS(tMMj5cxRlr{9MGKE4iWSuV zoN!qP;nGARLXNd4zD5Ly`7e~jUoMB4-S{?BfT?EC>{};!&-Rgcd>aQA3g72Q$D!Xh zH*uKHVH3yNj-rUyJfHP^`Q;(A>L@?#P70UXw_AD@sV%LbBaJs?H~xHI@jfC_CeDC8 zRu55-g6H}bah8*}RKSB{9zwQ}9PdF95o50VZ9+@f1j4&EEl&lyarCqTdPTdE?3NEP zq7<;Q!pp59tRT8=gxA^_1&K})Xcx6Nyrx7vmUg zZc~ s=;|{nopa84(z2lWRwGK9gcyqe)PZGHLiWR`Pq|&i{QZlz*qYN z!7Lr*hZnzhA^wx4)6+HI9xVe_(+0-uf&}bg{@+7*>ANzNjTepSFW2c`d+Y1#!R8|q zIMFHFavTgPx;Z(Yc)@ZIUgl9)1-O2y*kK$}bW#kug4D9PJGrs*@Z!e#W;V{|4l|oX zD7}=NxjiR^Yxid2(Gb}eFRseS6^R(8%c!5(02*cKb@>yB_GcOQ9gohmac*yvEFN%= z{*eLxWy`a#puy|k+b1o<;vOnu =G`ho8a9@}*sFPpI@gC2Km+yyyfNYQA6! z4cY#L@g{l@hOPr=PPq861jb~HVMKofJWqvMqj=z;e6?hF8jp6sCvF7nj{pw(dc0RZ z5_QGcwBY^EA;tDzKq>$fz$>L@Sb{0GkiI7AS^0fzm}uhhafTr)&vTa`aYTqvQ4i;P z%Y!@i?B|~&iQMB`zE#EhgB3oCs6c8(I18K5kiV6>$cZ3k5KQRYZzqiH!DH(Eo)*H{ zSZCv)VfQ&ndO|4mu0yabESN865XB+Js6zyt3d9F0B}y-XlIWBDJ-b->It7Ucb%}eq zzc| UU!8+FDfH}e(P+L(e}iRfz*MIlt*)z`%7#j%^ -=UHQ zrVf6r*a2Y^0o0lX7bxL}F6lO#%j}^2%iqxJaaslNXvLQy@(5}Px>aKJs84)?&xW%~ zqdeR}IrW))gr8dDt_YH1l~Mh;`2|qcenxd|7(`~RrZA5CfBrEj3CrrMqn+bs!6s9K zcFh2;9fyw2&ic7JuUwFQ(h@Z?_-Vs~f$DCEbx1P8_4t7WmEQPf%*N(ozk-9XM}~9& z)Am4AjGN$D(}-HEYN3>PUo~J=r5@?SRK0Mg+|5VZ{(g8mCD9l8A*l$P9{>0*9_%nV z%Hb5ip>o%4QlD`Gbl+4<@@>yLR)dxZJ~3j7n*?=l(DIDLe>}~$9i3-0_HPpIv$5Se z`z>O1MmA?$qK5G#FL9GLNM+>auvWiwaruadBqtY*ib~MH`*q1nY^VQ%9p$BufY@@J zUD3n +PT++eKh}+Ooc~CL`y3r^@Hw@x^ Qln`ehc6dMK;1Wq6q z2|@q>|L?^UB*JKK4z3Y3hY$mg;9Q?7b(85w5dL`>eyTx98%aZ!UD?jLWAX2mqjf{n z)IGnWdZ`X7nFoR6`NQvtMs4e8BN!b-GE?EtQ-f&z2}fBD)Q|Uvi|I$ad+ JPbgNWjycW<-l11`g9pxFus42PxKJU00?zu`BlV$vl|1mW*g10S* z-HYTKyEB;(tCw!EaEUW^eb`@rbzh7!qlXWCeR**kFpwHhW2nLPL*qUdM$P4x;p$$f z@ bL) z3AP28ShkSpH2upn?iUL;k|)F{QkN;0d`P(rs7^%=Y`zRgwvcHnXl4bJbcv%7ZQMb_ zna316XqrRJGVbcPy5FLcR3o`Ci*s0(d+yhrrvwh&1rOc*mRf!G$8;NC r zPL}@4P5CU6%LIPFN*0vL&Ew3`qA#{nn9`^G$Bkq@S|Q%87+c5mygSMGvhT^1c*u~@ zUaYL|An=&=Aq|R0Xs%;CjB7a_K|RvM^{X!1C(9KjYJG9tg;SX;ZTxQ?{S>}nchS1K z#L1Xy5W92EugHw|muB6RLDfKbPm!cAwcY8F#}dZ>>|+1#IM#A39Q9A=8uXp9Yx-1a2z z)Zm$Cmm6>`IZbxr%9(;)yE}06&OplJ`o7gkbN<445-(m@9FfPxSa5IKV)Fr1noOp& z-aW_3#&&O+nF!V K!{q5lKS=t^og~p20Tld %cGjfRHquh$ls`inGAG>gT|%b~_#g76L~6BXWk?A*+hG>x*mAOmEPUU&yd z_ZK(*EE}6tU-Q~>hh%hvrx;Hkx=hxqRB=y2TEaqI&p(4=pd=>ZUamHVHnIVrllO`C zBwBL~b2D7~b#mF#uEc+(=+TSC-5kLl!`q|zzvGDmuYG-o`V&+R3SkPic6PbIcN~6M z087&7!#w_DUDS>~lP|X-J6~#;>Dk&~i@S!oxj6!OB(2&Y^Ey8KAZcsk>d|J^mPEOD z!TP?-2G5b)niapY{qXmpaIIQTdH(CvL9O2t>G6xFS67?oiR*V@ocE4Bhr^3Z{4Wg| z|FgJ#1Ct_35IVC@qzbM5L@5P@bZQ#QW0?>x`SH`{w{a%&i9lCk%vY;~yOvS$xqX(_ z3G?b9^A~YvSq~&`MocNgr@udJ?s!@QW5yK@m4l TM-BP$o#iiJNP7afj_7!N{ zxfFNNHAufs)6Ewa^39bzw;%G0pKBpyLRS5GPGQ?$AGJ>I&gG?f=!o#C!4c5NP86Qo z^BU`_s-~){-IykyW2f s4Eu;7%a?q)4x%JvU^*CeFd z1S$v*=%x$dCOKCOaaO3BO3*aJ33U94bdu$h0r8vV!o>*<3tC>zIwb3w+|?wS+o}rb zsKJ>WQR~>%-%=5|F1)5u7MVt{F?>*AK_#zRrBV-_eP{Y@er2U3Sj=~zM#rxC52X|| zaKC?p^XqH$T;$*wrDxK@(C-TIT@@)%b81XYA}yOc@n{&Oy$Qb-)j7k(P^u!QrZ;1t z$uwRyLVcJ^{&~xCrHJwAJX$EK@r7GH&bF{DQsqO!PhIriBEfUURq#B(dW&oegh;mi z84l6kV7am~S%SOlnIydhHkoQbpXJ ym>d?d|^9cmb|yi+2LVHV^6b# zXq=1Tuq9J&rES7aD$&{0Q1R6ut{UQe!4MjE2`n#5KFfI%T8Cgw{n)u?o&LF)f4pm8 zn93(@t4f*jR}s{>Z46CJIky{dIdM3iWyi6&0y^Jz$YsNBJfp>5MJ???a-g0_1F;Dr zv?f}{JdGyLrw78K;EoGS(Ky;*sD`b=wYpu*b+SNqIVuNI4{N3)%8UTZvH7&@7tjeo zPl-Kdd=nTbWFIWpHKno{Tz}~z0j07E+UlYR=S`bt=y;S@S&ebz`RB^c7XH9?2fD@a zsda{8qn#l2U{Vm^dpO*m2i;dqkqY|It-o%r<;Q-KiRMnmrT}y<9#l0Z&nveX*?&-# zAR+#K1>@SOLf8@vM0ymd4%7Z~Kr-JSj^Mzfm_gs!{b?+Qj}FDi8Xj>xbFD!snuL9U zfJ4=&9zVNWFf8te14eKq&?Lzw0;b700tJISnGGxHD_>`G^KJ1-Y}rlYTtYgkA|<>A z2H)f~;|eL=USV(u3=HP$e|iKco8+rl7$fo7F1vi)M lFM>o-BJ!@2LeJI}PU7v+GIIwsqnxN3jD2iFnacb+jV zqrq0s7xw%TvTN&{nX}@i&xh5kD8)U73*s)n|0IEl(_%_^G)zf0>tZUd$JDxfhUubi z3?nR2TeJidyLWD^s8y&;geyWd%%1R-hAWKh{n+`jXbBE5geu9MZku0J(7$FGv>YD$ z(~(R#578xV{?{xU@-#sTaBt}6CAn>z*!q@id72o>Vo+!J`LQCc2vY!;wz>#vq&lHv zl`tDgS`1OXMF&03^jX}>LDmc-sOd{v?d!?Y)6@0S^{=sEvHrC0z50*A$dGh$db#ja zMN;)SYZfl-A|&i1=2)Un+U$`u0p8KCep>}x!D?1*8Z+dMiH_9a*~uGGt=&qf_@Z<; z6aYgd-GE|j*Po8lM{`7 Yex*gJSy!aPoe^HMwW=sL+bfY0gibczt*f zJE;}p_6bap7@TWjIxq1smTj&oU4eTSwVOM7OB>UO$k2Dy@B=$%3>lw6Ia?W=iFqrF z0&-MDPAOIlx0TDwhU3clBNVHAgbr_VVZSle7v;Epq|M@&tqHEEz^`CG-oHPSs+yvs zznn%_oqpy9pQQ=CWR_;}8koC#*iz_1(NHs|{dCoi`^oUABph+S!Z|gi1o=OEpnfMM zRQI0H&e*8QC;!3dgHk}^`%!|-?c=XHx4>hRXEy#F{eL>JgWu4 z!AiX@2(E_0PG<-ZCA*Ht`^Ci~75kAXd)qS}(2%~sEhXw|eir#vGA4+tY(;BsnHwRS z_53y$iVY(Ccp6SukDo@UIA?ix{LL3(6re)2fe!?E1(}LF!~7s@YEtc(+DeC`JSyJR zGZ%g(JDC(Ht@s+fM~g@FoqSS=Bw{%=2pdjGzW^K2Wn3oL6+Yt(S5diOI0u($8nz6I zHf>uj2`xBcG=d-wm~c;*#DH94y2dPWxNKxiXJkdQLq<;q?A#z+lV)$MD+0aVYub+4 z)D=^65%-&0-ED2X|Lz{2smE734N?B$P6DI?eJR(_AjKPyklU@>aR&zU@1FnTMx}}C z!f_YJIsR(%+WWWn)(dOdKrJxhA$#`~qRCICOlF$4Jltr4Z;_8O)N;ghotd9FasoIO zpa>Wr=p_mG1d~cnB^^4`?GSlnq?AiIG7#_SIllZvD8s}>n{m-1olTBn4=YvKfg_2Y ze9S(S{4Mxs !Y?8U%gU2iTOM9~X^zh*7Ro#EYaA(Y+}1%JL2o^=GGvkV=U*YdD< z;kA+8D;^>3{}q0WLj2{v_N2^UhQi$kF-eE{vQwH6SbYEY%6>Y9=TGgG46V9(Hgo3> z$C|n+uKSBrD|%7m&UYevKiuTCX{9V8guXQFXp!~He}*odoYxpoLaABSBDR6?(8@ z>W~jy;{G2k2!dB6qMUr3i@%L6qFbvAda=x8PZ|T32|AuNgQy7k#PHsiEY6|!2g9%( zPvF&6^}pAWW!zy rY%QQs9v!p@_ z<3;pI#Y|ly2b)@&6bYy#ulK;pPqdP@Xe6&-ZtU;|BYiX$lSGw4UKEc_%F !=O*}-EMs^!WKRwe%<-HHtFe=S%wp4{z{Mc zaVFm5k5C<_3<$ES6?>q{&P-MnhHkp=6rY^3I1FdHc#u-iwQ8U@w%>^JJzEVDn9qX` zG%fiy@l1i2Bcp38_@^coR;#vJ94kJkzN1n#H8M*8M-vja|3EuhJ|=qnM{nH#t-0ni z%z>PM(q+QD<#$sT03PpOoBNIZ#t)-eu(v=OZa!1-rL_hAf~1&fvK)LeA~E_#2x1_& zUKaE7?!t$)Y%`z$AW-~Iw@c}tNmXLrqj?W(ePTzK; ;8Uw21xPCc1h4OQRBsj)d1|2io>is zPf0Ql{> nqvS%5ju8Gn9?$9Gn~a*X}DUc VZ0t*BEH6pN7 zLqfjAS?0s_g{#ZNf+H$d)}StP+uBC=v38f&`=;(s)0S#nisNw)uj9IAn?Jm~X}fPP z_{&*_`xzOZHjO?{Im{`zev}Z{(n>ZzGB(H3+7VHPujH`8i!&Wv1?=x|oRpPmi}cvh zbp6s<%EVUxVxx(~rHYQ7zcKqc8165~U9lBtz v_>9PY#X JujA+E zf!X8H2^z7sH-_;z#2UEW8q}YYF93VK)AT9qQ_nlE(Ss9Al{$mOEjJA9A{J4{%`CVH z$+#+to)YmangTWhHX^fH#$lf)$h-*@3g_f>i%(CbYU6o{<=ec?Y;G04*tLxtz3*O2 zblmKl$^@U~PP8T*1@9lfd(Q!}uzn1nMTxVqIVL7z`V=IPVUp1f+ l_@aSq;{RF7HpMr`L^NtFm;D$MOrh6mh2y%>g0!`F^*51CYMRRi1|AEs(B_c z4Fz+pZTt2pB*D-YX;Y4hXw~0uM_EvtQPT+KQivx&{}~U{xA|ZtAKsoR;6%X4PW7|` z0y#tC!gmpaczJ&?MK39ApLcRt&!g?n#mp$Cm5 j`&l(R3&pg`#|AZ zEkVL?W-4W(1-g_82$^u<@(C57?v7C}x1bku<}_=Yl#D)}y?XwfjSumN;~!~s#tv;- z+xvYCPJnAl)B@@?^rK 4O`$ zTo~Q9 #OGqdnOpy#;P=h5!{)8TLhsT+EG9}E9{EA~B7IR?@PjSthQX$FCllE_G{vg7LG zz>Xv?pS_h^qktKv8<4Gf6!+I}T)T}BB8Qfc+1p*~>#>Z?Ov234c)!OQ@3~P~*`_t~ z^$i0(9 HVsWMu9MF<=om_0l0_T|-5B$@E`aJwk_IwPE;7}=)G($Ag~(wLE^zz^ zjRoW*1b(>q2$G;_Nu8$*Tln!1VZ-5ST(2e%fAiUMdDUKG2-)n*ulRx3Sh`T?ua5`x z*j+1HtZh=9wp}jLmoCx^!lxWNE~=a8+3GHo;lGPuWNL^bplwxVu-H+;TevBwA@WPg z{s%w2G6eT(jK=3Lzs3+Qq#8w&{t}QJPkvKV++|vNNVEU$7b s76;Do?b|Jss@jA5?@4rv%)X56F>EiD1?|NkoX(=u#Yy^H7E%FxGwD`fo z1WZ8?6I~FY6T+pHmy|)_k3f?U?*fG7;3r0mx{YXq#31000Nw|&kpQgzfAI7a3Ymc9 zkaN#d3SJw_A#u3Xmfk;slbj!11QkUOI|?Vp;&MmGZjNvs0IG- ^{nR5kI+=^&vxwL~9S^ba0$dklxm|=xJd~ zYaDALT~KEmin-+p(vU))z7QYn4Sjej}(uTp27w)5@K0mOl7qrA1$uaVo#1j4B)H z(*r1r83h?lTQ^(VhjEWZMG^!-=xK#4l9|eO(As8TIdumFx=ZS#d6Z6mAUF61fbVzq z;~oWO74jr*dI?fATiO-K3En^RAKrJiyHmypX?=Vn&Fs_g;6+wR=k&DUk9oAz!s@!5 zn6uwpv^zVaKh )h{EGI1(1+LGtDW~nH(?UGF{+j2p%n?9@ z-;$=C*h8S$T*mP%7*RDA%HW_+@`uW-c>rN E zhk_okTb EJG9cq|+smn8m>&Jr9>325_f+3MzDMT;s!mW0TWzSm{PpAx#nI!^^|#uI&Sba_vE z{YE+cmYhWr3dl6H5=4R=H4B0me1;I4PgDN@19RcGB #isQp#UN$K zgDUo(vwBT aQKVB$0Z0L < z$G=>!~G|Z!0W+<4TkLY`WwE@$-ljS{i|fVDUuhK?}^MN zU{uNcs!q%F55HN=^f&K}*4X(I7QW;pluIpALlOG%jtA#cuZ{s;SUwfhpyxg6aavfI zMbS30Dp4MT8*4+G1ng)rW#ef&0|*Q&92k8x?=S=X7KUQWDK|u@sYz%I#=CPbQu(x| z`#%i4=}1wL#bY8(7NJ*GR)A0dK`9aBXBQ??x#CGlSjiUs?P+uD>b3a)FeSLv9GfGS zmPgo^Dail4iFnM>lp%~W0+1@a9AYZjzYmt4`$~@4iF5c2NH|N;0)1=)&2&xGXYn?# zuidtKw`6RxNi?NY|E6HVgZJ$aMARRw|7-lZ3P8s)!ao=d9^|w7mcFP;%_g)k$tu-H tG4}F$6=oC{
=baoMVJn{ZuPZ7HL_%J?R@5-)zW}G0oOS>J literal 0 HcmV?d00001 diff --git a/doc/user/project/img/merge_request_maintainers_v17_9.png b/doc/user/project/img/merge_request_maintainers_v17_9.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ecad4fc996a43827acedbdff5c247392fece38 GIT binary patch literal 9694 zcma)hbyOYO^5!A9Lm;?IAh^5R!QI{6Ik>yK1$TG10Ko$UJwO5k3w9v5JCoe|=J)P; z^JdNTTHU>ORekkU?fvc5e{{66A`lgc00{s9pvp)~r~&}ckl$ki1nA#SsFY9fzh5v` zq6(q_KvN>}lL_n}vzsbV3{XE!bnttE=q#<{1_0nt|GA-LRH@DYfH&W5)U`p{3i5nr zP7X{a=1!&-OkNJozoP*F0WZGaLkA0x37MCJy`vkSmmtMo5q!VLf6UAjWPgQ#>;x&a z6_m-uom?%*xR_X&SSW;$$jHb9T+J={R3)VT1^+!0q_75oocWlUJv}{{JlUC?T& |j*_OG^nH^}_Q!_3OW!u;Q0AREj73+#{QZ`fab{Y@wECmElzjhBVJ zwuFs?g`?Z=(u7#qICup9;`u*Z|E~0JsD_(`tGJWHZzM=a&CJ!}cLr-G(7(X{bp9vs zFHW6*aI$j!lky*)e<1(#z^7{A=49{wXALzRZ9qb70?hx-{GTYDf5U`WIXM0W`lt0j z5!(M1@lWf2B9vThey@YcpVEX_|Hbf6+rQ!knE&kkKeX_-(EhUiR+ &xry&E3||js5+D zmF1P~?VlItmmBLFb-_04KYsk&+!-Aio1L9KJUG0&eRzI(ot&KP{nS@pUUl^As5#s* zJ^ka!@u`lMp`NA`Kc9r5q3Qhm!uXd-2{HMo$XIJ*6 4whP@8A-Ku zEv>O`aWSFW^CQOkN s@uecWvqhFUG8col@1^PEj43L|?;lKl-8nb}3_%JMVP;yYqO zTZ*%sR75p-nQis771`()tC9+$eM3Vdz0@W2RKzxChQif>wHZ-MeYL&0VF5l4Y4M?L zm3fKk3T`62^Tl!FRp|+lzM`!3{{A6_rn;JP`~%6}kxqtV13g)x?!Trwf&*Q$Y;->+ zg|@W!PWLtSru(NPMRlbmb>*ZCe++1j@R@CC;GiM}=_sfu$+rgDSg5Ijr1?F~G{;+t zGe9=R3W_x@Rv+CB!Cl2YAwISFN#07LK(2SzYSKHy^(KN`&;Wwk5btJxcRMp Q60HmQZ5~Au}D@QrL@@h5&k-Q!GhLCtwU0vE k2S- z>7r0PjHipGUhivIdyk39Q_2VUl((;+@2MyL*hg{5d;P@AdBq?yK+{z_UIE9P%p$AX zSJ}UOz;IAdu@Jk6cMk`Ph{K%A9F;U0ZY%#@0VE>NBl^jNYXG{8`{4oZ;p9eRM}vo+ z?x?DdV=6=#A0SVdl2&T;$%{4L6(?SV8eX{N{CvE%Owv@i_ey6!eRq1#(rQ2Sg<3US zL_w*ZH-|F5XUTM>yoHJi@2xz}C8yKP4i?5z`~5)B((!WMa?aiAx-fcLC#jz*NtAdw z^k>j^F81^B&jen=$4vg%40N@f($>ega3mboWM!DqZ$Rm^XRTcCioo=&tgg$oJ^pan z@Ifsh8L`VjGtEdFl+}3 ?P8fTf?da(`~N%!NK`Vrl%$;k8feOHA0pU zsG_%zMU}Vj07eOed^1dfWL-}yGc8O)tExFEloYS*O}HV4XnCx8vDp{5Q1Uwbk*=~a zRnrd#2iy&NVUzD!^61^|j`v%=Cw=^zWeMr&3EAP`;eR;>hRELc8O`J$NN qPRwyzyl%vvSd%mlVh!grBCx7}?R4su*t7^42w zp6u*H |Go=CGO_l9h2MZfTn)kQNn$U zIL*#BDPO;VNrF8$6%_isoYnyVeeIc0~$B`(9}vB;fHQR+{JNLgEI z!kjW-W;AN+Y@*&oyvrtNE)OJ8%1s cr^^k-}+(yvDhlE+JF6_cNR}4Cp;G z2L1P^zv!#0Pu6~kmTS2q_gU3gT)6NdxqMo(=J4^j;nU?OU}s}X+RJ2e=mc$D#wL^0 zJ=Uk>=XnBfu}e!C&UO_)bt*ePU@eM$g6N)M*eN4;#*~{;0VymKu}Q uU8`C|KRzn7ZOsS^3yx1Kw < uvx s z^&13J^eVAlCW38}qv_CwU354!h+ZZP SvM@OaMA_WljK)bm zW;MK`lYT)FQ=&rUW@m9Jgzvr~n*+D3HdFehUhp9^H}Dno2g =n8FPNa zS48GM;(>upKYjIu-p{kvH_te7uOvh07NaLd31HN^Ux}mhd&b;{x6eBtO|ANffRW1T zDATFi57!GdbOb{d?I9sNXz}%*1VqHM9H9JM%_Dhv;gqs6AlWN0t7q-=7}@qCEOAjf z7`lX;awV>GU*6M*i<#X)6~PAJF`==+chu&~B?$=IIXL9TnY-M#$&!$O-ws71w@I P6Sc(xXA*2(ZO?nY5#4zjw1 z%7;BZGD}MvHVL{PucTYpE!E!Bm+(zoE&S5Z*roaw2r4zx?-N{45Gj7-ktMWavsFG1 zpsP3-EF0^3{kqmN4LL_a@w&Q`ukmDyVq;fe8CX*0`dPNGN(}D!m8P#7lAi}DDg?&Z zS&-qP=OsXDhTd>ks`r44O%?Q6WI=-t5;Bvk;jTZr-c@qaVJ?Se_(+vz^SfqrE%? z#3Ug3UN|B-SqM2z^@bp^5qgyGZz3sjt^Bbi1RMGC zVUN-tq Rg$F<3*D`9ZY$Jj2ZU}rt5c(BwuuvJ~(V}mG0_~To z!bp?gaeYz$QIIz7(;6(sgVDuJLlc#~f5837ZhPtIVy4lj&N@D!+}$9O^^(9cvGPWf z$mx;~WBtNvEfu~3 u{fzsWfZ^ebg!iMwy za%q5s7`ev` &y&k>?oYVRi4rb#pKWyb3P z=mZkJ+Tqk;;n_ZvxL>{fyv@oQyK#xe+_!bgp;xjQQbt5kveB*McIe`3^hm?2p;0rv zcyoQd_|5X^86#|ZrxCWHKKne=OB4d-deDKv#~Mvp`s&s#7kE*qjE*64;pIN}SoRp4 z9Lz%D#`d6u`clon&P@wi)xNz;{rPI|opQl&B|ZN CLc58)1TH2-zc3x$uUs2NdZEOo0DHb8nkyYh4Zp5?S8 z0yu^a#G^5dhXqMoV0I5sQk8!wz=w>Zq5uR4 k#v0cLgJIFSMR zqHYE=^&gQRln!7eq?D943Os9nL8`%dlJR+YZQlxn0=am-f1<;H+`Qyj33(K%sNQg; zj+Zhr)^~A&Os%(<;$`rA71G {a{R0KV0!*V3lt0l5y-dG2-!CzI5vZYP^l*P|A036otrN#rkUOFrYObgvy{lWk zg3O}36fo^fahFlTslyYABM4UK=QCi0-eJO?mA>)BryCT&@2kXP$+g^eLdi3m?FLiZ za>fTKjsk`X-f5zB1lvHy&rt9Zc!@qpjKb%eix3rI$T`&FL+(w(WXmIVBB9y^!$yjm ztZfNV&yIcGrppd2mVXFG|D@GfZ>9AWwih7gX@2$Gqvm#NSoz?jx4+zj-`19y!`Fb) z#t?h%%gp#Ri8LUmXdZ{NIT|_(dszFq*8f>1k(4HHB|}>WxlC)%JkH^n3m|5N zx*6>2O&&xk+%zy_+O%h0dK48E)jgYI%KK^8$v0w04(jLbF0)H>s55e)QF1zLU7!!p zl8#~V%h6iFI}LvoUCu^_nc?AVXn5J~I2YvCuld{{iW02ZaZ~l!8Igj6RhfL &-$27#IvS&y>&hveVqMMq>NWbc;~LE+RlYKO=>JKBPu$li&LAIpVmAJa~L&=<4!3#Ss)VP*3we zF;ubIH`kwRwunW91j|}M ?wb?nj7tObp`ssDFs+nsHRH=k4Er5b6+&mBPPWY9w~LYKo9XC&rZl|Q zF!XhH(6TY`yq*j0=~~QT;KV`)&j|Q{zTn~Bu6?R-iNax|nN`J8vmQeMbu(WnJozVB zv8)V8uD<~Z)wWJaQDcZfq=V*os8x~p7u-b0Ky2|H(s-LF#96^is#)$}@_Sf5#FFz# z2RfZr tf$H|JZJYPWh zNirh)HLXTe3ybNFwM4#2-#;$!=y#nz(L^J<*Jz1Ba=#U&B0%!DzG*If%3;&FWuT_! z{AoYCIopC )Hd#GMc123MXTadi zfzx3 >PH-RD0Lr Wv1}kumsCOkwo#gY zZ{6|E^-}xR%0qkk%U;G$yIAgu@EN4MQTp|>Rly!Ob^`XByg;i$R+@8tQ^hBKV877v z?r8ArkY%vqYU5hNUZX{%#e(hHdCtPz+#K
bC&S^$;vyqezl(;2%GJU{$Vbr9Ub< z`Z?{D)8H~&p8U|cc+QH34rHMHz)R9a>-sXw%1U=#s8$u#kFu*IEJd|cflq~(E^k^n zx7nfGO%IS{)|f0gxWb2s1(ETAkIH(`X8rvXI7BKAtwN{Sq+zM}M%yLbSm8lz<&Dw! zfHM@^`agR*0r7@yYU%+8?nmMn)-mY1H}oAF-c&EO_oHpt`TY1Kq4(tCC4ZOsik>%A z+C{8$Kd|px8(OqPuhG@j%PU$`B#PW!-LuW^-C_=+E0x!A>TPnAlTcqsri^5)QaO`T z$7W4ziEF`}ubuVv@u7>Wz)90MUBk)dRdHC>Y44!gx{Qu?awPFellgS!8u&81CZ$Y# zRvZf9_w-_C2QmO8?*T(WpA+byyz^&6%aDQ^$v`;aBxGyyQWDK@MLFwK9ik98_Jjiq zG>9o`7?bM##ag3vj}x9=Eh|U_(u_fbaGXHW`(ql;Ja5!FsjhW5Q;pUS|Mx-v_Ho$0 zui@*eyI_wg6J+wT@N%sO++p|(u{AOMRbAeGgj`W`sATDxUu$Pb%lzW?*nG^WBeqYH zhxK{enTNx2xgOn)VJR#utjP=-8XBLssK^MO@h-BBK2Q7#;@BW^dq?VPMFLiDA9FeB z?VWwYSSER>rUN%{%xXVE_MHtS%79@BsR*g6FbHgd8SEX?td2)Z_Qvc(P6%N$?P^dA zLx{Ap@sAKF3$>CUK9m8`xh!upwp0BD+=HIblP?6K(|?MMb4?1>sxYfc@N!23-JX z@B|G7L{>2&OLJ!RjcIIrjgwP!kN^ok;+ht0wXlF&k4cS*NxiHhJmSV9L}J>| )E&M9Lo;ZUo!yMhVt` zk&<3p!OrSYQySH9u1^ECD8lKb{5j{0S?h4Hd!vBLmT4#hI`}d)CpO#V!4ib}Rr~t6 z 6NoKl9%9t+oWWEH77{RrPQ&aBJeHt=Z z13yMbM{V!V8kOn*h0{A{&DKEo#$(RBgPN0A)bjWHKz=E7brUZwP2M-mo}gT!ETZLJ z{_lZWQ%)@MP1${iil25Sn4*na Jvk){ 2=5_!_Uvt*Hi4IICe(mbjgb%S$(7 zy$S4ciHW (&rE`?4e`CK;JX;$b;R-ygYKw6eWCC-wI80D+ zxN1G9{Wh1NHaO^3jC3sb0YL(KG#0XzO$xHjI-M)A2N`d2cE8ShE!AnBS8;oLd#4b# zuNv}Ve@xiZY@Csl(a=|0o0hkdm2tV(pePr6|28~=EpkM#jKk3TURkJJ6*#B0yda4J zs#2$z8x&wvgSorOMAmzH)_WQn{0tMIRW>_*{*7k6zpqT*Qzl-L+u%DB6B8_~(K0Zf zwbX(I71dj5;BLwjzJcx(dwp0WkoX0^9;&oxghkJ(QG28Wld<<5{gn`Q9GUJ{?HM@K zK(nVRx~u9LwTALu^#k~-E9HXq?V%vj#{dT4 ^r8Tln2-BUZ6naxNo-6iNJz>z_YD*!!5Oz7FH^*+}2ETN`f_@ihx4m^NLU{JBB58 zQ1)67^0!x>gR5g1h#iQdV=J>}!W19O$XT}ky=ei9d3$XsAkWw>X{I^~qREICs;#Xe z9pu(*WvQWDBDRSccwVd*mm{J=(^%osIR^keyiE@$M<}XS;AZnNrm90itGAp86S `eKd&uiw?L=X0-^ScrKlZ7SI5RV$?MCUTkq8Imz6Bq;Y!7t9h&(h~yo0y; zjw>-YhLY#U#hBVdS$-_5x4Df>qiSlZY6^36BRM0zvf|-UJ;qAzbugYMCB+|4;+(a} zv5aPEWfdDJSCRCghLa;rOQ@;M-`ZEr9g6tto7xuoxAgk@bj61)okZcKRn+#bR6F%< znwFNYZ8 8=hCQrC}&Re=c3tR5*`zE4a0))%vpZVf-=u}$Umn;xq&g)c_2nj{H zIg$mSLBn2%ji9&7^*xm=yf?CXQWvPsR@U*n$rNW8rE_g<2XbH7Yn1RSDxBP1mb0~~ zw$(k6PGhUlh%m-;!rIz$4@ZTW(fw}S4k7Y-8vDQ|O1ighblUAx0@-&{XQf)wE?+PW zbuFE)))|eu>R$%J;${k6wsf`qWUr1-Pqnx>u<08De1TuFW2upXB{o$&Nu3>sI+|qk zIUW_dA6BT|>d;g)%pSWG_BL`f>~~41dXh_GFyoEfEYY@1 XGS)Hxw4CffWwAhG%EBzdpD%50_8YG6fX*3?b zW$@$r$mcs_l~gLF`QdYE)URKnZWrd-Te9=06DoIc60C3xCR;nu>oX>Yf^u^Qby2c% z_nv)kUSIZMb98n$$aoGi++|ED Riw?^V|<3|;+xq*2b^A(Dp&Fp^H zM7yIiza9j*4d`bp^M$TZ*F>Vip~~ZgPjDPcnO5n>7mK@Qou4|nUhG*zLvxJNYLz__ zR8>t$Jyikh?=0(eV+1EJaZfYSZ?Cx3>@vPk#JRmH7RBNBSJc$}TI#QKdosfbe8Sdv z!(%1$Z5W3O+&PeZuOYMT()HZ`Y5UndfRUIvFAVwHps3ac$ib=AH_J!|6x73(@By36 z;s=$IoABo@bv|wd1o1$@#w+*hYlrKOW5E=s1w@$f;Huych(VXU^m!uL-8PcjR>whQ z@X-=4#S^!M?4CylJ~od|;4ZX25dyd)D7y~@Gp6SX(4nZ4l#~T03IxZ;$E1E^tKKB& zvo2c?dL|D)6}l#x0hh;fdlqwZP^g@!s8CRQdjSFPI(`oVCM<(2dJRL3zl8kmoL40i zQ9711xeK-Jp1pYrV!FzhtlU!xoGQj^_?s@-7>g66PI~JjwPuA;_Vk6cBJmlxVrMd& z?^N|pH!o_%1upIOZ(b^T`H>ucCP4E9-1a}xa7M~ZRM{w?Q?5!FWG^|?FP%^`m-u|0 za=;cBrKN6Ez!xRztDNQ1Nk~`{O%G?KDKQZ@lF}JDkVAAwC_!U1orH6w-4U&9q$xn^ z0X=-=SoX6{JdNBrpCKN5zFT_@?0 @AfPf+vUpty!Vr{0uz+AE?viDOS*_ zkR)0mAvZWwcK;|F@iAoO{?T>wEB{w|Uj8(%714grvi^RC^x_aqt!Wp%Fqc4}y+z6i zoR8D()_srXZivP%XBGhh&}|j1o7ewor)nIC%Oo&u$RwD1|NICXhq-hs9FW$!lCDmh z>C{H6-P*X?$jQ zJ9>P5jZTI5eh9jIT_tGzazoKLkGwr5;pMm(T4sJ^&dg84A${Sx3@*dk{~VZI`OCH? zZ({GVYz(VRo^Is;4s4rVlx>RRR#Nc9OH-EnSo~bq+-!^5Sw40&`2DybGc&Va$j2Vi z;QIZv;h8t0`hJMmm)RgU&m9eg7*AK=*~ROc9a=eFc(%2P7w_nhO+16{P#1iIJoIA@ z^Y~Y+2T1q&n7bB{(KN$hl?lEeLN8qNTarlaa(k@DtRDeb_uJMBlAm5g8;IL>=7!k5 zj@zPINK&BU@sQ}!33_P4q?PY9u%H^AK%k|KnvC}6P`0c RI;F0+ f|Bv6>XTnd=EF}udFLN^4e?ny>6(#D$j6?nx1{YF| literal 0 HcmV?d00001 diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 70212a51f94..b9c58bdc699 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -184,7 +184,7 @@ If a single push includes changes to more than three branches or tags, integrati supported by `push_hooks` and `tag_push_hooks` events are not executed. To change the number of supported branches or tags, configure the -[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). +[`push_event_hooks_limit` setting](../../../api/settings.md#available-settings). ## SSL verification diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index cbf7341e018..b93e0de2339 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -56,7 +56,7 @@ Push events are triggered when you push to the repository, except when: - You push tags. - A single push includes changes for more than three branches by default - (depending on the [`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)). + (depending on the [`push_event_hooks_limit` setting](../../../api/settings.md#available-settings)). If you push more than 20 commits at once, the `commits` attribute in the payload contains information about the newest 20 commits only. @@ -155,7 +155,7 @@ Tag events are triggered when you create or delete tags in the repository. This hook is not executed if a single push includes changes for more than three tags by default (depending on the -[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)). +[`push_event_hooks_limit` setting](../../../api/settings.md#available-settings)). Request header: diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 04e6fb4fb54..0f031b77065 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -286,7 +286,7 @@ DETAILS: Administrators can set the maximum import file size one of two ways: -- With the `max_import_size` option in the [Application settings API](../../../api/settings.md#change-application-settings). +- With the `max_import_size` option in the [Application settings API](../../../api/settings.md#update-application-settings). - In the [**Admin** area UI](../../../administration/settings/import_and_export_settings.md#max-import-size). The default is `0` (unlimited). @@ -342,7 +342,7 @@ The maximum import file size depends on whether you import to GitLab Self-Manage - If importing to a GitLab Self-Managed instance, you can import a import file of any size. Administrators can change this behavior using either: - - The `max_import_size` option in the [Application settings API](../../../api/settings.md#change-application-settings). + - The `max_import_size` option in the [Application settings API](../../../api/settings.md#update-application-settings). - The [**Admin** area](../../../administration/settings/account_and_limit_settings.md). - On GitLab.com, you can import groups using import files of no more than [5 GB](../../gitlab_com/index.md#account-and-limit-settings) in size. diff --git a/keeps/overdue_finalize_background_migration.rb b/keeps/overdue_finalize_background_migration.rb index 83c6130bd16..ce8c0d121cc 100644 --- a/keeps/overdue_finalize_background_migration.rb +++ b/keeps/overdue_finalize_background_migration.rb @@ -44,23 +44,28 @@ module Keeps migration_name = truncate_migration_name("Finalize#{migration['migration_job_name']}") PostDeploymentMigration::PostDeploymentMigrationGenerator .source_root('generator_templates/post_deployment_migration/post_deployment_migration/') - generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name], { skip: true }) - migration_file = generator.invoke_all.first - change.changed_files = [migration_file] - add_ensure_call_to_migration(migration_file, queue_method_node, job_name, migration_record) - ::Gitlab::Housekeeper::Shell.rubocop_autocorrect(migration_file) + begin + generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name]) + migration_file = generator.invoke_all.first + change.changed_files = [migration_file] - digest = Digest::SHA256.hexdigest(generator.migration_number) - digest_file = Pathname.new('db').join('schema_migrations', generator.migration_number.to_s).to_s - File.open(digest_file, 'w') { |f| f.write(digest) } + add_ensure_call_to_migration(migration_file, queue_method_node, job_name, migration_record) + ::Gitlab::Housekeeper::Shell.rubocop_autocorrect(migration_file) - add_finalized_by_to_yaml(migration_yaml_file, generator.migration_number) + digest = Digest::SHA256.hexdigest(generator.migration_number) + digest_file = Pathname.new('db').join('schema_migrations', generator.migration_number.to_s).to_s + File.open(digest_file, 'w') { |f| f.write(digest) } - change.changed_files << digest_file - change.changed_files << migration_yaml_file + add_finalized_by_to_yaml(migration_yaml_file, generator.migration_number) - yield(change) + change.changed_files << digest_file + change.changed_files << migration_yaml_file + + yield(change) + rescue Rails::Generators::Error + next + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3f188b5b873..21e4c17d54c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1361,24 +1361,6 @@ msgid_plural "%{strongStart}%{count}%{strongEnd} commits" msgstr[0] "" msgstr[1] "" -msgid "%{strongStart}%{you}%{strongEnd}%{break}%{name} left feedback" -msgstr "" - -msgid "%{strongStart}%{you}%{strongEnd}%{break}%{name} requested changes" -msgstr "" - -msgid "%{strongStart}%{you}%{strongEnd}%{break}%{name} started a review" -msgstr "" - -msgid "%{strongStart}%{you}%{strongEnd}%{break}Approved by %{name}" -msgstr "" - -msgid "%{strongStart}%{you}%{strongEnd}%{break}Assigned to %{name}" -msgstr "" - -msgid "%{strongStart}%{you}%{strongEnd}%{break}Review requested from %{name}" -msgstr "" - msgid "%{strongStart}Merge reports (%{reportsCount}):%{strongEnd} %{summaryText}" msgstr "" @@ -3647,6 +3629,9 @@ msgstr "" msgid "Adjust how frequently the GitLab UI polls for updates." msgstr "" +msgid "Admin Area > GitLab Duo" +msgstr "" + msgid "Admin Mode" msgstr "" @@ -5826,6 +5811,9 @@ msgid_plural "All projects in %{groupsLength} groups:" msgstr[0] "" msgstr[1] "" +msgid "All projects in authentication log" +msgstr "" + msgid "All projects selected" msgstr "" @@ -7770,9 +7758,6 @@ msgstr "" msgid "Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}" msgstr "" -msgid "Are you ABSOLUTELY SURE you wish to delete this group?" -msgstr "" - msgid "Are you absolutely sure?" msgstr "" @@ -17186,6 +17171,9 @@ msgstr "" msgid "CredentialsInventory|Expiration date" msgstr "" +msgid "CredentialsInventory|Expired" +msgstr "" + msgid "CredentialsInventory|GPG Keys" msgstr "" @@ -17207,6 +17195,9 @@ msgstr "" msgid "CredentialsInventory|Project and group access tokens" msgstr "" +msgid "CredentialsInventory|Revoke" +msgstr "" + msgid "CredentialsInventory|Revoked" msgstr "" @@ -19063,6 +19054,9 @@ msgstr "" msgid "Delete group" msgstr "" +msgid "Delete group immediately" +msgstr "" + msgid "Delete group immediately?" msgstr "" @@ -19093,6 +19087,9 @@ msgstr "" msgid "Delete project" msgstr "" +msgid "Delete project immediately" +msgstr "" + msgid "Delete release" msgstr "" @@ -19137,9 +19134,6 @@ msgstr "" msgid "Delete this epic and release all child items?" msgstr "" -msgid "Delete this project" -msgstr "" - msgid "Delete user list" msgstr "" @@ -19200,9 +19194,6 @@ msgstr "" msgid "Deleted commits:" msgstr "" -msgid "Deleted group can not be restored!" -msgstr "" - msgid "Deleted the source branch." msgstr "" @@ -19221,9 +19212,6 @@ msgstr "" msgid "Deleting protected branches is blocked by security policies" msgstr "" -msgid "Deleting this group also deletes all child projects, including archived projects, and their resources." -msgstr "" - msgid "Deletion scheduled at: %{time}" msgstr "" @@ -27345,6 +27333,9 @@ msgstr "" msgid "Group navigation" msgstr "" +msgid "Group or project" +msgstr "" + msgid "Group overview content" msgstr "" @@ -41282,9 +41273,6 @@ msgstr "" msgid "Permalink" msgstr "" -msgid "Permanently delete group" -msgstr "" - msgid "Permissions" msgstr "" @@ -53540,6 +53528,9 @@ msgstr "" msgid "Settings" msgstr "" +msgid "Settings > GitLab Duo" +msgstr "" + msgid "Settings for the License Compliance feature" msgstr "" @@ -57832,16 +57823,16 @@ msgstr "" msgid "This action cannot be undone, and will permanently delete the %{key} SSH key. All commits signed using this SSH key will be marked as unverified." msgstr "" -msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}" +msgid "This action will permanently delete this group, including its subgroups and projects." msgstr "" -msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains." +msgid "This action will permanently delete this project, including all its resources." msgstr "" -msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}" +msgid "This action will place this group, including its subgroups and projects, in a pending deletion state for %{deletion_delayed_period} days, and delete it permanently on %{date}." msgstr "" -msgid "This action will %{strongOpen}permanently remove%{strongClose} %{codeOpen}%{group}%{codeClose} %{strongOpen}immediately%{strongClose}." +msgid "This action will place this project, including all its resources, in a pending deletion state for %{deletion_adjourned_period} days, and delete it permanently on %{strongOpen}%{date}%{strongClose}." msgstr "" msgid "This also resolves all related threads" @@ -58036,9 +58027,6 @@ msgstr "" msgid "This group and its subgroups and projects are pending deletion, and will be deleted on %{date}." msgstr "" -msgid "This group and its subgroups and projects will be placed in a 'pending deletion' state for %{deletion_delayed_period} days, then permanently deleted on %{date}. The group can be fully restored before that date." -msgstr "" - msgid "This group can be restored until %{date}. %{linkStart}Learn more%{linkEnd}." msgstr "" @@ -58075,6 +58063,9 @@ msgstr "" msgid "This group is not permitted to create compliance violations" msgstr "" +msgid "This group is scheduled for deletion on %{date}. This action will permanently delete this group, including its subgroups and projects, %{strong_open}immediately%{strong_close}. This action cannot be undone." +msgstr "" + msgid "This group is scheduled to be deleted on %{date}. You are about to delete this group, including its subgroups and projects, immediately. This action cannot be undone." msgstr "" @@ -58141,9 +58132,6 @@ msgstr "" msgid "This is the only time the secret is accessible. Copy the secret and store it securely." msgstr "" -msgid "This is you." -msgstr "" - msgid "This is your current session" msgstr "" @@ -58401,6 +58389,9 @@ msgstr "" msgid "This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}" msgstr "" +msgid "This project is scheduled for deletion on %{strongOpen}%{date}%{strongClose}. This action will permanently delete this project, including all its resources, %{strongOpen}immediately%{strongClose}. This action cannot be undone." +msgstr "" + msgid "This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}" msgstr "" diff --git a/qa/qa/support/formatters/allure_metadata_formatter.rb b/qa/qa/support/formatters/allure_metadata_formatter.rb index 34731b11536..e8f6c4bcafe 100644 --- a/qa/qa/support/formatters/allure_metadata_formatter.rb +++ b/qa/qa/support/formatters/allure_metadata_formatter.rb @@ -42,7 +42,7 @@ module QA example = example_notification.example add_quarantine_issue_link(example) - add_failure_issues_link(example) + add_failure_issues_link(example, example_notification) add_ci_job_link(example) set_flaky_status(example) set_behaviour_categories(example) @@ -68,19 +68,21 @@ module QA # # @param [RSpec::Core::Example] example # @return [void] - def add_failure_issues_link(example) + def add_failure_issues_link(example, example_notification) return unless example.execution_result.status == :failed search_parameters = { - sort: "updated_desc", - scope: "all", - state: "opened" - }.map { |key, value| "#{key}=#{value}" }.join("&") + sort: 'updated_desc', + scope: 'all', + state: 'opened' + }.map { |key, value| "#{key}=#{value}" }.join('&') + exception_message = example.exception.message || "" + exception_message_lines = strip_ansi_codes(example_notification.message_lines) || [] search_terms = { - test_file_path: ERB::Util.url_encode(example.file_path.gsub('./qa/specs/features/', '').to_s), - exception_message: ERB::Util.url_encode(example.exception.message) - }.map { |_, value| "search=#{value}" }.join("&") + test_file_path: example.file_path.gsub('./qa/specs/features/', '').to_s, + exception_message: exception_message_lines.empty? ? exception_message : exception_message_lines.join("\n") + }.map { |_, value| "search=#{ERB::Util.url_encode(value)}" }.join('&') search_url = "https://gitlab.com/gitlab-org/gitlab/-/issues?#{search_parameters}{search_terms}" example.issue('Failure issues', search_url) @@ -88,6 +90,11 @@ module QA log(:error, "Failed to add failure issue link for example '#{example.description}', error: #{e}") end + def strip_ansi_codes(strings) + modified = Array(strings).map { |string| string.dup.gsub(/\x1b\[{1,2}[0-9;:?]*m/m, '') } + modified.size == 1 ? modified[0] : modified + end + # Add ci job link # # @param [RSpec::Core::Example] example diff --git a/qa/spec/support/formatters/allure_metadata_formatter_spec.rb b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb index 9a7bb84a1b2..53b515bfbe9 100644 --- a/qa/spec/support/formatters/allure_metadata_formatter_spec.rb +++ b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb @@ -6,7 +6,11 @@ describe QA::Support::Formatters::AllureMetadataFormatter do let(:formatter) { described_class.new(StringIO.new) } let(:rspec_example_notification) do - instance_double(RSpec::Core::Notifications::ExampleNotification, example: rspec_example) + instance_double( + RSpec::Core::Notifications::FailedExampleNotification, + example: rspec_example, + message_lines: ["Some failure", "message"] + ) end # rubocop:disable RSpec/VerifiedDoubles @@ -49,9 +53,29 @@ describe QA::Support::Formatters::AllureMetadataFormatter do expect(rspec_example).to have_received(:issue).with( 'Failure issues', 'https://gitlab.com/gitlab-org/gitlab/-/issues?sort=updated_desc&scope=all&state=opened&' \ - 'search=spec.rb&search=Some%20failure%20message' + 'search=spec.rb&search=Some%20failure%0Amessage' ) end + + context 'when message_lines is empty' do + let(:rspec_example_notification) do + instance_double( + RSpec::Core::Notifications::FailedExampleNotification, + example: rspec_example, + message_lines: [] + ) + end + + it 'uses exception message for the search URL', :aggregate_failures do + formatter.example_finished(rspec_example_notification) + + expect(rspec_example).to have_received(:issue).with( + 'Failure issues', + 'https://gitlab.com/gitlab-org/gitlab/-/issues?sort=updated_desc&scope=all&state=opened&' \ + 'search=spec.rb&search=Some%20failure%20message' + ) + end + end end context 'with flaky test data', :aggregate_failures do diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 7a4b6f139fa..4c16d26ea6c 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -61,6 +61,21 @@ RSpec.describe Oauth::AuthorizationsController, :with_current_organization, feat end end + shared_examples 'RequestPayloadLogger information appended' do + it 'logs custom information in the payload' do + expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload| + method.call(payload) + + expect(payload[:remote_ip]).to be_present + expect(payload[:username]).to eq(user.username) + expect(payload[:user_id]).to be_present + expect(payload[:ua]).to be_present + end + + subject + end + end + describe 'GET #new' do subject { get :new, params: params } @@ -336,11 +351,15 @@ RSpec.describe Oauth::AuthorizationsController, :with_current_organization, feat end end end + + it_behaves_like "RequestPayloadLogger information appended" end describe 'POST #create' do subject { post :create, params: params } + it_behaves_like "RequestPayloadLogger information appended" + include_examples 'OAuth Authorizations require confirmed user' end diff --git a/spec/factories/merge_request_approval_metrics.rb b/spec/factories/merge_request_approval_metrics.rb new file mode 100644 index 00000000000..78f93f9427e --- /dev/null +++ b/spec/factories/merge_request_approval_metrics.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :merge_request_approval_metrics, class: "MergeRequest::ApprovalMetrics" do + merge_request + last_approved_at { Time.current } + target_project { merge_request.target_project } + end +end diff --git a/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js index f64f9358d42..e5b62ed4e36 100644 --- a/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js +++ b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js @@ -287,20 +287,39 @@ describe('Job table app', () => { }); describe('cancel jobs button', () => { - it('should display cancel all jobs button', async () => { - createComponent({ cancelableHandler: cancelHandler, stubs: { JobsTableTabs } }); + const options = { + cancelableHandler: cancelHandler, + stubs: { JobsTableTabs }, + }; - await waitForPromises(); + describe('when there are cancelable jobs', () => { + it('should display cancel all jobs button', async () => { + createComponent({ ...options, provideOptions: { canUpdateAllJobs: true } }); - expect(findCancelJobsButton().exists()).toBe(true); + await waitForPromises(); + + expect(findCancelJobsButton().exists()).toBe(true); + }); + + describe('when canUpdateAllJobs is false', () => { + it('should not display cancel all jobs button', async () => { + createComponent({ ...options, provideOptions: { canUpdateAllJobs: false } }); + + await waitForPromises(); + + expect(findCancelJobsButton().exists()).toBe(false); + }); + }); }); - it('should not display cancel all jobs button', async () => { - createComponent(); + describe('when there are no cancelable jobs', () => { + it('should not display cancel all jobs button', async () => { + createComponent({ provideOptions: { canUpdateAllJobs: true } }); - await waitForPromises(); + await waitForPromises(); - expect(findCancelJobsButton().exists()).toBe(false); + expect(findCancelJobsButton().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/merge_request_dashboard/components/__snapshots__/assigned_users_spec.js.snap b/spec/frontend/merge_request_dashboard/components/__snapshots__/assigned_users_spec.js.snap index 4a40fccacb9..70b8b32b23e 100644 --- a/spec/frontend/merge_request_dashboard/components/__snapshots__/assigned_users_spec.js.snap +++ b/spec/frontend/merge_request_dashboard/components/__snapshots__/assigned_users_spec.js.snap @@ -29,8 +29,10 @@ exports[`Merge request dashboard assigned users component renders user avatars 1 class="gl-avatars-inline-child" > @@ -60,8 +62,10 @@ exports[`Merge request dashboard assigned users component renders user avatars 1 class="gl-avatars-inline-child" > diff --git a/spec/frontend/merge_request_dashboard/components/assigned_users_spec.js b/spec/frontend/merge_request_dashboard/components/assigned_users_spec.js index 9a237aa8f17..df295ec0986 100644 --- a/spec/frontend/merge_request_dashboard/components/assigned_users_spec.js +++ b/spec/frontend/merge_request_dashboard/components/assigned_users_spec.js @@ -2,7 +2,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import AssignedUsers from '~/merge_request_dashboard/components/assigned_users.vue'; let wrapper; -let glTooltipDirectiveMock; const createMockUsers = () => [ { @@ -27,12 +26,7 @@ function createComponent({ type = 'ASSIGNEES', newListsEnabled = false, } = {}) { - glTooltipDirectiveMock = jest.fn(); - wrapper = mountExtended(AssignedUsers, { - directives: { - GlTooltip: glTooltipDirectiveMock, - }, provide: { newListsEnabled, }, @@ -70,14 +64,6 @@ describe('Merge request dashboard assigned users component', () => { expect(findCurrentUserIcon().exists()).toBe(true); }); - it('adds this is you text to tooltip', () => { - createComponent(); - - expect(glTooltipDirectiveMock.mock.calls[1][1].value).toBe( - 'This is you.
Assigned to Admin', - ); - }); - it('renders current user last', () => { createComponent(); @@ -100,30 +86,5 @@ describe('Merge request dashboard assigned users component', () => { expect(findReviewStateIcon().exists()).toBe(true); expect(findReviewStateIcon().html()).toMatchSnapshot(); }); - - it.each` - state | title - ${'REQUESTED_CHANGES'} | ${'Admin requested changes'} - ${'APPROVED'} | ${'Approved by Admin'} - ${'REVIEWED'} | ${'Admin left feedback'} - ${'UNREVIEWED'} | ${'Review requested from Admin'} - `('sets title as $title for review state $state', ({ state, title }) => { - createComponent({ - type: 'REVIEWER', - users: [ - { - id: 'gid://gitlab/user/2', - webUrl: '/root', - name: 'Admin', - avatarUrl: '/root', - mergeRequestInteraction: { - reviewState: state, - }, - }, - ], - }); - - expect(glTooltipDirectiveMock.mock.calls[0][1].value).toBe(title); - }); }); }); diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js index 506cc7a43e4..b3d54bc7608 100644 --- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -272,20 +272,39 @@ describe('Job table app', () => { }); describe('cancel jobs button', () => { - it('should display cancel all jobs button', async () => { - createComponent({ cancelableHandler: cancelHandler, stubs: { JobsTableTabs } }); + describe('when there are cancelable jobs', () => { + const options = { + cancelableHandler: cancelHandler, + stubs: { JobsTableTabs }, + }; - await waitForPromises(); + it('should display cancel all jobs button', async () => { + createComponent({ ...options, provideOptions: { canUpdateAllJobs: true } }); - expect(findCancelJobsButton().exists()).toBe(true); + await waitForPromises(); + + expect(findCancelJobsButton().exists()).toBe(true); + }); + + describe('when canUpdateAllJobs is false', () => { + it('should not display cancel all jobs button', async () => { + createComponent({ ...options, provideOptions: { canUpdateAllJobs: false } }); + + await waitForPromises(); + + expect(findCancelJobsButton().exists()).toBe(false); + }); + }); }); - it('should not display cancel all jobs button', async () => { - createComponent(); + describe('when there are no cancelable jobs', () => { + it('should not display cancel all jobs button', async () => { + createComponent({ provideOptions: { canUpdateAllJobs: true } }); - await waitForPromises(); + await waitForPromises(); - expect(findCancelJobsButton().exists()).toBe(false); + expect(findCancelJobsButton().exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js index bae76e7eeb6..8e3238cf2f2 100644 --- a/spec/frontend/projects/components/project_delete_button_spec.js +++ b/spec/frontend/projects/components/project_delete_button_spec.js @@ -18,6 +18,7 @@ describe('Project remove modal', () => { mergeRequestsCount: 2, forksCount: 3, starsCount: 4, + buttonText: 'Delete project', }; const createComponent = (props = {}) => { @@ -51,6 +52,7 @@ describe('Project remove modal', () => { issuesCount: defaultProps.issuesCount, mergeRequestsCount: defaultProps.mergeRequestsCount, starsCount: defaultProps.starsCount, + buttonText: defaultProps.buttonText, }); }); }); diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js index 556c1ae7084..b8758641d41 100644 --- a/spec/frontend/projects/components/shared/delete_button_spec.js +++ b/spec/frontend/projects/components/shared/delete_button_spec.js @@ -10,6 +10,7 @@ describe('DeleteButton', () => { const findForm = () => wrapper.findComponent(GlForm); const findModal = () => wrapper.findComponent(DeleteModal); + const findDeleteButton = () => wrapper.findComponent(GlButton); const defaultPropsData = { confirmPhrase: 'foo', @@ -58,7 +59,7 @@ describe('DeleteButton', () => { describe('when button is clicked', () => { beforeEach(() => { createComponent(); - wrapper.findComponent(GlButton).vm.$emit('click'); + findDeleteButton().vm.$emit('click'); }); it('opens modal', () => { @@ -85,4 +86,20 @@ describe('DeleteButton', () => { expect(wrapper.findByTestId('modal-footer-slot').exists()).toBe(true); }); + + it('renders default text', () => { + createComponent(); + + const button = findDeleteButton(); + + expect(button.text()).toBe('Delete project'); + }); + + it('renders custom text', () => { + createComponent({ buttonText: 'Delete project immediately' }); + + const button = findDeleteButton(); + + expect(button.text()).toBe('Delete project immediately'); + }); }); diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js index b9076598071..0401416a979 100644 --- a/spec/frontend/token_access/inbound_token_access_spec.js +++ b/spec/frontend/token_access/inbound_token_access_spec.js @@ -6,6 +6,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue'; +import { JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT } from '~/token_access/constants'; import NamespaceForm from '~/token_access/components/namespace_form.vue'; import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql'; import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql'; @@ -58,6 +59,7 @@ describe('TokenAccess component', () => { const failureHandler = jest.fn().mockRejectedValue(error); const mockToastShow = jest.fn(); + const findFormSelector = () => wrapper.findByTestId('form-selector'); const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findToggleFormBtn = () => wrapper.findByTestId('crud-form-toggle'); @@ -72,13 +74,18 @@ describe('TokenAccess component', () => { const createComponent = ( requestHandlers, - { addPoliciesToCiJobToken = false, enforceAllowlist = false, stubs = {} } = {}, + { + addPoliciesToCiJobToken = false, + authenticationLogsMigrationForAllowlist = false, + enforceAllowlist = false, + stubs = {}, + } = {}, ) => { wrapper = shallowMountExtended(InboundTokenAccess, { provide: { fullPath: projectPath, enforceAllowlist, - glFeatures: { addPoliciesToCiJobToken }, + glFeatures: { addPoliciesToCiJobToken, authenticationLogsMigrationForAllowlist }, }, apolloProvider: createMockApollo(requestHandlers), mocks: { @@ -349,6 +356,53 @@ describe('TokenAccess component', () => { }); }); + describe('when authenticationLogsMigrationForAllowlist feature flag is disabled', () => { + beforeEach(() => + createComponent( + [ + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + ], + { authenticationLogsMigrationForAllowlist: false, stubs: { CrudComponent } }, + ), + ); + + it('renders toggle form button and hides actions dropdown', () => { + expect(findToggleFormBtn().exists()).toBe(true); + expect(findFormSelector().exists()).toBe(false); + }); + }); + + describe('when authenticationLogsMigrationForAllowlist feature flag is enabled', () => { + beforeEach(() => + createComponent( + [ + [ + inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, + inboundGroupsAndProjectsWithScopeResponseHandler, + ], + ], + { authenticationLogsMigrationForAllowlist: true, stubs: { CrudComponent } }, + ), + ); + + it('toggle form button is replaced by actions dropdown', () => { + expect(findToggleFormBtn().exists()).toBe(false); + expect(findFormSelector().exists()).toBe(true); + }); + + it('Add group or project option renders the namespace form', async () => { + expect(findNamespaceForm().exists()).toBe(false); + + findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT); + await nextTick(); + + expect(findNamespaceForm().exists()).toBe(true); + }); + }); + describe.each` type | mutation | handler ${'Group'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler} diff --git a/spec/frontend/vue_shared/components/crud_component_spec.js b/spec/frontend/vue_shared/components/crud_component_spec.js index d7b17bcc77a..01bc9914852 100644 --- a/spec/frontend/vue_shared/components/crud_component_spec.js +++ b/spec/frontend/vue_shared/components/crud_component_spec.js @@ -241,4 +241,15 @@ describe('CRUD Component', () => { ); }); }); + + describe('actions slot', () => { + it('passes the showForm function to the actions slot', () => { + const actionsSlot = jest.fn(); + createComponent({}, { actions: actionsSlot }); + + expect(actionsSlot).toHaveBeenCalledWith( + expect.objectContaining({ showForm: wrapper.vm.showForm }), + ); + }); + }); }); diff --git a/spec/helpers/admin/jobs_helper_spec.rb b/spec/helpers/admin/jobs_helper_spec.rb new file mode 100644 index 00000000000..740e7b5d6e5 --- /dev/null +++ b/spec/helpers/admin/jobs_helper_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Admin::JobsHelper, feature_category: :continuous_integration do + let_it_be(:user) { build_stubbed(:user, :admin) } + + before do + allow(helper).to receive_messages( + current_user: user, + job_statuses: {} + ) + end + + describe '#admin_jobs_app_data', :enable_admin_mode do + subject(:data) { helper.admin_jobs_app_data } + + it 'contains the correct data' do + expect(data).to include( + job_statuses: {}.to_json, + empty_state_svg_path: helper.image_path('illustrations/empty-state/empty-pipeline-md.svg'), + url: cancel_all_admin_jobs_path, + can_update_all_jobs: 'true' + ) + end + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index bebbdc6e915..ff31479e15f 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -2024,15 +2024,13 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do subject { helper.delete_immediately_message(project) } it 'returns correct message' do - expect(subject).to eq "This action deletes#{project.path_with_namespace}and everything this project contains. There is no going back." + expect(subject).to eq "This action will permanently delete this project, including all its resources." end end describe '#project_delete_immediately_button_data' do - subject { helper.project_delete_immediately_button_data(project) } - - it 'returns expected hash' do - expect(subject).to match({ + let(:base_button_data) do + { form_path: project_path(project, permanently_delete: true), confirm_phrase: project.path_with_namespace, is_fork: 'false', @@ -2040,7 +2038,23 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do merge_requests_count: '0', forks_count: '0', stars_count: '0' - }) + } + end + + describe 'with default button text' do + subject { helper.project_delete_immediately_button_data(project) } + + it 'returns expected hash' do + expect(subject).to match(base_button_data.merge(button_text: 'Delete project')) + end + end + + describe 'with custom button text' do + subject { helper.project_delete_immediately_button_data(project, 'Delete project immediately') } + + it 'returns expected hash' do + expect(subject).to match(base_button_data.merge(button_text: 'Delete project immediately')) + end end end diff --git a/spec/keeps/overdue_finalize_background_migration_spec.rb b/spec/keeps/overdue_finalize_background_migration_spec.rb index 64ec2e4211b..8cd32d32c48 100644 --- a/spec/keeps/overdue_finalize_background_migration_spec.rb +++ b/spec/keeps/overdue_finalize_background_migration_spec.rb @@ -67,4 +67,51 @@ RSpec.describe Keeps::OverdueFinalizeBackgroundMigration, feature_category: :too end end end + + describe '#each_change' do + let(:migration) do + { 'milestone' => '15.0', 'migration_job_name' => 'TestMigration', 'feature_category' => 'shared' } + end + + let(:migration_record) { MigrationRecord.new(id: 1, finished_at: "2020-04-01 12:00:01") } + let(:queue_method_node) do + instance_double(RuboCop::AST::SendNode, children: [nil, nil, nil, + instance_double(RuboCop::AST::StrNode, source: 'table'), + instance_double(RuboCop::AST::StrNode, source: 'column')]) + end + + let(:generator) { instance_double(::PostDeploymentMigration::PostDeploymentMigrationGenerator) } + let(:groups_helper) { instance_double(::Keeps::Helpers::Groups) } + + before do + allow(keep).to receive_messages(batched_background_migrations: { 'path/to/migration.yml' => migration }, + before_cuttoff_milestone?: true, migration_finalized?: false, fetch_migration_status: migration_record, + last_migration_for_job: 'path/to/last_migration.rb', find_queue_method_node: queue_method_node, + groups_helper: groups_helper + ) + allow(groups_helper).to receive_messages(labels_for_feature_category: [], + pick_reviewer_for_feature_category: "reviewer") + allow(PostDeploymentMigration::PostDeploymentMigrationGenerator).to receive(:source_root) + + stub_request(:any, /.*/).to_return(status: 200, body: "", headers: {}) + end + + context 'when generator raises Rails::Generators::Error' do + before do + allow(::PostDeploymentMigration::PostDeploymentMigrationGenerator).to receive(:new) + .and_raise(Rails::Generators::Error) + end + + it 'skips to the next iteration' do + changes = [] + keep.each_change { |change| changes << change } + + expect(changes).to be_empty + end + + it 'does not raise the error' do + expect { keep.each_change { |change| change } }.not_to raise_error + end + end + end end diff --git a/spec/models/merge_request/approval_metrics_spec.rb b/spec/models/merge_request/approval_metrics_spec.rb new file mode 100644 index 00000000000..9e3a3c0681b --- /dev/null +++ b/spec/models/merge_request/approval_metrics_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequest::ApprovalMetrics, feature_category: :code_review_workflow do + describe 'associations' do + it { is_expected.to belong_to(:merge_request).required } + it { is_expected.to belong_to(:target_project).class_name('Project').inverse_of(:merge_requests).required } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:last_approved_at) } + end + + describe '.refresh_last_approved_at' do + let(:merge_request) { create(:merge_request) } + + it 'creates a new record if it does not exist' do + expect do + described_class.refresh_last_approved_at( + merge_request: merge_request, + last_approved_at: Time.current + ) + end.to change { described_class.count }.by(1) + end + + it 'updates an existing record if it exists' do + existing_metrics = create(:merge_request_approval_metrics, merge_request: merge_request) + new_timestamp = Time.current + + described_class.refresh_last_approved_at( + merge_request: merge_request, + last_approved_at: new_timestamp + ) + + existing_metrics.reload + expect(existing_metrics.last_approved_at).to be_within(1.second).of(new_timestamp) + end + + it 'sets the last_approved_at to the expected time', :freeze_time do + described_class.refresh_last_approved_at( + merge_request: merge_request, + last_approved_at: Time.current + ) + + metrics = described_class.find_by!(merge_request_id: merge_request.id) + expect(metrics.last_approved_at).to be_within(1.second).of(Time.current) + end + + it 'does not update last_approved_at if the new timestamp is older' do + newer_timestamp = 2.days.from_now + older_timestamp = 1.day.from_now + + existing_metrics = create( + :merge_request_approval_metrics, + merge_request: merge_request, + last_approved_at: newer_timestamp + ) + + described_class.refresh_last_approved_at( + merge_request: merge_request, + last_approved_at: older_timestamp + ) + + existing_metrics.reload + expect(existing_metrics.last_approved_at).to be_within(1.second).of(newer_timestamp) + end + end +end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index 5f0fd9f459e..19554407025 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -741,4 +741,16 @@ RSpec.describe GlobalPolicy, feature_category: :shared do it { is_expected.to be_disallowed(:create_organizatinon) } end end + + describe 'admin pages' do + context 'with regular user' do + it { is_expected.to be_disallowed(:read_admin_cicd) } + end + + context 'with an admin', :enable_admin_mode do + let(:current_user) { admin_user } + + it { is_expected.to be_allowed(:read_admin_cicd) } + end + end end diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb index bfa3dc98d0a..c40313b90f0 100644 --- a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb +++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb @@ -242,11 +242,11 @@ RSpec.describe 'Query.project.pipelineAnalytics', :aggregate_failures, :click_ho context 'with no pipelines in time window', time_travel_to: Time.utc(2024, 1, 1) do let(:expected_duration_statistics) do { - 'p50' => nil, - 'p75' => nil, - 'p90' => nil, - 'p95' => nil, - 'p99' => nil + 'p50' => 0, + 'p75' => 0, + 'p90' => 0, + 'p95' => 0, + 'p99' => 0 } end diff --git a/spec/services/ci/collect_aggregate_pipeline_analytics_service_spec.rb b/spec/services/ci/collect_aggregate_pipeline_analytics_service_spec.rb index e0ba7bbf8cb..765bcc924eb 100644 --- a/spec/services/ci/collect_aggregate_pipeline_analytics_service_spec.rb +++ b/spec/services/ci/collect_aggregate_pipeline_analytics_service_spec.rb @@ -105,7 +105,7 @@ RSpec.describe ::Ci::CollectAggregatePipelineAnalyticsService, :click_house, :en it 'returns empty aggregate analytics' do expect(result.errors).to eq([]) expect(result.success?).to be true - expect(result.payload[:aggregate]).to eq(count: { any: 0 }, duration_statistics: { p50: nil, p99: nil }) + expect(result.payload[:aggregate]).to eq(count: { any: 0 }, duration_statistics: { p50: 0, p99: 0 }) end end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 14faf6bca07..d99d9197885 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -97,9 +97,12 @@ module Ci retrieve_queue_duration_s: anything, process_build_duration_s: { count: 1, max: anything, sum: anything }, process_build_runner_matched_duration_s: { count: 1, max: anything, sum: anything }, - process_build_link_identity_duration_s: { count: 1, max: anything, sum: anything }, process_build_present_build_duration_s: { count: 1, max: anything, sum: anything }, - process_build_assign_runner_duration_s: { count: 1, max: anything, sum: anything } + present_build_presenter_duration_s: { count: 1, max: anything, sum: anything }, + present_build_logs_duration_s: { count: 1, max: anything, sum: anything }, + present_build_response_json_duration_s: { count: 1, max: anything, sum: anything }, + process_build_assign_runner_duration_s: { count: 1, max: anything, sum: anything }, + assign_runner_run_duration_s: { count: 1, max: anything, sum: anything } ) ) @@ -943,8 +946,7 @@ module Ci total_duration_s: anything, process_queue_duration_s: anything, retrieve_queue_duration_s: anything, - process_build_duration_s: { count: 1, max: anything, sum: anything }, - process_build_not_pending_duration_s: { count: 1, max: anything, sum: anything } + process_build_duration_s: { count: 1, max: anything, sum: anything } ) ) diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb index 5697183561a..8fd78519893 100644 --- a/spec/services/merge_requests/approval_service_spec.rb +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -108,7 +108,9 @@ RSpec.describe MergeRequests::ApprovalService, feature_category: :code_review_wo it 'publishes MergeRequests::ApprovedEvent' do expect { service.execute(merge_request) } .to publish_event(MergeRequests::ApprovedEvent) - .with(current_user_id: user.id, merge_request_id: merge_request.id) + .with(current_user_id: user.id, + merge_request_id: merge_request.id, + approved_at: anything) end it 'changes reviewers state to unapproved' do diff --git a/spec/support/shared_examples/workers/auto_merge_from_event_shared_examples.rb b/spec/support/shared_examples/workers/auto_merge_from_event_shared_examples.rb index 9e76fc6971e..9c6120022b3 100644 --- a/spec/support/shared_examples/workers/auto_merge_from_event_shared_examples.rb +++ b/spec/support/shared_examples/workers/auto_merge_from_event_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'process auto merge from event worker' do let_it_be(:merge_request) { create(:merge_request, source_project: project, merge_user: user) } let(:merge_request_id) { merge_request.id } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request_id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request_id, approved_at: Time.current.iso8601 } } it_behaves_like 'subscribes to event' do it 'calls AutoMergeService' do diff --git a/spec/workers/merge_requests/create_approval_event_worker_spec.rb b/spec/workers/merge_requests/create_approval_event_worker_spec.rb index a9a46420fd5..72c94c7c3ee 100644 --- a/spec/workers/merge_requests/create_approval_event_worker_spec.rb +++ b/spec/workers/merge_requests/create_approval_event_worker_spec.rb @@ -6,7 +6,7 @@ RSpec.describe MergeRequests::CreateApprovalEventWorker, feature_category: :code let!(:user) { create(:user) } let!(:project) { create(:project) } let!(:merge_request) { create(:merge_request, source_project: project) } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id, approved_at: Time.current.iso8601 } } let(:approved_event) { MergeRequests::ApprovedEvent.new(data: data) } it_behaves_like 'subscribes to event' do diff --git a/spec/workers/merge_requests/create_approval_note_worker_spec.rb b/spec/workers/merge_requests/create_approval_note_worker_spec.rb index 1a9e15c18f0..413bd63605d 100644 --- a/spec/workers/merge_requests/create_approval_note_worker_spec.rb +++ b/spec/workers/merge_requests/create_approval_note_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::CreateApprovalNoteWorker, feature_category: :code_ let_it_be(:project) { create(:project) } let_it_be(:merge_request) { create(:merge_request, source_project: project) } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id, approved_at: Time.current.iso8601 } } let(:approved_event) { MergeRequests::ApprovedEvent.new(data: data) } it_behaves_like 'subscribes to event' do diff --git a/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb b/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb index 13c97f8fe49..4338b8482c2 100644 --- a/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb +++ b/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::ExecuteApprovalHooksWorker, feature_category: :sou let_it_be(:project) { create(:project) } let_it_be(:merge_request) { create(:merge_request, source_project: project) } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id, approved_at: Time.current.iso8601 } } let(:approved_event) { MergeRequests::ApprovedEvent.new(data: data) } it_behaves_like 'subscribes to event' do diff --git a/spec/workers/merge_requests/process_auto_merge_from_event_worker_spec.rb b/spec/workers/merge_requests/process_auto_merge_from_event_worker_spec.rb index bd006ab5b7b..a9581a868b1 100644 --- a/spec/workers/merge_requests/process_auto_merge_from_event_worker_spec.rb +++ b/spec/workers/merge_requests/process_auto_merge_from_event_worker_spec.rb @@ -8,7 +8,7 @@ RSpec.describe MergeRequests::ProcessAutoMergeFromEventWorker, feature_category: let_it_be(:merge_request) { create(:merge_request, source_project: project, merge_user: user) } let(:merge_request_id) { merge_request.id } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request_id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request_id, approved_at: Time.current.iso8601 } } it_behaves_like 'process auto merge from event worker' do let(:event) { ::MergeRequests::DiscussionsResolvedEvent.new(data: data) } diff --git a/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb b/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb index 39443d48ff6..fe7eb747e13 100644 --- a/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb +++ b/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::ResolveTodosAfterApprovalWorker, feature_category: let_it_be(:project) { create(:project) } let_it_be(:merge_request) { create(:merge_request, source_project: project) } - let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id } } + let(:data) { { current_user_id: user.id, merge_request_id: merge_request.id, approved_at: Time.current.iso8601 } } let(:approved_event) { MergeRequests::ApprovedEvent.new(data: data) } it_behaves_like 'subscribes to event' do