├── host.json ├── jenkinsjirafunction ├── project.json ├── function.json ├── README.md └── run.csx └── LICENSE /host.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /jenkinsjirafunction/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "frameworks": { 3 | "net46":{ 4 | "dependencies": { 5 | "Octokit": "0.23.0", 6 | "Atlassian.SDK": "9.2.0" 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /jenkinsjirafunction/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "req", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "webHookType": "genericJson", 8 | "methods": [ 9 | "post" 10 | ] 11 | }, 12 | { 13 | "name": "$return", 14 | "type": "http", 15 | "direction": "out" 16 | } 17 | ], 18 | "disabled": false 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alex Earl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jenkinsjirafunction/README.md: -------------------------------------------------------------------------------- 1 | # Jenkins JIRA Checker 2 | 3 | The Jenkins JIRA Checker is used to check new hosting requests for validity, relieving some of the manual aspects of approving plugin hosting requests. 4 | 5 | ## Checks 6 | 7 | The Jenkins JIRA Checker currently has the following checks implemented: 8 | * Basic JIRA field checks 9 | * Checks that all fields are filled in and have ok values 10 | * GitHub checks 11 | * Checks 'GitHub Users to Authorize as Committers' to see if they are valid GitHub users 12 | * Checks 'Repository URL' to verify it is a valid GitHub repo 13 | * Maven checks 14 | * Checks if there is a pom.xml and verifies various requirements 15 | * Checks vs. the 'New Repository Name' to verify they match 16 | * Checks to verify it doesn't have bad values 17 | * Checks for recommended versions (LTS and minimum versions) 18 | * Checks that a license is specified 19 | * Gradle checks 20 | * NOT CURRENTLY IMPLEMENTED, JUST STUBBED 21 | 22 | ## Configuration 23 | * To deploy the checker, you will need to setup the following Application settings: 24 | * JIRA_URL - the URL for your JIRA instance 25 | * JIRA_USERNAME - the username for the JIRA user for authentication 26 | * JIRA_PASSWORD - the password for the JIRA user for authentication 27 | * GITHUB_APP_KEY - the GitHub app key to use for GitHub API accesses 28 | -------------------------------------------------------------------------------- /jenkinsjirafunction/run.csx: -------------------------------------------------------------------------------- 1 | #r "System.Xml.Linq" 2 | 3 | using System; 4 | using System.Net; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Xml.Linq; 8 | using System.Xml.XPath; 9 | 10 | using Octokit; 11 | using Atlassian.Jira; 12 | 13 | public delegate Task VerificationDelegate(Atlassian.Jira.Issue issue, HashSet hostingIssues, TraceWriter log); 14 | 15 | static string INVALID_FORK_FROM = "Repository URL '{0}' is not a valid GitHub repository (check that you do not have .git at the end, GitHub API doesn't support this)."; 16 | static string INVALID_POM = "The pom.xml file in the root of the origin repository is not valid"; 17 | static bool debug = false; 18 | 19 | public class VerificationMessage : IEquatable { 20 | public enum Severity { 21 | Info, 22 | Warning, 23 | Required, 24 | } 25 | 26 | public string Message { get; private set; } 27 | public Severity SeverityLevel { get; private set; } 28 | public IList Subitems { get; private set; } 29 | 30 | public VerificationMessage(Severity severity, List subitems, string format, params object[] args) { 31 | Message = string.Format(format, args); 32 | SeverityLevel = severity; 33 | Subitems = subitems; 34 | } 35 | 36 | public VerificationMessage(Severity severity, string format, params object[] args) : this(severity, null, format, args) { 37 | // we just call the other constructor 38 | } 39 | 40 | public bool Equals(VerificationMessage other) { 41 | if(SeverityLevel != other.SeverityLevel) { 42 | return false; 43 | } 44 | 45 | if(Message != other.Message) { 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | 53 | public static string SeverityToFriendlyString(VerificationMessage.Severity severity) { 54 | switch(severity) { 55 | case VerificationMessage.Severity.Info: 56 | return "INFO"; 57 | case VerificationMessage.Severity.Warning: 58 | return "WARNING"; 59 | } 60 | return "REQUIRED"; 61 | } 62 | 63 | public static string SeverityToColor(VerificationMessage.Severity severity) { 64 | switch(severity) { 65 | case VerificationMessage.Severity.Info: 66 | return "black"; 67 | case VerificationMessage.Severity.Warning: 68 | return "orange"; 69 | } 70 | return "red"; 71 | } 72 | 73 | public static async Task Run(HttpRequestMessage req, TraceWriter log) 74 | { 75 | var verifications = new Dictionary() { 76 | { "JIRA Fields", VerifyJiraFields }, 77 | { "GitHub", VerifyGitHubInfo }, 78 | { "Maven", VerifyMaven }, 79 | /*{ "Gradle", VerifyGradle }*/ 80 | }; 81 | 82 | var debugSetting = GetEnvironmentVariable("DEBUG"); 83 | if(!string.IsNullOrWhiteSpace(debugSetting)) { 84 | debug = true; 85 | log.Info("Running in debug mode"); 86 | } 87 | 88 | // Get request body 89 | dynamic data = await req.Content.ReadAsAsync(); 90 | 91 | string webhookEvent = data?.webhookEvent; 92 | 93 | if(string.IsNullOrEmpty(webhookEvent)) { 94 | return req.CreateResponse(HttpStatusCode.BadRequest, "Invalid payload for webhook"); 95 | } 96 | 97 | if((webhookEvent == "jira:issue_updated" || webhookEvent == "jira:issue_created") && data?.issue != null) { 98 | var hostingIssues = new HashSet(); 99 | 100 | string key = data.issue.key; 101 | var jira = CreateJiraClient(); 102 | var issue = await jira.Issues.GetIssueAsync(key); 103 | if(issue == null) { 104 | log.Error($"Could not retrieve JIRA issue {key}"); 105 | return req.CreateResponse(HttpStatusCode.BadRequest, "Could not retrieve JIRA issue"); 106 | } 107 | 108 | foreach(var verification in verifications) { 109 | log.Info($"Running verification '{verification.Key}'"); 110 | try { 111 | await verification.Value(issue, hostingIssues, log); 112 | } catch(Exception ex) { 113 | log.Info($"Error running verification '{verification.Key}': {ex.ToString()}"); 114 | } 115 | } 116 | 117 | var msg = new StringBuilder("Hello from your friendly Jenkins Hosting Checker\n\n"); 118 | log.Info("Checking if there were errors"); 119 | if(hostingIssues.Count > 0) { 120 | msg.AppendLine("It appears you have some issues with your hosting request. Please see the list below and " 121 | + "correct all issues marked {color:red}REQUIRED{color}. Your hosting request will not be " 122 | + "approved until these issues are corrected. Issues marked with {color:orange}WARNING{color} " 123 | + "or INFO are just recommendations and will not stall the hosting process.\n"); 124 | log.Info("Appending issues to msg"); 125 | AppendIssues(msg, hostingIssues, 1); 126 | } else { 127 | msg.Append("It looks like you have everything in order for your hosting request. " 128 | + "A human volunteer will check over things that I am not able to check for " 129 | + "(code review, README content, etc) and process the request as quickly as possible. " 130 | + "Thank you for your patience."); 131 | } 132 | 133 | log.Info(msg.ToString()); 134 | if(!debug) { 135 | await issue.AddCommentAsync(msg.ToString()); 136 | } 137 | } else { 138 | return req.CreateResponse(HttpStatusCode.BadRequest, "Invalid webhook type"); 139 | } 140 | 141 | return req.CreateResponse(HttpStatusCode.OK); 142 | } 143 | 144 | public static void AppendIssues(StringBuilder msg, IEnumerable issues, int level) { 145 | foreach(var issue in issues.OrderByDescending(x => x.SeverityLevel)) { 146 | if(level == 1) { 147 | msg.AppendLine(string.Format("{0} {{color:{1}}}{2}: {3}{{color}}", new String('*', level), SeverityToColor(issue.SeverityLevel), SeverityToFriendlyString(issue.SeverityLevel), issue.Message)); 148 | } else { 149 | msg.AppendLine(string.Format("{0} {1}", new String('*', level), issue.Message)); 150 | } 151 | if(issue.Subitems != null) { 152 | AppendIssues(msg, issue.Subitems, level+1); 153 | } 154 | } 155 | } 156 | 157 | public static string GetEnvironmentVariable(string name) 158 | { 159 | return System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process); 160 | } 161 | 162 | public static GitHubClient CreateGitHubClient() { 163 | var ghClient = new GitHubClient(new ProductHeaderValue("jenkins-hosting-checker")); 164 | ghClient.Credentials = new Credentials(GetEnvironmentVariable("GITHUB_APP_KEY")); 165 | return ghClient; 166 | } 167 | 168 | public static Jira CreateJiraClient() { 169 | return Jira.CreateRestClient(GetEnvironmentVariable("JIRA_URL"), GetEnvironmentVariable("JIRA_USERNAME"), GetEnvironmentVariable("JIRA_PASSWORD")); 170 | } 171 | 172 | public static async Task VerifyJiraFields(Atlassian.Jira.Issue issue, HashSet hostingIssues, TraceWriter log) { 173 | var userList = issue["GitHub Users to Authorize as Committers"]?.Value; 174 | var forkFrom = issue["Repository URL"]?.Value; 175 | var forkTo = issue["New Repository Name"]?.Value; 176 | bool hasUpdate = false; 177 | 178 | // check list of users 179 | if(string.IsNullOrWhiteSpace(userList)) { 180 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "Missing list of users to authorize in 'GitHub Users to Authorize as Committers'")); 181 | } else { 182 | if(userList.Contains(" ")) { 183 | userList = userList.Replace(" ", "\n"); 184 | issue["GitHub Users to Authorize as Committers"] = userList; 185 | hasUpdate = true; 186 | } 187 | } 188 | 189 | if(string.IsNullOrWhiteSpace(forkFrom)) { 190 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, "")); 191 | } else { 192 | if(forkFrom.EndsWith(".git")) { 193 | issue["Repository URL"] = forkFrom = forkFrom.Substring(0, forkFrom.Length - 4); 194 | hasUpdate = true; 195 | } 196 | 197 | // check the repo they want to fork from to make sure it conforms 198 | var m = Regex.Match(forkFrom, @"(?:https:\/\/github\.com/)?(\S+)\/(\S+)", RegexOptions.IgnoreCase); 199 | if(!m.Success) { 200 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, forkFrom)); 201 | } 202 | } 203 | 204 | if(string.IsNullOrWhiteSpace(forkTo)) { 205 | var subitems = new List() { 206 | new VerificationMessage(VerificationMessage.Severity.Required, "It must match the artifactId (with -plugin added) from your build file (pom.xml/build.gradle)."), 207 | new VerificationMessage(VerificationMessage.Severity.Required, "It must end in -plugin if hosting request is for a Jenkins plugin."), 208 | new VerificationMessage(VerificationMessage.Severity.Required, "It must be all lowercase."), 209 | new VerificationMessage(VerificationMessage.Severity.Required, "It must NOT contain \"Jenkins\"."), 210 | new VerificationMessage(VerificationMessage.Severity.Required, "It must use hyphens ( - ) instead of spaces or camel case.") 211 | }; 212 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, subitems, "You must specify the repository name to fork to in 'New Repository Name' field with the following rules:")); 213 | } else { 214 | // we don't like camel case - ThisIsCamelCase becomes this-is-camel-case 215 | var camelCaseRegex = new Regex(@"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))"); 216 | if(camelCaseRegex.IsMatch(forkTo)) { 217 | forkTo = camelCaseRegex.Replace(forkTo, "-$1"); 218 | } 219 | 220 | var forkToLower = forkTo.ToLower(); 221 | if(forkToLower.Contains("jenkins") || forkToLower.Contains("hudson")) { 222 | forkToLower = forkToLower.Replace("jenkins", string.Empty).Replace("hudson", string.Empty); 223 | hasUpdate = true; 224 | } 225 | 226 | if(!forkToLower.EndsWith("-plugin")) { 227 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "'New Repository Name' must end with \"-plugin\" (disregard if you are not requesting hosting of a plugin)")); 228 | } 229 | 230 | // we don't like spaces... 231 | if(forkToLower.Contains(" ")) { 232 | forkToLower = forkToLower.Replace(" ", "-"); 233 | } 234 | 235 | if(forkToLower != forkTo) { 236 | issue["New Repository Name"] = forkToLower; 237 | hasUpdate = true; 238 | } 239 | } 240 | 241 | if(hasUpdate) { 242 | issue.SaveChanges(); 243 | } 244 | return null; 245 | } 246 | 247 | public static async Task VerifyGitHubInfo(Atlassian.Jira.Issue issue, HashSet hostingIssues, TraceWriter log) { 248 | var ghClient = CreateGitHubClient(); 249 | var userList = issue["GitHub Users to Authorize as Committers"]?.Value; 250 | var forkFrom = issue["Repository URL"]?.Value; 251 | 252 | if(!string.IsNullOrWhiteSpace(userList)) { 253 | var users = userList.Split(new char[] { '\n', ';', ','}, StringSplitOptions.RemoveEmptyEntries); 254 | var invalidUsers = new List(); 255 | var orgs = new List(); 256 | foreach(var user in users) { 257 | try { 258 | var ghUser = await ghClient.User.Get(user.Trim()); 259 | if(ghUser == null) { 260 | invalidUsers.Add(user.Trim()); 261 | } 262 | } catch(Exception) { 263 | try { 264 | var ghOrg = await ghClient.Organization.Get(user.Trim()); 265 | if(ghOrg != null) { 266 | orgs.Add(user.Trim()); 267 | } 268 | } catch { 269 | invalidUsers.Add(user.Trim()); 270 | } 271 | } 272 | } 273 | 274 | if(invalidUsers.Count > 0) { 275 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The following usernames in 'GitHub Users to Authorize as Committers' are not valid GitHub usernames: {0}", string.Join(",", invalidUsers.ToArray()))); 276 | } 277 | 278 | if(orgs.Count > 0) { 279 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The following names in 'GitHub Users to Authorize as Committers' are organizations instead of users, this is not supported: {0}", string.Join(",", orgs.ToArray()))); 280 | } 281 | } 282 | 283 | if(!string.IsNullOrWhiteSpace(forkFrom)) { 284 | var m = Regex.Match(forkFrom, @"(?:https:\/\/github\.com/)?(\S+)\/(\S+)", RegexOptions.IgnoreCase); 285 | if(m.Success) { 286 | string owner = m.Groups[1].Value; 287 | string repoName = m.Groups[2].Value; 288 | Repository repo = null; 289 | 290 | if(repoName.EndsWith(".git")) { 291 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The origin repository '{0}' ends in .git, please remove this", forkFrom)); 292 | repoName = repoName.Substring(0, repoName.Length - 4); 293 | } 294 | 295 | try { 296 | repo = await ghClient.Repository.Get(owner, repoName); 297 | } catch(Exception) { 298 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, forkFrom)); 299 | } 300 | 301 | if(repo != null) { 302 | try { 303 | var readme = ghClient.Repository.Content.GetReadme(owner, repoName); 304 | } catch(Octokit.NotFoundException) { 305 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "Repository '{0}' does not contain a README.", forkFrom)); 306 | } 307 | 308 | // check if the repo was originally forked from jenkinsci 309 | try { 310 | Repository parent = repo.Parent; 311 | if(parent != null && parent.FullName.StartsWith("jenkinsci")) { 312 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "Repository '{0}' is currently showing as forked from a jenkinsci org repository, this relationship needs to be broken", forkFrom)); 313 | } 314 | } catch { 315 | 316 | } 317 | } else { 318 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, forkFrom)); 319 | } 320 | } 321 | } 322 | return null; 323 | } 324 | 325 | #region Maven Checks 326 | 327 | public static async Task VerifyMaven(Atlassian.Jira.Issue issue, HashSet hostingIssues, TraceWriter log) { 328 | var ghClient = CreateGitHubClient(); 329 | var forkFrom = issue["Repository URL"]?.Value; 330 | var forkTo = issue["New Repository Name"]?.Value; 331 | 332 | if(!string.IsNullOrEmpty(forkFrom)) { 333 | var m = Regex.Match(forkFrom, @"(?:https:\/\/github\.com/)?(\S+)\/(\S+)", RegexOptions.IgnoreCase); 334 | if(m.Success) { 335 | string owner = m.Groups[1].Value; 336 | string repoName = m.Groups[2].Value; 337 | 338 | Repository repo = await ghClient.Repository.Get(owner, repoName); 339 | try { 340 | var pomXml = await ghClient.Repository.Content.GetAllContents(owner, repoName, "pom.xml"); 341 | if(pomXml.Count > 0) { 342 | XNamespace ns = "http://maven.apache.org/POM/4.0.0"; 343 | 344 | // the pom.xml file should be text, so we can just use .Content 345 | try { 346 | var doc = XDocument.Parse(pomXml[0].Content); 347 | // var modulesNode = doc.Element(ns + "project")?.Element(ns + "modules"); 348 | // if(modulesNode != null) { 349 | // foreach(var moduleNode in modulesNode.Elements(ns + "module")) { 350 | // pomXml = await ghClient.Repository.Content.GetAllContents(owner, repoName, moduleNode.Value + "/pom.xml"); 351 | // if(pomXml.Count > 0) { 352 | // doc = XDocument.Parse(pomXml[0].Content); 353 | // if(doc.Element(ns + "project")?.Element()) { 354 | 355 | // } 356 | // } 357 | // } 358 | // } 359 | 360 | if(!string.IsNullOrWhiteSpace(forkTo)) { 361 | CheckArtifactId(doc, forkTo, hostingIssues, log); 362 | } 363 | CheckParentInfo(doc, hostingIssues, log); 364 | CheckName(doc, hostingIssues, log); 365 | CheckLicenses(doc, hostingIssues, log); 366 | } catch(Exception ex) { 367 | log.Info(string.Format("Exception occured trying to look at pom.xml: {0}", ex.ToString())); 368 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_POM)); 369 | } 370 | } 371 | } catch(Octokit.NotFoundException) { 372 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Warning, 373 | "No pom.xml found in root of project, if you are using a different build system, or this is not a plugin, you can disregard this message")); 374 | } 375 | } else { 376 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, forkFrom)); 377 | } 378 | } 379 | return null; 380 | } 381 | 382 | public static void CheckArtifactId(XDocument doc, string forkTo, HashSet hostingIssues, TraceWriter log) { 383 | XNamespace ns = "http://maven.apache.org/POM/4.0.0"; 384 | try { 385 | var artifactIdNode = doc.Element(ns + "project").Element(ns + "artifactId"); 386 | if(artifactIdNode != null && artifactIdNode.Value != null) { 387 | var artifactId = artifactIdNode.Value; 388 | if(string.Compare(artifactId, forkTo.Replace("-plugin", string.Empty), true) != 0) { 389 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The from the pom.xml ({0}) is incorrect, it should be {1} (new repository name with -plugin removed)", artifactId, forkTo.Replace("-plugin", string.Empty))); 390 | } 391 | 392 | if(artifactId.ToLower().Contains("jenkins")) { 393 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The from the pom.xml ({0}) should not contain \"Jenkins\"", artifactId)); 394 | } 395 | 396 | if(artifactId.ToLower() != artifactId) { 397 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The from the pom.xml ({0}) should be all lower case", artifactId)); 398 | } 399 | } else { 400 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The pom.xml file does not contain a valid for the project")); 401 | } 402 | } catch(Exception ex) { 403 | log.Info($"Error trying to access artifactId: {ex.ToString()}"); 404 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_POM)); 405 | } 406 | } 407 | 408 | public static void CheckName(XDocument doc, HashSet hostingIssues, TraceWriter log) { 409 | XNamespace ns = "http://maven.apache.org/POM/4.0.0"; 410 | try { 411 | var nameNode = doc.Element(ns + "project").Element(ns + "name"); 412 | if(nameNode != null && nameNode.Value != null) { 413 | var name = nameNode.Value; 414 | if(string.IsNullOrWhiteSpace(name)) { 415 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The from the pom.xml is blank or missing")); 416 | } 417 | 418 | if(name.ToLower().Contains("jenkins")) { 419 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The should not contain \"Jenkins\"")); 420 | } 421 | } else { 422 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The pom.xml file does not contain a valid for the project")); 423 | } 424 | } catch(Exception ex) { 425 | log.Info($"Error trying to access : {ex.Message}"); 426 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_POM)); 427 | } 428 | } 429 | 430 | public static void CheckParentInfo(XDocument doc, HashSet hostingIssues, TraceWriter log) { 431 | XNamespace ns = "http://maven.apache.org/POM/4.0.0"; 432 | try { 433 | var parentNode = doc.Element(ns + "project").Element(ns + "parent"); 434 | if(parentNode != null) { 435 | var groupIdNode = parentNode.Element(ns + "groupId"); 436 | if(groupIdNode != null && groupIdNode.Value != null && groupIdNode.Value != "org.jenkins-ci.plugins") { 437 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The groupId for your parent pom is not \"org.jenkins-ci.plugins\".")); 438 | } 439 | 440 | var versionNode = parentNode.Element(ns + "version"); 441 | if(versionNode != null && versionNode.Value != null) { 442 | var jenkinsVersion = new Version(versionNode.Value); 443 | if(jenkinsVersion.Major == 2) { 444 | versionNode = doc.Element(ns + "project").Element(ns + "properties").Element(ns + "jenkins.version"); 445 | if(versionNode != null && versionNode.Value != null) { 446 | jenkinsVersion = new Version(versionNode.Value); 447 | } 448 | 449 | if(jenkinsVersion.Build <= 0) { 450 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Info, "Your plugin does not seem to have a LTS Jenkins release. In general, " 451 | + "it's preferable to use an LTS version as parent version.")); 452 | } 453 | } else { 454 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, "The parent pom version '{0}' should be at least 2.11 or higher", jenkinsVersion)); 455 | } 456 | } 457 | } 458 | } catch(Exception ex) { 459 | log.Info($"Error trying to access the information: {ex.Message}"); 460 | } 461 | } 462 | 463 | public static void CheckLicenses(XDocument doc, HashSet hostingIssues, TraceWriter log) { 464 | XNamespace ns = "http://maven.apache.org/POM/4.0.0"; 465 | var SPECIFY_LICENSE = "Specify an open source license for your code (most plugins use MIT)."; 466 | try { 467 | var licensesNode = doc.Element(ns + "project").Element(ns + "licenses"); 468 | if(licensesNode != null) { 469 | if(licensesNode.Elements(ns + "license").Count() == 0) { 470 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, SPECIFY_LICENSE)); 471 | } 472 | } else { 473 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, SPECIFY_LICENSE)); 474 | } 475 | } catch(Exception ex) { 476 | log.Info($"Error trying to access the information: {ex.Message}"); 477 | } 478 | } 479 | 480 | #endregion 481 | 482 | public static async Task VerifyGradle(Atlassian.Jira.Issue issue, HashSet hostingIssues, TraceWriter log) { 483 | var ghClient = CreateGitHubClient(); 484 | var forkFrom = issue["Repository URL"]?.Value; 485 | var forkTo = issue["New Repository Name"]?.Value; 486 | 487 | if(!string.IsNullOrWhiteSpace(forkFrom)) { 488 | var m = Regex.Match(forkFrom, @"(?:https:\/\/github\.com/)?(\S+)\/(\S+)", RegexOptions.IgnoreCase); 489 | if(m.Success) { 490 | string owner = m.Groups[1].Value; 491 | string repoName = m.Groups[2].Value; 492 | 493 | Repository repo = await ghClient.Repository.Get(owner, repoName); 494 | try { 495 | var buildGradle = await ghClient.Repository.Content.GetAllContents(owner, repoName, "build.gradle"); 496 | if(buildGradle != null && buildGradle.Count > 0) { 497 | // look through looking for artifactId 498 | } 499 | } catch(Octokit.NotFoundException) { 500 | // we don't do anything here...this is much less common 501 | } 502 | } else { 503 | hostingIssues.Add(new VerificationMessage(VerificationMessage.Severity.Required, INVALID_FORK_FROM, forkFrom)); 504 | } 505 | } 506 | return null; 507 | } --------------------------------------------------------------------------------