All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.liferay.jenkins.results.parser.github.webhook.GitHubWebhookPayloadProcessor Maven / Gradle / Ivy

There is a newer version: 1.0.1492
Show newest version
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

package com.liferay.jenkins.results.parser.github.webhook;

import com.liferay.jenkins.results.parser.GitCommit;
import com.liferay.jenkins.results.parser.GitHubRemoteGitCommit;
import com.liferay.jenkins.results.parser.GitHubRemoteGitRepository;
import com.liferay.jenkins.results.parser.JenkinsMaster;
import com.liferay.jenkins.results.parser.JenkinsResultsParserUtil;
import com.liferay.jenkins.results.parser.JenkinsResultsParserUtil.HttpRequestMethod;
import com.liferay.jenkins.results.parser.JenkinsStopBuildUtil;
import com.liferay.jenkins.results.parser.MultiPattern;
import com.liferay.jenkins.results.parser.PullRequest;
import com.liferay.jenkins.results.parser.RemoteGitBranch;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * @author Brian Wing Shun Chan
 */
public class GitHubWebhookPayloadProcessor {

	public static void main(String[] args) {
		try {
			GitHubWebhookPayloadProcessor gitHubWebhookPayloadProcessor =
				new GitHubWebhookPayloadProcessor(
					JenkinsResultsParserUtil.read(new File(args[0])));

			gitHubWebhookPayloadProcessor.process();
		}
		catch (IOException ioException) {
			throw new RuntimeException(ioException);
		}
	}

	public GitHubWebhookPayloadProcessor(String payloadJSONSource) {
		JenkinsResultsParserUtil.setBuildProperties(
			_URLS_JENKINS_BUILD_PROPERTIES);

		try {
			_jenkinsBuildProperties =
				JenkinsResultsParserUtil.getBuildProperties();
		}
		catch (IOException ioException) {
			throw new RuntimeException(
				"Unable to get build properties", ioException);
		}

		_payload = PayloadFactory.newPayload(
			new JSONObject(_cleanupJSONSource(payloadJSONSource)));
	}

	public void addTestPullRequestQueryString(String queryString) {
		_testPullRequestQueryStrings.put(
			queryString, JenkinsResultsParserUtil.getCurrentTimeMillis());
	}

	public void addTestPullRequestURL(String url) {
		_testPullRequestURLs.put(
			url, JenkinsResultsParserUtil.getCurrentTimeMillis());
	}

	public String getCIJobName(
		PullRequestTesterParameters pullRequestTesterParameters) {

		if (_ciForwardEligible) {
			return "forward-pullrequest";
		}

		String ciReevaluateBuildId =
			pullRequestTesterParameters.getCiReevaluateBuildId();

		if (ciReevaluateBuildId != null) {
			return "test-portal-evaluate-pullrequest";
		}

		String ciTestSuiteName =
			pullRequestTesterParameters.getCiTestSuiteName();

		if (ciTestSuiteName.equals("sf")) {
			return "test-portal-source-format";
		}

		PullRequest pullRequest = pullRequestTesterParameters.getPullRequest();

		String repositoryName = pullRequest.getGitRepositoryName();

		if (repositoryName.equals("liferay-fix-pack-builder-ee")) {
			return "test-fixpack-builder-pullrequest";
		}

		if (repositoryName.equals("liferay-jenkins-ee")) {
			return "test-jenkins-acceptance-pullrequest";
		}

		StringBuilder sb = new StringBuilder();

		if (repositoryName.startsWith("com-liferay-")) {
			sb.append("test-subrepository-acceptance-pullrequest");
		}
		else if (repositoryName.startsWith("liferay-plugins")) {
			sb.append("test-plugins-acceptance-pullrequest");
		}
		else if (repositoryName.startsWith("liferay-portal")) {
			sb.append("test-portal-acceptance-pullrequest");
		}

		sb.append("(");
		sb.append(pullRequest.getUpstreamRemoteGitBranchName());
		sb.append(")");

		return sb.toString();
	}

	public List getCITestAutoTestSuiteNames(PullRequest pullRequest) {
		String ciTestAutoRecipientsProperty =
			JenkinsResultsParserUtil.getCIProperty(
				pullRequest.getUpstreamRemoteGitBranchName(),
				"ci.test.auto.recipients",
				pullRequest.getGitHubRemoteGitRepositoryName());

		if ((ciTestAutoRecipientsProperty == null) ||
			ciTestAutoRecipientsProperty.isEmpty()) {

			return Collections.emptyList();
		}

		Pattern pattern = Pattern.compile(
			pullRequest.getOwnerUsername() + "\\[(?[^\\]]+)]");

		for (String ciTestAutoRecipient :
				ciTestAutoRecipientsProperty.split("\\s*,\\s*")) {

			Matcher matcher = pattern.matcher(ciTestAutoRecipient);

			if (!matcher.matches()) {
				continue;
			}

			String ciTestAutoTestSuiteNames = matcher.group("testSuiteNames");

			return Arrays.asList(ciTestAutoTestSuiteNames.split(":"));
		}

		return Collections.emptyList();
	}

	public Set getPassingTestSuites(PullRequest pullRequest)
		throws JSONException {

		if ((_passingTestSuites != null) && !_passingTestSuites.isEmpty()) {
			return _passingTestSuites;
		}

		_passingTestSuites = new HashSet<>();

		for (String statusDescription : pullRequest.getStatusDescriptions()) {
			Matcher matcher = _passingTestSuiteStatusDescriptionPattern.matcher(
				statusDescription);

			if (matcher.matches()) {
				_passingTestSuites.add(matcher.group("testSuiteName"));
			}
		}

		return _passingTestSuites;
	}

	public List getTestPullRequestQueryStrings() {
		long expiredTime = getTestPullRequestQueryStringExpiredTime();

		for (Map.Entry testPullRequestQueryStringsEntry :
				_testPullRequestQueryStrings.entrySet()) {

			long time = testPullRequestQueryStringsEntry.getValue();

			if (time < expiredTime) {
				_testPullRequestQueryStrings.remove(
					testPullRequestQueryStringsEntry.getKey());
			}
		}

		return new ArrayList<>(_testPullRequestQueryStrings.keySet());
	}

	public List getTestPullRequestURLs() {
		long expiredTime = getTestPullRequestURLExpiredTime();

		for (Map.Entry testPullRequestQueryStringsEntry :
				_testPullRequestQueryStrings.entrySet()) {

			long time = testPullRequestQueryStringsEntry.getValue();

			if (time < expiredTime) {
				_testPullRequestURLs.remove(
					testPullRequestQueryStringsEntry.getKey());
			}
		}

		return new ArrayList<>(_testPullRequestURLs.keySet());
	}

	public void invokePullRequestTester(
		String masterURL,
		PullRequestTesterParameters pullRequestTesterParameters) {

		PullRequest pullRequest = pullRequestTesterParameters.getPullRequest();

		String invocationURL = JenkinsResultsParserUtil.combine(
			masterURL, "/job/", getCIJobName(pullRequestTesterParameters),
			"/buildWithParameters?",
			pullRequestTesterParameters.toQueryString());

		addTestPullRequestURL(invocationURL);

		if (isJenkinsJobEnabled()) {
			processURL(invocationURL);

			if (_ciForwardEligible ||
				!JenkinsResultsParserUtil.isNullOrEmpty(
					pullRequestTesterParameters.getCiReevaluateBuildId())) {

				return;
			}

			String publicJobURL = JenkinsResultsParserUtil.getRemoteURL(
				invocationURL.replaceFirst("([^\\?]*)/.*", "$1"));

			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Pull request test invoked at ", publicJobURL, "."));
			}

			pullRequest.setTestSuiteStatus(
				pullRequestTesterParameters.getCiTestSuiteName(),
				PullRequest.TestSuiteStatus.PENDING, publicJobURL);
		}
		else {
			if (_log.isInfoEnabled()) {
				_log.info("Pull request test URL " + invocationURL);
			}
		}
	}

	public boolean isGitHubAutopullEnabled() {
		return Boolean.valueOf(
			_jenkinsBuildProperties.getProperty(
				"github.webhook.pullrequest.autopull.enabled",
				Boolean.FALSE.toString()));
	}

	public boolean isGitHubPostEnabled() {
		return Boolean.valueOf(
			_jenkinsBuildProperties.getProperty(
				"github.webhook.pullrequest.post.enabled",
				Boolean.FALSE.toString()));
	}

	public boolean isGitHubRepositorySyncEnabled() {
		return Boolean.valueOf(
			_jenkinsBuildProperties.getProperty(
				"github.webhook.repository.sync.enabled",
				Boolean.FALSE.toString()));
	}

	public boolean isGitHubSubrepoSyncEnabled() {
		return Boolean.valueOf(
			_jenkinsBuildProperties.getProperty(
				"github.webhook.subrepository.sync.enabled",
				Boolean.FALSE.toString()));
	}

	public boolean isJenkinsJobEnabled() {
		return Boolean.valueOf(
			_jenkinsBuildProperties.getProperty(
				"github.webhook.pullrequest.jenkins.job.enabled",
				Boolean.FALSE.toString()));
	}

	public boolean isValidAutopull(String repo) {
		if (repo.startsWith("com-liferay-")) {
			return true;
		}

		return false;
	}

	public boolean isValidCIMergeFile(PullRequest pullRequest) {
		List fileNames = pullRequest.getFileNames();

		if (fileNames.size() > 1) {
			return false;
		}

		for (String fileName : fileNames) {
			if (fileName.endsWith("/ci-merge")) {
				return true;
			}
		}

		return false;
	}

	public void process() {
		if (_payload instanceof PushEventPayload) {
			PushEventPayload pushEventPayload = (PushEventPayload)_payload;

			syncAutopull(pushEventPayload);
			syncRepository(pushEventPayload);
			syncSubrepo(pushEventPayload);
		}

		if (_payload instanceof PullRequestCommentPayload) {
			_processCommentCreated((PullRequestCommentPayload)_payload);

			return;
		}

		if (_payload instanceof PullRequestPayload) {
			PullRequestPayload pullRequestPayload =
				(PullRequestPayload)_payload;

			String action = _payload.getAction();

			if (action.equals("opened")) {
				_processPullRequestOpened(pullRequestPayload);

				return;
			}

			if (action.equals("synchronize")) {
				_processPullRequestSynchronize(pullRequestPayload);
			}
		}
	}

	public String processURL(String url) {
		return processURL(url, null, HttpRequestMethod.GET);
	}

	public String processURL(String url, String body) {
		return processURL(url, body, HttpRequestMethod.POST);
	}

	public String processURL(
		String url, String body, HttpRequestMethod method) {

		if ((method == HttpRequestMethod.PATCH) ||
			(method == HttpRequestMethod.POST)) {

			if (body == null) {
				throw new IllegalArgumentException(
					method.toString() + " method requires a body");
			}

			body = JenkinsResultsParserUtil.combine(
				"token=",
				_jenkinsBuildProperties.getProperty(
					"jenkins.authentication.token"),
				"&", body);
		}
		else if ((method == HttpRequestMethod.DELETE) ||
				 (method == HttpRequestMethod.PUT)) {

			body = JenkinsResultsParserUtil.combine(
				"token=",
				_jenkinsBuildProperties.getProperty(
					"jenkins.authentication.token"));
		}
		else if (method == HttpRequestMethod.GET) {
			if (body != null) {
				throw new IllegalArgumentException(
					method.toString() + " method should not have a body");
			}
		}

		try {
			return JenkinsResultsParserUtil.toString(url, false, method, body);
		}
		catch (IOException ioException) {
			throw new RuntimeException(
				"Unable to retrieve URL " + url, ioException);
		}
	}

	public void removeTestPullRequestQueryString(String queryString) {
		_testPullRequestQueryStrings.remove(queryString);
	}

	public void removeTestPullRequestURL(String url) {
		_testPullRequestURLs.remove(url);
	}

	protected void commentMergeSubrepoPullRequest(PullRequest pullRequest) {
		try {
			String currentSHA = "";

			StringBuilder sb = new StringBuilder();

			sb.append("https://raw.githubusercontent.com/liferay/");
			sb.append(pullRequest.getGitHubRemoteGitRepositoryName());
			sb.append("/");
			sb.append(pullRequest.getUpstreamRemoteGitBranchName());
			sb.append("/");
			sb.append(pullRequest.getCIMergeSubrepo());
			sb.append("/.gitrepo");

			String gitrepoContent = processURL(sb.toString());

			Matcher matcher = _gitrepoSHAPattern.matcher(gitrepoContent);

			while (matcher.find()) {
				currentSHA = matcher.group(1);
			}

			matcher = _gitrepoRepoPattern.matcher(gitrepoContent);

			String repo = "";

			while (matcher.find()) {
				repo = matcher.group(1);
			}

			String mergeSHA = pullRequest.getCIMergeSHA();

			String compareURL =
				"https://github.com/liferay/" + repo + "/compare/" +
					currentSHA + "..." + mergeSHA;

			if (_log.isInfoEnabled()) {
				_log.info("Subrepo compare URL " + compareURL);
			}

			String message =
				"Subrepo changes: " + compareURL +
					"\n\nci:test:sf and ci:test:relevant must pass in order " +
						"for auto-merge to initiate.";

			pullRequest.addComment(message);
		}
		catch (Exception exception) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Skip generation of the ci:merge diff because of an " +
						"exception",
					exception);
			}
		}
	}

	protected String formatCSV(String string) {
		string = string.replaceAll("\\s*,\\s*", ", ");

		string = string.replaceFirst(",\\s+$", "");

		return string.replaceFirst("(.*), (.+)", "$1 or $2");
	}

	protected List getAllowedSenderUsernames(PullRequest pullRequest) {
		String allowedSenderNamesProperty =
			JenkinsResultsParserUtil.getCIProperty(
				pullRequest.getUpstreamRemoteGitBranchName(),
				JenkinsResultsParserUtil.combine(
					"allowed.sender.names[", pullRequest.getOwnerUsername(),
					"]"),
				pullRequest.getGitHubRemoteGitRepositoryName());

		if ((allowedSenderNamesProperty == null) ||
			allowedSenderNamesProperty.isEmpty()) {

			return Collections.emptyList();
		}

		return Arrays.asList(allowedSenderNamesProperty.split("\\s*,\\s*"));
	}

	protected List getBuildURLs(PullRequest pullRequest) {
		List buildURLs = new ArrayList<>();

		for (PullRequest.Comment comment : pullRequest.getComments()) {
			Matcher buildURLMatcher = _buildURLPattern.matcher(
				comment.getBody());

			if (buildURLMatcher.find()) {
				buildURLs.add(buildURLMatcher.group("buildURL"));
			}
		}

		return buildURLs;
	}

	protected String[] getCIForwardRequiredPassingSuites() {
		String ciForwardRequiredPassingSuites =
			_jenkinsBuildProperties.getProperty(
				"pull.request.forward.required.passing.suites", "");

		if (ciForwardRequiredPassingSuites.isEmpty()) {
			return new String[0];
		}

		return ciForwardRequiredPassingSuites.split("\\s*,\\s*");
	}

	protected String[] getCIForwardRequiredTestSuites() {
		String ciForwardRequiredTestSuites =
			_jenkinsBuildProperties.getProperty(
				"pull.request.forward.required.test.suites", "");

		if (ciForwardRequiredTestSuites.isEmpty()) {
			return new String[0];
		}

		return ciForwardRequiredTestSuites.split("\\s*,\\s*");
	}

	protected String getCompanionBranchName(String branchName) {
		if (branchName.contains("-private")) {
			return branchName.replace("-private", "");
		}

		return branchName + "-private";
	}

	protected List getJiraProjectKeys(PullRequest pullRequest) {
		if (_jiraProjectKeys != null) {
			return _jiraProjectKeys;
		}

		String jiraProjectKeysProperty = JenkinsResultsParserUtil.getCIProperty(
			pullRequest.getUpstreamRemoteGitBranchName(), "jira.project.keys",
			pullRequest.getGitRepositoryName());

		if ((jiraProjectKeysProperty != null) &&
			jiraProjectKeysProperty.isEmpty()) {

			_jiraProjectKeys = Arrays.asList(
				jiraProjectKeysProperty.split("\\s*,\\s*"));
		}

		return _jiraProjectKeys;
	}

	protected String getSubrepoCentralMergePullRequestRecipientName(
		String refName) {

		return _jenkinsBuildProperties.getProperty(
			JenkinsResultsParserUtil.combine(
				"subrepo.merge.receiver.name[", refName, "]"),
			"liferay");
	}

	protected String getSubrepoPath(PushEventPayload pushEventPayload) {
		GitHubRemoteGitCommit headGitHubRemoteGitCommit =
			pushEventPayload.getHeadGitHubRemoteGitCommit();

		if (headGitHubRemoteGitCommit == null) {
			return null;
		}

		String commitMessage = headGitHubRemoteGitCommit.getMessage();

		if ((commitMessage != null) && commitMessage.contains("LPS-0 Clear")) {
			String subrepoPath = commitMessage.replaceAll(".* ", "");

			if (subrepoPath.startsWith("modules/apps") ||
				subrepoPath.startsWith("modules/private/apps")) {

				return subrepoPath;
			}
		}

		for (String filename :
				headGitHubRemoteGitCommit.getModifiedFilenames()) {

			if (filename.endsWith(".gitrepo")) {
				String subrepoPath = filename.replaceAll("/\\.gitrepo", "");

				if (subrepoPath.startsWith("modules/apps") ||
					subrepoPath.startsWith("modules/private/apps")) {

					return subrepoPath;
				}
			}
		}

		return null;
	}

	protected long getTestPullRequestQueryStringExpiredTime() {
		long currentTimeMillis =
			JenkinsResultsParserUtil.getCurrentTimeMillis();

		// return currentTimeMillis - 3600000; // 1 hour

		return currentTimeMillis - 21600000; // 6 hours
	}

	protected long getTestPullRequestURLExpiredTime() {
		long currentTimeMillis =
			JenkinsResultsParserUtil.getCurrentTimeMillis();

		// return currentTimeMillis - 3600000; // 1 hour

		return currentTimeMillis - 21600000; // 6 hours
	}

	protected boolean hasLiferayEmailAddress(String githubUsername) {
		if (githubUsername.equals("liferay")) {
			return true;
		}

		JSONObject jsonObject = new JSONObject(
			processURL("https://api.github.com/users/" + githubUsername));

		String emailAddress = jsonObject.optString("email");

		if ((emailAddress != null) && emailAddress.endsWith("@liferay.com")) {
			return true;
		}

		return false;
	}

	protected boolean hasValidJIRAReferences(PullRequest pullRequest) {
		String ownerUsername = pullRequest.getOwnerUsername();

		if (!ownerUsername.equals("brianchandotcom")) {
			return true;
		}

		List jiraProjectKeys = getJiraProjectKeys(pullRequest);

		if (jiraProjectKeys.isEmpty()) {
			return true;
		}

		for (GitCommit commit : pullRequest.getGitHubRemoteCommits()) {
			String message = commit.getMessage();

			if (message.contains("subrepo:ignore")) {
				return true;
			}

			boolean hasJIRAProjectKey = false;

			for (String jiraProjectKey : _jiraProjectKeys) {
				if (message.contains(jiraProjectKey + "-")) {
					if (_log.isInfoEnabled()) {
						_log.info(
							"Contains JIRA project keys " + jiraProjectKey);
					}

					hasJIRAProjectKey = true;
				}
			}

			String emailAddress = commit.getEmailAddress();

			if (emailAddress.equals("[email protected]") ||
				emailAddress.equals("[email protected]") ||
				emailAddress.equals("[email protected]")) {

				if (_log.isInfoEnabled()) {
					_log.info("Allow commit from Brian, Sam, or CI");
				}

				continue;
			}

			if (!hasJIRAProjectKey) {
				return false;
			}
		}

		return true;
	}

	protected boolean isBlank(String string) {
		if (string == null) {
			return true;
		}

		string = string.trim();

		if (string.isEmpty()) {
			return true;
		}

		return false;
	}

	protected boolean isBotPush(PushEventPayload pushEventPayload) {
		if (JenkinsResultsParserUtil.isNullOrEmpty(
				getSubrepoPath(pushEventPayload))) {

			return false;
		}

		return true;
	}

	protected boolean isLiferayUser(String gitHubUsername) {
		if (gitHubUsername.equals("liferay") ||
			_validLiferayUsers.contains(gitHubUsername)) {

			return true;
		}

		JSONArray jsonArray = new JSONArray(
			processURL(
				JenkinsResultsParserUtil.combine(
					"https://api.github.com/users/", gitHubUsername, "/orgs")));

		for (int i = 0; i < jsonArray.length(); i++) {
			JSONObject jsonObject = jsonArray.getJSONObject(i);

			String organizationLogin = jsonObject.getString("login");

			if (organizationLogin.equals("liferay")) {
				if (_log.isInfoEnabled()) {
					_log.info("Valid Liferay member " + gitHubUsername);
				}

				_validLiferayUsers.add(gitHubUsername);

				return true;
			}
		}

		if (_log.isInfoEnabled()) {
			_log.info("Invalid Liferay member " + gitHubUsername);
		}

		return false;
	}

	protected boolean isSynchronizeablePullRequest(PullRequest pullRequest) {
		String receiverUsername = pullRequest.getReceiverUsername();

		if (receiverUsername.equals("brianchandotcom")) {
			return false;
		}

		return true;
	}

	protected boolean isTestablePullRequest(PullRequest pullRequest) {
		String branchName = pullRequest.getUpstreamRemoteGitBranchName();

		String repositoryName = pullRequest.getGitRepositoryName();

		List ciEnabledBranchNames = _getCIEnabledBranchNames(
			repositoryName);

		if (!_acRepositories.contains(repositoryName) &&
			!ciEnabledBranchNames.contains(branchName)) {

			StringBuilder sb = new StringBuilder(4);

			sb.append("Closing pull request because pulls for reference ");
			sb.append(branchName);
			sb.append(" should not be sent to repository ");
			sb.append(repositoryName);

			if (_log.isInfoEnabled()) {
				_log.info(sb.toString());
			}

			pullRequest.addComment(sb.toString());

			pullRequest.close();

			return false;
		}

		String ownerUsername = pullRequest.getOwnerUsername();

		if (!_whiteListedOwnerNames.isEmpty() &&
			!_whiteListedOwnerNames.contains(ownerUsername)) {

			String message = JenkinsResultsParserUtil.combine(
				"Skip pull request because the owner ", ownerUsername,
				" is not on the whitelist\n\nPull request tests have been ",
				"temporarily suspended.");

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			return false;
		}

		String senderUsername = pullRequest.getSenderUsername();

		if (ownerUsername.equals(
				getSubrepoCentralMergePullRequestRecipientName(branchName)) &&
			pullRequest.isMergeSubrepoRequest()) {

			if (!pullRequest.isValidCIMergeFile()) {
				String message = JenkinsResultsParserUtil.combine(
					"Closing pull request because a subrepo merge ",
					"request must only contain a single change to a ",
					"single ci-merge file.");

				if (_log.isInfoEnabled()) {
					_log.info(message);
				}

				pullRequest.addComment(message);

				pullRequest.close();

				return false;
			}

			String sha = pullRequest.getCIMergeSHA();

			if (sha.equals("")) {
				String message =
					"Closing pull request because the ci-merge file " +
						"modification is missing or incorrectly formatted";

				if (_log.isInfoEnabled()) {
					_log.info(message);
				}

				pullRequest.addComment(message);

				pullRequest.close();

				return false;
			}
		}

		GitHubRemoteGitRepository gitHubRemoteGitRepository =
			pullRequest.getGitHubRemoteGitRepository();

		if (ownerUsername.equals("liferay") &&
			!gitHubRemoteGitRepository.isSubrepository() &&
			!repositoryName.equals("liferay-portal-ee")) {

			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Skip pull request because it is a pull sent to ",
						"Liferay that is not a subrepo request"));
			}

			return false;
		}

		if (_whiteListedRepositoryMultiPattern.matches(repositoryName) ==
				null) {

			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Skip pull request because the repository ",
						repositoryName, " is not on the whitelist"));
			}

			return false;
		}

		if (!isLiferayUser(ownerUsername)) {
			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Skip pull request because the owner ", ownerUsername,
						" does not have access"));
			}

			return false;
		}

		if (!isLiferayUser(senderUsername)) {
			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Skip pull request because the tester ", senderUsername,
						" does not have access"));
			}

			if (hasLiferayEmailAddress(senderUsername)) {
				StringBuilder sb = new StringBuilder();

				sb.append("Your pull request was not tested because you ");
				sb.append("are not a member of the Liferay organization. ");
				sb.append("Please make sure that you have been added and ");
				sb.append("that your organization membership is set as ");
				sb.append("Public. See https://help.github.com/articles");
				sb.append("/publicizing-or-hiding-organization-");
				sb.append("membership for more information.");

				pullRequest.addComment(sb.toString());
			}

			return false;
		}

		String githubCIUsername = _jenkinsBuildProperties.getProperty(
			"github.ci.username");

		if (ownerUsername.equals("brianchandotcom") &&
			branchName.equals("master") &&
			repositoryName.equals("liferay-portal") &&
			!senderUsername.equals(githubCIUsername)) {

			StringBuilder sb = new StringBuilder(4);

			sb.append("Closing pull request because all `liferay-portal` pull");
			sb.append("requests sent to Brian Chan must be sent by using ");
			sb.append("`ci:forward` on a pull request that was sent to ");
			sb.append("someone else.");

			String message = sb.toString();

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			pullRequest.close();

			return false;
		}

		List allowedSenderUsernames = getAllowedSenderUsernames(
			pullRequest);

		if (!allowedSenderUsernames.isEmpty() &&
			!allowedSenderUsernames.contains(senderUsername)) {

			StringBuilder sb = new StringBuilder(7);

			sb.append("Closing pull request because ");
			sb.append(senderUsername);
			sb.append(" is not an allowed sender on this branch. Please ");
			sb.append("resend this pull request to one of the following ");
			sb.append("allowed senders: ");
			sb.append(
				JenkinsResultsParserUtil.join(", ", allowedSenderUsernames));
			sb.append(".");

			String message = sb.toString();

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			pullRequest.close();

			return false;
		}

		List collaboratorUsernames =
			gitHubRemoteGitRepository.getCollaboratorUsernames();

		if (!collaboratorUsernames.contains("liferay-continuous-integration")) {
			if (_log.isInfoEnabled()) {
				StringBuilder sb = new StringBuilder(4);

				sb.append("Skip pull request because ");
				sb.append("liferay-continuous-integration does not have ");
				sb.append("write access to ");
				sb.append(gitHubRemoteGitRepository.getHtmlURL());

				_log.info(sb.toString());
			}

			StringBuilder sb = new StringBuilder(5);

			sb.append("Your pull request was not tested because your ");
			sb.append("repository has not set liferay-continuous-integration ");
			sb.append("as a collaborator. See https://grow.liferay.com/share/");
			sb.append("Pull+Request+Tester+for+Liferay+Developers for more ");
			sb.append("information.");

			pullRequest.addComment(sb.toString());

			return false;
		}

		if (!hasValidJIRAReferences(pullRequest)) {
			StringBuilder sb = new StringBuilder(7);

			sb.append("Closing pull request because at least one commit ");
			sb.append("message is missing a reference to a required JIRA ");
			sb.append("project: ");
			sb.append(
				JenkinsResultsParserUtil.join(
					", ", getJiraProjectKeys(pullRequest)));
			sb.append(". Please verify that the JIRA project keys are ");
			sb.append("specified in ci.properties in the liferay-portal ");
			sb.append("repository.");

			String message = sb.toString();

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			pullRequest.close();

			return false;
		}

		// TODO: Ensure every commit has a required key. Make sure all keys are
		// valid via https://issues.liferay.com/rest/api/2/issue/LPS-5331. Make
		// sure sender is a team member of the component via
		// https://api.github.com/search/[email protected]+in%3A
		// email&type=Users.

		return true;
	}

	protected boolean isValidPullRequestRefSHA(
		PullRequest pullRequest, String refSHA) {

		StringBuilder sb = new StringBuilder();

		sb.append("https://api.github.com/repos/liferay/");
		sb.append(pullRequest.getGitHubRemoteGitRepositoryName());
		sb.append("/commits/");
		sb.append(refSHA);

		JSONObject commitJSONObject = new JSONObject(processURL(sb.toString()));

		if (!commitJSONObject.has("sha")) {
			return false;
		}

		return true;
	}

	protected String join(String[] array) {
		StringBuilder sb = new StringBuilder();

		for (int i = 0; i < array.length; i++) {
			if ((i + 1) == array.length) {
				sb.append(" or ");
			}

			sb.append(array[i]);

			if ((i + 1) < array.length) {
				sb.append(", ");
			}
		}

		return sb.toString();
	}

	protected void mergeSubrepo(PullRequest pullRequest, boolean force) {
		String ownerUsername = pullRequest.getOwnerUsername();

		String branchName = pullRequest.getUpstreamRemoteGitBranchName();

		String subrepoCentralMergePullRequestRecipientName =
			getSubrepoCentralMergePullRequestRecipientName(branchName);

		if (!ownerUsername.equals(
				subrepoCentralMergePullRequestRecipientName)) {

			if (_log.isInfoEnabled()) {
				_log.info(
					"Skip merge subrepo because the user is not " +
						subrepoCentralMergePullRequestRecipientName);
			}
		}

		String repositoryName = pullRequest.getGitHubRemoteGitRepositoryName();

		JSONObject jsonObject = new JSONObject();

		jsonObject.put(
			"branch", branchName
		).put(
			"command", "pull"
		).put(
			"pullRequestNumber", pullRequest.getNumber()
		).put(
			"repo", repositoryName
		);

		try {
			if (!pullRequest.isValidCIMergeFile()) {
				String message = JenkinsResultsParserUtil.combine(
					"Closing pull request because a subrepo merge request ",
					"must only contain a single change to a single ",
					"ci-merge file");

				if (_log.isInfoEnabled()) {
					_log.info(message);
				}

				pullRequest.addComment(message);

				pullRequest.close();

				return;
			}
		}
		catch (Exception exception) {
			String message = "Skip merge subrepo because of a GitHub error";

			if (_log.isInfoEnabled()) {
				_log.info(message, exception);
			}

			pullRequest.addComment(message + ".");

			return;
		}

		String sha = pullRequest.getCIMergeSHA();

		if (sha.equals("")) {
			String message =
				"Closing pull request because the ci-merge file modification " +
					"is missing or incorrectly formatted";

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			pullRequest.close();

			return;
		}

		if (_log.isInfoEnabled()) {
			_log.info("Merge subrepo SHA " + sha);
		}

		jsonObject.put("sha", sha);

		String subrepo = pullRequest.getCIMergeSubrepo();

		if (_log.isInfoEnabled()) {
			_log.info("Merge subrepo name " + subrepo);
		}

		jsonObject.put("subrepo", subrepo);

		String statusURL =
			"https://api.github.com/repos/" +
				subrepoCentralMergePullRequestRecipientName + "/" +
					repositoryName + "/commits/" + pullRequest.getSenderSHA() +
						"/status";

		JSONArray statusesJSONArray = null;

		for (int i = 0; i < 3; i++) {
			try {
				JSONObject statusJSONObject = new JSONObject(
					processURL(statusURL));

				statusesJSONArray = statusJSONObject.getJSONArray("statuses");

				break;
			}
			catch (Exception exception) {
				if (_log.isInfoEnabled()) {
					_log.info("Retrying " + statusURL, exception);
				}

				JenkinsResultsParserUtil.sleep(1000);
			}
		}

		if (statusesJSONArray == null) {
			String message = "Skip merge subrepo because of a GitHub error";

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message);

			pullRequest.close();

			return;
		}

		if (_log.isInfoEnabled()) {
			_log.info("Merge subrepo force " + force);
		}

		if (!force) {
			Map statuses = new HashMap<>();

			for (int i = 0; i < statusesJSONArray.length(); i++) {
				JSONObject statusJSONObject = statusesJSONArray.getJSONObject(
					i);

				String context = statusJSONObject.getString("context");
				String state = statusJSONObject.getString("state");

				statuses.put(context, state);
			}

			boolean validStatus = false;

			if (statuses.containsKey("liferay/ci:test:relevant") &&
				statuses.containsKey("liferay/ci:test:sf")) {

				String relevantStatus = statuses.get(
					"liferay/ci:test:relevant");
				String sfStatus = statuses.get("liferay/ci:test:sf");

				if (relevantStatus.equals("success") &&
					sfStatus.equals("success")) {

					validStatus = true;
				}
			}

			if (!validStatus) {
				String message =
					"Skip merge subrepo because tests have not passed";

				if (_log.isInfoEnabled()) {
					_log.info(message);
				}

				pullRequest.addComment(message);

				pullRequest.close();

				return;
			}
		}

		String gitHubWebSubrepoHostname = _jenkinsBuildProperties.getProperty(
			"github.webhook.pullrequest.web.subrepo.hostname");

		try {
			jsonObject.put("remove", "true");

			processURL(
				"http://" + gitHubWebSubrepoHostname +
					"/osb-github-web/subrepo",
				jsonObject.toString());
		}
		catch (Exception exception) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Unable to remove key from subrepo processor queue",
					exception);
			}
		}

		jsonObject.remove("remove");

		try {
			String subrepoJSON = processURL(
				"http://" + gitHubWebSubrepoHostname +
					"/osb-github-web/subrepo",
				jsonObject.toString());

			JSONObject subrepoJSONObject = new JSONObject(subrepoJSON);

			int queueSize = subrepoJSONObject.getInt("queueSize");

			String message =
				"This subrepo merge request was added to the processor queue " +
					"at position " + queueSize;

			if (_log.isInfoEnabled()) {
				_log.info(message);
			}

			pullRequest.addComment(message + ".");
		}
		catch (Exception exception) {
			exception.printStackTrace();

			String message = "Skip merge subrepo because of an internal error";

			if (_log.isInfoEnabled()) {
				_log.info(message, exception);
			}

			pullRequest.addComment(message + ".");
		}
	}

	protected void openPullRequest(PullRequest pullRequest) {
		if (!isGitHubPostEnabled()) {
			return;
		}

		JSONObject jsonObject = new JSONObject();

		jsonObject.put("state", "open");

		String repositoryName = pullRequest.getGitHubRemoteGitRepositoryName();

		String pullRequestJSON = processURL(
			JenkinsResultsParserUtil.combine(
				"https://api.github.com/repos/", pullRequest.getOwnerUsername(),
				"/", repositoryName, "/pulls/",
				String.valueOf(pullRequest.getNumber())),
			jsonObject.toString(), HttpRequestMethod.PATCH);

		JSONObject pullRequestJSONObject = new JSONObject(pullRequestJSON);

		JSONArray errorsJSONArray = pullRequestJSONObject.optJSONArray(
			"errors");

		if (errorsJSONArray == null) {
			return;
		}

		for (int i = 0; i < errorsJSONArray.length(); i++) {
			JSONObject errorJSONObject = errorsJSONArray.getJSONObject(i);

			String message = errorJSONObject.optString("message");

			if (message == null) {
				continue;
			}

			pullRequest.addComment("GitHub error message: " + message);
		}
	}

	protected void stopJenkinsTests(
		PullRequestCommentPayload pullRequestCommentPayload) {

		String ciStopSuite = null;

		PullRequest.Comment comment = pullRequestCommentPayload.getComment();

		String commentBody = comment.getBody();

		String regex = "ci:stop:([^:\\s]+).*";

		if (commentBody.matches(regex)) {
			ciStopSuite = commentBody.replaceAll(regex, "$1");
		}

		for (String buildURL :
				getBuildURLs(pullRequestCommentPayload.getPullRequest())) {

			if (!JenkinsResultsParserUtil.isNullOrEmpty(ciStopSuite)) {
				Map buildParameters =
					JenkinsResultsParserUtil.getBuildParameters(buildURL);

				String ciTestSuite = buildParameters.getOrDefault(
					"CI_TEST_SUITE", "");

				if (!ciTestSuite.equals(ciStopSuite)) {
					continue;
				}
			}

			try {
				JenkinsStopBuildUtil.stopBuild(buildURL);
			}
			catch (Exception exception) {
				throw new RuntimeException(
					"Unable to stop build " + buildURL, exception);
			}
		}
	}

	protected void syncAutopull(PushEventPayload pushEventPayload) {
		RemoteGitBranch pusherRemoteGitBranch =
			pushEventPayload.getPusherRemoteGitBranch();

		if (pusherRemoteGitBranch == null) {
			return;
		}

		String branchName = pusherRemoteGitBranch.getName();

		if (_log.isInfoEnabled()) {
			_log.info("Sync autopull branch " + branchName);
		}

		GitHubRemoteGitRepository gitHubRemoteGitRepository =
			pushEventPayload.getRemoteGitRepository();

		String ownerName = gitHubRemoteGitRepository.getUsername();

		if (_log.isInfoEnabled()) {
			_log.info("Sync autopull owner " + ownerName);
		}

		if (!ownerName.equals("liferay")) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Skip sync autopull because the owner " + ownerName +
						" is not on the whitelist");
			}

			return;
		}

		String repositoryName = gitHubRemoteGitRepository.getName();

		if (_log.isInfoEnabled()) {
			_log.info("Sync autopull repo " + repositoryName);
		}

		if (!isValidAutopull(repositoryName)) {
			_log.info("Skip sync autopull because the repo name is invalid");

			return;
		}

		String jenkinsAuthenticationToken = _jenkinsBuildProperties.getProperty(
			"jenkins.authentication.token");

		List urls = new ArrayList<>();

		urls.add(
			JenkinsResultsParserUtil.combine(
				"http://test-1-0/job/merge-central-subrepository(", branchName,
				")/buildWithParameters?token=", jenkinsAuthenticationToken));
		urls.add(
			JenkinsResultsParserUtil.combine(
				"http://test-1-0/job/merge-central-subrepository(",
				getCompanionBranchName(branchName),
				")/buildWithParameters?token=", jenkinsAuthenticationToken));

		for (String url : urls) {
			if (isGitHubAutopullEnabled()) {
				try {
					processURL(url);
				}
				catch (Exception exception) {
					if (_log.isInfoEnabled()) {
						_log.info("Skip sync autopull", exception);
					}

					return;
				}
			}
			else if (_log.isInfoEnabled()) {
				_log.info("Sync autopull URL " + url);
			}
		}
	}

	protected void syncRepository(PushEventPayload pushEventPayload) {
		if (!isGitHubRepositorySyncEnabled()) {
			return;
		}

		GitHubRemoteGitRepository gitHubRemoteGitRepository =
			pushEventPayload.getRemoteGitRepository();

		String ownerName = gitHubRemoteGitRepository.getUsername();

		if (!ownerName.equals("liferay")) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Skip sync mirror because the owner " + ownerName +
						" is not on the whitelist");
			}

			return;
		}

		String repositoryName = gitHubRemoteGitRepository.getName();

		if (_log.isInfoEnabled()) {
			_log.info("Sync repo " + repositoryName);
		}

		JSONObject jsonObject = new JSONObject();

		jsonObject.put("repo", repositoryName);

		String sha = pushEventPayload.getAfterSHA();

		if (_log.isInfoEnabled()) {
			_log.info("Sync SHA " + sha);
		}

		jsonObject.put("sha", sha);

		String gitHubWebMirrorHostname = _jenkinsBuildProperties.getProperty(
			"github.webhook.repository.sync.web.hostname");

		try {
			processURL(
				"http://" + gitHubWebMirrorHostname + "/osb-github-web/mirror",
				jsonObject.toString());
		}
		catch (Exception exception) {
			if (_log.isInfoEnabled()) {
				_log.info("Skip sync mirror", exception);
			}
		}
	}

	protected void syncSubrepo(PushEventPayload pushEventPayload) {
		if (!isGitHubSubrepoSyncEnabled()) {
			return;
		}

		RemoteGitBranch pusherRemoteGitBranch =
			pushEventPayload.getPusherRemoteGitBranch();

		if (pusherRemoteGitBranch == null) {
			return;
		}

		String branchName = pusherRemoteGitBranch.getName();

		if (_log.isInfoEnabled()) {
			_log.info("Sync subrepo branch " + branchName);
		}

		JSONObject jsonObject = new JSONObject();

		jsonObject.put("branch", branchName);

		GitHubRemoteGitRepository gitHubRemoteGitRepository =
			pushEventPayload.getRemoteGitRepository();

		String ownerName = gitHubRemoteGitRepository.getUsername();

		if (!ownerName.equals("liferay")) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Skip sync subrepo because the owner " + ownerName +
						" is not on the whitelist");
			}

			return;
		}

		String repositoryName = _payload.get("repository/name");

		if (_log.isInfoEnabled()) {
			_log.info("Sync subrepo repo " + repositoryName);
		}

		jsonObject.put("repo", repositoryName);

		String sha = pushEventPayload.getAfterSHA();

		if (_log.isInfoEnabled()) {
			_log.info("Sync subrepo SHA " + sha);
		}

		jsonObject.put(
			"pullRequestNumber", "0"
		).put(
			"sha", sha
		);

		String command = "push";
		String propertyName =
			"github.webhook.subrepository.sync.subrepo.web.hostname";
		String subrepo = "all";

		if (isBotPush(pushEventPayload)) {
			command = "release";
			propertyName =
				"github.webhook.subrepository.sync.release.web.hostname";
			subrepo = getSubrepoPath(pushEventPayload);
		}

		jsonObject.put("command", command);

		if (_log.isInfoEnabled()) {
			_log.info("Sync subrepo command " + command);
		}

		jsonObject.put("subrepo", subrepo);

		if (_log.isInfoEnabled()) {
			_log.info("Sync subrepo argument " + subrepo);
		}

		String gitHubWebSubrepoHostname = _jenkinsBuildProperties.getProperty(
			propertyName);

		try {
			processURL(
				"http://" + gitHubWebSubrepoHostname +
					"/osb-github-web/subrepo",
				jsonObject.toString());
		}
		catch (Exception exception) {
			if (_log.isInfoEnabled()) {
				_log.info("Skip sync subrepo", exception);
			}
		}
	}

	protected void testPullRequest(
		PullRequestTesterParameters pullRequestTesterParameters) {

		if (!isTestablePullRequest(
				pullRequestTesterParameters.getPullRequest())) {

			return;
		}

		String pullRequestTesterQueryString =
			pullRequestTesterParameters.toQueryString();

		addTestPullRequestQueryString(pullRequestTesterQueryString);

		String masterURL = "http://test-1.liferay.com";

		try {
			masterURL = JenkinsResultsParserUtil.getMostAvailableMasterURL(
				"http://test-1.liferay.com",
				_jenkinsBuildProperties.getProperty(
					"jenkins.load.balancer.blacklist", ""),
				1, JenkinsMaster.getSlaveRAMMinimumDefault(),
				JenkinsMaster.getSlavesPerHostDefault());
		}
		catch (Exception exception) {
			if (_log.isInfoEnabled()) {
				_log.info(
					"Setting base invocation URL to " +
						"http://test-1.liferay.com because load balancer " +
							"threw an exception");
			}
		}

		invokePullRequestTester(masterURL, pullRequestTesterParameters);
	}

	private String _cleanupJSONSource(String source) {
		StringBuilder sb = new StringBuilder();

		while (!source.isEmpty()) {
			int start = source.indexOf("\"");

			if (start == -1) {
				sb.append(source);

				source = "";

				continue;
			}

			sb.append(source.substring(0, start));

			int end = source.indexOf("\"", start + 1);

			if (end == -1) {
				throw new IllegalArgumentException(
					"Unterminated quote found after index " + start);
			}

			String quotedString = source.substring(start, end + 1);

			if (quotedString.contains("\n")) {
				quotedString = quotedString.replaceAll("\n", "");

				System.out.println("quotedString:\n" + quotedString);
			}

			sb.append(quotedString);

			source = source.substring(end + 1);
		}

		return sb.toString();
	}

	private List _getCIEnabledBranchNames(String repositoryName) {
		String ciEnabledBranchNames = _jenkinsBuildProperties.getProperty(
			JenkinsResultsParserUtil.combine(
				"github.ci.enabled.branch.names[", repositoryName, "]"),
			"");

		return Arrays.asList(ciEnabledBranchNames.split(","));
	}

	private void _processCommentCreated(
		PullRequestCommentPayload pullRequestCommentPayload) {

		PullRequest.Comment comment = pullRequestCommentPayload.getComment();

		String body = comment.getBody();

		String login = comment.getUserLogin();

		PullRequest pullRequest = pullRequestCommentPayload.getPullRequest();

		if (body.startsWith("ci:") && !body.contains("ci:help") &&
			!isLiferayUser(login)) {

			if (_log.isInfoEnabled()) {
				_log.info(
					JenkinsResultsParserUtil.combine(
						"Skip CI action because ", login,
						" is not a Liferay member"));
			}

			if (hasLiferayEmailAddress(login)) {
				StringBuilder sb = new StringBuilder();

				sb.append("You cannot perform that action because you ");
				sb.append("are not a member of the Liferay organization. ");
				sb.append("Please make sure that you have been added and ");
				sb.append("that your organization membership is set as ");
				sb.append("Public. See https://help.github.com/articles");
				sb.append("/publicizing-or-hiding-organization-");
				sb.append("membership for more information.");

				pullRequest.addComment(sb.toString());
			}

			return;
		}

		PullRequestTesterParameters pullRequestTesterParameters =
			new PullRequestTesterParameters(pullRequest);

		if (body.startsWith("ci:close")) {
			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered close pull request");
			}

			pullRequest.close();
		}

		if (body.startsWith("ci:forward")) {
			pullRequestTesterParameters.setCiForwardReceiverUsername(
				_jenkinsBuildProperties.getProperty(
					"pull.request.forward.default.receiver.username"));

			String[] ciForwardRequiredTestSuites =
				getCIForwardRequiredTestSuites();

			if (ciForwardRequiredTestSuites.length == 0) {
				StringBuilder sb = new StringBuilder();

				sb.append("There are no required test suites specified ");
				sb.append("for `ci:forward`.\nNo test will be triggered.");
				sb.append("\nIf you think this is a mistake please ");
				sb.append("contact the CI Infrastructure team.");

				pullRequest.addComment(sb.toString());

				return;
			}

			String[] ciForwardRequiredPassingSuites =
				getCIForwardRequiredPassingSuites();

			if (ciForwardRequiredPassingSuites.length == 0) {
				StringBuilder sb = new StringBuilder();

				sb.append("There are no required passing suites specified");
				sb.append(" for `ci:forward`.\nNo test will be triggered.");
				sb.append("\nIf you think this is a mistake please ");
				sb.append("contact the CI Infrastructure team.");

				pullRequest.addComment(sb.toString());

				return;
			}

			StringBuilder sb = new StringBuilder();

			sb.append("CI is automatically triggering the following ");
			sb.append("test suites:\n");

			for (String ciForwardRequiredTestSuite :
					ciForwardRequiredTestSuites) {

				sb.append("-     ci:test:**");
				sb.append(ciForwardRequiredTestSuite);
				sb.append("**\n");
			}

			sb.append("\n");
			sb.append("The pull request will automatically be forwarded ");
			sb.append("to the user `");
			sb.append(
				pullRequestTesterParameters.getCiForwardReceiverUsername());
			sb.append("` if the following test suites pass:\n");

			_ciForwardEligible = true;

			Set passingTestSuites = getPassingTestSuites(pullRequest);

			for (String ciForwardRequiredPassingSuite :
					ciForwardRequiredPassingSuites) {

				sb.append("-     ci:test:**");
				sb.append(ciForwardRequiredPassingSuite);
				sb.append("**\n");

				if (!passingTestSuites.contains(
						ciForwardRequiredPassingSuite)) {

					_ciForwardEligible = false;
				}
			}

			pullRequest.addComment(sb.toString());

			List skippedTestSuites = new ArrayList<>(
				ciForwardRequiredTestSuites.length);

			for (String ciForwardRequiredTestSuite :
					ciForwardRequiredTestSuites) {

				if (passingTestSuites.contains(ciForwardRequiredTestSuite)) {
					skippedTestSuites.add(ciForwardRequiredTestSuite);

					continue;
				}

				pullRequestTesterParameters.setCiTestSuiteName(
					ciForwardRequiredTestSuite);

				testPullRequest(pullRequestTesterParameters);
			}

			if (!skippedTestSuites.isEmpty()) {
				sb = new StringBuilder();

				sb.append("Skipping previously passed test suites:\n");

				for (String skippedTestSuite : skippedTestSuites) {
					sb.append("`ci:test:");
					sb.append(skippedTestSuite);
					sb.append("`\n");
				}

				if (_log.isInfoEnabled()) {
					_log.info(sb.toString());
				}

				pullRequest.addComment(sb.toString());
			}

			if (_ciForwardEligible) {
				testPullRequest(pullRequestTesterParameters);
			}
		}

		if (body.startsWith("ci:help")) {
			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered help message");
			}

			StringBuilder sb = new StringBuilder();

			sb.append("## Available CI commands:\n");
			sb.append("#### ci:close\n");
			sb.append("    Close the pull request.\n");

			String ciForwardReceiverUsername =
				_jenkinsBuildProperties.getProperty(
					"pull.request.forward.default.receiver.username");

			if ((ciForwardReceiverUsername != null) &&
				!ciForwardReceiverUsername.isEmpty()) {

				sb.append("#### ci:forward\n");
				sb.append("    Test the pull request ");
				sb.append(" and forward the pullrequest to `");
				sb.append(ciForwardReceiverUsername);
				sb.append("` if the required test suites pass.\n");
			}

			sb.append("#### ci:merge[:force]\n");
			sb.append("    Merge in the changes from ");
			sb.append("the subrepo. All tests must pass before this ");
			sb.append("command will successfully run. Optionally use the ");
			sb.append("force flag to bypass failed tests.\n");
			sb.append("#### ci:reevaluate:[buildID]\n");
			sb.append("    Reevaluate the pull request ");
			sb.append("result from a generated build ID.\n");
			sb.append("#### ci:reopen\n");
			sb.append("    Reopen the pull request.\n");
			sb.append("#### ci:stop[:suite]\n");
			sb.append("    Stop all currrently ");
			sb.append("running tests. Optionally specify the name of the ");
			sb.append("test suite.\n");

			String ciTestAvailableSuites =
				JenkinsResultsParserUtil.getCIProperty(
					pullRequest.getUpstreamRemoteGitBranchName(),
					"ci.test.available.suites",
					pullRequest.getGitHubRemoteGitRepositoryName());

			if (ciTestAvailableSuites != null) {
				sb.append("#### ci:test[:suite][:SHA|nocompile|");
				sb.append("norebase]\n");
				sb.append("    Test the pull request.");
				sb.append("Optionally specify the name of the test suite ");
				sb.append("and/or optionally specify the upstream SHA to ");
				sb.append("test against or use \"nocompile\" to test ");
				sb.append("with a prebuilt bundle or use \"norebase\" to ");
				sb.append("test without rebasing.");
				sb.append("\n\n    List of ");
				sb.append("available test suites:\n");

				for (String ciTestAvailableSuite :
						ciTestAvailableSuites.split(",")) {

					sb.append("-     ci:test:**");
					sb.append(ciTestAvailableSuite);
					sb.append("** - ");

					String ciTestSuiteDescription =
						JenkinsResultsParserUtil.getCIProperty(
							pullRequest.getUpstreamRemoteGitBranchName(),
							JenkinsResultsParserUtil.combine(
								"ci.test.suite.description[",
								ciTestAvailableSuite, "]"),
							pullRequest.getGitHubRemoteGitRepositoryName());

					if (ciTestSuiteDescription != null) {
						sb.append(ciTestSuiteDescription);
					}
					else {
						sb.append("No description is available.");
					}

					sb.append("\n");
				}
			}
			else {
				sb.append("#### ci:test[:SHA|nocompile|norebase]\n");
				sb.append("    Test the pull request");
				sb.append(". Optionally specify the upstream SHA to test ");
				sb.append("against or use \"nocompile\" to test with a ");
				sb.append("prebuilt bundle or use \"norebase\" to test ");
				sb.append("without rebasing.\n");
			}

			sb.append("\n");
			sb.append("For more details, see ");
			sb.append("[GROW](https://grow.liferay.com/share/CI+");
			sb.append("liferay-continuous-integration+GitHub+Commands).");

			pullRequest.addComment(sb.toString());
		}

		if (body.startsWith("ci:merge")) {
			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered merge subrepo");
			}

			if (body.startsWith("ci:merge:force")) {
				if (!login.equals("brianchandotcom") &&
					!login.equals("brianwulbern") && !login.equals("jpince") &&
					!login.equals("pyoo47") && !login.equals("shuyangzhou") &&
					!login.equals("stsquared99")) {

					String message = "Only Brian Chan can force a merge";

					if (_log.isInfoEnabled()) {
						_log.info(message);
					}

					pullRequest.addComment(message + ".");

					return;
				}

				mergeSubrepo(pullRequest, true);
			}
			else {
				mergeSubrepo(pullRequest, false);
			}
		}

		if (body.startsWith("ci:reevaluate")) {
			Matcher matcher = _reevaluatePattern.matcher(body);

			StringBuilder sb = new StringBuilder();

			if (matcher.find()) {
				if (_log.isInfoEnabled()) {
					_log.info("Comment triggered reevaluate");
				}

				String buildID = matcher.group("buildID");

				pullRequestTesterParameters.setCiReevaluateBuildId(buildID);

				sb.append("CI is reevaluating the build with build ID: `");
				sb.append(buildID);
				sb.append("` against the latest valid upstream results.");

				pullRequest.addComment(sb.toString());

				testPullRequest(pullRequestTesterParameters);
			}
			else {
				sb.append("There is a missing or invalid build ID. ");
				sb.append("No reevaluation was triggered.");

				pullRequest.addComment(sb.toString());
			}
		}

		if (body.startsWith("ci:reopen")) {
			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered open pull request");
			}

			openPullRequest(pullRequest);
		}

		if (body.startsWith("ci:retest") || body.startsWith("ci:test")) {
			Matcher matcher = _testPattern.matcher(body);

			if (matcher.find()) {
				String testOption1 = matcher.group("testOption1");

				if (testOption1.matches("[0-9a-f]{7,40}")) {
					if (isValidPullRequestRefSHA(pullRequest, testOption1)) {
						pullRequestTesterParameters.setUpstreamBranchSHA(
							testOption1);
					}
					else {
						StringBuilder sb = new StringBuilder();

						sb.append("The test option '");
						sb.append(testOption1);
						sb.append("' matching SHA pattern [0-9a-f]{7,40}");
						sb.append(" is not a valid upstream SHA.\n");
						sb.append("The test will start with '");
						sb.append(testOption1);
						sb.append("' as the test suite");

						String message = sb.toString();

						if (_log.isInfoEnabled()) {
							_log.info(message);
						}

						pullRequest.addComment(message + ".");

						pullRequestTesterParameters.setCiTestSuiteName(
							testOption1);
					}
				}
				else if (testOption1.equals("forward")) {
					StringBuilder sb = new StringBuilder();

					sb.append("The test will not be initiated because ");
					sb.append("`ci:test:forward` is not a valid command. ");
					sb.append("Please use `ci:forward` instead");

					String message = sb.toString();

					if (_log.isInfoEnabled()) {
						_log.info(message);
					}

					pullRequest.addComment(message + ".");

					return;
				}
				else if (!testOption1.equals("nocompile") &&
						 !testOption1.equals("norebase")) {

					pullRequestTesterParameters.setCiTestSuiteName(testOption1);
				}

				String testOption2 = matcher.group("testOption2");

				if (testOption2 != null) {
					if (testOption2.matches("[0-9a-f]{7,40}")) {
						pullRequestTesterParameters.setUpstreamBranchSHA(
							testOption2);
					}
					else if (testOption2.equals("forward")) {
						StringBuilder sb = new StringBuilder(5);

						sb.append("The test will not be initiated ");
						sb.append("because ");
						sb.append("`ci:test:[testsuite]:forward` ");
						sb.append("is not a valid command. Please use");
						sb.append("`ci:forward` instead");

						String message = sb.toString();

						if (_log.isInfoEnabled()) {
							_log.info(message);
						}

						pullRequest.addComment(message + ".");

						return;
					}
				}

				List testOptions = Arrays.asList(
					testOption1, testOption2);

				if (testOptions.contains("nocompile")) {
					String distPortalBundlesBuildURL =
						JenkinsResultsParserUtil.getDistPortalBundlesBuildURL(
							pullRequest.getUpstreamRemoteGitBranchName());

					String message = null;

					if (distPortalBundlesBuildURL != null) {
						pullRequestTesterParameters.setPortalBundlesDistURL(
							distPortalBundlesBuildURL);

						String upstreamBranchSHA = processURL(
							distPortalBundlesBuildURL + "/git-hash");

						pullRequestTesterParameters.setUpstreamBranchSHA(
							upstreamBranchSHA.trim());

						message = "The test will run with a prebuilt bundle.";
					}
					else {
						message = "No valid prebuilt bundle is available.";
					}

					if (_log.isInfoEnabled()) {
						_log.info(message);
					}

					pullRequest.addComment(message + ".");
				}
				else if (testOptions.contains("norebase")) {
					String message = "The test will run without rebasing.";

					if (_log.isInfoEnabled()) {
						_log.info(message);
					}

					pullRequest.addComment(message + ".");

					pullRequestTesterParameters.setUpstreamBranchSHA(
						pullRequest.getCommonParentSHA());
				}

				String testSuiteName =
					pullRequestTesterParameters.getCiTestSuiteName();

				if ((testSuiteName != null) &&
					testSuiteName.equals("gauntlet") &&
					!_gauntletUsernames.contains(login)) {

					String message =
						"You do not have permission to run the test gauntlet";

					if (_log.isInfoEnabled()) {
						_log.info(message);
					}

					pullRequest.addComment(message + ".");

					return;
				}
			}

			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered test");
			}

			testPullRequest(pullRequestTesterParameters);
		}

		if (body.startsWith("ci:stop")) {
			if (_log.isInfoEnabled()) {
				_log.info("Comment triggered stop");
			}

			stopJenkinsTests(pullRequestCommentPayload);
		}
	}

	private void _processPullRequestOpened(
		PullRequestPayload pullRequestPayload) {

		PullRequest pullRequest = pullRequestPayload.getPullRequest();

		PullRequestTesterParameters pullRequestTesterParameters =
			new PullRequestTesterParameters(pullRequest);

		List pullRequestComments =
			pullRequest.getComments();

		PullRequest.Comment initialComment = pullRequestComments.get(0);

		String body = initialComment.getBody();

		String githubCIUsername = _jenkinsBuildProperties.getProperty(
			"github.ci.username");
		String ownerName = pullRequest.getOwnerUsername();
		String pullRequestForwardDefaultReceiverUsername =
			_jenkinsBuildProperties.getProperty(
				"pull.request.forward.default.receiver.username");
		String refName = pullRequest.getUpstreamRemoteGitBranchName();
		String repositoryName = pullRequest.getGitHubRemoteGitRepositoryName();
		String senderName = pullRequest.getSenderUsername();

		if (body.startsWith("Forwarded from:") &&
			ownerName.equals(pullRequestForwardDefaultReceiverUsername) &&
			senderName.equals(githubCIUsername)) {

			StringBuilder sb = new StringBuilder(2);

			sb.append("To conserve resources, the PR Tester does not ");
			sb.append("automatically run for forwarded pull requests.");

			pullRequest.addComment(sb.toString());

			return;
		}

		if (ownerName.equals(
				getSubrepoCentralMergePullRequestRecipientName(refName)) &&
			pullRequest.isMergeSubrepoRequest()) {

			commentMergeSubrepoPullRequest(pullRequest);

			pullRequestTesterParameters.setCiTestSuiteName("relevant");

			testPullRequest(pullRequestTesterParameters);

			pullRequestTesterParameters.setCiTestSuiteName("sf");

			testPullRequest(pullRequestTesterParameters);

			return;
		}

		if (ownerName.equals("brianchandotcom") &&
			repositoryName.contains("liferay-portal")) {

			if (!isTestablePullRequest(pullRequest)) {
				return;
			}

			if (refName.startsWith("ee-")) {
				StringBuilder sb = new StringBuilder(4);

				sb.append("CI is automatically triggering "ci:");
				sb.append("test:sf" for this pull to run Source ");
				sb.append("Formatter.\n\nComment "ci:test" ");
				sb.append("to run the full PR Tester for this pull.");

				pullRequest.addComment(sb.toString());
			}
			else {
				StringBuilder sb = new StringBuilder(6);

				sb.append("CI is automatically triggering "ci:");
				sb.append("test:sf" and "ci:test:relevant");
				sb.append("" for this pull to run Source ");
				sb.append("Formatter and relevant tests.\n\nComment ");
				sb.append(""ci:test" to run the full PR ");
				sb.append("Tester for this pull.");

				pullRequest.addComment(sb.toString());

				pullRequestTesterParameters.setCiTestSuiteName("relevant");

				testPullRequest(pullRequestTesterParameters);
			}

			pullRequestTesterParameters.setCiTestSuiteName("sf");

			testPullRequest(pullRequestTesterParameters);

			return;
		}

		if (!ownerName.equals("liferay")) {
			List ciTestAutoTestSuiteNames = getCITestAutoTestSuiteNames(
				pullRequest);

			if (!ciTestAutoTestSuiteNames.isEmpty() &&
				isTestablePullRequest(pullRequest)) {

				StringBuilder sb = new StringBuilder();

				sb.append("CI is automatically triggering the following ");
				sb.append("test suites:\n");

				for (String ciTestAutoTestSuiteName :
						ciTestAutoTestSuiteNames) {

					sb.append("-     ci:test:**");
					sb.append(ciTestAutoTestSuiteName);
					sb.append("**\n");
				}

				pullRequest.addComment(sb.toString());

				for (String ciTestAutoTestSuiteName :
						ciTestAutoTestSuiteNames) {

					pullRequestTesterParameters.setCiTestSuiteName(
						ciTestAutoTestSuiteName);

					testPullRequest(pullRequestTesterParameters);
				}

				return;
			}
		}

		if (isLiferayUser(pullRequest.getSenderUsername())) {
			StringBuilder sb = new StringBuilder(7);

			sb.append("To conserve resources, the PR Tester does not ");
			sb.append("automatically run for every pull.\n\nIf your ");
			sb.append("code changes were already tested in another ");
			sb.append("pull, reference that pull in this pull so ");
			sb.append("the test results can be analyzed.\n\nIf your ");
			sb.append("pull was never tested, comment ");
			sb.append(""ci:test" to run the PR Tester for ");
			sb.append("this pull.");

			GitHubRemoteGitRepository gitHubRemoteGitRepository =
				pullRequest.getGitHubRemoteGitRepository();

			if (!ownerName.equals("liferay") ||
				gitHubRemoteGitRepository.isSubrepository() ||
				repositoryName.equals("liferay-portal-ee")) {

				pullRequest.addComment(sb.toString());
			}
		}
	}

	private void _processPullRequestSynchronize(
		PullRequestPayload pullRequestPayload) {

		PullRequest pullRequest = pullRequestPayload.getPullRequest();

		if (isSynchronizeablePullRequest(pullRequest)) {
			return;
		}

		if (_log.isInfoEnabled()) {
			_log.info("Synchronize triggered close pull request");
		}

		String message = JenkinsResultsParserUtil.combine(
			"Closing and locking pull request because pull requests ",
			"sent to this user may not be updated. Please resend ",
			"this pull request.");

		pullRequest.addComment(message);

		pullRequest.close();

		pullRequest.lock();
	}

	private static final String[] _URLS_JENKINS_BUILD_PROPERTIES = {
		"http://mirrors-no-cache.lax.liferay.com/github.com/liferay" +
			"/liferay-jenkins-ee/build.properties",
		"http://mirrors-no-cache.lax.liferay.com/github.com/liferay" +
			"/liferay-jenkins-ee/commands/build.properties"
	};

	private static final Log _log = LogFactory.getLog(
		GitHubWebhookPayloadProcessor.class);

	private static final List _acRepositories = Arrays.asList(
		"com-liferay-osb-asah-private");
	private static final Pattern _buildURLPattern = Pattern.compile(
		"Build[\\w\\s]*started.*Job Link: [^\"]+)\"");
	private static final List _gauntletUsernames = Arrays.asList(
		"CsabaTurcsan", "Hanlf", "HarryC0204", "Songyuewen", "SylviaLuan",
		"ZoltanTakacs", "brianchandotcom", "brianwulbern", "ctampoya",
		"gergelyszaz", "jpince", "kiyoshilee", "lesliewong92",
		"liferay-continuous-integration-hu", "michaelhashimoto",
		"michaelprigge", "pyoo47", "sharonchoi", "shuyangzhou", "stsquared99",
		"suilin", "vicnate5", "xbrianlee", "yunlinsun");
	private static final Pattern _gitrepoRepoPattern = Pattern.compile(
		"remote = .*/([^\\.]*)\\.git");
	private static final Pattern _gitrepoSHAPattern = Pattern.compile(
		"commit = ([0-9a-f]{40})");
	private static Set _passingTestSuites;
	private static final Pattern _passingTestSuiteStatusDescriptionPattern =
		Pattern.compile("\"ci:test:(?[^\"]+)\"\\s*has PASSED.");
	private static final Pattern _reevaluatePattern = Pattern.compile(
		"ci:reevaluate:(?[\\d]+_[\\d]+)");
	private static final Pattern _testPattern = Pattern.compile(
		"ci:(re)?test:(?[^:\\s]+)(:(?[^:\\s]+))?");
	private static final List _whiteListedOwnerNames =
		Collections.emptyList();
	private static final MultiPattern _whiteListedRepositoryMultiPattern =
		new MultiPattern(
			"com-liferay-.*", "liferay-fix-pack-builder-ee",
			"liferay-jenkins-ee", "liferay-plugins(-ee)?",
			"liferay-portal(-ee)?");

	private boolean _ciForwardEligible;
	private final Properties _jenkinsBuildProperties;
	private List _jiraProjectKeys;
	private final Payload _payload;
	private final Map _testPullRequestQueryStrings =
		new ConcurrentHashMap<>(100);
	private final Map _testPullRequestURLs =
		new ConcurrentHashMap<>(100);
	private final Set _validLiferayUsers = new HashSet<>();

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy