Skip to content

Commit

Permalink
Use Connector.lookupScanCredentials to contextualize GitHubAppCredent…
Browse files Browse the repository at this point in the history
…ials (#398)
  • Loading branch information
jeromepochat authored Sep 17, 2024
1 parent 5f785f1 commit 845136f
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import org.apache.commons.lang3.StringUtils;

import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.Nullable;

import org.kohsuke.github.GHCheckRun;
import org.kohsuke.github.GHCheckRunBuilder;
Expand All @@ -36,9 +34,6 @@ public class GitHubChecksPublisher extends ChecksPublisher {
private final PluginLogger buildLogger;
private final String gitHubUrl;

@Nullable
private StandardUsernameCredentials credentials;

/**
* Creates a new instance of GitHubChecksPublisher.
*
Expand Down Expand Up @@ -67,8 +62,10 @@ public GitHubChecksPublisher(final GitHubChecksContext context, final PluginLogg
@Override
public void publish(final ChecksDetails details) {
try {
final var credentials = context.getCredentials();

// Prevent publication with unsupported credential types
switch (getCredentials().getClass().getSimpleName()) {
switch (credentials.getClass().getSimpleName()) {

Check warning on line 68 in src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 68 is only partially covered, one branch is missing
case "GitHubAppCredentials":
case "VaultUsernamePasswordCredentialImpl":
break;
Expand All @@ -77,12 +74,12 @@ public void publish(final ChecksDetails details) {
}

String apiUri = null;
if (getCredentials() instanceof GitHubAppCredentials) {
apiUri = ((GitHubAppCredentials) getCredentials()).getApiUri();
if (credentials instanceof GitHubAppCredentials) {

Check warning on line 77 in src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 77 is only partially covered, one branch is missing
apiUri = ((GitHubAppCredentials) credentials).getApiUri();
}

GitHub gitHub = Connector.connect(StringUtils.defaultIfBlank(apiUri, gitHubUrl),
getCredentials());
credentials);

GitHubChecksDetails gitHubDetails = new GitHubChecksDetails(details);

Expand Down Expand Up @@ -134,18 +131,8 @@ GHCheckRunBuilder getCreator(final GitHub gitHub, final GitHubChecksDetails deta
}

@VisibleForTesting
StandardUsernameCredentials getCredentials() {
if (credentials == null) {
credentials = context.getCredentials();
if (credentials instanceof GitHubAppCredentials) {
final var gitHubAppCredentials = (GitHubAppCredentials) credentials;
if (context instanceof GitHubSCMSourceChecksContext) {
final var gitHubSCMSourceChecksContext = (GitHubSCMSourceChecksContext) context;
credentials = gitHubAppCredentials.withOwner(gitHubSCMSourceChecksContext.getOwner());
}
}
}
return credentials;
GitHubChecksContext getContext() {
return context;
}

private GHCheckRunBuilder applyDetails(final GHCheckRunBuilder builder, final GitHubChecksDetails details) {
Expand Down
19 changes: 8 additions & 11 deletions src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package io.jenkins.plugins.checks.github;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.model.AbstractProject;
import hudson.model.Job;
Expand All @@ -11,14 +10,15 @@
import hudson.plugins.git.UserRemoteConfig;
import hudson.scm.NullSCM;
import hudson.scm.SCM;
import hudson.security.ACL;
import jenkins.plugins.git.AbstractGitSCMSource;
import jenkins.plugins.git.GitSCMSource;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import jenkins.triggers.SCMTriggerItem;

import org.jenkinsci.plugins.github_branch_source.Connector;
import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials;
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision;
Expand All @@ -28,7 +28,6 @@

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -124,13 +123,11 @@ UserRemoteConfig getUserRemoteConfig(final GitSCM scm) {
* @return the found GitHub App credentials or empty
*/
public Optional<StandardUsernameCredentials> findGitHubAppCredentials(final Job<?, ?> job, final String credentialsId) {
List<StandardUsernameCredentials> standardUsernameCredentials = CredentialsProvider.lookupCredentials(
StandardUsernameCredentials.class, job, ACL.SYSTEM, Collections.emptyList());

StandardUsernameCredentials appCredentials =
CredentialsMatchers.firstOrNull(standardUsernameCredentials, CredentialsMatchers.withId(credentialsId));

return Optional.ofNullable(appCredentials);
final var source = findGitHubSCMSource(job);
final var apiUri = source.map(GitHubSCMSource::getApiUri).orElse(null);
final var owner = source.map(GitHubSCMSource::getRepoOwner).orElse(null);
final var appCredentials = Connector.lookupScanCredentials(job, apiUri, credentialsId, owner);
return Optional.ofNullable(appCredentials).filter(StandardUsernameCredentials.class::isInstance).map(StandardUsernameCredentials.class::cast);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.checks.github;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
Expand Down Expand Up @@ -52,7 +53,6 @@
import hudson.util.Secret;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.scm.api.SCMHead;

import io.jenkins.plugins.checks.api.ChecksAction;
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationBuilder;
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel;
Expand Down Expand Up @@ -159,56 +159,66 @@ public static Collection<Object[]> contextBuilders() {
public WireMockRule wireMockRule = new WireMockRule(
WireMockConfiguration.options().dynamicPort());

private MockedStatic<CredentialsMatchers> mockCredentialsMatchers() {
final var gitHubAppCredentials = new GitHubAppCredentials(CredentialsScope.GLOBAL, "cred-id", null, "app-id", Secret.fromString(TEST_PRIVATE_KEY));

final var credentialsMatchers = mockStatic(CredentialsMatchers.class);
credentialsMatchers.when(() -> CredentialsMatchers.firstOrNull(any(), any())).thenReturn(gitHubAppCredentials);
return credentialsMatchers;
}

/**
* Checks should be published to GitHub correctly when GitHub SCM is found and parameters are correctly set.
*/
@Test
public void shouldPublishGitHubCheckRunCorrectly() {
ChecksDetails details = new ChecksDetailsBuilder()
.withName("Jenkins")
.withStatus(ChecksStatus.COMPLETED)
.withDetailsURL("https://ci.jenkins.io")
.withStartedAt(LocalDateTime.ofEpochSecond(999_999, 0, ZoneOffset.UTC))
.withCompletedAt(LocalDateTime.ofEpochSecond(999_999, 0, ZoneOffset.UTC))
.withConclusion(ChecksConclusion.SUCCESS)
.withOutput(new ChecksOutputBuilder()
.withTitle("Jenkins Check")
.withSummary("# A Successful Build")
.withText("## 0 Failures")
.withAnnotations(Arrays.asList(
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withLine(1)
.withAnnotationLevel(ChecksAnnotationLevel.NOTICE)
.withMessage("say hello to Jenkins")
.withStartColumn(0)
.withEndColumn(20)
.withTitle("Hello Jenkins")
.withRawDetails("a simple echo command")
.build(),
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withLine(2)
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
.withMessage("say hello to GitHub Checks API")
.withStartColumn(0)
.withEndColumn(30)
.withTitle("Hello GitHub Checks API")
.withRawDetails("a simple echo command")
.build()))
.withImages(Collections.singletonList(
new ChecksImage("Jenkins",
"https://ci.jenkins.io/static/cd5757a8/images/jenkins-header-logo-v2.svg",
"Jenkins Symbol")))
.build())
.withActions(Collections.singletonList(
new ChecksAction("re-run", "re-run Jenkins build", "#0")))
.build();

new GitHubChecksPublisher(contextBuilder.apply(this),
new PluginLogger(j.createTaskListener().getLogger(), "GitHub Checks"),
wireMockRule.baseUrl())
.publish(details);
try (var credentialsMatchers = mockCredentialsMatchers()) {
ChecksDetails details = new ChecksDetailsBuilder()
.withName("Jenkins")
.withStatus(ChecksStatus.COMPLETED)
.withDetailsURL("https://ci.jenkins.io")
.withStartedAt(LocalDateTime.ofEpochSecond(999_999, 0, ZoneOffset.UTC))
.withCompletedAt(LocalDateTime.ofEpochSecond(999_999, 0, ZoneOffset.UTC))
.withConclusion(ChecksConclusion.SUCCESS)
.withOutput(new ChecksOutputBuilder()
.withTitle("Jenkins Check")
.withSummary("# A Successful Build")
.withText("## 0 Failures")
.withAnnotations(Arrays.asList(
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withLine(1)
.withAnnotationLevel(ChecksAnnotationLevel.NOTICE)
.withMessage("say hello to Jenkins")
.withStartColumn(0)
.withEndColumn(20)
.withTitle("Hello Jenkins")
.withRawDetails("a simple echo command")
.build(),
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withLine(2)
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
.withMessage("say hello to GitHub Checks API")
.withStartColumn(0)
.withEndColumn(30)
.withTitle("Hello GitHub Checks API")
.withRawDetails("a simple echo command")
.build()))
.withImages(Collections.singletonList(
new ChecksImage("Jenkins",
"https://ci.jenkins.io/static/cd5757a8/images/jenkins-header-logo-v2.svg",
"Jenkins Symbol")))
.build())
.withActions(Collections.singletonList(
new ChecksAction("re-run", "re-run Jenkins build", "#0")))
.build();

new GitHubChecksPublisher(contextBuilder.apply(this),
new PluginLogger(j.createTaskListener().getLogger(), "GitHub Checks"),
wireMockRule.baseUrl())
.publish(details);
}
}

/**
Expand All @@ -217,48 +227,50 @@ public void shouldPublishGitHubCheckRunCorrectly() {
@Issue("issue-20")
@Test
public void shouldLogChecksParametersIfExceptionHappensWhenPublishChecks() {
loggerRule.record(GitHubChecksPublisher.class.getName(), Level.WARNING).capture(1);

ChecksDetails details = new ChecksDetailsBuilder()
.withName("Jenkins")
.withStatus(ChecksStatus.COMPLETED)
.withConclusion(ChecksConclusion.SUCCESS)
.withOutput(new ChecksOutputBuilder()
.withTitle("Jenkins Check")
.withSummary("# A Successful Build")
.withAnnotations(Collections.singletonList(
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withStartLine(1)
.withEndLine(2)
.withStartColumn(0)
.withEndColumn(20)
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
.withMessage("say hello to Jenkins")
.build()))
.build())
.build();

new GitHubChecksPublisher(contextBuilder.apply(this),
new PluginLogger(j.createTaskListener().getLogger(), "GitHub Checks"),
wireMockRule.baseUrl())
.publish(details);

assertThat(loggerRule.getRecords().size()).isEqualTo(1);
assertThat(loggerRule.getMessages().get(0))
.contains("Failed Publishing GitHub checks: ")
.contains("name='Jenkins'")
.contains("status=COMPLETED")
.contains("conclusion=SUCCESS")
.contains("title='Jenkins Check'")
.contains("summary='# A Successful Build'")
.contains("path='Jenkinsfile'")
.contains("startLine=1")
.contains("endLine=2")
.contains("startColumn=0")
.contains("endColumn=20")
.contains("annotationLevel=WARNING")
.contains("message='say hello to Jenkins'");
try (var credentialsMatchers = mockCredentialsMatchers()) {
loggerRule.record(GitHubChecksPublisher.class.getName(), Level.WARNING).capture(1);

ChecksDetails details = new ChecksDetailsBuilder()
.withName("Jenkins")
.withStatus(ChecksStatus.COMPLETED)
.withConclusion(ChecksConclusion.SUCCESS)
.withOutput(new ChecksOutputBuilder()
.withTitle("Jenkins Check")
.withSummary("# A Successful Build")
.withAnnotations(Collections.singletonList(
new ChecksAnnotationBuilder()
.withPath("Jenkinsfile")
.withStartLine(1)
.withEndLine(2)
.withStartColumn(0)
.withEndColumn(20)
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
.withMessage("say hello to Jenkins")
.build()))
.build())
.build();

new GitHubChecksPublisher(contextBuilder.apply(this),
new PluginLogger(j.createTaskListener().getLogger(), "GitHub Checks"),
wireMockRule.baseUrl())
.publish(details);

assertThat(loggerRule.getRecords().size()).isEqualTo(1);
assertThat(loggerRule.getMessages().get(0))
.contains("Failed Publishing GitHub checks: ")
.contains("name='Jenkins'")
.contains("status=COMPLETED")
.contains("conclusion=SUCCESS")
.contains("title='Jenkins Check'")
.contains("summary='# A Successful Build'")
.contains("path='Jenkinsfile'")
.contains("startLine=1")
.contains("endLine=2")
.contains("startColumn=0")
.contains("endColumn=20")
.contains("annotationLevel=WARNING")
.contains("message='say hello to Jenkins'");
}
}

/**
Expand Down Expand Up @@ -313,8 +325,9 @@ public void testChecksPublisherUpdatesCorrectly() throws Exception {
when(repository.createCheckRun(eq(checksName2), anyString())).thenReturn(createBuilder2);
when(repository.updateCheckRun(checksId1)).thenReturn(updateBuilder1);

try (MockedStatic<Connector> connector = mockStatic(Connector.class)) {
connector.when(() -> Connector.connect(anyString(), any(GitHubAppCredentials.class))).thenReturn(gitHub);
try (var credentialsMatchers = mockCredentialsMatchers(); var connector = mockStatic(Connector.class)) {
connector.when(() -> Connector.lookupScanCredentials(any(), any(), any(), any())).thenCallRealMethod();
connector.when(() -> Connector.connect(anyString(), any())).thenReturn(gitHub);

GitHubChecksContext context = contextBuilder.apply(this);

Expand All @@ -330,7 +343,7 @@ public void testChecksPublisherUpdatesCorrectly() throws Exception {

// Check that the owner is passed from context to credentials
if (context instanceof GitHubSCMSourceChecksContext) {
var credentials = publisher.getCredentials();
var credentials = publisher.getContext().getCredentials();
if (credentials instanceof GitHubAppCredentials) {
var gitHubAppCredentials = (GitHubAppCredentials) credentials;
assertThat(gitHubAppCredentials.getOwner()).isEqualTo("XiongKezhi");
Expand Down Expand Up @@ -435,11 +448,8 @@ GitHubChecksContext createGitHubChecksContextWithGitHubSCM(final J job) throws E
when(source.getRepoOwner()).thenReturn("XiongKezhi");
when(source.getRepository()).thenReturn("Sandbox");

GitHubAppCredentials gitHubAppCredentials = new GitHubAppCredentials(CredentialsScope.GLOBAL,
"cred-id", null, "app-id", Secret.fromString(TEST_PRIVATE_KEY));

when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source));
when(scmFacade.findGitHubAppCredentials(job, "1")).thenReturn(Optional.of(gitHubAppCredentials));
when(scmFacade.findGitHubAppCredentials(job, "1")).thenCallRealMethod();
when(scmFacade.findHead(job)).thenReturn(Optional.of(head));
when(scmFacade.findRevision(source, run)).thenReturn(Optional.of(revision));
when(scmFacade.findRevision(source, head)).thenReturn(Optional.of(revision));
Expand Down

0 comments on commit 845136f

Please sign in to comment.