From 235236c5a9bb38ace72e96df6652ba9f4dc04465 Mon Sep 17 00:00:00 2001 From: Poojawa Date: Fri, 22 Sep 2017 21:56:34 -0500 Subject: [PATCH] updates tool folder (#2905) * updates tool folder * impliments #2874 changes --- html/ban.png | Bin 0 -> 773 bytes html/changelog.css | 5 + html/chrome-wrench.png | Bin 0 -> 571 bytes html/coding.png | Bin 0 -> 566 bytes tools/CreditsTool/CreditsTool.csproj | 47 +++ tools/CreditsTool/CreditsTool.exe | Bin 0 -> 11776 bytes tools/CreditsTool/Program.cs | 150 ++++++++ tools/CreditsTool/UpdateCreditsDMI.sh | 13 + .../github_webhook_processor.php | 328 ++++++++++++++---- tools/WebhookProcessor/secret.php | 4 + tools/ss13_genchangelog.py | 7 +- 11 files changed, 488 insertions(+), 66 deletions(-) create mode 100644 html/ban.png create mode 100644 html/chrome-wrench.png create mode 100644 html/coding.png create mode 100644 tools/CreditsTool/CreditsTool.csproj create mode 100644 tools/CreditsTool/CreditsTool.exe create mode 100644 tools/CreditsTool/Program.cs create mode 100644 tools/CreditsTool/UpdateCreditsDMI.sh diff --git a/html/ban.png b/html/ban.png new file mode 100644 index 0000000000000000000000000000000000000000..54f68f47708dc908312cd7555e9e9c273c642ad0 GIT binary patch literal 773 zcmV+g1N!`lP)){4J?}Yiyv5Zf)Pcg zelUmv2~`{lw8M1pPD_JU+<20cnatdK-?``B$6x~<4%lM>+$Ny31gLZZRtrKUCWs{* zBnpWz3}9mO@l#8AvPR-DJJ4K~PcC2`2x%I`)9tPNng*Ueh433{9KdlO6(~;3C9flt zQ6aXS5A%t{Co~?|@24UneUBFxV!}531(F%>>=bxe{)|9iFBD}NKA#86%2I0N(PNof z2^L~mbqy$HT7}ftH!5+QIW;ly9$s%H91aJ{oDMrF$tffnPnhaV*b2j09n)WD7Mld2 z)|bg-z_Kjn<`&@gx}CE#vrVMZ@MV~-t^xw}KuZ8PBLIgQ0dF}_w*#?N6_}q-rBsz9 zN#k%j%aC5v>;+@BEWi~XZH!N$0=`Q02nTC@{e!Y>Ed`Dqsqv4Ey|oJhkHI_lB^q^< zF+A8xKUMCrSJj7Kzxp_&>j;u3(dRFwWWT=}w|j>q`X8nKV>!cv+h}3TfQ3$>g)v4n zVDtEcFGmtX4jUr#KWJ3I2l9k}GyVlMfhZ}8QLOR4*@8~auB0Ibn_K+xXHR9f3lYPM z*26$j0pw{!loh~k1$KJ!$p*MWGL2e1+tJqeVBp?386MqU0oW{?z6ksNY;!fUQGj|3 z{Y*i;bmann^G3g9B-6<;YN>&GZKs&sL|R(V&SIJPjrfjumzZGAxe&j0{UYCewO3jo zb0$^9%ygMLcH!Zv@JZfPQEt)nY^kCsrtisQ)@HS-dN!LLc`_;uOydO8z@e*#Mc0&$ zTL~?9>v9mxxa3w}@7_(HKK=Odp6uGSYwphHj~+dG`0(NC!>`sId%gC^s~xA`Za(p*Y1#ctmo9nN zUOamA==t;K3uoS$v;A59;``I4O)H*rZ^eOETeohVGG)r%3-7zvJYKr*<)n>I4jnqw zzVhMb6K|_$U)r;0&zi%p=Inf)KmB%ldwXE>)mht~&Di#=V&1)-XWy+}y?WKERfi8B z-f`yLl+90{KYw1o=+3UQ?~WZicK!PG6)RR0&%V>M?$PoCFXzskJA23TmgNuc-o3kY z|I6)X-d4`PTQcX~%+74n{n?)L?`O}Redf%Wo|QM|&!7MQ|9=LW0mYvzj0_Bk3_2hiKykvrKBFPA zskxbj(KWWEtBECnk4>Sw%UsYVoO4o3B%cgpViXh8Om+`x3r9u=hDk!4EUdcxnnC_u zs(~sTaUAxR!3?Y|T>2*QlFAGWhU^EMx#eQGopqSlCYkwix$uZH+fEYWweyS+5b;y% z4h^$Xw~iJy4VmQa#iL=Y=c6^#K--O1t>wT3%`+1j16)cZ0sWxZs1|?#%oAy_tE;aU9r0IBX~)l}i1WBQpJpzrBCdF9JX73@b97PFb;7e6np@ z?R8y;OeS+F(6C=96c&bcLe7;+1?u&>T9c4Jf{V#y!iU2loIgH;^q8P&ngTf*h5@D_ z;o)>w1w>dOV=d;l+x!{i*_qO|RF3TCE1LSZtPn zXpnJ#I{*#_`}}_J`h2Q?oz8%dRq$Xia1Ra=iGCQtS5V%DwDs(y>qucG?&9~n}p^##A_WKM(NT}z)=s6>H5MNBwOvU5zLpOnC zS%2E?_AfOwoL{edWNbcARM!8A;hIu)#Dr}+&hG*Y0N_IWL<|H~O8@`>07*qoM6N<$ Ef(u3dw*UYD literal 0 HcmV?d00001 diff --git a/tools/CreditsTool/CreditsTool.csproj b/tools/CreditsTool/CreditsTool.csproj new file mode 100644 index 0000000000..68187a0815 --- /dev/null +++ b/tools/CreditsTool/CreditsTool.csproj @@ -0,0 +1,47 @@ + + + + + Debug + AnyCPU + {BA95D3D9-1940-4183-8563-BE617D752D0B} + Exe + CreditsTool + CreditsTool + v4.6.1 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/CreditsTool/CreditsTool.exe b/tools/CreditsTool/CreditsTool.exe new file mode 100644 index 0000000000000000000000000000000000000000..da0187aaf7c3e8d34f9575227f9706759aa91a0d GIT binary patch literal 11776 zcmeHNe{dXkb$`3Jx3@Y;XWgA8IguQ#Wjmbp<1AUWiCtN?Y{|A_OO`Fgj)}v0rCUoI z-`!qzSF&Y|f`A6dU_zjTHg3`(beKY?P*O5L=+KlIH$xfHlxgWC6XPbpOo3rS8PaJ9 zA?fFR_xA2&?36z`(;1qz_w9S{`@ZkJ?|bk2-S2l#qwji%LPVtCe)CPDuj0wq9s%DP zETcKr_O%#2S@Z0=uS$oXT{k&vxq8vDXB;E1PaB1T?demd?#vZ*tDp}Znb7lg)=bye zM>mvIj}H?amO`XF@z}}0+Dp_<_eUdu#v;3r8_ZzMTfb1HBb@)wKr!NjqlFh9LV&%eYxr7w)QS4-rPY z@+y`%?3+wsdOZcd*Kq7)lB&YGve%8NmL8Q(!#4WHjnJmMJ4Z-i{Ok9US0 z6RbA|)CbZbi0ML&bjduUx)w+1ND*`eC@`__YxW+~(qkf0-D4j2%MT|EUf}=^r z`7s#Rgu=ZE9g|v9vZltqnbGKNKSb0M(NqglwTapk$}yU(i!1TEM7_NhL|0?H-kF!7 z9HWcwd=m2UdPryxZUe`L<571VKr$AOx$TUsim$TQ18LLynt*Qb(c1W`cwGu^HUBf9ZwLQp*YF<&odN$>*YL+c|0npvP#VLNFV?>g{07_; zAOiiDWaqCiT#?QDHenbS5jZI%$|cnS+U^ACpkz;&{>0Z47lj*j{(&>sh1MM(M1xOu zgRE_;>vSU8rTUsw8f5oWO_RMDNZb1KiJIn*u>OrrZTe$2R?Mi|3y@UeioFF$^OkLK zrP=NSn%WA`eQR845@a&W*f#Lgc-Z+VXGlqH2PtY=YreBii#%v;`H-6uDWnHAlbK#BP>E zOM)ocX!p1LILfPiWg?m&gbQny@WN4UdzvFAFGgA4B7zQx=mK)9c-2(-juFD8U#;|= znb-IIHQ)DJt9-AQYkc3=lzhK>x7_B-RXhHypX0LQoM{lkzVHkyrXt4`F$UjPKNABz zSU#JgS0i!?qg1b`9W;nH1}aiq|AIjAEeZvkLXcdMfv#HF&jgzB+Ae2Fgq|Bk3PfY2 zBm2Z{A`C&tQi!h8@fLZJbJuaRdk97@z=tH}&Ruw-Z>bjB-soE+FY?3^Q>?F6Zt_DO z?Tj>uRVk{S>j9r=T&Nn^QrPAMoJNny%V?ZmBdW+*YEkQ z>7I07PhT(AAfCZFz>6@x{yw7r!t)9Yt)K85t1#oT#6LsnqX_8wJ0_@zh@gUL{ee41 zhVWbl{UOYw^?P&n6bs|RYeG8F_UYOh_(!iveT3x+DWEiNo^t3ym_!7>dHo>7Mev!% z@PsUMh<6Xua7XYsr>uu>qU|C7snYl4SHmj(TEGkP%i&eD8lzC@FXTr;Dt%qupr|y9 zZYs?Ph`ovwrDWu?+D8A#vzzV|d{ucK@4cs$%jk8h;QUzPz7Gn`7z}#7B3)La^fB$S zs!~SqKPzNX;dg`csKoH6(37BKcmjR@4*sb0l#uy~_B?ETDa1OTfQ*WrGUIi^?kAYvFPT`p0tNuQT%D7}#-Kd|OS zM7*B1mnjL|18CzM?L@sIj>wx#&mmzP zamH&J`F@FgzE`~)_?zmKGz85yAp151&lkXd zR{JpU&#DrSLV1t$2ra4~mqw%(<&V%h6M9U*PXm4}^jYbsw4c5pos>r9uL7RO411jJ z(-?*$4RliCzJt;s@J~un`Dv+FM8J?*q|4HG1pWxU8oDa|y%?h<6{H`6e<<`L>0XSH z>q((jRE|bFQ1x|~p=s>+UjR(f_W@f-3Z-d1MMKlnNAC?SQzzXSBFP1=3;2?NBnys! zdjvc!;9(k)9+18yeN+0j^g6y$@S3qH@@>@GW0CJ-My^5~$*7<(o(Vy%1{K0iE(xep zBVao<18zWtg)qHZsEzvQQTkK*F8v$5LYAZ~L+Og^M|c^aY|ED<^XcPSp1+z^%SGkO z{2E!&Hz^;)9OBs`)5GG#1ESw`2DW7~y)-ghn9G}vF_kkoMU;O zSzw6*(@h)fgw%Vjgxv|Y%!+%{6kn&*$4 zAud;B?u<1f%4@tZ6O_!5W1PcgGR>*SGhmloOdmHL7sC)OV%JF%b5m{!^J~4Nfhz<0 z#|>{**f?QjfmM0TnGe#69js~OY1GWy&O(V8_rKYS&PYhU9B2DFsm4*qkXoxD>H)}XlM2(Nl<)=)C{PFP| z1Tl$kt|hcQg^rxV%%|IpdBb-+@Egovb%f<$fE8EEYeqT{Lsj>eCdzfiD$8LY2Ir96 zu2QEXQ}_NRn8{ zkDOtpVv(>RHGCfL9v7zb_B_@Ce`(xrJ9)z+KT5%P#!N3js|=P?f?%Xk_7IxE#6D?H|~Z$xQn%Q@;dkti2-egVQ{%<#Q)SarRH@|$!xUj#*T?;V6X#fjFW zo!Eh;X*2C32UM0Uf_0iC8~<`>G3h*R9s6r;FM_M%K1T&m4h^hmVd^gEor8{9q2ny1 z3OGej3)`5kvXOzV0zAM+f=z=o1)UChu?OtYBK6P`o^0KM4EwZ>Xvb0LX1fdEa&H|v zZD1aF7PPQs*5smPkj7~QBjA73w-5Xr{+a0Em3tdB4}U$}4tm@{$LKKnmijbSw`QQ* z;jbOFIcS;@8mQU#bCG)CtxLP$_ZDCj!Y9)Zj)@~Ht@7w}(en&+=){oi2BW>vSm6J* zWAHo=Ka2PmjGevUNY7#9I(!TKDvx0K!$V+lvbfO@>Gi{AfXB}GCg(*^uOAi;B?qI-sb2l_{9rG**4ELhu zdvN-xa7xt_Ie=hI60P&IpMx*c=*8(`?VITyc*+>{VL1?ksxo3nlK}fuGxAvBtz&nIZZB2EBb9vBV~=G2YEB@m|1KEU_)IBe6pvSOt({ zVFJ4~Oj2SgruwSzVEd{rC50tOMM!~EYimT8!AeSSON+u6sAf|2Z3s?pz+rp7s+gwB z;5H;BSOb?w#=~D579(F9hAAnre190)HLV6PvHU=M`6Kvh!&YTA6vnTh%a5@27@mnp z$u&BNz!U~xXg%YkuZ1Hv@k-s{5ZOb z{u*48wGg;6+7KcwrbMK~Q56wd{ye*$7{!5G3q>R?9tq2uA1ojGy6#7GVrq{h-9oXt zs4(w0v!XifD;G{3ziI1B4`^T9oq13EyV0Fm@6)#(|NQo`ZLM!L^KZWK8wXC|1DP6= zRgELV$qT7)Q%#t-ty=JcvOt^=9hDH`9Wf0Hh$x#lDU}*ajG{~+rzJ);QUs-mx?{a{ zegT$-oAUd&hEmk3N#ffye$nF3qLXsd34A{ovkT=NJb(niwv2<&a}e-Qy${D!4tqk!Lnu*an3m0tM2n&3D6-{7;{HB4}P`PtuS z;u(a$X_AQ^I!#z01S!CzycN&mG=cpCZ={X^8^NY(4D<-#e*dqv=AzUK|+ zZ)$o-$ncKG|F;!;*d8e8!fGd#<%eM16?+#8`=V+)I%IpWf>vaYLzZ{`{;r69>A{|@ zaue-A<|Hv1K501c-+=#PfO*jPTNUssd!$K>xcUr4loGxjhRlrU$-Da^&LX^Tn!$z_ z+fCAO9~5?ul=!?`XFEX*nELJPZ587ihaMX{w>iYYL%vszvNAp%E8%x2j>#b6;6gHw zsOI3I{y+A6b&BXG$VqVh?605h-+exx({cFZZx*}Sd(%Dbx>=aEv-kqCtNo72{oS{< z>sa0kStDl`%w6pZrrW-Ipgvk3?KfQ4%unSObf_q}yV~cR!cKR3*327jcix(IY}Y>H zbx+&*orarF&-b?Ld81&R!O1pwsR?^frt4*8Blw8v;XJ#-C;S`@zxbo>uJ+M|!6H6f z_}|;oMzPqw+1KDXbFRl9#(qnGwzP|WuSox^5{2FLMF;AVE*vF7m&7vJ1}ORar@ zRvlMG4;K3AIeteTHs{Tpo@2PH-Ec<=^Z20XwCi&ge+zIC#xq9FHA{mMN;Y4|o51zW nD}3+YTn+*R`Zos|8VEA}_Vb1j*yI20!$bIUvA3`Pe-Zd^!Tc6l literal 0 HcmV?d00001 diff --git a/tools/CreditsTool/Program.cs b/tools/CreditsTool/Program.cs new file mode 100644 index 0000000000..d5dd162412 --- /dev/null +++ b/tools/CreditsTool/Program.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Net; +using System.Web.Script.Serialization; + +namespace CreditsTool +{ + class Program + { + const string ConfigPath = "remappings.txt"; + const string OutputLocation = "./credit_pngs"; + const int world_icon_size = 32; + //this downloads all the user images of contributors of a passed github repository + //usage ./CreditsTool [authToken (helps with github rate limiter)] + static void Main(string[] args) + { + if (args.Length < 2) + { + Console.WriteLine("Usage: ./CreditsTool.exe [authToken]"); + return; + } + + if (Directory.Exists(OutputLocation)) + { + Console.WriteLine(String.Format("Aborted: {0} exists!", OutputLocation)); + return; + } + Directory.CreateDirectory(OutputLocation); + + string repoOwner = args[0], + repoName = args[1], + authToken = args.Length > 2 ? args[2] : null; + + Console.WriteLine("Querying contributors API..."); + + var FirstResponse = GetPageResponse(repoOwner, repoName, authToken, 1); + + Console.WriteLine("Collecting avatar URLs..."); + + var LoginAvatars = new Dictionary(); + //now list the things we want: avatar urls and logins + foreach (var I in LoadPages(FirstResponse, repoOwner, repoName, authToken)) { + var avurl = (string)I["avatar_url"]; + LoginAvatars.Add((string)I["login"], String.Format("{0}{1}s={2}", avurl, avurl.Contains("?") ? "&" : "?", world_icon_size)); + } + + Console.WriteLine(String.Format("Collected info for {0} contributors.", LoginAvatars.Count)); + + Console.WriteLine("Remapping github logins..."); + + var remaps = LoadConfig(); + + Console.WriteLine(String.Format("Downloading and converting avatars to {0} (this will take a while)...", OutputLocation)); + + using (var client = new WebClient()) + { + var count = 0; + foreach (var I in LoginAvatars) + { + var writtenFilename = I.Key; + if (remaps.TryGetValue(writtenFilename, out string tmp)) + { + if (tmp == "__REMOVE__") + continue; + writtenFilename = tmp; + } + using (var stream = new MemoryStream(client.DownloadData(I.Value))) + using (var originalBMP = new Bitmap(stream)) + { + if (originalBMP.Width == world_icon_size && originalBMP.Height == world_icon_size) //no need to resize + SaveBMP(originalBMP, writtenFilename); + else + using (var resizedBMP = new Bitmap(originalBMP, new Size(world_icon_size, world_icon_size))) + SaveBMP(resizedBMP, writtenFilename); + } + Console.WriteLine(String.Format("Done {0}.png! {1}%", writtenFilename, (int)((((float)(count + 1)) / LoginAvatars.Count) * 100))); + ++count; + } + } + } + + static void SaveBMP(Bitmap bmp, string writtenFilename) + { + bmp.Save(String.Format("{0}{1}{2}.png", OutputLocation, Path.DirectorySeparatorChar, writtenFilename), ImageFormat.Png); + } + + static IDictionary LoadConfig() + { + var result = new Dictionary(); + if (File.Exists(ConfigPath)) + foreach (var I in File.ReadAllLines(ConfigPath)) + if (!String.IsNullOrWhiteSpace(I) && I[0] != '#') + { + var splits = new List(I.Split(' ')); + if (splits.Count >= 1 && !String.IsNullOrEmpty(splits[1])) + { + var key = splits[0]; + splits.RemoveAt(0); + result.Add(key, String.Join(" ", splits)); + } + } + return result; + } + + static IEnumerable> LoadPages(WebResponse firstResponse, string repoOwner, string repoName, string authToken) + { + int numPages = GetNumPagesOfContributors(firstResponse); + Console.WriteLine(String.Format("Downloading {0} pages of contributor info...", numPages)); + //load and combine json for all pages + var jss = new JavaScriptSerializer(); + using (var sr = new StreamReader(firstResponse.GetResponseStream())) + foreach (var J in jss.Deserialize>>(sr.ReadToEnd())) + yield return J; + + //skip the first + for (var I = 2; I <= numPages; ++I) + using (var sr = new StreamReader(GetPageResponse(repoOwner, repoName, authToken, I).GetResponseStream())) + foreach (var J in jss.Deserialize>>(sr.ReadToEnd())) + yield return J; + } + + static int GetNumPagesOfContributors(WebResponse response) + { + var splits = response.Headers["Link"].Split(','); + foreach (var I in splits) + if (I.Contains("rel=\"last\"")) //our boy + { + var pagestrIndex = I.IndexOf("&page=") + 6; + var closingIndex = I.IndexOf('>', pagestrIndex + 1); + var thedroidswerelookingfor = I.Substring(pagestrIndex, closingIndex - pagestrIndex); + return Convert.ToInt32(thedroidswerelookingfor); + } + return 1; + } + + static WebResponse GetPageResponse(string repoOwner, string repoName, string authToken, int pageNumber) + { + HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(String.Format("https://api.github.com/repos/{0}/{1}/contributors?per_page=100&page={2}", repoOwner, repoName, pageNumber)); + httpWebRequest.Method = WebRequestMethods.Http.Get; + httpWebRequest.Accept = "application/json"; + httpWebRequest.UserAgent = "tgstation-13-credits-tool"; + if (authToken != null) + httpWebRequest.Headers.Add(String.Format("Authorization: token {0}", authToken)); + return httpWebRequest.GetResponse(); + } + } +} diff --git a/tools/CreditsTool/UpdateCreditsDMI.sh b/tools/CreditsTool/UpdateCreditsDMI.sh new file mode 100644 index 0000000000..89d24e1e8f --- /dev/null +++ b/tools/CreditsTool/UpdateCreditsDMI.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +#If you hit github's rate limit, add a 3rd parameter here that is a github personal access token +./CreditsTool tgstation tgstation + +rm ../../icons/credits.dmi + +for filename in credit_pngs/*.png; do + realname=$(basename "$filename") + java -jar ../dmitool/dmitool.jar import ../../icons/credits.dmi "${realname%.*}" "$filename" +done + +rm -rf credit_pngs diff --git a/tools/WebhookProcessor/github_webhook_processor.php b/tools/WebhookProcessor/github_webhook_processor.php index dde21e4f43..4e20a7e902 100644 --- a/tools/WebhookProcessor/github_webhook_processor.php +++ b/tools/WebhookProcessor/github_webhook_processor.php @@ -15,10 +15,8 @@ */ -//CONFIG START (all defaults are random examples, do change them) -//Use single quotes for config options that are strings. +//CONFIGS ARE IN SECRET.PHP, THESE ARE JUST DEFAULTS! -//These are all default settings that are described in secret.php $hookSecret = '08ajh0qj93209qj90jfq932j32r'; $apiKey = '209ab8d879c0f987d06a09b9d879c0f987d06a09b9d8787d0a089c'; $repoOwnerAndName = "tgstation/tgstation"; @@ -33,6 +31,7 @@ $maintainer_team_id = 133041; $validation = "org"; $validation_count = 1; $tracked_branch = 'master'; +$require_changelogs = false; require_once 'secret.php'; @@ -43,6 +42,7 @@ set_error_handler(function($severity, $message, $file, $line) { set_exception_handler(function($e) { header('HTTP/1.1 500 Internal Server Error'); echo "Error on line {$e->getLine()}: " . htmlSpecialChars($e->getMessage()); + file_put_contents('htwebhookerror.log', "Error on line {$e->getLine()}: " . $e->getMessage(), FILE_APPEND); die(); }); $rawPost = NULL; @@ -96,6 +96,16 @@ switch (strtolower($_SERVER['HTTP_X_GITHUB_EVENT'])) { case 'pull_request': handle_pr($payload); break; + case 'pull_request_review': + if($payload['action'] == 'submitted'){ + $lower_state = strtolower($payload['review']['state']); + if(($lower_state == 'approved' || $lower_state == 'changes_requested') && is_maintainer($payload, $payload['review']['user']['login'])){ + $lower_association = strtolower($payload['review']['author_association']); + if($lower_association == 'member' || $lower_association == 'contributor' || $lower_association == 'owner') + remove_ready_for_review($payload); + } + } + break; default: header('HTTP/1.0 404 Not Found'); echo "Event:$_SERVER[HTTP_X_GITHUB_EVENT] Payload:\n"; @@ -103,36 +113,87 @@ switch (strtolower($_SERVER['HTTP_X_GITHUB_EVENT'])) { die(); } -//rip bs-12 -function tag_pr($payload, $opened) { +function apisend($url, $method = 'GET', $content = NULL) { global $apiKey; - - //We need to reget the pull_request part of the payload to actually see the mergeable field populated - //http://stackoverflow.com/questions/30619549/why-does-github-api-return-an-unknown-mergeable-state-in-a-pull-request + if (is_array($content)) + $content = json_encode($content); + $scontext = array('http' => array( - 'method' => 'GET', + 'method' => $method, 'header' => "Content-type: application/json\r\n". 'Authorization: token ' . $apiKey, 'ignore_errors' => true, 'user_agent' => 'tgstation13.org-Github-Automation-Tools' )); + if ($content) + $scontext['http']['content'] = $content; + + return file_get_contents($url, false, stream_context_create($scontext)); +} +function validate_user($payload) { + global $validation, $validation_count; + $query = array(); + if (empty($validation)) + $validation = 'org'; + switch (strtolower($validation)) { + case 'disable': + return TRUE; + case 'repo': + $query['repo'] = $payload['pull_request']['base']['repo']['full_name']; + break; + default: + $query['user'] = $payload['pull_request']['base']['repo']['owner']['login']; + break; + } + $query['author'] = $payload['pull_request']['user']['login']; + $query['is'] = 'merged'; + $querystring = ''; + foreach($query as $key => $value) + $querystring .= ($querystring == '' ? '' : '+') . urlencode($key) . ':' . urlencode($value); + $res = apisend('https://api.github.com/search/issues?q='.$querystring); + $res = json_decode($res, TRUE); + return $res['total_count'] >= (int)$validation_count; + +} +function get_labels($payload){ + $url = $payload['pull_request']['issue_url'] . '/labels'; + $existing_labels = json_decode(apisend($url), true); + $existing = array(); + foreach($existing_labels as $label) + $existing[] = $label['name']; + return $existing; +} + +function check_tag_and_replace($payload, $title_tag, $label, &$array_to_add_label_to){ + $title = $payload['pull_request']['title']; + if(stripos($title, $title_tag) !== FALSE){ + $array_to_add_label_to[] = $label; + $title = trim(str_ireplace($title_tag, '', $title)); + apisend($payload['pull_request']['url'], 'PATCH', array('title' => $title)); + return true; + } + return false; +} + +//rip bs-12 +function tag_pr($payload, $opened) { + //get the mergeable state $url = $payload['pull_request']['url']; - $payload['pull_request'] = json_decode(file_get_contents($url, false, stream_context_create($scontext)), true); + $payload['pull_request'] = json_decode(apisend($url), TRUE); if($payload['pull_request']['mergeable'] == null) { //STILL not ready. Give it a bit, then try one more time sleep(10); - $payload['pull_request'] = json_decode(file_get_contents($url, false, stream_context_create($scontext)), true); + $payload['pull_request'] = json_decode(apisend($url), TRUE); } $tags = array(); $title = $payload['pull_request']['title']; if($opened) { //you only have one shot on these ones so as to not annoy maintainers - $tags = checkchangelog($payload, true, false); + $tags = checkchangelog($payload, false); - $lowertitle = strtolower($title); - if(strpos($lowertitle, 'refactor') !== FALSE) + if(strpos(strtolower($title), 'refactor') !== FALSE) $tags[] = 'Refactor'; if(strpos(strtolower($title), 'revert') !== FALSE || strpos(strtolower($title), 'removes') !== FALSE) @@ -148,7 +209,7 @@ function tag_pr($payload, $opened) { $tags[] = 'Merge Conflict'; $treetags = array('_maps' => 'Map Edit', 'tools' => 'Tools', 'SQL' => 'SQL'); - $addonlytags = array('icons' => 'Sprites', 'sounds' => 'Sound'); + $addonlytags = array('icons' => 'Sprites', 'sounds' => 'Sound', 'config' => 'Config Update'); foreach($treetags as $tree => $tag) if(has_tree_been_edited($payload, $tree)) $tags[] = $tag; @@ -158,21 +219,14 @@ function tag_pr($payload, $opened) { if(has_tree_been_edited($payload, $tree)) $tags[] = $tag; - //only maintners should be able to remove these - if(strpos($lowertitle, '[dnm]') !== FALSE) - $tags[] = 'Do Not Merge'; - - if(strpos($lowertitle, '[wip]') !== FALSE) - $tags[] = 'Work In Progress'; + check_tag_and_replace($payload, '[dnm]', 'Do Not Merge', $tags); + if(!check_tag_and_replace($payload, '[wip]', 'Work In Progress', $tags)) + check_tag_and_replace($payload, '[ready]', 'Work In Progress', $remove); $url = $payload['pull_request']['issue_url'] . '/labels'; - $existing_labels = file_get_contents($url, false, stream_context_create($scontext)); - $existing_labels = json_decode($existing_labels, true); + $existing = get_labels($payload); - $existing = array(); - foreach($existing_labels as $label) - $existing[] = $label['name']; $tags = array_merge($tags, $existing); $tags = array_unique($tags); $tags = array_diff($tags, $remove); @@ -181,27 +235,153 @@ function tag_pr($payload, $opened) { foreach($tags as $t) $final[] = $t; - $scontext['http']['method'] = 'PUT'; - $scontext['http']['content'] = json_encode($final); - echo file_get_contents($url, false, stream_context_create($scontext)); + echo apisend($url, 'PUT', $final); + + return $final; +} + +function remove_ready_for_review($payload, $labels = null){ + if($labels == null) + $labels = get_labels($payload); + $index = array_search('Ready for Review', $labels); + if($index !== FALSE) + unset($labels[$index]); + $url = $payload['pull_request']['issue_url'] . '/labels'; + apisend($url, 'PUT', $labels); +} + +function dismiss_review($payload, $id, $reason){ + $content = array('message' => $reason); + apisend($payload['pull_request']['url'] . '/reviews/' . $id . '/dismissals', 'PUT', $content); +} + +function get_reviews($payload){ + return json_decode(apisend($payload['pull_request']['url'] . '/reviews'), true); +} + +function check_ready_for_review($payload, $labels = null){ + $r4rlabel = 'Ready for Review'; + $labels_which_should_not_be_ready = array('Do Not Merge', 'Work In Progress', 'Merge Conflict'); + $has_label_already = false; + $should_not_have_label = false; + if($labels == null) + $labels = get_labels($payload); + //if the label is already there we may need to remove it + foreach($labels as $L){ + if(in_array($L, $labels_which_should_not_be_ready)) + $should_not_have_label = true; + if($L == $r4rlabel) + $has_label_already = true; + } + + if($has_label_already && $should_not_have_label){ + remove_ready_for_review($payload, $labels, $r4rlabel); + return; + } + + //find all reviews to see if changes were requested at some point + $reviews = get_reviews($payload); + + $reviews_ids_with_changes_requested = array(); + $dismissed_an_approved_review = false; + + foreach($reviews as $R){ + $lower_association = strtolower($R['author_association']); + if($lower_association == 'member' || $lower_association == 'contributor' || $lower_association == 'owner'){ + $lower_state = strtolower($R['state']); + if($lower_state == 'changes_requested') + $reviews_ids_with_changes_requested[] = $R['id']; + else if ($lower_state == 'approved'){ + dismiss_review($payload, $R['id'], 'Out of date review'); + $dismissed_an_approved_review = true; + } + } + } + + if(!$dismissed_an_approved_review && count($reviews_ids_with_changes_requested) == 0){ + if($has_label_already) + remove_ready_for_review($payload, $labels); + return; //no need to be here + } + + if(count($reviews_ids_with_changes_requested) > 0){ + //now get the review comments for the offending reviews + + $review_comments = json_decode(apisend($payload['pull_request']['review_comments_url']), true); + + foreach($review_comments as $C){ + //make sure they are part of an offending review + if(!in_array($C['pull_request_review_id'], $reviews_ids_with_changes_requested)) + continue; + + //review comments which are outdated have a null position + if($C['position'] !== null){ + if($has_label_already) + remove_ready_for_review($payload, $labels); + return; //no need to tag + } + } + } + + //finally, add it if necessary + if(!$has_label_already){ + $labels[] = $r4rlabel; + $url = $payload['pull_request']['issue_url'] . '/labels'; + apisend($url, 'PUT', $labels); + } +} + +function check_dismiss_changelog_review($payload){ + global $require_changelog; + global $no_changelog; + + if(!$require_changelog) + return; + + if(!$no_changelog) + checkchangelog($payload, false); + + $review_message = 'Your changelog for this PR is either malformed or non-existent. Please create one to document your changes.'; + + $reviews = get_reviews($payload); + if($no_changelog){ + //check and see if we've already have this review + foreach($reviews as $R) + if($R['body'] == $review_message && strtolower($R['state']) == 'changes_requested') + return; + //otherwise make it ourself + apisend($payload['pull_request']['url'] . '/reviews', 'POST', array('body' => $review_message, 'event' => 'REQUEST_CHANGES')); + } + else + //kill previous reviews + foreach($reviews as $R) + if($R['body'] == $review_message && strtolower($R['state']) == 'changes_requested') + dismiss_review($payload, $R['id'], 'Changelog added/fixed.'); } function handle_pr($payload) { + global $no_changelog; $action = 'opened'; + $validated = validate_user($payload); switch ($payload["action"]) { case 'opened': tag_pr($payload, true); + if($no_changelog) + check_dismiss_changelog_review($payload); if(get_pr_code_friendliness($payload) < 0){ $balances = pr_balances(); $author = $payload['pull_request']['user']['login']; - if(isset($balances[$author]) && $balances[$author] < 0) + if(isset($balances[$author]) && $balances[$author] < 0 && !is_maintainer($payload, $author)) create_comment($payload, 'You currently have a negative Fix/Feature pull request delta of ' . $balances[$author] . '. Maintainers may close this PR at will. Fixing issues or improving the codebase will improve this score.'); } break; case 'edited': + check_dismiss_changelog_review($payload); case 'synchronize': - tag_pr($payload, false); + $labels = tag_pr($payload, false); + if($payload['action'] == 'synchronize') + check_ready_for_review($payload, $labels); return; case 'reopened': $action = $payload['action']; @@ -213,7 +393,7 @@ function handle_pr($payload) { else { $action = 'merged'; auto_update($payload); - checkchangelog($payload, true, true); + checkchangelog($payload, true); update_pr_balance($payload); $validated = TRUE; //pr merged events always get announced. } @@ -226,7 +406,11 @@ function handle_pr($payload) { echo "PR Announcement Halted; Secret tag detected.\n"; return; } - + if (!$validated) { + echo "PR Announcement Halted; User not validated.\n"; + return; + } + $msg = '['.$payload['pull_request']['base']['repo']['full_name'].'] Pull Request '.$action.' by '.htmlSpecialChars($payload['sender']['login']).': '.htmlSpecialChars('#'.$payload['pull_request']['number'].' '.$payload['pull_request']['user']['login'].' - '.$payload['pull_request']['title']).''; sendtoallservers('?announce='.urlencode($msg), $payload); } @@ -306,7 +490,7 @@ function is_maintainer($payload, $author){ global $maintainer_team_id; $repo_is_org = $payload['pull_request']['base']['repo']['owner']['type'] == 'Organization'; if($maintainer_team_id == null || !$repo_is_org) { - $collaburl = $payload['pull_request']['base']['repo']['collaborators_url'] . '/' . $author . '/permissions'; + $collaburl = str_replace('{/collaborator}', '/' . $author, $payload['pull_request']['base']['repo']['collaborators_url']) . '/permission'; $perms = json_decode(apisend($collaburl), true); $permlevel = $perms['permission']; return $permlevel == 'admin' || $permlevel == 'write'; @@ -314,7 +498,7 @@ function is_maintainer($payload, $author){ else { $check_url = 'https://api.github.com/teams/' . $maintainer_team_id . '/memberships/' . $author; $result = json_decode(apisend($check_url), true); - return isset($result['state']); //this field won't be here if they aren't a member + return isset($result['state']) && $result['state'] == 'active'; } } @@ -325,17 +509,17 @@ function update_pr_balance($payload) { if(!$trackPRBalance) return; $author = $payload['pull_request']['user']['login']; - if(is_maintainer($payload, $author)) //immune - return; $balances = pr_balances(); if(!isset($balances[$author])) $balances[$author] = $startingPRBalance; $friendliness = get_pr_code_friendliness($payload, $balances[$author]); $balances[$author] += $friendliness; - if($balances[$author] < 0 && $friendliness < 0) - create_comment($payload, 'Your Fix/Feature pull request delta is currently below zero (' . $balances[$author] . '). Maintainers may close future Feature/Tweak/Balance PRs. Fixing issues or helping to improve the codebase will raise this score.'); - else if($balances[$author] >= 0 && ($balances[$author] - $friendliness) < 0) - create_comment($payload, 'Your Fix/Feature pull request delta is now above zero (' . $balances[$author] . '). Feel free to make Feature/Tweak/Balance PRs.'); + if(!is_maintainer($payload, $author)){ //immune + if($balances[$author] < 0 && $friendliness < 0) + create_comment($payload, 'Your Fix/Feature pull request delta is currently below zero (' . $balances[$author] . '). Maintainers may close future Feature/Tweak/Balance PRs. Fixing issues or helping to improve the codebase will raise this score.'); + else if($balances[$author] >= 0 && ($balances[$author] - $friendliness) < 0) + create_comment($payload, 'Your Fix/Feature pull request delta is now above zero (' . $balances[$author] . '). Feel free to make Feature/Tweak/Balance PRs.'); + } $balances_file = fopen(pr_balance_json_path(), 'w'); fwrite($balances_file, json_encode($balances)); fclose($balances_file); @@ -372,10 +556,9 @@ function has_tree_been_edited($payload, $tree){ return $github_diff !== FALSE && strpos($github_diff, 'diff --git a/' . $tree) !== FALSE; } -function checkchangelog($payload, $merge = false, $compile = true) { - global $apiKey; - if (!$merge) - return; +$no_changelog = false; +function checkchangelog($payload, $compile = true) { + global $no_changelog; if (!isset($payload['pull_request']) || !isset($payload['pull_request']['body'])) { return; } @@ -449,10 +632,6 @@ function checkchangelog($payload, $merge = false, $compile = true) { $currentchangelogblock[] = array('type' => 'bugfix', 'body' => $item); } break; - case 'wip': - if($item != 'added a few works in progress') - $currentchangelogblock[] = array('type' => 'wip', 'body' => $item); - break; case 'rsctweak': case 'tweaks': case 'tweak': @@ -510,11 +689,6 @@ function checkchangelog($payload, $merge = false, $compile = true) { $currentchangelogblock[] = array('type' => 'spellcheck', 'body' => $item); } break; - case 'experimental': - case 'experiment': - if($item != 'added an experimental thingy') - $currentchangelogblock[] = array('type' => 'experiment', 'body' => $item); - break; case 'balance': case 'rebalance': if($item != 'rebalanced something'){ @@ -525,6 +699,35 @@ function checkchangelog($payload, $merge = false, $compile = true) { case 'tgs': $currentchangelogblock[] = array('type' => 'tgs', 'body' => $item); break; + case 'code_imp': + case 'code': + if($item != 'changed some code'){ + $tags[] = 'Code Improvement'; + $currentchangelogblock[] = array('type' => 'code_imp', 'body' => $item); + } + break; + case 'refactor': + if($item != 'refactored some code'){ + $tags[] = 'Refactor'; + $currentchangelogblock[] = array('type' => 'refactor', 'body' => $item); + } + break; + case 'config': + if($item != 'changed some config setting'){ + $tags[] = 'Config Update'; + $currentchangelogblock[] = array('type' => 'config', 'body' => $item); + } + break; + case 'admin': + if($item != 'messed with admin stuff'){ + $tags[] = 'Administration'; + $currentchangelogblock[] = array('type' => 'admin', 'body' => $item); + } + break; + case 'server': + if($item != 'something server ops should know') + $currentchangelogblock[] = array('type' => 'server', 'body' => $item); + break; default: //we add it to the last changelog entry as a separate line if (count($currentchangelogblock) > 0) @@ -533,7 +736,10 @@ function checkchangelog($payload, $merge = false, $compile = true) { } } - if (!count($changelogbody) || !$compile) + if(!count($changelogbody)) + $no_changelog = true; + + if ($no_changelog || !$compile) return $tags; $file = 'author: "'.trim(str_replace(array("\\", '"'), array("\\\\", "\\\""), $username)).'"'."\n"; @@ -550,17 +756,9 @@ function checkchangelog($payload, $merge = false, $compile = true) { 'message' => 'Automatic changelog generation for PR #'.$payload['pull_request']['number'].' [ci skip]', 'content' => base64_encode($file) ); - $scontext = array('http' => array( - 'method' => 'PUT', - 'header' => - "Content-type: application/json\r\n". - 'Authorization: token ' . $apiKey, - 'content' => json_encode($content), - 'ignore_errors' => true, - 'user_agent' => 'tgstation13.org-Github-Automation-Tools' - )); + $filename = '/html/changelogs/AutoChangeLog-pr-'.$payload['pull_request']['number'].'.yml'; - echo file_get_contents($payload['pull_request']['base']['repo']['url'].'/contents'.$filename, false, stream_context_create($scontext)); + echo apisend($payload['pull_request']['base']['repo']['url'].'/contents'.$filename, 'PUT', $content); } function sendtoallservers($str, $payload = null) { diff --git a/tools/WebhookProcessor/secret.php b/tools/WebhookProcessor/secret.php index abea10564f..93c9b318f6 100644 --- a/tools/WebhookProcessor/secret.php +++ b/tools/WebhookProcessor/secret.php @@ -8,6 +8,7 @@ $hookSecret = '08ajh0qj93209qj90jfq932j32r'; //Api key for pushing changelogs. +//This requires the public_repo (or repo for private repositories) and read:org permissions $apiKey = '209ab8d879c0f987d06a09b9d879c0f987d06a09b9d8787d0a089c'; //Used to prevent potential RCEs @@ -47,3 +48,6 @@ $validation = "org"; //how many merged prs must they have under the rules above to have their pr announced to the game servers. $validation_count = 1; + +//enforce changelogs on PRs +$require_changelogs = false; diff --git a/tools/ss13_genchangelog.py b/tools/ss13_genchangelog.py index c4e22014df..c7a31325b1 100644 --- a/tools/ss13_genchangelog.py +++ b/tools/ss13_genchangelog.py @@ -57,7 +57,12 @@ validPrefixes = [ 'spellcheck', 'experiment', 'tgs', - 'balance' + 'balance', + 'code_imp', + 'refactor', + 'config', + 'admin', + 'server' ] def dictToTuples(inp):