Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add label trigger support #62

Merged
merged 3 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.adobe.jenkins.github_pr_comment_build;

import hudson.model.Cause;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
import org.kohsuke.stapler.export.Exported;

import java.io.Serializable;

/**
* Created by Agyaey on 04/04/2023.
*/
public final class GitHubPullRequestLabelCause extends Cause implements Serializable {
private final String labelUrl;
private final String labellingAuthor;
private final String label;

/**
* Constructor.
*
* @param labelUrl the URL for the GitHub Label
* @param labellingAuthor the author of the GitHub Label
* @param label the body for the GitHub Label
*/
public GitHubPullRequestLabelCause(String labelUrl, String labellingAuthor, String label) {
this.labelUrl = labelUrl;
this.labellingAuthor = labellingAuthor;
this.label = label;
}

@Whitelisted
@Override
public String getShortDescription() {
return "GitHub pull request Label";
}

/**
* Retrieves the URL for the GitHub Label for this cause.
*
* @return the URL for the GitHub Label
*/
@Whitelisted
@Exported(visibility = 3)
public String getLabelUrl() {
return labelUrl;
}

/**
* Retrieves the author of the GitHub Label for this cause.
*
* @return the author of the GitHub Label
*/
@Whitelisted
@Exported(visibility = 3)
public String getLabellingAuthor() {
return labellingAuthor;
}

/**
* Retrieves the body for the GitHub Label for this cause.
*
* @return the body for the GitHub Label
*/
@Whitelisted
@Exported(visibility = 3)
public String getLabel() {
return label;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package com.adobe.jenkins.github_pr_comment_build;

import com.cloudbees.jenkins.GitHubRepositoryName;
import hudson.Extension;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Job;
import hudson.security.ACL;
import hudson.security.ACLContext;
import jenkins.branch.BranchProperty;
import jenkins.branch.MultiBranchProject;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceOwner;
import jenkins.scm.api.SCMSourceOwners;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
import org.kohsuke.github.GHEvent;

import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Sets.immutableEnumSet;
import static hudson.security.ACL.as;
import static org.kohsuke.github.GHEvent.PULL_REQUEST;

/**
* This subscriber manages {@link GHEvent} Label.
*/
@Extension
public class IssueLabelGHEventSubscriber extends GHEventsSubscriber {
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(IssueLabelGHEventSubscriber.class.getName());
/**
* Regex pattern for a GitHub repository.
*/
private static final Pattern REPOSITORY_NAME_PATTERN = Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)");
/**
* Regex pattern for a pull request ID.
*/
private static final Pattern PULL_REQUEST_ID_PATTERN = Pattern.compile("https?://[^/]+/[^/]+/[^/]+/pull/(\\d+)");

/**
* String representing the created action on labeled PR.
*/
private static final String ACTION_LABELED = "labeled";

@Override
protected boolean isApplicable(Item item) {
if (item != null && item instanceof Job<?, ?>) {
Job<?, ?> project = (Job<?, ?>) item;
if (project.getParent() instanceof SCMSourceOwner) {
SCMSourceOwner owner = (SCMSourceOwner) project.getParent();
for (SCMSource source : owner.getSCMSources()) {
if (source instanceof GitHubSCMSource) {
return true;
}
}
}
}
return false;
}

@Override
protected Set<GHEvent> events() {
return immutableEnumSet(PULL_REQUEST);
}

/**
* Handles Labels on pull requests.
*
* @param event only label event
* @param payload payload of gh-event. Never blank
*/
@Override
protected void onEvent(GHEvent event, String payload) {
JSONObject json = JSONObject.fromObject(payload);

// Make sure this issue is a PR
//final String pullRequestUrl = json.getJSONObject("pull_request").getString("html_url");
JSONObject pullRequest = json.getJSONObject("pull_request");
final String pullRequestUrl = pullRequest.getString("html_url");
Integer pullRequestId = pullRequest.getInt("number");
String labellingAuthor = json.getJSONObject("sender").getString("login");
LOGGER.fine(() -> String.format("PR Review Author: %s", labellingAuthor));

final Pattern pullRequestJobNamePattern = Pattern.compile("^PR-" + pullRequestId + "\\b.*$", Pattern.CASE_INSENSITIVE);

final String label = json.getJSONObject("label").getString("name");
final String labelUrl = json.getJSONObject("label").getString("url");

// Make sure the action is edited or created (not deleted)
String action = json.getString("action");
if (!ACTION_LABELED.equals(action)) {
LOGGER.log(Level.FINER, "Event is labeled ({0}) for PR {1}",
new Object[]{action, pullRequestUrl}
);
return;
}

// Make sure the repository URL is valid
String repoUrl = json.getJSONObject("repository").getString("html_url");
Matcher matcher = REPOSITORY_NAME_PATTERN.matcher(repoUrl);
if (!matcher.matches()) {
LOGGER.log(Level.WARNING, "Malformed repository URL {0}", repoUrl);
return;
}
final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(repoUrl);
if (changedRepository == null) {
LOGGER.log(Level.WARNING, "Malformed repository URL {0}", repoUrl);
return;
}

LOGGER.log(Level.FINE, "Received label on PR {0} for {1}", new Object[]{pullRequestId, repoUrl});
try (ACLContext aclContext = as(ACL.SYSTEM)) {
boolean jobFound = false;
Set<Job<?, ?>> alreadyTriggeredJobs = new HashSet<>();
for (final SCMSourceOwner owner : SCMSourceOwners.all()) {
for (SCMSource source : owner.getSCMSources()) {
if (!(source instanceof GitHubSCMSource)) {
continue;
}
GitHubSCMSource gitHubSCMSource = (GitHubSCMSource) source;
if (gitHubSCMSource.getRepoOwner().equalsIgnoreCase(changedRepository.getUserName()) &&
gitHubSCMSource.getRepository().equalsIgnoreCase(changedRepository.getRepositoryName())) {
for (Job<?, ?> job : owner.getAllJobs()) {
if (pullRequestJobNamePattern.matcher(job.getName()).matches()) {
if (!(job.getParent() instanceof MultiBranchProject)) {
continue;
}
boolean propFound = false;
for (BranchProperty prop : ((MultiBranchProject) job.getParent()).getProjectFactory().
getBranch(job).getProperties()) {
if (!(prop instanceof TriggerPRLabelBranchProperty)) {
continue;
}
propFound = true;
TriggerPRLabelBranchProperty branchProp = (TriggerPRLabelBranchProperty) prop;
String expectedLabel = branchProp.getLabel();
Pattern pattern = Pattern.compile(expectedLabel,
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
if (pattern.matcher(label).matches()) {
if (alreadyTriggeredJobs.add(job)) {
ParameterizedJobMixIn.scheduleBuild2(job, 0,
new CauseAction(new GitHubPullRequestLabelCause(
labelUrl, labellingAuthor, label)));
LOGGER.log(Level.FINE,
"Triggered build for {0} due to PR Label on {1}:{2}/{3}",
new Object[]{
job.getFullName(),
changedRepository.getHost(),
changedRepository.getUserName(),
changedRepository.getRepositoryName()
}
);
} else {
LOGGER.log(Level.FINE, "Skipping already triggered job {0}", new Object[]{job});
}
} else {
LOGGER.log(Level.FINER,
"Label does not match the trigger build label string ({0}) for {1}",
new Object[]{expectedLabel, job.getFullName()}
);
}
break;
}

if (!propFound) {
LOGGER.log(Level.FINE,
"Job {0} for {1}:{2}/{3} does not have a trigger PR Label branch property",
new Object[]{
job.getFullName(),
changedRepository.getHost(),
changedRepository.getUserName(),
changedRepository.getRepositoryName()
}
);
}

jobFound = true;
} else { // failed to match 'pullRequestJobNamePattern'
LOGGER.log(Level.FINE,
"Skipping job [{0}] as it does not match the 'PR-' pattern." +
"If this is unexpected, make sure the job is configured with a 'discover pull requests...' behavior (see README)",
new Object[]{job.getName()});
}
}
}
}
}
if (!jobFound) {
LOGGER.log(Level.FINE, "PR label on {0}:{1}/{2} did not match any job",
new Object[]{
changedRepository.getHost(), changedRepository.getUserName(),
changedRepository.getRepositoryName()
}
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.adobe.jenkins.github_pr_comment_build;

import hudson.Extension;
import hudson.model.Job;
import hudson.model.Run;
import jenkins.branch.BranchProperty;
import jenkins.branch.BranchPropertyDescriptor;
import jenkins.branch.JobDecorator;
import org.kohsuke.stapler.DataBoundConstructor;

/**
* Allows a GitHub pull request comment to trigger an immediate build based on a comment string.
*/
public class TriggerPRLabelBranchProperty extends BranchProperty {
/**
* The comment body to trigger a new build on.
*/
private final String label;

/**
* Constructor.
*
* @param label the comment body to trigger a new build on
*/
@DataBoundConstructor
public TriggerPRLabelBranchProperty(String label) {
this.label = label;
}

/**
* The comment body to trigger a new build on.
*
* @return the comment body to use
*/
public String getLabel() {
return this.label;
}

@Override
public <P extends Job<P, B>, B extends Run<P, B>> JobDecorator<P, B> jobDecorator(Class<P> clazz) {
return null;
}

@Extension
public static class DescriptorImpl extends BranchPropertyDescriptor {

@Override
public String getDisplayName() {
return Messages.TriggerPRLabelBranchProperty_trigger_on_pull_request_label();
}

}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
TriggerPRCommentBranchProperty.trigger_on_pull_request_comment=Trigger build on pull request comment
TriggerPRUpdateBranchProperty.trigger_on_pull_request_update=Trigger build on pull request update
TriggerPRReviewBranchProperty.trigger_on_pull_request_review=Trigger build on pull request review
TriggerPRReviewBranchProperty.trigger_on_pull_request_review=Trigger build on pull request review
TriggerPRLabelBranchProperty.trigger_on_pull_request_label=Trigger build on pull request label
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="Label" field="label">
<f:textbox />
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
The Label to trigger a new build for a PR job when it is received.
AgyaeyTiwari marked this conversation as resolved.
Show resolved Hide resolved
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
This property will cause a job for a pull request (PR-*) to be triggered immediately when a label is placed
on the PR in GitHub. This has no effect on jobs that are not for pull requests.
</div>