Skip to content

Commit

Permalink
Merge pull request #162 from steven-foster/JENKINS-36574
Browse files Browse the repository at this point in the history
[JENKINS-36574] Custom Github status context
  • Loading branch information
stephenc committed Dec 18, 2017
2 parents e8c6c77 + a622b0f commit 267e41c
Show file tree
Hide file tree
Showing 9 changed files with 885 additions and 79 deletions.
50 changes: 50 additions & 0 deletions docs/implementation.adoc
@@ -0,0 +1,50 @@
= Implementation Guide

== Extension Points

=== AbstractGitHubNotificationStrategy
This extension points allows traits to modify the contents of a Github status notification for a build.
There are currently 3 points in a build lifecycle where notifications are sent:

* On entering the queue

* On checkout

* On completion


To add or modify a status notification at these points you should implement an `AbstractGithubNotificationStrategy`.
This class will use a method, `notifications`, to create and return a list of one or more notifications to be made.
The notification strategy must also implement `equals()` and `hashCode()`.

A `GitHubNotificationContext` object supplies build related information to this method depending on which of the above
events has triggered. In all cases, the `SCMSource` and `SCMHead` are available. On entering the queue, the `Job` of the
build is available. On checkout and completion, the `Run` of the build is available.

A Github status notification has 4 parts: the `context`, `url`, `message` and `state`.

The `notifications` method returns a `List<GitHubNotificationRequests>` which each contain the 4 parts of a Github
notification and a boolean to indicate if errors should be ignored when sending that notification.

The default implementation for any of these parts can be accessed in the `GitHubNotificationContext` object. This is
useful for strategies which are only replacing a single part and want to retain the defaults for the rest.

The strategy or strategies are applied through a `trait`, like many other branch api extension points.
Create a class extending `SCMSourceTrait` and override the `decorateContext` to apply the strategy. `GitHubSCMSourceContext`
has two methods for applying strategies:

* `withNotificationStrategies` takes a list of `AbstractGitHubNotificationStrategy` and overwrites any existing strategies with
this list.

* `withNotificationStrategy` takes a single strategy and adds it to a list.

The default notification will not be sent if any strategy has been added to the `GitHubSCMSourceContext`. If your trait/strategy
sends an entirely different kind of notification and you want the default notification to be sent alongside it, you should
explicitly apply a `DefaultGitHubNotificationStrategy` to the source context in your trait.

Duplicate (by equality) strategies are ignored when applied to the source context.

==== Implementations:
https://github.com/steven-foster/github-scm-trait-notification-context[github-scm-trait-notification-context]


@@ -0,0 +1,56 @@
/*
* The MIT License
*
* Copyright 2017 Steven Foster
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.github_branch_source;

import hudson.ExtensionPoint;
import hudson.model.TaskListener;

import java.util.List;

/**
* Represents a strategy for constructing GitHub status notifications
* @since TODO
*/
public abstract class AbstractGitHubNotificationStrategy implements ExtensionPoint {

/**
* Creates the list of {@link GitHubNotificationRequest} for the given context.
* @param notificationContext {@link GitHubNotificationContext} the context details
* @param listener the listener
* @return a list of notification requests
* @since TODO
*/
public abstract List<GitHubNotificationRequest> notifications(GitHubNotificationContext notificationContext, TaskListener listener);

/**
* {@inheritDoc}
*/
public abstract boolean equals(Object o);

/**
* {@inheritDoc}
*/
public abstract int hashCode();
}
@@ -0,0 +1,64 @@
/*
* The MIT License
*
* Copyright 2017 Steven Foster
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.jenkinsci.plugins.github_branch_source;

import hudson.model.TaskListener;

import java.util.Collections;
import java.util.List;

/**
* Default implementation of {@link AbstractGitHubNotificationStrategy}
* @since TODO
*/
public final class DefaultGitHubNotificationStrategy extends AbstractGitHubNotificationStrategy {

/**
* {@inheritDoc}
*/
public List<GitHubNotificationRequest> notifications(GitHubNotificationContext notificationContext, TaskListener listener) {
return Collections.singletonList(GitHubNotificationRequest.build(notificationContext.getDefaultContext(listener),
notificationContext.getDefaultUrl(listener),
notificationContext.getDefaultMessage(listener),
notificationContext.getDefaultState(listener),
notificationContext.getDefaultIgnoreError(listener)));
}

/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
return this == o || (o != null && getClass() == o.getClass());
}

/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return 42;
}
}
@@ -1,7 +1,7 @@
/*
* The MIT License
*
* Copyright 2016 CloudBees, Inc.
* Copyright 2016-2017 CloudBees, Inc., Steven Foster
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -42,6 +42,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
Expand All @@ -51,7 +52,6 @@
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
import org.kohsuke.github.GHCommitState;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
Expand All @@ -69,23 +69,6 @@ public class GitHubBuildStatusNotification {

private static final Logger LOGGER = Logger.getLogger(GitHubBuildStatusNotification.class.getName());

private static void createCommitStatus(@NonNull GHRepository repo, @NonNull String revision,
@NonNull GHCommitState state, @NonNull String url, @NonNull String message,
@NonNull SCMHead head) throws IOException {
LOGGER.log(Level.FINE, "{0}/commit/{1} {2} from {3}", new Object[] {repo.getHtmlUrl(), revision, state, url});
String context;
if (head instanceof PullRequestSCMHead) {
if (((PullRequestSCMHead) head).isMerge()) {
context = "continuous-integration/jenkins/pr-merge";
} else {
context = "continuous-integration/jenkins/pr-head";
}
} else {
context = "continuous-integration/jenkins/branch";
}
repo.createCommitStatus(revision, state, url, message, context);
}

private static void createBuildCommitStatus(Run<?, ?> build, TaskListener listener) {
SCMSource src = SCMSource.SourceByItem.findSource(build.getParent());
SCMRevision revision = src != null ? SCMRevisionAction.getRevision(src, build) : null;
Expand All @@ -95,52 +78,40 @@ private static void createBuildCommitStatus(Run<?, ?> build, TaskListener listen
try {
GHRepository repo = lookUpRepo(gitHub, build.getParent());
if (repo != null) {
String url = null;
try {
url = DisplayURLProvider.get().getRunURL(build);
} catch (IllegalStateException e) {
listener.getLogger().println(
"Can not determine Jenkins root URL. Commit status notifications are disabled "
+ "until a root URL is"
+ " configured in Jenkins global configuration.");
return;
}
boolean ignoreError = false;
try {
Result result = build.getResult();
String revisionToNotify = resolveHeadCommit(revision);
SCMHead head = revision.getHead();
if (Result.SUCCESS.equals(result)) {
createCommitStatus(repo, revisionToNotify, GHCommitState.SUCCESS, url, Messages.GitHubBuildStatusNotification_CommitStatus_Good(), head);
} else if (Result.UNSTABLE.equals(result)) {
createCommitStatus(repo, revisionToNotify, GHCommitState.FAILURE, url, Messages.GitHubBuildStatusNotification_CommitStatus_Unstable(), head);
} else if (Result.FAILURE.equals(result)) {
createCommitStatus(repo, revisionToNotify, GHCommitState.ERROR, url, Messages.GitHubBuildStatusNotification_CommitStatus_Failure(), head);
} else if (Result.ABORTED.equals(result)) {
createCommitStatus(repo, revisionToNotify, GHCommitState.ERROR, url, Messages.GitHubBuildStatusNotification_CommitStatus_Aborted(), head);
} else if (result != null) { // NOT_BUILT etc.
createCommitStatus(repo, revisionToNotify, GHCommitState.ERROR, url, Messages.GitHubBuildStatusNotification_CommitStatus_Other(), head);
} else {
ignoreError = true;
createCommitStatus(repo, revisionToNotify, GHCommitState.PENDING, url, Messages.GitHubBuildStatusNotification_CommitStatus_Pending(), head);
}
if (result != null) {
listener.getLogger().format("%n" + Messages.GitHubBuildStatusNotification_CommitStatusSet() + "%n%n");
}
} catch (FileNotFoundException fnfe) {
if (!ignoreError) {
listener.getLogger().format("%nCould not update commit status, please check if your scan " +
"credentials belong to a member of the organization or a collaborator of the " +
"repository and repo:status scope is selected%n%n");
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Could not update commit status, for run "
+ build.getFullDisplayName()
+ " please check if your scan "
+ "credentials belong to a member of the organization or a "
+ "collaborator of the repository and repo:status scope is selected", fnfe);
Result result = build.getResult();
String revisionToNotify = resolveHeadCommit(revision);
SCMHead head = revision.getHead();
List<AbstractGitHubNotificationStrategy> strategies = new GitHubSCMSourceContext(null, SCMHeadObserver.none())
.withTraits(((GitHubSCMSource) src).getTraits()).notificationStrategies();
for (AbstractGitHubNotificationStrategy strategy : strategies) {
// TODO allow strategies to combine/cooperate on a notification
GitHubNotificationContext notificationContext = GitHubNotificationContext.build(null, build,
src, head);
List<GitHubNotificationRequest> details = strategy.notifications(notificationContext, listener);
for (GitHubNotificationRequest request : details) {
boolean ignoreError = request.isIgnoreError();
try {
repo.createCommitStatus(revisionToNotify, request.getState(), request.getUrl(), request.getMessage(),
request.getContext());
} catch (FileNotFoundException fnfe) {
if (!ignoreError) {
listener.getLogger().format("%nCould not update commit status, please check if your scan " +
"credentials belong to a member of the organization or a collaborator of the " +
"repository and repo:status scope is selected%n%n");
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Could not update commit status, for run "
+ build.getFullDisplayName()
+ " please check if your scan "
+ "credentials belong to a member of the organization or a "
+ "collaborator of the repository and repo:status scope is selected", fnfe);
}
}
}
}
}
if (result != null) {
listener.getLogger().format("%n" + Messages.GitHubBuildStatusNotification_CommitStatusSet() + "%n%n");
}
}
} finally {
Connector.release(gitHub);
Expand Down Expand Up @@ -229,22 +200,15 @@ public void onEnterWaiting(Queue.WaitingItem wi) {
if (!(head instanceof PullRequestSCMHead)) {
return;
}
if (new GitHubSCMSourceContext(null, SCMHeadObserver.none())
.withTraits(((GitHubSCMSource) source).getTraits())
.notificationsDisabled()) {
final GitHubSCMSourceContext sourceContext = new GitHubSCMSourceContext(null, SCMHeadObserver.none())
.withTraits(((GitHubSCMSource) source).getTraits());
if (sourceContext.notificationsDisabled()) {
return;
}
// prevent delays in the queue when updating github
Computer.threadPoolForRemoting.submit(new Runnable() {
@Override
public void run() {
String url;
try {
url = DisplayURLProvider.get().getJobURL(job);
} catch (IllegalStateException e) {
// no root url defined, cannot notify, let's get out of here
return;
}
GitHub gitHub = null;
try {
gitHub = lookUpGitHub(job);
Expand All @@ -269,8 +233,26 @@ public void run() {
// status. JobCheckOutListener is now responsible for setting the pending status.
return;
}
createCommitStatus(repo, hash, GHCommitState.PENDING, url,
Messages.GitHubBuildStatusNotification_CommitStatus_Queued(), head);
List<AbstractGitHubNotificationStrategy> strategies = sourceContext.notificationStrategies();
for (AbstractGitHubNotificationStrategy strategy : strategies) {
// TODO allow strategies to combine/cooperate on a notification
GitHubNotificationContext notificationContext = GitHubNotificationContext.build(job, null,
source, head);
List<GitHubNotificationRequest> details = strategy.notifications(notificationContext, null);
for (GitHubNotificationRequest request : details) {
boolean ignoreErrors = request.isIgnoreError();
try {
repo.createCommitStatus(hash, request.getState(), request.getUrl(), request.getMessage(),
request.getContext());
} catch (FileNotFoundException e) {
if (!ignoreErrors) {
LOGGER.log(Level.WARNING,
"Could not update commit status to PENDING. Valid scan credentials? Valid scopes?",
LOGGER.isLoggable(Level.FINE) ? e : null);
}
}
}
}
}
} finally {
Connector.release(gitHub);
Expand Down

0 comments on commit 267e41c

Please sign in to comment.