diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksContext.java b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksContext.java index 83b59ae0..06215753 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksContext.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksContext.java @@ -1,15 +1,12 @@ package io.jenkins.plugins.checks.github; -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; - +import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.CheckForNull; - -import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; import hudson.model.Job; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; -import io.jenkins.plugins.util.PluginLogger; +import java.util.Optional; /** * Base class for a context that publishes GitHub checks. @@ -25,19 +22,10 @@ abstract class GitHubChecksContext { this.scmFacade = scmFacade; } - /** - * Returns the Jenkins job. - * - * @return job for which the checks will be based on - */ - public Job getJob() { - return job; - } - /** * Returns the commit sha of the run. * - * @return the commit sha of the run or null + * @return the commit sha of the run */ public abstract String getHeadSha(); @@ -50,22 +38,26 @@ abstract class GitHubChecksContext { public abstract String getRepository(); /** - * Returns the credentials to access the remote GitHub repository. + * Returns whether the context is valid (with all properties functional) to use. * - * @return the credentials or null + * @param logger + * the filtered logger + * @return whether the context is valid to use */ - public GitHubAppCredentials getCredentials() { - String credentialsId = getCredentialsId(); - if (credentialsId == null) { - throw new IllegalStateException("No credentials available for job: " + getJob().getName()); - } - - return getGitHubAppCredentials(credentialsId); - } + public abstract boolean isValid(FilteredLog logger); @CheckForNull protected abstract String getCredentialsId(); + /** + * Returns the credentials to access the remote GitHub repository. + * + * @return the credentials + */ + public GitHubAppCredentials getCredentials() { + return getGitHubAppCredentials(StringUtils.defaultIfEmpty(getCredentialsId(), "")); + } + /** * Returns the URL of the run's summary page, e.g. https://ci.jenkins.io/job/Core/job/jenkins/job/master/2000/. * @@ -75,25 +67,19 @@ public String getURL() { return url; } - SCMFacade getScmFacade() { - return scmFacade; + protected Job getJob() { + return job; } - protected GitHubAppCredentials getGitHubAppCredentials(final String credentialsId) { - Optional foundCredentials = findGitHubAppCredentials(credentialsId); - if (!foundCredentials.isPresent()) { - throw new IllegalStateException("No GitHub APP credentials available for job: " + getJob().getName()); - } - - return foundCredentials.get(); + protected SCMFacade getScmFacade() { + return scmFacade; } - Optional findGitHubAppCredentials(final String credentialsId) { - return getScmFacade().findGitHubAppCredentials(getJob(), credentialsId); + protected GitHubAppCredentials getGitHubAppCredentials(final String credentialsId) { + return findGitHubAppCredentials(credentialsId).orElseThrow(() -> + new IllegalStateException("No GitHub APP credentials available for job: " + getJob().getName())); } - abstract boolean isValid(PluginLogger listener); - protected boolean hasGitHubAppCredentials() { return findGitHubAppCredentials(StringUtils.defaultIfEmpty(getCredentialsId(), "")).isPresent(); } @@ -102,20 +88,24 @@ protected boolean hasCredentialsId() { return StringUtils.isNoneBlank(getCredentialsId()); } - protected boolean hasValidCredentials(final PluginLogger logger) { + protected boolean hasValidCredentials(final FilteredLog logger) { if (!hasCredentialsId()) { - logger.log("No credentials found"); + logger.logError("No credentials found"); return false; } if (!hasGitHubAppCredentials()) { - logger.log("No GitHub app credentials found: '%s'", getCredentialsId()); - logger.log("See: https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc"); + logger.logError("No GitHub app credentials found: '%s'", getCredentialsId()); + logger.logError("See: https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc"); return false; } - + return true; } + + private Optional findGitHubAppCredentials(final String credentialsId) { + return getScmFacade().findGitHubAppCredentials(getJob(), credentialsId); + } } diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java index 065e3d61..af951dc3 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java @@ -1,25 +1,21 @@ package io.jenkins.plugins.checks.github; +import edu.hm.hafner.util.VisibleForTesting; +import io.jenkins.plugins.checks.api.ChecksDetails; +import io.jenkins.plugins.checks.api.ChecksPublisher; +import io.jenkins.plugins.util.PluginLogger; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.github_branch_source.Connector; +import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; +import org.kohsuke.github.GHCheckRunBuilder; +import org.kohsuke.github.GitHub; + import java.io.IOException; import java.time.Instant; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.commons.lang3.StringUtils; - -import edu.hm.hafner.util.VisibleForTesting; -import edu.umd.cs.findbugs.annotations.CheckForNull; - -import org.kohsuke.github.GHCheckRunBuilder; -import org.kohsuke.github.GitHub; -import org.jenkinsci.plugins.github_branch_source.Connector; -import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; -import hudson.model.TaskListener; - -import io.jenkins.plugins.checks.api.ChecksDetails; -import io.jenkins.plugins.checks.api.ChecksPublisher; - /** * A publisher which publishes GitHub check runs. */ @@ -28,8 +24,7 @@ public class GitHubChecksPublisher extends ChecksPublisher { private static final Logger LOGGER = Logger.getLogger(GitHubChecksPublisher.class.getName()); private final GitHubChecksContext context; - @CheckForNull - private final TaskListener listener; + private final PluginLogger logger; private final String gitHubUrl; /** @@ -38,16 +33,15 @@ public class GitHubChecksPublisher extends ChecksPublisher { * @param context * a context which contains SCM properties */ - public GitHubChecksPublisher(final GitHubChecksContext context, @CheckForNull final TaskListener listener) { - this(context, listener, GITHUB_URL); + public GitHubChecksPublisher(final GitHubChecksContext context, final PluginLogger logger) { + this(context, logger, GITHUB_URL); } - GitHubChecksPublisher(final GitHubChecksContext context, @CheckForNull final TaskListener listener, - final String gitHubUrl) { + GitHubChecksPublisher(final GitHubChecksContext context, final PluginLogger logger, final String gitHubUrl) { super(); this.context = context; - this.listener = listener; + this.logger = logger; this.gitHubUrl = gitHubUrl; } @@ -66,17 +60,13 @@ public void publish(final ChecksDetails details) { GitHubChecksDetails gitHubDetails = new GitHubChecksDetails(details); createBuilder(gitHub, gitHubDetails).create(); - if (listener != null) { - listener.getLogger().printf("GitHub check (name: %s, status: %s) has been published.%n", - gitHubDetails.getName(), gitHubDetails.getStatus()); - } + logger.log("GitHub check (name: %s, status: %s) has been published.", gitHubDetails.getName(), + gitHubDetails.getStatus()); } - catch (IllegalStateException | IOException e) { + catch (IOException e) { String message = "Failed Publishing GitHub checks: "; LOGGER.log(Level.WARNING, (message + details).replaceAll("[\r\n]", ""), e); - if (listener != null) { - listener.getLogger().println(message + e); - } + logger.log(message + e); } } diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactory.java b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactory.java index 0d1c88fb..dee48f02 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactory.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactory.java @@ -1,18 +1,17 @@ package io.jenkins.plugins.checks.github; -import java.util.Optional; - -import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; - +import edu.hm.hafner.util.FilteredLog; import edu.hm.hafner.util.VisibleForTesting; -import io.jenkins.plugins.checks.api.ChecksPublisher; -import io.jenkins.plugins.checks.api.ChecksPublisherFactory; -import io.jenkins.plugins.util.PluginLogger; - import hudson.Extension; import hudson.model.Job; import hudson.model.Run; import hudson.model.TaskListener; +import io.jenkins.plugins.checks.api.ChecksPublisher; +import io.jenkins.plugins.checks.api.ChecksPublisherFactory; +import io.jenkins.plugins.util.PluginLogger; +import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; + +import java.util.Optional; /** * An factory which produces {@link GitHubChecksPublisher}. @@ -20,68 +19,46 @@ @Extension public class GitHubChecksPublisherFactory extends ChecksPublisherFactory { private final SCMFacade scmFacade; + private final DisplayURLProvider urlProvider; /** * Creates a new instance of {@link GitHubChecksPublisherFactory}. */ public GitHubChecksPublisherFactory() { - this(new SCMFacade()); + this(new SCMFacade(), DisplayURLProvider.get()); } @VisibleForTesting - GitHubChecksPublisherFactory(final SCMFacade scmFacade) { + GitHubChecksPublisherFactory(final SCMFacade scmFacade, final DisplayURLProvider urlProvider) { super(); this.scmFacade = scmFacade; + this.urlProvider = urlProvider; } @Override protected Optional createPublisher(final Run run, final TaskListener listener) { - return createPublisher(run, DisplayURLProvider.get().getRunURL(run), listener); - } - - @VisibleForTesting - Optional createPublisher(final Run run, final String runURL, final TaskListener listener) { - PluginLogger logger = createLogger(getListener(listener)); - - GitSCMChecksContext gitSCMContext = new GitSCMChecksContext(run, runURL); - if (gitSCMContext.isValid(logger)) { - return Optional.of(new GitHubChecksPublisher(gitSCMContext, getListener(listener))); - } - - return createPublisher(listener, logger, new GitHubSCMSourceChecksContext(run, runURL, scmFacade)); + final String runURL = urlProvider.getRunURL(run); + return createPublisher(listener, new GitSCMChecksContext(run, runURL, scmFacade), + new GitHubSCMSourceChecksContext(run, runURL, scmFacade)); } @Override protected Optional createPublisher(final Job job, final TaskListener listener) { - return createPublisher(job, DisplayURLProvider.get().getJobURL(job), listener); - } - - @VisibleForTesting - Optional createPublisher(final Job job, final String jobURL, final TaskListener listener) { - PluginLogger logger = createLogger(getListener(listener)); - - return createPublisher(listener, logger, new GitHubSCMSourceChecksContext(job, jobURL, scmFacade)); + return createPublisher(listener, new GitHubSCMSourceChecksContext(job, urlProvider.getJobURL(job), scmFacade)); } - private Optional createPublisher(final TaskListener listener, final PluginLogger logger, - final GitHubChecksContext gitHubSCMSourceContext) { - if (gitHubSCMSourceContext.isValid(logger)) { - return Optional.of(new GitHubChecksPublisher(gitHubSCMSourceContext, getListener(listener))); - } - return Optional.empty(); - } + private Optional createPublisher(final TaskListener listener, final GitHubChecksContext... contexts) { + FilteredLog causeLogger = new FilteredLog("Causes for no suitable publisher found: "); + PluginLogger consoleLogger = new PluginLogger(listener.getLogger(), "GitHub Checks"); - - private TaskListener getListener(final TaskListener taskListener) { - // FIXME: checks-API should use a Null listener - if (taskListener == null) { - return TaskListener.NULL; + for (GitHubChecksContext ctx : contexts) { + if (ctx.isValid(causeLogger)) { + return Optional.of(new GitHubChecksPublisher(ctx, consoleLogger)); + } } - return taskListener; - } - private PluginLogger createLogger(final TaskListener listener) { - return new PluginLogger(listener.getLogger(), "GitHub Checks"); + consoleLogger.logEachLine(causeLogger.getErrorMessages()); + return Optional.empty(); } } diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContext.java b/src/main/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContext.java index be10a0df..2259c236 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContext.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContext.java @@ -1,119 +1,136 @@ package io.jenkins.plugins.checks.github; -import java.util.Optional; - -import edu.hm.hafner.util.VisibleForTesting; +import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.CheckForNull; - -import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; -import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; -import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; import hudson.model.Job; import hudson.model.Run; -import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; -import io.jenkins.plugins.util.PluginLogger; +import java.util.Optional; /** * Provides a {@link GitHubChecksContext} for a Jenkins job that uses a supported {@link GitHubSCMSource}. */ class GitHubSCMSourceChecksContext extends GitHubChecksContext { + @CheckForNull + private final String sha; + /** * Creates a {@link GitHubSCMSourceChecksContext} according to the run. All attributes are computed during this period. * - * @param run a run of a GitHub Branch Source project + * @param run + * a run of a GitHub Branch Source project + * @param runURL + * the URL to the Jenkins run + * @param scmFacade + * a facade for Jenkins SCM */ - GitHubSCMSourceChecksContext(final Run run) { - this(run, DisplayURLProvider.get().getRunURL(run), new SCMFacade()); + GitHubSCMSourceChecksContext(final Run run, final String runURL, final SCMFacade scmFacade) { + super(run.getParent(), runURL, scmFacade); + sha = resolveHeadSha(run); } /** * Creates a {@link GitHubSCMSourceChecksContext} according to the job. All attributes are computed during this period. * - * @param job a GitHub Branch Source project + * @param job + * a GitHub Branch Source project + * @param jobURL + * the URL to the Jenkins job + * @param scmFacade + * a facade for Jenkins SCM */ - GitHubSCMSourceChecksContext(final Job job) { - this(job, DisplayURLProvider.get().getJobURL(job), new SCMFacade()); - } - - @VisibleForTesting - GitHubSCMSourceChecksContext(final Run run, final String runURL, final SCMFacade scmFacade) { - super(run.getParent(), runURL, scmFacade); - } - - @VisibleForTesting GitHubSCMSourceChecksContext(final Job job, final String jobURL, final SCMFacade scmFacade) { super(job, jobURL, scmFacade); + sha = resolveHeadSha(job); } @Override public String getHeadSha() { - return resolveHeadSha(); + if (StringUtils.isBlank(sha)) { + throw new IllegalStateException("No SHA found for job: " + getJob().getName()); + } + + return sha; } @Override public String getRepository() { GitHubSCMSource source = resolveSource(); - return source.getRepoOwner() + "/" + source.getRepository(); - } - - @Override @CheckForNull - protected String getCredentialsId() { - return resolveSource().getCredentialsId(); + if (source == null) { + throw new IllegalStateException("No GitHub SCM source found for job: " + getJob().getName()); + } + else { + return source.getRepoOwner() + "/" + source.getRepository(); + } } - private GitHubSCMSource resolveSource() { - Optional source = getScmFacade().findGitHubSCMSource(getJob()); - if (!source.isPresent()) { - throw new IllegalStateException("No GitHub SCM source available for job: " + getJob().getName()); - } + @Override + public boolean isValid(final FilteredLog logger) { + logger.logError("Trying to resolve checks parameters from GitHub SCM..."); - return source.get(); - } + if (resolveSource() == null) { + logger.logError("Job does not use GitHub SCM"); - private String resolveHeadSha() { - Optional head = getScmFacade().findHead(getJob()); - if (!head.isPresent()) { - throw new IllegalStateException("No SCM head available for job: " + getJob().getName()); + return false; } - Optional revision = getScmFacade().findRevision(resolveSource(), head.get()); - if (!revision.isPresent()) { - throw new IllegalStateException( - String.format("No SCM revision available for repository: %s and head: %s", - getRepository(), head.get().getName())); + if (!hasValidCredentials(logger)) { + return false; } - if (revision.get() instanceof AbstractGitSCMSource.SCMRevisionImpl) { - return ((AbstractGitSCMSource.SCMRevisionImpl) revision.get()).getHash(); - } - else if (revision.get() instanceof PullRequestSCMRevision) { - return ((PullRequestSCMRevision) revision.get()).getPullHash(); - } - else { - throw new IllegalStateException("Unsupported revision type: " + revision.get().getClass().getName()); + if (StringUtils.isBlank(sha)) { + logger.logError("No HEAD SHA found for %s", getRepository()); + + return false; } + + return true; } @Override - boolean isValid(final PluginLogger logger) { - Job job = getJob(); - - Optional source = getScmFacade().findGitHubSCMSource(job); - if (!source.isPresent()) { - logger.log("No GitHub SCM source found"); - - return false; + @CheckForNull + protected String getCredentialsId() { + GitHubSCMSource source = resolveSource(); + if (source == null) { + return null; } - if (!hasValidCredentials(logger)) { - return false; + return source.getCredentialsId(); + } + + @CheckForNull + private GitHubSCMSource resolveSource() { + return getScmFacade().findGitHubSCMSource(getJob()).orElse(null); + } + + @CheckForNull + private String resolveHeadSha(final Run run) { + GitHubSCMSource source = resolveSource(); + if (source != null) { + Optional revision = getScmFacade().findRevision(source, run); + if (revision.isPresent()) { + return getScmFacade().findHash(revision.get()).orElse(null); + } } - logger.log("Using GitHub SCM source '%s' for GitHub checks", getRepository()); - - return true; + return null; + } + + @CheckForNull + private String resolveHeadSha(final Job job) { + GitHubSCMSource source = resolveSource(); + Optional head = getScmFacade().findHead(job); + if (source != null && head.isPresent()) { + Optional revision = getScmFacade().findRevision(source, head.get()); + if (revision.isPresent()) { + return getScmFacade().findHash(revision.get()).orElse(null); + } + } + + return null; } } diff --git a/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java b/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java index 9d541a74..056a01ae 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java +++ b/src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java @@ -1,20 +1,17 @@ package io.jenkins.plugins.checks.github; -import java.io.IOException; -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; - +import edu.hm.hafner.util.FilteredLog; import edu.umd.cs.findbugs.annotations.CheckForNull; - import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.plugins.git.Revision; import hudson.plugins.git.UserRemoteConfig; import hudson.plugins.git.util.BuildData; +import org.apache.commons.lang3.StringUtils; -import io.jenkins.plugins.util.PluginLogger; +import java.io.IOException; +import java.util.Optional; /** * Provides a {@link GitHubChecksContext} for a Jenkins job that uses a supported {@link GitSCM}. @@ -32,7 +29,11 @@ class GitSCMChecksContext extends GitHubChecksContext { * @param runURL the URL to the Jenkins run */ GitSCMChecksContext(final Run run, final String runURL) { - super(run.getParent(), runURL, new SCMFacade()); + this(run, runURL, new SCMFacade()); + } + + GitSCMChecksContext(final Run run, final String runURL, final SCMFacade scmFacade) { + super(run.getParent(), runURL, scmFacade); this.run = run; } @@ -82,7 +83,8 @@ private String removeProtocol(final String url) { return StringUtils.removeStart(StringUtils.removeStart(url, GIT_PROTOCOL), HTTPS_PROTOCOL); } - @Override @CheckForNull + @Override + @CheckForNull protected String getCredentialsId() { return getUserRemoteConfig().getCredentialsId(); } @@ -101,16 +103,18 @@ private GitSCM resolveGitSCM() { } @Override - boolean isValid(final PluginLogger logger) { + public boolean isValid(final FilteredLog logger) { + logger.logError("Trying to resolve checks parameters from Git SCM..."); + if (!getScmFacade().findGitSCM(run).isPresent()) { - logger.log("Job does not use GitSCM"); + logger.logError("Job does not use Git SCM"); return false; } String remoteUrl = getRemoteUrl(); if (!isValidUrl(remoteUrl)) { - logger.log("No supported GitSCM repository URL: " + remoteUrl); + logger.logError("No supported GitSCM repository URL: " + remoteUrl); return false; } @@ -121,18 +125,18 @@ boolean isValid(final PluginLogger logger) { String repository = getRepository(); if (getHeadSha().isEmpty()) { - logger.log("No HEAD SHA found for '%s'", repository); + logger.logError("No HEAD SHA found for '%s'", repository); return false; } - logger.log("Using GitSCM repository '%s' for GitHub checks", repository); + logger.logInfo("Using GitSCM repository '%s' for GitHub checks", repository); return true; } private boolean isValidUrl(@CheckForNull final String remoteUrl) { - return StringUtils.startsWith(remoteUrl, GIT_PROTOCOL) + return StringUtils.startsWith(remoteUrl, GIT_PROTOCOL) || StringUtils.startsWith(remoteUrl, HTTPS_PROTOCOL); } } diff --git a/src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java b/src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java index aa75ab0c..937730a2 100644 --- a/src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java +++ b/src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java @@ -6,6 +6,9 @@ import java.util.List; import java.util.Optional; +import jenkins.plugins.git.AbstractGitSCMSource; +import jenkins.scm.api.SCMRevisionAction; + import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsProvider; @@ -13,6 +16,7 @@ import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; +import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; import org.jenkinsci.plugins.workflow.flow.FlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; @@ -135,6 +139,39 @@ public Optional findRevision(final SCMSource source, final SCMHead } } + /** + * Find the current {@link SCMRevision} of the {@code source} and {@code run} locally through + * {@link jenkins.scm.api.SCMRevisionAction}. + * + * @param source + * the GitHub repository + * @param run + * the Jenkins run + * @return the found revision or empty + */ + public Optional findRevision(final GitHubSCMSource source, final Run run) { + return Optional.ofNullable(SCMRevisionAction.getRevision(source, run)); + } + + /** + * Find the hash value in {@code revision}. + * + * @param revision + * the revision for a build + * @return the found hash or empty + */ + public Optional findHash(final SCMRevision revision) { + if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { + return Optional.of(((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash()); + } + else if (revision instanceof PullRequestSCMRevision) { + return Optional.of(((PullRequestSCMRevision) revision).getPullHash()); + } + else { + return Optional.empty(); + } + } + /** * Returns the SCM in a given build. If no SCM can be determined, then a {@link NullSCM} instance will be returned. * diff --git a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java index 055234e2..2363eb14 100644 --- a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java +++ b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java @@ -1,9 +1,16 @@ package io.jenkins.plugins.checks.github; +import java.io.IOException; import java.util.Optional; +import hudson.EnvVars; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.UserRemoteConfig; +import jenkins.scm.api.SCMHead; +import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; +import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; @@ -13,85 +20,105 @@ import hudson.model.Run; import hudson.model.TaskListener; -@SuppressWarnings({"PMD.CloseResource", "rawtypes"})// no need to close mocked PrintStream class GitHubChecksPublisherFactoryTest { - private static final String URL = "URL"; - @Test - void shouldCreateGitHubChecksPublisherFromRun() { + void shouldCreateGitHubChecksPublisherFromRunForProjectWithValidGitHubSCMSource() { Run run = mock(Run.class); Job job = mock(Job.class); GitHubSCMSource source = mock(GitHubSCMSource.class); GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); + PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); SCMFacade scmFacade = mock(SCMFacade.class); when(run.getParent()).thenReturn(job); + when(job.getLastBuild()).thenReturn(run); when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); when(source.getCredentialsId()).thenReturn("credentials id"); when(scmFacade.findGitHubAppCredentials(job, "credentials id")).thenReturn(Optional.of(credentials)); + when(scmFacade.findRevision(source, run)).thenReturn(Optional.of(revision)); + when(scmFacade.findHash(revision)).thenReturn(Optional.of("a1b2c3")); - GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade); - assertThat(factory.createPublisher(run, URL, TaskListener.NULL)) - .isPresent() - .containsInstanceOf(GitHubChecksPublisher.class); + GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, + job)); + assertThat(factory.createPublisher(run, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); } @Test - void shouldReturnGitHubChecksPublisherFromJob() { - Job job = mock(Job.class); + void shouldReturnGitHubChecksPublisherFromJobProjectWithValidGitHubSCMSource() { + Run run = mock(Run.class); + Job job = mock(Job.class); GitHubSCMSource source = mock(GitHubSCMSource.class); + GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); + PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); + SCMHead head = mock(SCMHead.class); SCMFacade scmFacade = mock(SCMFacade.class); + when(run.getParent()).thenReturn(job); + when(job.getLastBuild()).thenReturn(run); when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); when(source.getCredentialsId()).thenReturn("credentials id"); - when(scmFacade.findGitHubAppCredentials(job, "credentials id")) - .thenReturn(Optional.empty()); + when(scmFacade.findGitHubAppCredentials(job, "credentials id")).thenReturn(Optional.of(credentials)); + when(scmFacade.findHead(job)).thenReturn(Optional.of(head)); + when(scmFacade.findRevision(source, head)).thenReturn(Optional.of(revision)); + when(scmFacade.findHash(revision)).thenReturn(Optional.of("a1b2c3")); - GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade); - assertThat(factory.createPublisher(job, URL, TaskListener.NULL)) - .isNotPresent(); + GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, + job)); + assertThat(factory.createPublisher(job, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); } @Test - void shouldReturnEmptyWhenNoGitHubSCMSourceIsConfigured() { + void shouldCreateGitHubChecksPublisherFromRunForProjectWithValidGitSCM() throws IOException, InterruptedException { + Job job = mock(Job.class); Run run = mock(Run.class); + GitSCM gitSCM = mock(GitSCM.class); + UserRemoteConfig config = mock(UserRemoteConfig.class); + GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); + SCMFacade scmFacade = mock(SCMFacade.class); + EnvVars envVars = mock(EnvVars.class); - GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(); - assertThat(factory.createPublisher(run, URL, TaskListener.NULL)) - .isNotPresent(); + when(run.getParent()).thenReturn(job); + when(run.getEnvironment(TaskListener.NULL)).thenReturn(envVars); + when(envVars.get("GIT_COMMIT")).thenReturn("a1b2c3"); + when(scmFacade.getScm(job)).thenReturn(gitSCM); + when(scmFacade.findGitSCM(run)).thenReturn(Optional.of(gitSCM)); + when(scmFacade.getUserRemoteConfig(gitSCM)).thenReturn(config); + when(config.getCredentialsId()).thenReturn("1"); + when(scmFacade.findGitHubAppCredentials(job, "1")).thenReturn(Optional.of(credentials)); + when(config.getUrl()).thenReturn("https://github.com/jenkinsci/github-checks-plugin"); + + GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, + job)); + assertThat(factory.createPublisher(run, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); } @Test - void shouldReturnEmptyWhenNoCredentialsIsConfigured() { + void shouldReturnEmptyFromRunForInvalidProject() { Run run = mock(Run.class); - Job job = mock(Job.class); - GitHubSCMSource source = mock(GitHubSCMSource.class); - SCMFacade scmFacade = mock(SCMFacade.class); - - when(run.getParent()).thenReturn(job); - when(scmFacade.findGitHubSCMSource(run.getParent())).thenReturn(Optional.of(source)); - when(source.getCredentialsId()).thenReturn(null); + SCMFacade facade = mock(SCMFacade.class); + DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); - GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade); - assertThat(factory.createPublisher(job, URL, TaskListener.NULL)) - .isNotPresent(); + GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(facade, urlProvider); + assertThat(factory.createPublisher(run, TaskListener.NULL)).isNotPresent(); } @Test - void shouldReturnEmptyWhenNoGitHubAppCredentialsIsConfigured() { - Run run = mock(Run.class); + void shouldCreateNullPublisherFromJobForInvalidProject() { Job job = mock(Job.class); - GitHubSCMSource source = mock(GitHubSCMSource.class); - SCMFacade scmFacade = mock(SCMFacade.class); + SCMFacade facade = mock(SCMFacade.class); + DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); - when(run.getParent()).thenReturn(job); - when(scmFacade.findGitHubSCMSource(run.getParent())).thenReturn(Optional.of(source)); - when(source.getCredentialsId()).thenReturn("credentials id"); - when(scmFacade.findGitHubAppCredentials(run.getParent(), "credentials id")) - .thenReturn(Optional.empty()); - - GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade); - assertThat(factory.createPublisher(job, URL, TaskListener.NULL)) + GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(facade, urlProvider); + assertThat(factory.createPublisher(job, TaskListener.NULL)) .isNotPresent(); } + + private DisplayURLProvider createDisplayURLProvider(final Run run, final Job job) { + DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); + + when(urlProvider.getRunURL(run)).thenReturn(null); + when(urlProvider.getJobURL(job)).thenReturn(null); + + return urlProvider; + } } diff --git a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherITest.java b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherITest.java index cc30248c..bf203dc9 100644 --- a/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherITest.java +++ b/src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherITest.java @@ -7,6 +7,7 @@ import java.util.Optional; import java.util.logging.Level; +import io.jenkins.plugins.util.PluginLogger; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -109,7 +110,8 @@ public void shouldPublishGitHubCheckRunCorrectly() { new ChecksAction("re-run", "re-run Jenkins build", "#0"))) .build(); - new GitHubChecksPublisher(createGitHubChecksContextWithGitHubSCM(), jenkinsRule.createTaskListener(), + new GitHubChecksPublisher(createGitHubChecksContextWithGitHubSCM(), + new PluginLogger(jenkinsRule.createTaskListener().getLogger(), "GitHub Checks"), wireMockRule.baseUrl()) .publish(details); } @@ -142,7 +144,8 @@ public void shouldLogChecksParametersIfExceptionHappensWhenPublishChecks() { .build()) .build(); - new GitHubChecksPublisher(createGitHubChecksContextWithGitHubSCM(), jenkinsRule.createTaskListener(), + new GitHubChecksPublisher(createGitHubChecksContextWithGitHubSCM(), + new PluginLogger(jenkinsRule.createTaskListener().getLogger(), "GitHub Checks"), wireMockRule.baseUrl()) .publish(details); @@ -174,16 +177,18 @@ private GitHubChecksContext createGitHubChecksContextWithGitHubSCM() { ClassicDisplayURLProvider urlProvider = mock(ClassicDisplayURLProvider.class); when(run.getParent()).thenReturn(job); + when(job.getLastBuild()).thenReturn(run); + when(source.getCredentialsId()).thenReturn("1"); when(source.getRepoOwner()).thenReturn("XiongKezhi"); when(source.getRepository()).thenReturn("Sandbox"); - when(revision.getPullHash()).thenReturn("18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f"); when(credentials.getPassword()).thenReturn(Secret.fromString("password")); when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); when(scmFacade.findGitHubAppCredentials(job, "1")).thenReturn(Optional.of(credentials)); when(scmFacade.findHead(job)).thenReturn(Optional.of(head)); - when(scmFacade.findRevision(source, head)).thenReturn(Optional.of(revision)); + when(scmFacade.findRevision(source, run)).thenReturn(Optional.of(revision)); + when(scmFacade.findHash(revision)).thenReturn(Optional.of("18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f")); when(urlProvider.getRunURL(run)).thenReturn("https://ci.jenkins.io"); diff --git a/src/test/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContextTest.java b/src/test/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContextTest.java index 7bc7f232..27bba632 100644 --- a/src/test/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContextTest.java +++ b/src/test/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContextTest.java @@ -2,6 +2,7 @@ import java.util.Optional; +import edu.hm.hafner.util.FilteredLog; import org.jenkinsci.plugins.displayurlapi.ClassicDisplayURLProvider; import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; @@ -17,7 +18,6 @@ import hudson.model.Job; import hudson.model.Run; -@SuppressWarnings("rawtypes") class GitHubSCMSourceChecksContextTest { private static final String URL = "URL"; @@ -28,9 +28,8 @@ void shouldGetHeadShaFromMasterBranch() { AbstractGitSCMSource.SCMRevisionImpl revision = mock(AbstractGitSCMSource.SCMRevisionImpl.class); GitHubSCMSource source = mock(GitHubSCMSource.class); - when(revision.getHash()).thenReturn("a1b2c3"); - - assertThat(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, head, revision)) + assertThat(new GitHubSCMSourceChecksContext(job, URL, + createGitHubSCMFacadeWithRevision(job, source, head, revision, "a1b2c3")) .getHeadSha()) .isEqualTo("a1b2c3"); } @@ -42,9 +41,24 @@ void shouldGetHeadShaFromPullRequest() { PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); GitHubSCMSource source = mock(GitHubSCMSource.class); - when(revision.getPullHash()).thenReturn("a1b2c3"); + assertThat(new GitHubSCMSourceChecksContext(job, URL, + createGitHubSCMFacadeWithRevision(job, source, head, revision, "a1b2c3")) + .getHeadSha()) + .isEqualTo("a1b2c3"); + } + + @Test + void shouldGetHeadShaFromRun() { + Job job = mock(Job.class); + Run run = mock(Run.class); + PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); + GitHubSCMSource source = mock(GitHubSCMSource.class); + + when(run.getParent()).thenReturn(job); + when(job.getLastBuild()).thenReturn(run); - assertThat(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, head, revision)) + assertThat(new GitHubSCMSourceChecksContext(run, URL, + createGitHubSCMFacadeWithRevision(run, source, revision, "a1b2c3")) .getHeadSha()) .isEqualTo("a1b2c3"); } @@ -52,14 +66,14 @@ void shouldGetHeadShaFromPullRequest() { @Test void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSCMHeadAvailable() { Job job = mock(Job.class); + GitHubSCMSource source = mock(GitHubSCMSource.class); + when(job.getName()).thenReturn("github-checks-plugin"); - assertThatThrownBy(() -> { - SCMFacade scmFacade = mock(SCMFacade.class); - new GitHubSCMSourceChecksContext(job, URL, scmFacade).getHeadSha(); - }) + assertThatThrownBy(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithSource(job, source)) + ::getHeadSha) .isInstanceOf(IllegalStateException.class) - .hasMessage("No SCM head available for job: github-checks-plugin"); + .hasMessage("No SHA found for job: github-checks-plugin"); } @Test @@ -68,15 +82,15 @@ void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSCMRevisionAvailable() { SCMHead head = mock(SCMHead.class); GitHubSCMSource source = mock(GitHubSCMSource.class); + when(job.getName()).thenReturn("github-checks-plugin"); when(source.getRepoOwner()).thenReturn("jenkinsci"); when(source.getRepository()).thenReturn("github-checks-plugin"); when(head.getName()).thenReturn("master"); - assertThatThrownBy(() -> new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, head, null)) - .getHeadSha()) + assertThatThrownBy(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, + head, null, null))::getHeadSha) .isInstanceOf(IllegalStateException.class) - .hasMessage("No SCM revision available for repository: jenkinsci/github-checks-plugin and " - + "head: master"); + .hasMessage("No SHA found for job: github-checks-plugin"); } @Test @@ -86,10 +100,12 @@ void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSuitableSCMRevisionAvail SCMRevision revision = mock(SCMRevision.class); GitHubSCMSource source = mock(GitHubSCMSource.class); - assertThatThrownBy(() -> new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, head, revision)) - .getHeadSha()) + when(job.getName()).thenReturn("github-checks-plugin"); + + assertThatThrownBy(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithRevision(job, source, + head, revision, null))::getHeadSha) .isInstanceOf(IllegalStateException.class) - .hasMessage("Unsupported revision type: " + revision.getClass().getName()); + .hasMessage("No SHA found for job: github-checks-plugin"); } @Test @@ -112,7 +128,7 @@ void shouldThrowIllegalStateExceptionWhenGetRepositoryButNoGitHubSCMSourceAvaila assertThatThrownBy(() -> new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithSource(job, null)) .getRepository()) .isInstanceOf(IllegalStateException.class) - .hasMessage("No GitHub SCM source available for job: github-checks-plugin"); + .hasMessage("No GitHub SCM source found for job: github-checks-plugin"); } @Test @@ -133,21 +149,20 @@ void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoCredentialsAvailable when(job.getName()).thenReturn("github-checks-plugin"); - assertThatThrownBy(() -> new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithCredentials(job, source, null, null)) - .getCredentials()) + assertThatThrownBy(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithCredentials(job, source, + null, null))::getCredentials) .isInstanceOf(IllegalStateException.class) - .hasMessage("No credentials available for job: github-checks-plugin"); + .hasMessage("No GitHub APP credentials available for job: github-checks-plugin"); } @Test - void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoGitHubAPPCredentialsAvailable() { + void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoSourceAvailable() { Job job = mock(Job.class); - GitHubSCMSource source = mock(GitHubSCMSource.class); + SCMFacade scmFacade = mock(SCMFacade.class); when(job.getName()).thenReturn("github-checks-plugin"); - assertThatThrownBy(() -> new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithCredentials(job, source, null, "1")) - .getCredentials()) + assertThatThrownBy(new GitHubSCMSourceChecksContext(job, URL, scmFacade)::getCredentials) .isInstanceOf(IllegalStateException.class) .hasMessage("No GitHub APP credentials available for job: github-checks-plugin"); } @@ -156,28 +171,89 @@ void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoGitHubAPPCredentials void shouldGetURLForJob() { Job job = mock(Job.class); - assertThat(new GitHubSCMSourceChecksContext(job, URL, null).getURL()) + assertThat(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithSource(job, null)).getURL()) .isEqualTo(URL); } @Test void shouldGetURLForRun() { Run run = mock(Run.class); + Job job = mock(Job.class); ClassicDisplayURLProvider urlProvider = mock(ClassicDisplayURLProvider.class); when(urlProvider.getRunURL(run)) .thenReturn("http://127.0.0.1:8080/job/github-checks-plugin/job/master/200"); - assertThat(new GitHubSCMSourceChecksContext(run, urlProvider.getRunURL(run), null).getURL()) + assertThat(new GitHubSCMSourceChecksContext(run, urlProvider.getRunURL(run), + createGitHubSCMFacadeWithSource(job, null)).getURL()) .isEqualTo("http://127.0.0.1:8080/job/github-checks-plugin/job/master/200"); } + @Test + void shouldReturnFalseWhenValidateContextButHasNoValidCredentials() { + Job job = mock(Job.class); + GitHubSCMSource source = mock(GitHubSCMSource.class); + FilteredLog logger = new FilteredLog(""); + + assertThat(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithSource(job, source)) + .isValid(logger)) + .isFalse(); + assertThat(logger.getErrorMessages()).contains("No credentials found"); + } + + @Test + void shouldReturnFalseWhenValidateContextButHasNoValidGitHubAppCredentials() { + Job job = mock(Job.class); + GitHubSCMSource source = mock(GitHubSCMSource.class); + FilteredLog logger = new FilteredLog(""); + + when(source.getCredentialsId()).thenReturn("oauth-credentials"); + + assertThat(new GitHubSCMSourceChecksContext(job, URL, createGitHubSCMFacadeWithSource(job, source)) + .isValid(logger)) + .isFalse(); + assertThat(logger.getErrorMessages()) + .contains("No GitHub app credentials found: 'oauth-credentials'") + .contains("See: https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc"); + } + + @Test + void shouldReturnFalseWhenValidateContextButHasNoValidSHA() { + Run run = mock(Run.class); + Job job = mock(Job.class); + GitHubSCMSource source = mock(GitHubSCMSource.class); + GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); + FilteredLog logger = new FilteredLog(""); + + when(run.getParent()).thenReturn(job); + + when(source.getRepoOwner()).thenReturn("jenkinsci"); + when(source.getRepository()).thenReturn("github-checks"); + + assertThat(new GitHubSCMSourceChecksContext(run, URL, createGitHubSCMFacadeWithCredentials(job, source, + credentials, "1")).isValid(logger)) + .isFalse(); + assertThat(logger.getErrorMessages()).contains("No HEAD SHA found for jenkinsci/github-checks"); + } + private SCMFacade createGitHubSCMFacadeWithRevision(final Job job, final GitHubSCMSource source, - final SCMHead head, final SCMRevision revision) { + final SCMHead head, final SCMRevision revision, + final String hash) { SCMFacade facade = createGitHubSCMFacadeWithSource(job, source); when(facade.findHead(job)).thenReturn(Optional.ofNullable(head)); when(facade.findRevision(source, head)).thenReturn(Optional.ofNullable(revision)); + when(facade.findHash(revision)).thenReturn(Optional.ofNullable(hash)); + + return facade; + } + + private SCMFacade createGitHubSCMFacadeWithRevision(final Run run, final GitHubSCMSource source, + final SCMRevision revision, final String hash) { + SCMFacade facade = createGitHubSCMFacadeWithSource(run.getParent(), source); + + when(facade.findRevision(source, run)).thenReturn(Optional.of(revision)); + when(facade.findHash(revision)).thenReturn(Optional.of(hash)); return facade; }