├── src ├── main │ ├── resources │ │ ├── lib │ │ │ └── github-checks │ │ │ │ ├── config │ │ │ │ ├── taglib │ │ │ │ └── config.jelly │ │ │ │ └── status │ │ │ │ ├── taglib │ │ │ │ ├── help-suppressLogs.html │ │ │ │ └── properties.jelly │ │ ├── index.jelly │ │ └── io │ │ │ └── jenkins │ │ │ └── plugins │ │ │ └── checks │ │ │ └── github │ │ │ ├── config │ │ │ ├── GitSCMChecksExtension │ │ │ │ ├── config.jelly │ │ │ │ └── help-verboseConsoleLog.html │ │ │ └── GitHubSCMSourceChecksTrait │ │ │ │ ├── config.jelly │ │ │ │ └── help-verboseConsoleLog.html │ │ │ └── status │ │ │ ├── GitSCMStatusChecksExtension │ │ │ └── config.jelly │ │ │ └── GitHubSCMSourceStatusChecksTrait │ │ │ ├── help-skipNotifications.html │ │ │ └── config.jelly │ ├── webapp │ │ └── help-globalConfig.html │ └── java │ │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── checks │ │ └── github │ │ ├── package-info.java │ │ ├── config │ │ ├── DefaultGitHubChecksConfig.java │ │ ├── GitHubChecksConfig.java │ │ ├── GitSCMChecksExtension.java │ │ └── GitHubSCMSourceChecksTrait.java │ │ ├── GitHubChecksAction.java │ │ ├── status │ │ ├── GitHubStatusChecksConfigurations.java │ │ ├── GitHubStatusChecksProperties.java │ │ ├── GitSCMStatusChecksExtension.java │ │ └── GitHubSCMSourceStatusChecksTrait.java │ │ ├── GitHubChecksPublisherFactory.java │ │ ├── GitHubChecksContext.java │ │ ├── GitHubSCMSourceChecksContext.java │ │ ├── GitSCMChecksContext.java │ │ ├── GitHubChecksPublisher.java │ │ ├── CheckRunGHEventSubscriber.java │ │ ├── SCMFacade.java │ │ └── GitHubChecksDetails.java └── test │ ├── resources │ ├── mappings │ │ ├── get-app.json │ │ ├── list-installations.json │ │ ├── create-access-token.json │ │ ├── check-run-with-invalid-columns.json │ │ ├── get-repo.json │ │ ├── rate-limit.json │ │ └── create-check-run.json │ ├── __files │ │ ├── check-run-with-invalid-columns-response.json │ │ ├── rate-limit.json │ │ ├── get-app.json │ │ ├── list-installations.json │ │ ├── create-check-run-response.json │ │ ├── get-repo-response.json │ │ └── create-access-token.json │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── checks │ │ └── github │ │ └── CheckRunGHEventSubscriberTest │ │ ├── check-run-event-with-rerun-action-for-pr-missing-check-suite.json │ │ ├── check-run-event-with-rerun-action-for-master.json │ │ └── check-run-event-with-rerun-action-for-pr.json │ └── java │ └── io │ └── jenkins │ └── plugins │ └── checks │ └── github │ ├── GitSCMChecksContextTest.java │ ├── config │ └── GitHubChecksConfigITest.java │ ├── status │ ├── GitHubSCMSourceStatusChecksTraitTest.java │ └── GitHubStatusChecksPropertiesTest.java │ ├── GitHubChecksDetailsTest.java │ ├── GitSCMChecksContextITest.java │ ├── GitHubChecksPublisherFactoryTest.java │ ├── CheckRunGHEventSubscriberTest.java │ └── GitHubSCMSourceChecksContextTest.java ├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ ├── release-drafter.yml │ ├── cd.yaml │ ├── jenkins-security-scan.yml │ └── maven.yml ├── .gitignore ├── .mvn ├── maven.config └── extensions.xml ├── docs └── images │ ├── coverage-checks.png │ ├── failed-checks.png │ ├── github-status.png │ ├── warning-checks.png │ ├── github-checks-config.png │ ├── status-checks-properties.png │ └── github-checks-plugin-cover.png ├── Jenkinsfile ├── etc └── assertj-templates │ ├── soft_assertions_entry_point_class_template.txt │ ├── assertions_entry_point_class_template.txt │ └── has_assertion_template.txt ├── LICENSE ├── README.md └── pom.xml /src/main/resources/lib/github-checks/config/taglib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/lib/github-checks/status/taglib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/github-checks-plugin-developers 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bin/ 3 | target/ 4 | work/ 5 | *.iml 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | 5 | -------------------------------------------------------------------------------- /docs/images/coverage-checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/coverage-checks.png -------------------------------------------------------------------------------- /docs/images/failed-checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/failed-checks.png -------------------------------------------------------------------------------- /docs/images/github-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/github-status.png -------------------------------------------------------------------------------- /docs/images/warning-checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/warning-checks.png -------------------------------------------------------------------------------- /docs/images/github-checks-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/github-checks-config.png -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Allows users to publish GitHub checks. 4 |
5 | -------------------------------------------------------------------------------- /docs/images/status-checks-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/status-checks-properties.png -------------------------------------------------------------------------------- /src/main/webapp/help-globalConfig.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | See help-projectConfig.html for more about what these HTMLs do. 4 |

5 |
-------------------------------------------------------------------------------- /docs/images/github-checks-plugin-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/github-checks-plugin/HEAD/docs/images/github-checks-plugin-cover.png -------------------------------------------------------------------------------- /src/test/resources/mappings/get-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/app", 4 | "method": "GET" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "bodyFileName": "get-app.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/config/GitSCMChecksExtension/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/config/GitHubSCMSourceChecksTrait/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/status/GitSCMStatusChecksExtension/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/test/resources/mappings/list-installations.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/app/installations", 4 | "method": "GET" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "bodyFileName": "list-installations.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/config/GitSCMChecksExtension/help-verboseConsoleLog.html: -------------------------------------------------------------------------------- 1 |
2 | If this option is checked, verbose log will be output to build console; the verbose log is useful for debugging 3 | the publisher creation. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/config/GitHubSCMSourceChecksTrait/help-verboseConsoleLog.html: -------------------------------------------------------------------------------- 1 |
2 | If this option is checked, verbose log will be output to build console; the verbose log is useful for debugging 3 | the publisher creation. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/lib/github-checks/config/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/lib/github-checks/status/help-suppressLogs.html: -------------------------------------------------------------------------------- 1 |
2 | If enabled, will prevent log output from appearing in status checks. This may be useful if your logs contain 3 | potentially sensitive information that you do not want to leave Jenkins. 4 |
5 | -------------------------------------------------------------------------------- /src/test/resources/mappings/create-access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/app/installations/11111111/access_tokens", 4 | "method": "POST" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "bodyFileName": "create-access-token.json" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/status/GitHubSCMSourceStatusChecksTrait/help-skipNotifications.html: -------------------------------------------------------------------------------- 1 |
2 | If this option is checked, the notifications sent by the GitHub Branch Source Plugin 3 | will be disabled. 4 |
5 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides default Findbugs annotations. 3 | */ 4 | @DefaultAnnotation(NonNull.class) 5 | package io.jenkins.plugins.checks.github; 6 | 7 | import edu.umd.cs.findbugs.annotations.DefaultAnnotation; 8 | import edu.umd.cs.findbugs.annotations.NonNull; 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommitsDisabled", 6 | "schedule:earlyMondays" 7 | ], 8 | "automerge": true, 9 | "labels": [ 10 | "dependencies" 11 | ], 12 | "rebaseWhen": "conflicted" 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/__files/check-run-with-invalid-columns-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Validation Failed", 3 | "errors": [ 4 | { 5 | "resource": "CheckRun", 6 | "code": "invalid", 7 | "field": "annotations" 8 | } 9 | ], 10 | "documentation_url": "https://docs.github.com/rest/reference/checks#create-a-check-run" 11 | } -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | def configurations = [ 2 | [platform: 'linux', jdk: 17], 3 | [platform: 'windows', jdk: 21], 4 | ] 5 | 6 | buildPlugin(failFast: false, configurations: configurations, 7 | useContainerAgent: true, 8 | checkstyle: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]]], 9 | pmd: [qualityGates: [[threshold: 1, type: 'NEW', unstable: true]]] ) 10 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/config/DefaultGitHubChecksConfig.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.config; 2 | 3 | /** 4 | * Default implementation for {@link GitHubChecksConfig}. 5 | */ 6 | public class DefaultGitHubChecksConfig implements GitHubChecksConfig { 7 | @Override 8 | public boolean isVerboseConsoleLog() { 9 | return false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/checks/github/status/GitHubSCMSourceStatusChecksTrait/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /etc/assertj-templates/soft_assertions_entry_point_class_template.txt: -------------------------------------------------------------------------------- 1 | package ${package}; 2 | 3 | /** 4 | * Entry point for soft assertions of different data types. 5 | */ 6 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("NM") 7 | @javax.annotation.Generated(value="assertj-assertions-generator") 8 | public class SoftAssertions extends org.assertj.core.api.AutoCloseableSoftAssertions { 9 | ${all_assertions_entry_points} 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/config/GitHubChecksConfig.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.config; 2 | 3 | /** 4 | * Project-level configurations for users to customize GitHub checks. 5 | */ 6 | public interface GitHubChecksConfig { 7 | /** 8 | * Defines whether to output verbose console log. 9 | * 10 | * @return true for verbose log 11 | */ 12 | boolean isVerboseConsoleLog(); 13 | } 14 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.13 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | permissions: 11 | checks: read 12 | contents: write 13 | 14 | jobs: 15 | maven-cd: 16 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 17 | secrets: 18 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 19 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 20 | -------------------------------------------------------------------------------- /etc/assertj-templates/assertions_entry_point_class_template.txt: -------------------------------------------------------------------------------- 1 | package ${package}; 2 | 3 | /** 4 | * Entry point for assertions of different data types. Each method in this class is a static factory for the 5 | * type-specific assertion objects. 6 | */ 7 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings("NM") 8 | @javax.annotation.Generated(value="assertj-assertions-generator") 9 | public class Assertions extends org.assertj.core.api.Assertions { 10 | ${all_assertions_entry_points} 11 | /** 12 | * Creates a new {@link Assertions}. 13 | */ 14 | protected Assertions() { 15 | // empty 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/resources/__files/rate-limit.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "core": { 4 | "limit": 5000, 5 | "remaining": 4944, 6 | "reset": 1570055937 7 | }, 8 | "search": { 9 | "limit": 30, 10 | "remaining": 30, 11 | "reset": 1570052463 12 | }, 13 | "graphql": { 14 | "limit": 5000, 15 | "remaining": 5000, 16 | "reset": 1570056003 17 | }, 18 | "integration_manifest": { 19 | "limit": 5000, 20 | "remaining": 5000, 21 | "reset": 1570056003 22 | } 23 | }, 24 | "rate": { 25 | "limit": 5000, 26 | "remaining": 4944, 27 | "reset": 1570055937 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | strategy: 13 | matrix: 14 | java: [17] 15 | os: [ubuntu-latest] 16 | 17 | runs-on: ${{ matrix.os }} 18 | name: on ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v6 22 | 23 | - name: Set up JDK ${{ matrix.java }} 24 | uses: actions/setup-java@v5 25 | with: 26 | distribution: 'temurin' 27 | java-version: ${{ matrix.java }} 28 | cache: 'maven' 29 | 30 | - name: Build with Maven 31 | run: mvn -Penable-jacoco clean verify -B -V -ntp 32 | 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | file: '*jacoco.xml' 37 | -------------------------------------------------------------------------------- /src/main/resources/lib/github-checks/status/properties.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import hudson.model.Run; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.mockito.Mockito.mock; 8 | 9 | class GitSCMChecksContextTest { 10 | @Test 11 | void shouldGetRepository() { 12 | for (String url : new String[]{ 13 | "git@197.168.2.0:jenkinsci/github-checks-plugin", 14 | "git@localhost:jenkinsci/github-checks-plugin", 15 | "git@github.com:jenkinsci/github-checks-plugin", 16 | "http://github.com/jenkinsci/github-checks-plugin.git", 17 | "https://github.com/jenkinsci/github-checks-plugin.git" 18 | }) { 19 | assertThat(new GitSCMChecksContext(mock(Run.class), "").getRepository(url)) 20 | .isEqualTo("jenkinsci/github-checks-plugin"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/mappings/check-run-with-invalid-columns.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/repos/XiongKezhi/Sandbox/check-runs", 4 | "method": "POST", 5 | "headers": { 6 | "Accept": { 7 | "equalTo": "application/vnd.github.antiope-preview+json" 8 | } 9 | }, 10 | "bodyPatterns": [ 11 | { 12 | "equalToJson": "{\"conclusion\":\"success\",\"output\":{\"title\":\"Jenkins Check\",\"summary\":\"# A Successful Build\",\"annotations\":[{\"path\":\"Jenkinsfile\",\"start_line\":1,\"end_line\":2,\"annotation_level\":\"warning\",\"message\":\"say hello to Jenkins\",\"start_column\":0,\"end_column\":20}]},\"name\":\"Jenkins\",\"details_url\":\"https://ci.jenkins.io\",\"head_sha\":\"18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f\",\"status\":\"completed\"}", 13 | "ignoreArrayOrder": true, 14 | "ignoreExtraElements": true 15 | } 16 | ] 17 | }, 18 | "response": { 19 | "status": 422, 20 | "bodyFileName": "check-run-with-invalid-columns-response.json" 21 | } 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kezhi Xiong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /etc/assertj-templates/has_assertion_template.txt: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Verifies that the actual ${class_to_assert}'s ${property} is equal to the given one. 4 | * @param ${property_safe} the given ${property} to compare the actual ${class_to_assert}'s ${property} to. 5 | * @return this assertion object. 6 | * @throws AssertionError - if the actual ${class_to_assert}'s ${property} is not equal to the given one.${throws_javadoc} 7 | */ 8 | public ${self_type} has${Property}(${propertyType} ${property_safe}) ${throws}{ 9 | // check that actual ${class_to_assert} we want to make assertions on is not null. 10 | isNotNull(); 11 | 12 | // overrides the default error message with a more explicit one 13 | String assertjErrorMessage = "\nExpecting ${property} of:\n <%s>\nto be:\n <%s>\nbut was:\n <%s>"; 14 | 15 | // null safe check 16 | ${propertyType} actual${Property} = actual.${getter}(); 17 | if (!java.util.Objects.deepEquals(actual${Property}, ${property_safe})) { 18 | failWithMessage(assertjErrorMessage, actual, ${property_safe}, actual${Property}); 19 | } 20 | 21 | // return the current assertion for method chaining 22 | return ${myself}; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubChecksAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import hudson.model.InvisibleAction; 4 | 5 | import static java.util.Objects.requireNonNull; 6 | 7 | /** 8 | * An invisible action to track the state of GitHub Checks so that the publisher can update existing checks by the 9 | * same name, and report back to the checks api the state of a named check (without having to go and check GitHub 10 | * each time). 11 | */ 12 | @SuppressWarnings("PMD.DataClass") 13 | public class GitHubChecksAction extends InvisibleAction { 14 | 15 | private final long id; 16 | private final String name; 17 | 18 | /** 19 | * Construct a {@link GitHubChecksAction} with the given details. 20 | * 21 | * @param id the id of the check run as reported by GitHub 22 | * @param name the name of the check 23 | */ 24 | public GitHubChecksAction(final long id, final String name) { 25 | super(); 26 | this.id = id; 27 | this.name = requireNonNull(name); 28 | } 29 | 30 | public long getId() { 31 | return id; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/config/GitHubChecksConfigITest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.config; 2 | 3 | import hudson.util.StreamTaskListener; 4 | import io.jenkins.plugins.checks.github.GitHubChecksPublisherFactory; 5 | import org.junit.jupiter.api.Test; 6 | import org.jvnet.hudson.test.JenkinsRule; 7 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | /** 16 | * Integration test for {@link GitHubChecksConfig}. 17 | */ 18 | @WithJenkins 19 | class GitHubChecksConfigITest { 20 | 21 | /** 22 | * When a job has not {@link org.jenkinsci.plugins.github_branch_source.GitHubSCMSource} or 23 | * {@link hudson.plugins.git.GitSCM}, the default config should be used and no verbose log should be output. 24 | */ 25 | @Test 26 | void shouldUseDefaultConfigWhenNoSCM(JenkinsRule j) throws IOException { 27 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 28 | GitHubChecksPublisherFactory.fromJob(j.createFreeStyleProject(), new StreamTaskListener(os, StandardCharsets.UTF_8)); 29 | 30 | assertThat(os.toString()).doesNotContain("Causes for no suitable publisher found: "); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/mappings/get-repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/repos/XiongKezhi/Sandbox", 4 | "method": "GET" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "bodyFileName": "get-repo-response.json", 9 | "headers": { 10 | "Server": "GitHub.com", 11 | "Date": "Fri, 26 Jun 2020 15:30:20 GMT", 12 | "Content-Type": "application/json; charset=utf-8", 13 | "Content-Length": "1208", 14 | "Status": "200 OK", 15 | "X-RateLimit-Limit": "60", 16 | "X-RateLimit-Remaining": "46", 17 | "X-RateLimit-Reset": "1593186865", 18 | "Cache-Control": "private, max-age=60, s-maxage=60", 19 | "Vary": [ 20 | "Accept, Authorization, Cookie, X-GitHub-OTP", 21 | "Accept-Encoding, Accept, X-Requested-With" 22 | ], 23 | "ETag": "W/\"92b4305ef8efc6a8616c16545fa12b66\"", 24 | "Last-Modified": "Tue, 19 May 2020 14:01:45 GMT", 25 | "X-GitHub-Media-Type": "github.nebula-preview; format=json", 26 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 27 | "X-Frame-Options": "deny", 28 | "X-Content-Type-Options": "nosniff", 29 | "X-XSS-Protection": "1; mode=block", 30 | "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 31 | "Content-Security-Policy": "default-src 'none'", 32 | "X-GitHub-Request-Id": "9A15:6AE8:61825D:D17E9C:5EF6148C" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/status/GitHubSCMSourceStatusChecksTraitTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | import jenkins.scm.api.SCMHeadObserver; 4 | import jenkins.scm.api.SCMSourceCriteria; 5 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSourceContext; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.mockito.Mockito.mock; 10 | 11 | class GitHubSCMSourceStatusChecksTraitTest { 12 | 13 | @Test 14 | void shouldOnlyApplyTraitConfigurationsToGitHubBranchSourceNotificationsWhenItsNotDisabled() { 15 | GitHubSCMSourceContext context = new GitHubSCMSourceContext(mock(SCMSourceCriteria.class), 16 | mock(SCMHeadObserver.class)); 17 | GitHubSCMSourceStatusChecksTrait trait = new GitHubSCMSourceStatusChecksTrait(); 18 | 19 | // disable notifications, the trait configuration should be ignored 20 | context.withNotificationsDisabled(true); 21 | trait.setSkipNotifications(false); 22 | trait.decorateContext(context); 23 | assertThat(context.notificationsDisabled()).isTrue(); 24 | 25 | // enable notifications, the trait configuration should be applied 26 | context.withNotificationsDisabled(false); 27 | trait.setSkipNotifications(true); 28 | trait.decorateContext(context); 29 | assertThat(context.notificationsDisabled()).isTrue(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/config/GitSCMChecksExtension.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.config; 2 | 3 | import hudson.Extension; 4 | import hudson.plugins.git.extensions.GitSCMExtension; 5 | import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; 6 | import org.kohsuke.stapler.DataBoundConstructor; 7 | import org.kohsuke.stapler.DataBoundSetter; 8 | 9 | /** 10 | * GitHub checks configurations for freestyle jobs with {@link hudson.plugins.git.GitSCM}. 11 | */ 12 | @Extension 13 | public class GitSCMChecksExtension extends GitSCMExtension implements GitHubChecksConfig { 14 | private boolean verboseConsoleLog; 15 | 16 | /** 17 | * Constructor for stapler. 18 | */ 19 | @DataBoundConstructor 20 | public GitSCMChecksExtension() { 21 | super(); 22 | } 23 | 24 | @DataBoundSetter 25 | public void setVerboseConsoleLog(final boolean verboseConsoleLog) { 26 | this.verboseConsoleLog = verboseConsoleLog; 27 | } 28 | 29 | @Override 30 | public boolean isVerboseConsoleLog() { 31 | return verboseConsoleLog; 32 | } 33 | 34 | /** 35 | * Descriptor for {@link GitSCMChecksExtension}. 36 | */ 37 | @Extension 38 | public static class DescriptorImpl extends GitSCMExtensionDescriptor { 39 | /** 40 | * Returns the display name. 41 | * 42 | * @return "Configure GitHub Checks" 43 | */ 44 | @Override 45 | public String getDisplayName() { 46 | return "Configure GitHub Checks"; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/resources/__files/get-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 11111, 3 | "node_id": "MDM6QXBwMzI2MTY=", 4 | "owner": { 5 | "login": "XiongKezhi", 6 | "id": 111111111, 7 | "node_id": "asdfasdfasdf", 8 | "avatar_url": "https://avatars2.githubusercontent.com/u/111111111?v=4", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/XiongKezhi", 11 | "html_url": "https://github.com/XiongKezhi", 12 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 13 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 17 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 18 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 19 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 21 | "type": "Organization", 22 | "site_admin": false 23 | }, 24 | "name": "XiongKezhi-Development", 25 | "description": "", 26 | "external_url": "https://XiongKezhi.domain.com", 27 | "html_url": "https://github.com/apps/XiongKezhi-development", 28 | "created_at": "2019-06-10T04:21:41Z", 29 | "updated_at": "2019-06-10T04:21:41Z", 30 | "permissions": { 31 | "checks": "write", 32 | "contents": "read", 33 | "metadata": "read", 34 | "pull_requests": "write" 35 | }, 36 | "events": [ 37 | "pull_request", 38 | "push" 39 | ], 40 | "installations_count": 1 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/mappings/rate-limit.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "195911b7-d3b7-4eb4-9d1e-f39465b89bb8", 3 | "name": "rate_limit", 4 | "request": { 5 | "url": "/rate_limit", 6 | "method": "GET", 7 | "headers": { 8 | "Accept": { 9 | "equalTo": "application/vnd.github.v3+json" 10 | } 11 | } 12 | }, 13 | "response": { 14 | "status": 200, 15 | "bodyFileName": "rate_limit-1.json", 16 | "headers": { 17 | "Date": "Wed, 02 Oct 2019 21:40:03 GMT", 18 | "Content-Type": "application/json; charset=utf-8", 19 | "Server": "GitHub.com", 20 | "Status": "200 OK", 21 | "X-RateLimit-Limit": "5000", 22 | "X-RateLimit-Remaining": "4944", 23 | "X-RateLimit-Reset": "1570055937", 24 | "Cache-Control": "no-cache", 25 | "X-OAuth-Scopes": "admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion", 26 | "X-Accepted-OAuth-Scopes": "", 27 | "X-GitHub-Media-Type": "unknown, github.v3", 28 | "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type", 29 | "Access-Control-Allow-Origin": "*", 30 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 31 | "X-Frame-Options": "deny", 32 | "X-Content-Type-Options": "nosniff", 33 | "X-XSS-Protection": "1; mode=block", 34 | "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 35 | "Content-Security-Policy": "default-src 'none'", 36 | "Vary": "Accept-Encoding", 37 | "X-GitHub-Request-Id": "C2CD:3CA2:BB7551:E0A7D6:5D951933" 38 | } 39 | }, 40 | "uuid": "195911b7-d3b7-4eb4-9d1e-f39465b89bb8", 41 | "persistent": true, 42 | "insertionIndex": 1 43 | } 44 | -------------------------------------------------------------------------------- /src/test/resources/__files/list-installations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 11111111, 4 | "account": { 5 | "login": "XiongKezhi", 6 | "id": 111111111, 7 | "node_id": "asdfasdfasdf", 8 | "avatar_url": "https://avatars2.githubusercontent.com/u/111111111?v=4", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/XiongKezhi", 11 | "html_url": "https://github.com/XiongKezhi", 12 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 13 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 17 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 18 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 19 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 21 | "type": "Organization", 22 | "site_admin": false 23 | }, 24 | "repository_selection": "selected", 25 | "access_tokens_url": "https://api.github.com/app/installations/11111111/access_tokens", 26 | "repositories_url": "https://api.github.com/installation/repositories", 27 | "html_url": "https://github.com/organizations/XiongKezhi/settings/installations/11111111", 28 | "app_id": 11111, 29 | "target_id": 111111111, 30 | "target_type": "Organization", 31 | "permissions": { 32 | "checks": "write", 33 | "pull_requests": "write", 34 | "contents": "read", 35 | "metadata": "read" 36 | }, 37 | "events": [ 38 | "pull_request", 39 | "push" 40 | ], 41 | "created_at": "2019-07-04T01:19:36.000Z", 42 | "updated_at": "2019-07-30T22:48:09.000Z", 43 | "single_file_name": null 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/status/GitHubStatusChecksConfigurations.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | /** 4 | * Configurations for users to customize status checks. 5 | */ 6 | public interface GitHubStatusChecksConfigurations { 7 | /** 8 | * Defines the status checks name which is also used as identifier for GitHub checks. 9 | * 10 | * @return the name of status checks 11 | */ 12 | String getName(); 13 | 14 | /** 15 | * Defines whether to skip publishing status checks. 16 | * 17 | * @return true to skip publishing checks 18 | */ 19 | boolean isSkip(); 20 | 21 | /** 22 | * Defines whether to publish unstable builds as neutral status checks. 23 | * 24 | * @return true to publish unstable builds as neutral status checks. 25 | */ 26 | boolean isUnstableBuildNeutral(); 27 | 28 | /** 29 | * Defines whether to suppress log output in status checks. 30 | * 31 | * @return true to suppress logs 32 | */ 33 | boolean isSuppressLogs(); 34 | 35 | /** 36 | * Returns whether to suppress progress updates from the {@code io.jenkins.plugins.checks.status.FlowExecutionAnalyzer}. 37 | * Queued, Checkout and Completed will still run but not 'onNewHead' 38 | * 39 | * @return true if progress updates should be skipped. 40 | */ 41 | boolean isSkipProgressUpdates(); 42 | } 43 | 44 | class DefaultGitHubStatusChecksConfigurations implements GitHubStatusChecksConfigurations { 45 | @Override 46 | public String getName() { 47 | return "Jenkins"; 48 | } 49 | 50 | @Override 51 | public boolean isSkip() { 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean isUnstableBuildNeutral() { 57 | return false; 58 | } 59 | 60 | @Override 61 | public boolean isSuppressLogs() { 62 | return false; 63 | } 64 | 65 | @Override 66 | public boolean isSkipProgressUpdates() { 67 | return false; 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/GitHubChecksDetailsTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import io.jenkins.plugins.checks.api.ChecksConclusion; 4 | import io.jenkins.plugins.checks.api.ChecksDetails; 5 | import io.jenkins.plugins.checks.api.ChecksDetails.ChecksDetailsBuilder; 6 | import io.jenkins.plugins.checks.api.ChecksStatus; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.junit.jupiter.api.Test; 9 | import org.kohsuke.github.GHCheckRun.Conclusion; 10 | import org.kohsuke.github.GHCheckRun.Status; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 14 | 15 | class GitHubChecksDetailsTest { 16 | 17 | @Test 18 | void shouldReturnAllGitHubObjectsCorrectly() { 19 | ChecksDetails details = new ChecksDetailsBuilder() 20 | .withName("checks") 21 | .withStatus(ChecksStatus.COMPLETED) 22 | .withConclusion(ChecksConclusion.SUCCESS) 23 | .withDetailsURL("https://ci.jenkins.io") 24 | .build(); 25 | 26 | GitHubChecksDetails gitHubDetails = new GitHubChecksDetails(details); 27 | assertThat(gitHubDetails.getName()).isEqualTo("checks"); 28 | assertThat(gitHubDetails.getStatus()).isEqualTo(Status.COMPLETED); 29 | assertThat(gitHubDetails.getConclusion()).isPresent().hasValue(Conclusion.SUCCESS); 30 | assertThat(gitHubDetails.getDetailsURL()).isPresent().hasValue("https://ci.jenkins.io"); 31 | } 32 | 33 | @Test 34 | void shouldReturnEmptyWhenDetailsURLIsBlank() { 35 | GitHubChecksDetails gitHubChecksDetails = 36 | new GitHubChecksDetails(new ChecksDetailsBuilder().withDetailsURL(StringUtils.EMPTY).build()); 37 | assertThat(gitHubChecksDetails.getDetailsURL()).isEmpty(); 38 | } 39 | 40 | @Test 41 | void shouldThrowIllegalStateExceptionWhenDetailsURLIsNotHttpOrHttpsScheme() { 42 | GitHubChecksDetails gitHubChecksDetails = 43 | new GitHubChecksDetails(new ChecksDetailsBuilder().withDetailsURL("ci.jenkins.io").build()); 44 | assertThatThrownBy(gitHubChecksDetails::getDetailsURL) 45 | .isInstanceOf(IllegalArgumentException.class) 46 | .hasMessage("The details url is not http or https scheme: ci.jenkins.io"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/config/GitHubSCMSourceChecksTrait.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.config; 2 | 3 | import hudson.Extension; 4 | import jenkins.scm.api.SCMSource; 5 | import jenkins.scm.api.trait.SCMSourceContext; 6 | import jenkins.scm.api.trait.SCMSourceTrait; 7 | import jenkins.scm.api.trait.SCMSourceTraitDescriptor; 8 | import jenkins.scm.impl.trait.Discovery; 9 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 10 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSourceContext; 11 | import org.kohsuke.stapler.DataBoundConstructor; 12 | import org.kohsuke.stapler.DataBoundSetter; 13 | import org.jenkinsci.Symbol; 14 | 15 | /** 16 | * GitHub checks configurations for jobs with {@link GitHubSCMSource}. 17 | */ 18 | @Extension 19 | public class GitHubSCMSourceChecksTrait extends SCMSourceTrait implements GitHubChecksConfig { 20 | private boolean verboseConsoleLog; 21 | 22 | /** 23 | * Constructor for stapler. 24 | */ 25 | @DataBoundConstructor 26 | public GitHubSCMSourceChecksTrait() { 27 | super(); 28 | } 29 | 30 | @DataBoundSetter 31 | public void setVerboseConsoleLog(final boolean verboseConsoleLog) { 32 | this.verboseConsoleLog = verboseConsoleLog; 33 | } 34 | 35 | @Override 36 | public boolean isVerboseConsoleLog() { 37 | return verboseConsoleLog; 38 | } 39 | 40 | /** 41 | * Descriptor implementation for {@link GitHubSCMSourceChecksTrait}. 42 | */ 43 | @Symbol("gitHubSourceChecks") 44 | @Extension 45 | @Discovery 46 | public static class DescriptorImpl extends SCMSourceTraitDescriptor { 47 | /** 48 | * Returns the display name. 49 | * 50 | * @return "Configure GitHub Checks" 51 | */ 52 | @Override 53 | public String getDisplayName() { 54 | return "Configure GitHub Checks"; 55 | } 56 | 57 | /** 58 | * The {@link GitHubSCMSourceChecksTrait} is only applicable to {@link GitHubSCMSourceContext}. 59 | * 60 | * @return {@link GitHubSCMSourceContext}.class 61 | */ 62 | @Override 63 | public Class getContextClass() { 64 | return GitHubSCMSourceContext.class; 65 | } 66 | 67 | /** 68 | * The {@link GitHubSCMSourceChecksTrait} is only applicable to {@link GitHubSCMSource}. 69 | * 70 | * @return {@link GitHubSCMSource}.class 71 | */ 72 | @Override 73 | public Class getSourceClass() { 74 | return GitHubSCMSource.class; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/resources/mappings/create-check-run.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/repos/XiongKezhi/Sandbox/check-runs", 4 | "method": "POST", 5 | "headers": { 6 | "Accept": { 7 | "equalTo": "application/vnd.github.antiope-preview+json" 8 | } 9 | }, 10 | "bodyPatterns": [ 11 | { 12 | "equalToJson": "{\"conclusion\":\"success\",\"output\":{\"title\":\"Jenkins Check\",\"summary\":\"# A Successful Build\",\"text\":\"## 0 Failures\",\"annotations\":[{\"path\":\"Jenkinsfile\",\"start_line\":1,\"end_line\":1,\"annotation_level\":\"notice\",\"message\":\"say hello to Jenkins\",\"start_column\":0,\"end_column\":20,\"title\":\"Hello Jenkins\",\"raw_details\":\"a simple echo command\"},{\"path\":\"Jenkinsfile\",\"start_line\":2,\"end_line\":2,\"annotation_level\":\"warning\",\"message\":\"say hello to GitHub Checks API\",\"start_column\":0,\"end_column\":30,\"title\":\"Hello GitHub Checks API\",\"raw_details\":\"a simple echo command\"}],\"images\":[{\"alt\":\"Jenkins\",\"image_url\":\"https://ci.jenkins.io/static/cd5757a8/images/jenkins-header-logo-v2.svg\",\"caption\":\"Jenkins Symbol\"}]},\"completed_at\":\"1970-01-12T13:46:39Z\",\"name\":\"Jenkins\",\"started_at\":\"1970-01-12T13:46:39Z\",\"details_url\":\"https://ci.jenkins.io\",\"actions\":[{\"label\":\"re-run\",\"description\":\"re-run Jenkins build\",\"identifier\":\"#0\"}],\"head_sha\":\"18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f\",\"status\":\"completed\"}", 13 | "ignoreArrayOrder": true, 14 | "ignoreExtraElements": true 15 | } 16 | ] 17 | }, 18 | "response": { 19 | "status": 201, 20 | "bodyFileName": "create-check-run-response.json", 21 | "headers": { 22 | "Server": "GitHub.com", 23 | "Date": "Fri, 26 Jun 2020 15:25:15 GMT", 24 | "Content-Type": "application/json; charset=utf-8", 25 | "Content-Length": "2550", 26 | "Status": "201 Created", 27 | "X-RateLimit-Limit": "5000", 28 | "X-RateLimit-Remaining": "4981", 29 | "X-RateLimit-Reset": "1593186146", 30 | "Cache-Control": "private, max-age=60, s-maxage=60", 31 | "Vary": [ 32 | "Accept, Authorization, Cookie, X-GitHub-OTP", 33 | "Accept-Encoding, Accept, X-Requested-With" 34 | ], 35 | "ETag": "\"8a410a0d1b506afde1c545250559eb84\"", 36 | "Location": "https://api.github.com/repos/XiongKezhi/Sandbox/check-runs/811669431", 37 | "X-GitHub-Media-Type": "github.antiope-preview; format=json", 38 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 39 | "X-Frame-Options": "deny", 40 | "X-Content-Type-Options": "nosniff", 41 | "X-XSS-Protection": "1; mode=block", 42 | "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 43 | "Content-Security-Policy": "default-src 'none'", 44 | "X-GitHub-Request-Id": "C4F3:6AE9:9EFFF5:112C58C:5EF6135A" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/status/GitHubStatusChecksProperties.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | import java.util.Optional; 4 | import java.util.stream.Stream; 5 | 6 | import edu.hm.hafner.util.VisibleForTesting; 7 | 8 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 9 | import hudson.Extension; 10 | import hudson.model.Job; 11 | import hudson.plugins.git.GitSCM; 12 | import jenkins.plugins.git.GitSCMSource; 13 | 14 | import io.jenkins.plugins.checks.github.SCMFacade; 15 | import io.jenkins.plugins.checks.status.AbstractStatusChecksProperties; 16 | 17 | /** 18 | * Implementing {@link io.jenkins.plugins.checks.status.AbstractStatusChecksProperties} to retrieve properties 19 | * from jobs with {@link GitHubSCMSource}, {@link GitSCM}, or {@link GitSCMSource}. 20 | */ 21 | @Extension 22 | public class GitHubStatusChecksProperties extends AbstractStatusChecksProperties { 23 | private static final GitHubStatusChecksConfigurations DEFAULT_CONFIGURATION 24 | = new DefaultGitHubStatusChecksConfigurations(); 25 | 26 | private final SCMFacade scmFacade; 27 | 28 | /** 29 | * Default Constructor. 30 | */ 31 | public GitHubStatusChecksProperties() { 32 | this(new SCMFacade()); 33 | } 34 | 35 | @VisibleForTesting 36 | GitHubStatusChecksProperties(final SCMFacade facade) { 37 | super(); 38 | 39 | this.scmFacade = facade; 40 | } 41 | 42 | @Override 43 | public boolean isApplicable(final Job job) { 44 | return scmFacade.findGitHubSCMSource(job).isPresent() || scmFacade.findGitSCM(job).isPresent(); 45 | } 46 | 47 | @Override 48 | public String getName(final Job job) { 49 | return getConfigurations(job).orElse(DEFAULT_CONFIGURATION).getName(); 50 | } 51 | 52 | @Override 53 | public boolean isSkipped(final Job job) { 54 | return getConfigurations(job).orElse(DEFAULT_CONFIGURATION).isSkip(); 55 | } 56 | 57 | @Override 58 | public boolean isUnstableBuildNeutral(final Job job) { 59 | return getConfigurations(job).orElse(DEFAULT_CONFIGURATION).isUnstableBuildNeutral(); 60 | } 61 | 62 | @Override 63 | public boolean isSuppressLogs(final Job job) { 64 | return getConfigurations(job).orElse(DEFAULT_CONFIGURATION).isSuppressLogs(); 65 | } 66 | 67 | @Override 68 | public boolean isSkipProgressUpdates(final Job job) { 69 | return getConfigurations(job).orElse(DEFAULT_CONFIGURATION).isSkipProgressUpdates(); 70 | } 71 | 72 | private Optional getConfigurations(final Job job) { 73 | Optional gitHubSCMSource = scmFacade.findGitHubSCMSource(job); 74 | if (gitHubSCMSource.isPresent()) { 75 | return getConfigurations(gitHubSCMSource.get().getTraits().stream()); 76 | } 77 | 78 | Optional gitSCM = scmFacade.findGitSCM(job); 79 | if (gitSCM.isPresent()) { 80 | return getConfigurations(gitSCM.get().getExtensions().stream()); 81 | } 82 | 83 | return Optional.empty(); 84 | } 85 | 86 | private Optional getConfigurations(final Stream stream) { 87 | return stream.filter(t -> t instanceof GitHubStatusChecksConfigurations) 88 | .findFirst() 89 | .map(t -> (GitHubStatusChecksConfigurations) t); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/resources/__files/create-check-run-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 811909212, 3 | "node_id": "MDg6Q2hlY2tSdW44MTE5MDkyMTI=", 4 | "head_sha": "18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f", 5 | "external_id": "", 6 | "url": "https://api.github.com/repos/XiongKezhi/Sandbox/check-runs/811909212", 7 | "html_url": "https://github.com/XiongKezhi/Sandbox/runs/811909212", 8 | "details_url": "https://ci.jenkins.io", 9 | "status": "completed", 10 | "conclusion": "success", 11 | "started_at": "1970-01-12T13:46:39Z", 12 | "completed_at": "1970-01-12T13:46:39Z", 13 | "output": { 14 | "title": "Jenkins Check", 15 | "summary": "# A Successful Build", 16 | "text": "## 0 Failures", 17 | "annotations_count": 2, 18 | "annotations_url": "https://api.github.com/repos/XiongKezhi/Sandbox/check-runs/811909212/annotations" 19 | }, 20 | "name": "Jenkins", 21 | "check_suite": { 22 | "id": 696648086 23 | }, 24 | "app": { 25 | "id": 55021, 26 | "slug": "jenkins-checks-api", 27 | "node_id": "MDM6QXBwNTUwMjE=", 28 | "owner": { 29 | "login": "XiongKezhi", 30 | "id": 30348893, 31 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 32 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 33 | "gravatar_id": "", 34 | "url": "https://api.github.com/users/XiongKezhi", 35 | "html_url": "https://github.com/XiongKezhi", 36 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 37 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 38 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 39 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 40 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 41 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 42 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 43 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 44 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 45 | "type": "User", 46 | "site_admin": false 47 | }, 48 | "name": "Jenkins Checks API", 49 | "description": "Integrate Jenkins as GitHub APP in order to create checks", 50 | "external_url": "https://smee.io/Bpa9LQrfHixLSOM7", 51 | "html_url": "https://github.com/apps/jenkins-checks-api", 52 | "created_at": "2020-02-22T08:15:07Z", 53 | "updated_at": "2020-05-19T13:55:16Z", 54 | "permissions": { 55 | "checks": "write", 56 | "contents": "write", 57 | "metadata": "read" 58 | }, 59 | "events": [ 60 | "check_run", 61 | "check_suite" 62 | ] 63 | }, 64 | "pull_requests": [ 65 | { 66 | "url": "https://api.github.com/repos/XiongKezhi/Sandbox/pulls/1", 67 | "id": 420202204, 68 | "number": 1, 69 | "head": { 70 | "ref": "checks-api", 71 | "sha": "18c8e2fd86e7aa3748e279c14a00dc3f0b963e7f", 72 | "repo": { 73 | "id": 265261970, 74 | "url": "https://api.github.com/repos/XiongKezhi/Sandbox", 75 | "name": "Sandbox" 76 | } 77 | }, 78 | "base": { 79 | "ref": "master", 80 | "sha": "d0a551f39d079ed617db3702cadb40dfe3d37e1b", 81 | "repo": { 82 | "id": 265261970, 83 | "url": "https://api.github.com/repos/XiongKezhi/Sandbox", 84 | "name": "Sandbox" 85 | } 86 | } 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/status/GitSCMStatusChecksExtension.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | import org.kohsuke.stapler.DataBoundSetter; 7 | import org.kohsuke.stapler.QueryParameter; 8 | import hudson.Extension; 9 | import hudson.plugins.git.extensions.GitSCMExtension; 10 | import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; 11 | import hudson.util.FormValidation; 12 | 13 | import io.jenkins.plugins.checks.status.AbstractStatusChecksProperties; 14 | 15 | /** 16 | * Git Extension that controls {@link AbstractStatusChecksProperties} for freestyle jobs using {@link hudson.plugins.git.GitSCM}. 17 | */ 18 | @SuppressWarnings("PMD.DataClass") 19 | public class GitSCMStatusChecksExtension extends GitSCMExtension implements GitHubStatusChecksConfigurations { 20 | private boolean skip = false; 21 | private boolean unstableBuildNeutral = false; 22 | private String name = "Jenkins"; 23 | private boolean suppressLogs; 24 | private boolean skipProgressUpdates = false; 25 | 26 | /** 27 | * Constructor for stapler. 28 | */ 29 | @DataBoundConstructor 30 | public GitSCMStatusChecksExtension() { 31 | super(); 32 | } 33 | 34 | @Override 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | @Override 40 | public boolean isSkip() { 41 | return skip; 42 | } 43 | 44 | @Override 45 | public boolean isUnstableBuildNeutral() { 46 | return unstableBuildNeutral; 47 | } 48 | 49 | @Override 50 | public boolean isSuppressLogs() { 51 | return suppressLogs; 52 | } 53 | 54 | @Override 55 | public boolean isSkipProgressUpdates() { 56 | return skipProgressUpdates; 57 | } 58 | 59 | @DataBoundSetter 60 | public void setSkipProgressUpdates(final boolean skipProgressUpdates) { 61 | this.skipProgressUpdates = skipProgressUpdates; 62 | } 63 | 64 | /** 65 | * Set the name of the status checks. 66 | * 67 | * @param name 68 | * name of the checks 69 | */ 70 | @DataBoundSetter 71 | public void setName(final String name) { 72 | this.name = name; 73 | } 74 | 75 | /** 76 | * Set if skip publishing status checks. 77 | * 78 | * @param skip 79 | * true if skip 80 | */ 81 | @DataBoundSetter 82 | public void setSkip(final boolean skip) { 83 | this.skip = skip; 84 | } 85 | 86 | @DataBoundSetter 87 | public void setUnstableBuildNeutral(final boolean unstableBuildNeutral) { 88 | this.unstableBuildNeutral = unstableBuildNeutral; 89 | } 90 | 91 | @DataBoundSetter 92 | public void setSuppressLogs(final boolean suppressLogs) { 93 | this.suppressLogs = suppressLogs; 94 | } 95 | 96 | /** 97 | * Descriptor implementation for {@link GitSCMStatusChecksExtension}. 98 | */ 99 | @Extension 100 | public static class DescriptorImpl extends GitSCMExtensionDescriptor { 101 | /** 102 | * Returns the display name. 103 | * 104 | * @return "Status Checks Properties" 105 | */ 106 | @Override 107 | public String getDisplayName() { 108 | return "Status Checks Properties"; 109 | } 110 | 111 | /** 112 | * Checks if the name of status checks is valid. 113 | * 114 | * @param name 115 | * name of status checks 116 | * @return ok if the name is not empty 117 | */ 118 | public FormValidation doCheckName(@QueryParameter final String name) { 119 | if (StringUtils.isBlank(name)) { 120 | return FormValidation.error("Name should not be empty!"); 121 | } 122 | 123 | return FormValidation.ok(); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/GitSCMChecksContextITest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import hudson.model.Action; 4 | import hudson.model.FreeStyleProject; 5 | import hudson.model.Result; 6 | import hudson.model.Run; 7 | import hudson.plugins.git.BranchSpec; 8 | import hudson.plugins.git.GitSCM; 9 | import jenkins.model.ParameterizedJobMixIn; 10 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 11 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 12 | import org.junit.jupiter.api.Test; 13 | import org.jvnet.hudson.test.JenkinsRule; 14 | import org.jvnet.hudson.test.junit.jupiter.WithJenkins; 15 | 16 | import java.util.Collections; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | /** 21 | * Integration tests for {@link GitSCMChecksContext}. 22 | */ 23 | @WithJenkins 24 | class GitSCMChecksContextITest { 25 | private static final String EXISTING_HASH = "4ecc8623b06d99d5f029b66927438554fdd6a467"; 26 | private static final String HTTP_URL = "https://github.com/jenkinsci/github-checks-plugin.git"; 27 | private static final String CREDENTIALS_ID = "credentials"; 28 | private static final String URL_NAME = "url"; 29 | 30 | /** 31 | * Creates a FreeStyle job that uses {@link hudson.plugins.git.GitSCM} and runs a successful build. 32 | * Then this build is used to create a new {@link GitSCMChecksContext}. So the build actually is not publishing 33 | * the checks we just ensure that we can create the context with the successful build (otherwise we would need 34 | * Wiremock to handle the requests to GitHub). 35 | */ 36 | @Test 37 | void shouldRetrieveContextFromFreeStyleBuild(JenkinsRule j) throws Exception { 38 | FreeStyleProject job = j.createFreeStyleProject(); 39 | 40 | BranchSpec branchSpec = new BranchSpec(EXISTING_HASH); 41 | GitSCM scm = new GitSCM(GitSCM.createRepoList(HTTP_URL, CREDENTIALS_ID), 42 | Collections.singletonList(branchSpec), 43 | null, null, Collections.emptyList()); 44 | job.setScm(scm); 45 | 46 | Run run = buildSuccessfully(j, job); 47 | 48 | GitSCMChecksContext gitSCMChecksContext = new GitSCMChecksContext(run, URL_NAME); 49 | 50 | assertThat(gitSCMChecksContext.getRepository()).isEqualTo("jenkinsci/github-checks-plugin"); 51 | assertThat(gitSCMChecksContext.getHeadSha()).isEqualTo(EXISTING_HASH); 52 | assertThat(gitSCMChecksContext.getCredentialsId()).isEqualTo(CREDENTIALS_ID); 53 | } 54 | 55 | private Run buildSuccessfully(JenkinsRule j, ParameterizedJobMixIn.ParameterizedJob job) throws Exception { 56 | return j.assertBuildStatus(Result.SUCCESS, job.scheduleBuild2(0, new Action[0])); 57 | } 58 | 59 | /** 60 | * Creates a pipeline that uses {@link hudson.plugins.git.GitSCM} and runs a successful build. 61 | * Then this build is used to create a new {@link GitSCMChecksContext}. 62 | */ 63 | @Test 64 | void shouldRetrieveContextFromPipeline(JenkinsRule j) throws Exception { 65 | WorkflowJob job = j.createProject(WorkflowJob.class); 66 | 67 | job.setDefinition(new CpsFlowDefinition("node {\n" 68 | + " stage ('Checkout') {\n" 69 | + " checkout scm: ([\n" 70 | + " $class: 'GitSCM',\n" 71 | + " userRemoteConfigs: [[credentialsId: '" + CREDENTIALS_ID + "', url: '" + HTTP_URL + "']],\n" 72 | + " branches: [[name: '" + EXISTING_HASH + "']]\n" 73 | + " ])" 74 | + " }\n" 75 | + "}\n", true)); 76 | 77 | Run run = buildSuccessfully(j, job); 78 | 79 | GitSCMChecksContext gitSCMChecksContext = new GitSCMChecksContext(run, URL_NAME); 80 | 81 | assertThat(gitSCMChecksContext.getRepository()).isEqualTo("jenkinsci/github-checks-plugin"); 82 | assertThat(gitSCMChecksContext.getCredentialsId()).isEqualTo(CREDENTIALS_ID); 83 | assertThat(gitSCMChecksContext.getHeadSha()).isEqualTo(EXISTING_HASH); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import edu.hm.hafner.util.FilteredLog; 4 | import edu.hm.hafner.util.VisibleForTesting; 5 | import hudson.Extension; 6 | import hudson.model.Job; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import hudson.plugins.git.GitSCM; 10 | import io.jenkins.plugins.checks.api.ChecksPublisher; 11 | import io.jenkins.plugins.checks.api.ChecksPublisherFactory; 12 | import io.jenkins.plugins.checks.github.config.DefaultGitHubChecksConfig; 13 | import io.jenkins.plugins.checks.github.config.GitHubChecksConfig; 14 | import io.jenkins.plugins.util.PluginLogger; 15 | import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; 16 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 17 | 18 | import java.util.Optional; 19 | import java.util.stream.Stream; 20 | 21 | /** 22 | * An factory which produces {@link GitHubChecksPublisher}. 23 | */ 24 | @Extension 25 | public class GitHubChecksPublisherFactory extends ChecksPublisherFactory { 26 | private final SCMFacade scmFacade; 27 | private final DisplayURLProvider urlProvider; 28 | 29 | /** 30 | * Creates a new instance of {@link GitHubChecksPublisherFactory}. 31 | */ 32 | public GitHubChecksPublisherFactory() { 33 | this(new SCMFacade(), DisplayURLProvider.get()); 34 | } 35 | 36 | @VisibleForTesting 37 | GitHubChecksPublisherFactory(final SCMFacade scmFacade, final DisplayURLProvider urlProvider) { 38 | super(); 39 | 40 | this.scmFacade = scmFacade; 41 | this.urlProvider = urlProvider; 42 | } 43 | 44 | @Override 45 | protected Optional createPublisher(final Run run, final TaskListener listener) { 46 | final String runURL = urlProvider.getRunURL(run); 47 | return createPublisher(listener, getChecksConfig(run.getParent()), 48 | GitHubSCMSourceChecksContext.fromRun(run, runURL, scmFacade), 49 | new GitSCMChecksContext(run, runURL, scmFacade)); 50 | } 51 | 52 | @Override 53 | protected Optional createPublisher(final Job job, final TaskListener listener) { 54 | return createPublisher(listener, getChecksConfig(job), 55 | GitHubSCMSourceChecksContext.fromJob(job, urlProvider.getJobURL(job), scmFacade)); 56 | } 57 | 58 | private Optional createPublisher(final TaskListener listener, final GitHubChecksConfig config, 59 | final GitHubChecksContext... contexts) { 60 | FilteredLog causeLogger = new FilteredLog("Causes for no suitable publisher found: "); 61 | PluginLogger consoleLogger = new PluginLogger(listener.getLogger(), "GitHub Checks"); 62 | 63 | for (GitHubChecksContext ctx : contexts) { 64 | if (ctx.isValid(causeLogger)) { 65 | return Optional.of(new GitHubChecksPublisher(ctx, consoleLogger)); 66 | } 67 | } 68 | 69 | if (config.isVerboseConsoleLog()) { 70 | consoleLogger.logEachLine(causeLogger.getErrorMessages()); 71 | } 72 | 73 | return Optional.empty(); 74 | } 75 | 76 | private GitHubChecksConfig getChecksConfig(final Job job) { 77 | Optional gitHubSCMSource = scmFacade.findGitHubSCMSource(job); 78 | if (gitHubSCMSource.isPresent()) { 79 | return getChecksConfig(gitHubSCMSource.get().getTraits().stream()) 80 | .orElseGet(DefaultGitHubChecksConfig::new); 81 | } 82 | 83 | Optional gitSCM = scmFacade.findGitSCM(job); 84 | return gitSCM.map(scm -> getChecksConfig(scm.getExtensions().stream()) 85 | .orElse(new DefaultGitHubChecksConfig())) 86 | .orElseGet(DefaultGitHubChecksConfig::new); 87 | 88 | } 89 | 90 | private Optional getChecksConfig(final Stream stream) { 91 | return stream.filter(t -> t instanceof GitHubChecksConfig) 92 | .findFirst() 93 | .map(t -> (GitHubChecksConfig) t); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Checks API Plugin 2 | 3 | [![Join the chat at https://gitter.im/jenkinsci/github-checks-api](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jenkinsci/github-checks-api) 4 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/XiongKezhi/checks-api-plugin/issues) 5 | [![Jenkins](https://ci.jenkins.io/job/Plugins/job/github-checks-plugin/job/master/badge/icon?subject=Jenkins%20CI)](https://ci.jenkins.io/job/Plugins/job/github-checks-plugin/job/master/) 6 | [![GitHub Actions](https://github.com/jenkinsci/github-checks-plugin/workflows/CI/badge.svg?branch=master)](https://github.com/jenkinsci/github-checks-plugin/actions) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2c7fa67496a743778ca60cc9604212d2)](https://www.codacy.com/gh/jenkinsci/github-checks-plugin?utm_source=github.com&utm_medium=referral&utm_content=jenkinsci/github-checks-plugin&utm_campaign=Badge_Grade) 8 | [![codecov](https://codecov.io/gh/jenkinsci/github-checks-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/jenkinsci/github-checks-plugin) 9 | 10 | ![GitHub Checks Plugin Cover](docs/images/github-checks-plugin-cover.png) 11 | 12 | This plugin publishes checks to GitHub through [GitHub Checks API](https://docs.github.com/en/rest/reference/checks#runs). 13 | It implements the extension points defined in [Checks API Plugin](https://github.com/jenkinsci/checks-api-plugin). 14 | 15 | This plugin has been installed, along with the [General API Plugin](https://github.com/jenkinsci/checks-api-plugin) on [ci.jenkins.io](https://ci.jenkins.io/Plugins) to help maintain over 1500 Jenkins plugins. You can take a look at the [action](https://github.com/jenkinsci/github-checks-plugin/runs/1025018883) for this repository or other plugin repositories under [Jenkins organization](https://github.com/jenkinsci) for the results. 16 | 17 | - [Features](#features) 18 | - [Build Status Check](#build-status-check) 19 | - [Rerun Failed Build](#rerun-failed-build) 20 | - [Contributing](#contributing) 21 | - [Acknowledgements](#acknowledgements) 22 | - [LICENSE](#license) 23 | 24 | ## Getting started 25 | 26 | Only GitHub Apps with proper permissions can publish checks, this [guide](https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc) helps you authenticate your Jenkins instance as a GitHub App. 27 | The permission *read/write* on *Checks* needs to be granted in addition to the ones already mentioned in the guide. 28 | 29 | ## Features 30 | 31 | ### Build Status Check 32 | 33 | ![GitHub Status](docs/images/github-status.png) 34 | 35 | This plugin implements [the status checks feature from Checks API Plugin](https://github.com/jenkinsci/checks-api-plugin#build-status-check) to publish statuses (pending, in progress, and completed) to GitHub. 36 | 37 | You can customize it by configuring the "Status Checks Properties" behavior for your GitHub SCM Source or Git SCM projects: 38 | 39 | ![Status Checks Properties](docs/images/status-checks-properties.png) 40 | 41 | *Note: If you are using [GitHub Branch Source Plugin](https://github.com/jenkinsci/github-branch-source-plugin), it will also send status notifications to GitHub through [Status API](https://docs.github.com/en/rest/reference/repos#statuses). You can disable those notifications by configuring Skip GitHub Branch Source notifications option.* 42 | 43 | ### Rerun Failed Build 44 | 45 | ![Failed Checks](docs/images/failed-checks.png) 46 | 47 | If your Jenkins build failed, a failed check will be published here. 48 | A "Re-run" button will be added automatically by GitHub, by clicking it, you can schedule a new build for the **last** commit of this branch. 49 | 50 | ### Configuration 51 | 52 | ![Checks Config](docs/images/github-checks-config.png) 53 | 54 | - *Verbose Console Log* : check for verbose build console log, the default is false 55 | 56 | ## Contributing 57 | 58 | Refer to our [contribution guidelines](https://github.com/jenkinsci/.github/blob/master/CONTRIBUTING.md) 59 | 60 | ## Acknowledgements 61 | 62 | This plugin was started as a [Google Summer of Code 2020 project](https://www.jenkins.io/projects/gsoc/2020/projects/github-checks/), special thanks to the support from [Jenkins GSoC SIG](https://www.jenkins.io/sigs/gsoc/) and the entire community. 63 | 64 | ## LICENSE 65 | 66 | Licensed under MIT, see [LICENSE](LICENSE) 67 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 5.2098.v4d48a_c4c68e7 9 | 10 | 11 | 12 | io.jenkins.plugins 13 | github-checks 14 | ${changelist} 15 | hpi 16 | 17 | GitHub Checks plugin 18 | 19 | 20 | 999999-SNAPSHOT 21 | 22 | 2.504 23 | ${jenkins.baseline}.3 24 | false 25 | 26 | 27 | 28 | 29 | MIT License 30 | https://opensource.org/licenses/MIT 31 | 32 | 33 | 34 | https://github.com/jenkinsci/${project.artifactId}-plugin 35 | 36 | scm:git:https://github.com/jenkinsci/${project.artifactId}-plugin.git 37 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 38 | https://github.com/jenkinsci/${project.artifactId}-plugin 39 | ${scmTag} 40 | 41 | 42 | 43 | 44 | 45 | io.jenkins.tools.bom 46 | bom-${jenkins.baseline}.x 47 | 5804.v80587a_38d937 48 | import 49 | pom 50 | 51 | 52 | 53 | org.jenkins-ci.plugins 54 | github-branch-source 55 | 1917.v9ee8a_39b_3d0d 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | io.jenkins.plugins 64 | checks-api 65 | 66 | 67 | org.jenkins-ci.plugins 68 | github-branch-source 69 | 70 | 71 | org.jenkins-ci.plugins 72 | github-api 73 | 74 | 75 | org.jenkins-ci.plugins.workflow 76 | workflow-cps 77 | 78 | 79 | org.jenkins-ci.plugins.workflow 80 | workflow-job 81 | 82 | 83 | 84 | org.mockito 85 | mockito-core 86 | test 87 | 88 | 89 | org.assertj 90 | assertj-core 91 | 3.27.6 92 | test 93 | 94 | 95 | org.wiremock 96 | wiremock-standalone 97 | 3.13.2 98 | test 99 | 100 | 101 | org.jenkins-ci.plugins.workflow 102 | workflow-durable-task-step 103 | test 104 | 105 | 106 | org.jenkins-ci.plugins 107 | pipeline-stage-step 108 | test 109 | 110 | 111 | 112 | 113 | 114 | repo.jenkins-ci.org 115 | https://repo.jenkins-ci.org/public/ 116 | 117 | 118 | 119 | 120 | repo.jenkins-ci.org 121 | https://repo.jenkins-ci.org/public/ 122 | 123 | 124 | 125 | 126 | 127 | xiongkezhi 128 | Kezhi Xiong 129 | august.xkz@gmail.com 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/status/GitHubStatusChecksPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | import hudson.model.Job; 4 | import hudson.plugins.git.GitSCM; 5 | import hudson.util.DescribableList; 6 | import io.jenkins.plugins.checks.github.SCMFacade; 7 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Collections; 11 | import java.util.Optional; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.when; 16 | 17 | class GitHubStatusChecksPropertiesTest { 18 | 19 | @Test 20 | void shouldUsePropertiesFromGitHubSCMSourceTrait() { 21 | Job job = mock(Job.class); 22 | SCMFacade scmFacade = mock(SCMFacade.class); 23 | GitHubSCMSource source = mock(GitHubSCMSource.class); 24 | GitHubSCMSourceStatusChecksTrait trait = new GitHubSCMSourceStatusChecksTrait(); 25 | 26 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); 27 | when(source.getTraits()).thenReturn(Collections.singletonList(trait)); 28 | 29 | trait.setName("GitHub SCM Source"); 30 | trait.setSkip(true); 31 | trait.setUnstableBuildNeutral(true); 32 | trait.setSuppressLogs(true); 33 | 34 | assertJobWithStatusChecksProperties(job, new GitHubStatusChecksProperties(scmFacade), 35 | true, "GitHub SCM Source", true, true, true); 36 | } 37 | 38 | @Test 39 | void shouldUsePropertiesFromGitSCMExtension() { 40 | Job job = mock(Job.class); 41 | SCMFacade scmFacade = mock(SCMFacade.class); 42 | GitSCM scm = mock(GitSCM.class); 43 | GitSCMStatusChecksExtension extension = new GitSCMStatusChecksExtension(); 44 | DescribableList extensionList = new DescribableList(null, Collections.singletonList(extension)); 45 | 46 | when(scmFacade.findGitSCM(job)).thenReturn(Optional.of(scm)); 47 | when(scm.getExtensions()).thenReturn(extensionList); 48 | 49 | extension.setName("Git SCM"); 50 | extension.setSkip(true); 51 | extension.setUnstableBuildNeutral(true); 52 | extension.setSuppressLogs(true); 53 | 54 | assertJobWithStatusChecksProperties(job, new GitHubStatusChecksProperties(scmFacade), 55 | true, "Git SCM", true, true, true); 56 | } 57 | 58 | @Test 59 | void shouldUseDefaultPropertiesWhenGitHubSCMSourceStatusChecksTraitIsNotAdded() { 60 | Job job = mock(Job.class); 61 | SCMFacade scmFacade = mock(SCMFacade.class); 62 | GitHubSCMSource source = mock(GitHubSCMSource.class); 63 | 64 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); 65 | 66 | assertJobWithStatusChecksProperties(job, new GitHubStatusChecksProperties(scmFacade), 67 | true, "Jenkins", false, false, false); 68 | } 69 | 70 | @Test 71 | void shouldUseDefaultPropertiesWhenGitSCMStatusChecksExtensionIsNotAdded() { 72 | Job job = mock(Job.class); 73 | SCMFacade scmFacade = mock(SCMFacade.class); 74 | GitSCM scm = mock(GitSCM.class); 75 | DescribableList extensionList = new DescribableList(null, Collections.emptyList()); 76 | 77 | when(scmFacade.findGitSCM(job)).thenReturn(Optional.of(scm)); 78 | when(scm.getExtensions()).thenReturn(extensionList); 79 | 80 | assertJobWithStatusChecksProperties(job, new GitHubStatusChecksProperties(scmFacade), 81 | true, "Jenkins", false, false, false); 82 | } 83 | 84 | @Test 85 | void shouldNotApplicableToJobWithoutSupportedSCM() { 86 | Job job = mock(Job.class); 87 | SCMFacade scmFacade = mock(SCMFacade.class); 88 | 89 | when(scmFacade.findGitSCM(job)).thenReturn(Optional.empty()); 90 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.empty()); 91 | assertJobWithStatusChecksProperties(job, new GitHubStatusChecksProperties(scmFacade), 92 | false, "Jenkins", false, false, false); 93 | } 94 | 95 | private static void assertJobWithStatusChecksProperties(final Job job, final GitHubStatusChecksProperties properties, 96 | final boolean isApplicable, final String name, 97 | final boolean isSkip, final boolean isUnstableBuildNeutral, 98 | final boolean isSuppressLogs) { 99 | assertThat(properties.isApplicable(job)).isEqualTo(isApplicable); 100 | assertThat(properties.getName(job)).isEqualTo(name); 101 | assertThat(properties.isSkipped(job)).isEqualTo(isSkip); 102 | assertThat(properties.isUnstableBuildNeutral(job)).isEqualTo(isUnstableBuildNeutral); 103 | assertThat(properties.isSuppressLogs(job)).isEqualTo(isSuppressLogs); 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubChecksContext.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import java.util.Optional; 4 | 5 | import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | import edu.hm.hafner.util.FilteredLog; 9 | import edu.umd.cs.findbugs.annotations.CheckForNull; 10 | 11 | import hudson.model.Job; 12 | import hudson.model.Run; 13 | 14 | /** 15 | * Base class for a context that publishes GitHub checks. 16 | */ 17 | public abstract class GitHubChecksContext { 18 | private final Job job; 19 | private final String url; 20 | private final SCMFacade scmFacade; 21 | 22 | protected GitHubChecksContext(final Job job, final String url, final SCMFacade scmFacade) { 23 | this.job = job; 24 | this.url = url; 25 | this.scmFacade = scmFacade; 26 | } 27 | 28 | /** 29 | * Returns the commit sha of the run. 30 | * 31 | * @return the commit sha of the run 32 | */ 33 | public abstract String getHeadSha(); 34 | 35 | /** 36 | * Returns the source repository's full name of the run. The full name consists of the owner's name and the 37 | * repository's name, e.g. jenkins-ci/jenkins 38 | * 39 | * @return the source repository's full name 40 | */ 41 | public abstract String getRepository(); 42 | 43 | /** 44 | * Returns whether the context is valid (with all properties functional) to use. 45 | * 46 | * @param logger 47 | * the filtered logger 48 | * @return whether the context is valid to use 49 | */ 50 | public abstract boolean isValid(FilteredLog logger); 51 | 52 | @CheckForNull 53 | protected abstract String getCredentialsId(); 54 | 55 | /** 56 | * Returns the credentials to access the remote GitHub repository. 57 | * 58 | * @return the credentials 59 | */ 60 | public StandardUsernameCredentials getCredentials() { 61 | return getGitHubAppCredentials(StringUtils.defaultIfEmpty(getCredentialsId(), "")); 62 | } 63 | 64 | /** 65 | * Returns the URL of the run's summary page, e.g. https://ci.jenkins.io/job/Core/job/jenkins/job/master/2000/. 66 | * 67 | * @return the URL of the summary page 68 | */ 69 | public String getURL() { 70 | return url; 71 | } 72 | 73 | protected Job getJob() { 74 | return job; 75 | } 76 | 77 | protected final SCMFacade getScmFacade() { 78 | return scmFacade; 79 | } 80 | 81 | protected StandardUsernameCredentials getGitHubAppCredentials(final String credentialsId) { 82 | return findGitHubAppCredentials(credentialsId).orElseThrow(() -> 83 | new IllegalStateException("No GitHub APP credentials available for job: " + getJob().getName())); 84 | } 85 | 86 | protected boolean hasGitHubAppCredentials() { 87 | return findGitHubAppCredentials(StringUtils.defaultIfEmpty(getCredentialsId(), "")).isPresent(); 88 | } 89 | 90 | protected boolean hasCredentialsId() { 91 | return StringUtils.isNoneBlank(getCredentialsId()); 92 | } 93 | 94 | protected boolean hasValidCredentials(final FilteredLog logger) { 95 | if (!hasCredentialsId()) { 96 | logger.logError("No credentials found"); 97 | 98 | return false; 99 | } 100 | 101 | if (!hasGitHubAppCredentials()) { 102 | logger.logError("No GitHub app credentials found: '%s'", getCredentialsId()); 103 | logger.logError("See: https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc"); 104 | 105 | return false; 106 | } 107 | 108 | return true; 109 | } 110 | 111 | private Optional findGitHubAppCredentials(final String credentialsId) { 112 | return getScmFacade().findGitHubAppCredentials(getJob(), credentialsId); 113 | } 114 | 115 | /** 116 | * Returns the id of a {@link GitHubChecksAction} for this run, if any. 117 | * 118 | * @param name 119 | * the name of the check 120 | * @return the id of the check run 121 | */ 122 | public Optional getId(final String name) { 123 | return getAction(name).map(GitHubChecksAction::getId); 124 | } 125 | 126 | protected abstract Optional> getRun(); 127 | 128 | private Optional getAction(final String name) { 129 | if (getRun().isEmpty()) { 130 | return Optional.empty(); 131 | } 132 | return getRun().get().getActions(GitHubChecksAction.class) 133 | .stream() 134 | .filter(a -> a.getName().equals(name)) 135 | .findFirst(); 136 | } 137 | 138 | void addActionIfMissing(final long id, final String name) { 139 | if (getRun().isEmpty()) { 140 | return; 141 | } 142 | Optional action = getAction(name); 143 | if (action.isEmpty()) { 144 | getRun().get().addAction(new GitHubChecksAction(id, name)); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContext.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import edu.hm.hafner.util.FilteredLog; 4 | import edu.umd.cs.findbugs.annotations.CheckForNull; 5 | import hudson.model.Job; 6 | import hudson.model.Run; 7 | import jenkins.scm.api.SCMHead; 8 | import jenkins.scm.api.SCMRevision; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 11 | 12 | import java.util.Optional; 13 | 14 | /** 15 | * Provides a {@link GitHubChecksContext} for a Jenkins job that uses a supported {@link GitHubSCMSource}. 16 | */ 17 | class GitHubSCMSourceChecksContext extends GitHubChecksContext { 18 | @CheckForNull 19 | private final String sha; 20 | @CheckForNull 21 | private final Run run; 22 | 23 | static GitHubSCMSourceChecksContext fromRun(final Run run, final String runURL, final SCMFacade scmFacade) { 24 | return new GitHubSCMSourceChecksContext(run.getParent(), run, runURL, scmFacade); 25 | } 26 | 27 | static GitHubSCMSourceChecksContext fromJob(final Job job, final String runURL, final SCMFacade scmFacade) { 28 | return new GitHubSCMSourceChecksContext(job, null, runURL, scmFacade); 29 | } 30 | 31 | /** 32 | * Creates a {@link GitHubSCMSourceChecksContext} according to the job and run, if provided. All attributes are computed during this period. 33 | * 34 | * @param job 35 | * a GitHub Branch Source project 36 | * @param run 37 | * a run of a GitHub Branch Source project 38 | * @param runURL 39 | * the URL to the Jenkins run 40 | * @param scmFacade 41 | * a facade for Jenkins SCM 42 | */ 43 | private GitHubSCMSourceChecksContext(final Job job, @CheckForNull final Run run, final String runURL, final SCMFacade scmFacade) { 44 | super(job, runURL, scmFacade); 45 | this.run = run; 46 | this.sha = Optional.ofNullable(run).map(this::resolveHeadSha).orElse(resolveHeadSha(job)); 47 | } 48 | 49 | @Override 50 | public String getHeadSha() { 51 | if (StringUtils.isBlank(sha)) { 52 | throw new IllegalStateException("No SHA found for job: " + getJob().getName()); 53 | } 54 | 55 | return sha; 56 | } 57 | 58 | @Override 59 | public String getRepository() { 60 | GitHubSCMSource source = resolveSource(); 61 | if (source == null) { 62 | throw new IllegalStateException("No GitHub SCM source found for job: " + getJob().getName()); 63 | } 64 | else { 65 | return source.getRepoOwner() + "/" + source.getRepository(); 66 | } 67 | } 68 | 69 | String getOwner() { 70 | return Optional.ofNullable(resolveSource()).map(GitHubSCMSource::getRepoOwner).orElse(null); 71 | } 72 | 73 | @Override 74 | public boolean isValid(final FilteredLog logger) { 75 | logger.logError("Trying to resolve checks parameters from GitHub SCM..."); 76 | 77 | if (resolveSource() == null) { 78 | logger.logError("Job does not use GitHub SCM"); 79 | 80 | return false; 81 | } 82 | 83 | if (!hasValidCredentials(logger)) { 84 | return false; 85 | } 86 | 87 | if (StringUtils.isBlank(sha)) { 88 | logger.logError("No HEAD SHA found for %s", getRepository()); 89 | 90 | return false; 91 | } 92 | 93 | return true; 94 | } 95 | 96 | @Override 97 | protected Optional> getRun() { 98 | return Optional.ofNullable(run); 99 | } 100 | 101 | @Override 102 | @CheckForNull 103 | protected String getCredentialsId() { 104 | GitHubSCMSource source = resolveSource(); 105 | if (source == null) { 106 | return null; 107 | } 108 | 109 | return source.getCredentialsId(); 110 | } 111 | 112 | @CheckForNull 113 | private GitHubSCMSource resolveSource() { 114 | return getScmFacade().findGitHubSCMSource(getJob()).orElse(null); 115 | } 116 | 117 | @CheckForNull 118 | private String resolveHeadSha(final Run theRun) { 119 | GitHubSCMSource source = resolveSource(); 120 | if (source != null) { 121 | Optional revision = getScmFacade().findRevision(source, theRun); 122 | if (revision.isPresent()) { 123 | return getScmFacade().findHash(revision.get()).orElse(null); 124 | } 125 | } 126 | 127 | return null; 128 | } 129 | 130 | @CheckForNull 131 | private String resolveHeadSha(final Job job) { 132 | GitHubSCMSource source = resolveSource(); 133 | Optional head = getScmFacade().findHead(job); 134 | if (source != null && head.isPresent()) { 135 | Optional revision = getScmFacade().findRevision(source, head.get()); 136 | if (revision.isPresent()) { 137 | return getScmFacade().findHash(revision.get()).orElse(null); 138 | } 139 | } 140 | 141 | return null; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/test/resources/__files/get-repo-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 265261970, 3 | "node_id": "MDEwOlJlcG9zaXRvcnkyNjUyNjE5NzA=", 4 | "name": "Sandbox", 5 | "full_name": "XiongKezhi/Sandbox", 6 | "private": false, 7 | "owner": { 8 | "login": "XiongKezhi", 9 | "id": 30348893, 10 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 11 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/XiongKezhi", 14 | "html_url": "https://github.com/XiongKezhi", 15 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 16 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 20 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 21 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 22 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "html_url": "https://github.com/XiongKezhi/Sandbox", 28 | "description": "Sandbox for testing Jenkins Checks API Plugin", 29 | "fork": false, 30 | "url": "https://api.github.com/repos/XiongKezhi/Sandbox", 31 | "forks_url": "https://api.github.com/repos/XiongKezhi/Sandbox/forks", 32 | "keys_url": "https://api.github.com/repos/XiongKezhi/Sandbox/keys{/key_id}", 33 | "collaborators_url": "https://api.github.com/repos/XiongKezhi/Sandbox/collaborators{/collaborator}", 34 | "teams_url": "https://api.github.com/repos/XiongKezhi/Sandbox/teams", 35 | "hooks_url": "https://api.github.com/repos/XiongKezhi/Sandbox/hooks", 36 | "issue_events_url": "https://api.github.com/repos/XiongKezhi/Sandbox/issues/events{/number}", 37 | "events_url": "https://api.github.com/repos/XiongKezhi/Sandbox/events", 38 | "assignees_url": "https://api.github.com/repos/XiongKezhi/Sandbox/assignees{/user}", 39 | "branches_url": "https://api.github.com/repos/XiongKezhi/Sandbox/branches{/branch}", 40 | "tags_url": "https://api.github.com/repos/XiongKezhi/Sandbox/tags", 41 | "blobs_url": "https://api.github.com/repos/XiongKezhi/Sandbox/git/blobs{/sha}", 42 | "git_tags_url": "https://api.github.com/repos/XiongKezhi/Sandbox/git/tags{/sha}", 43 | "git_refs_url": "https://api.github.com/repos/XiongKezhi/Sandbox/git/refs{/sha}", 44 | "trees_url": "https://api.github.com/repos/XiongKezhi/Sandbox/git/trees{/sha}", 45 | "statuses_url": "https://api.github.com/repos/XiongKezhi/Sandbox/statuses/{sha}", 46 | "languages_url": "https://api.github.com/repos/XiongKezhi/Sandbox/languages", 47 | "stargazers_url": "https://api.github.com/repos/XiongKezhi/Sandbox/stargazers", 48 | "contributors_url": "https://api.github.com/repos/XiongKezhi/Sandbox/contributors", 49 | "subscribers_url": "https://api.github.com/repos/XiongKezhi/Sandbox/subscribers", 50 | "subscription_url": "https://api.github.com/repos/XiongKezhi/Sandbox/subscription", 51 | "commits_url": "https://api.github.com/repos/XiongKezhi/Sandbox/commits{/sha}", 52 | "git_commits_url": "https://api.github.com/repos/XiongKezhi/Sandbox/git/commits{/sha}", 53 | "comments_url": "https://api.github.com/repos/XiongKezhi/Sandbox/comments{/number}", 54 | "issue_comment_url": "https://api.github.com/repos/XiongKezhi/Sandbox/issues/comments{/number}", 55 | "contents_url": "https://api.github.com/repos/XiongKezhi/Sandbox/contents/{+path}", 56 | "compare_url": "https://api.github.com/repos/XiongKezhi/Sandbox/compare/{base}...{head}", 57 | "merges_url": "https://api.github.com/repos/XiongKezhi/Sandbox/merges", 58 | "archive_url": "https://api.github.com/repos/XiongKezhi/Sandbox/{archive_format}{/ref}", 59 | "downloads_url": "https://api.github.com/repos/XiongKezhi/Sandbox/downloads", 60 | "issues_url": "https://api.github.com/repos/XiongKezhi/Sandbox/issues{/number}", 61 | "pulls_url": "https://api.github.com/repos/XiongKezhi/Sandbox/pulls{/number}", 62 | "milestones_url": "https://api.github.com/repos/XiongKezhi/Sandbox/milestones{/number}", 63 | "notifications_url": "https://api.github.com/repos/XiongKezhi/Sandbox/notifications{?since,all,participating}", 64 | "labels_url": "https://api.github.com/repos/XiongKezhi/Sandbox/labels{/name}", 65 | "releases_url": "https://api.github.com/repos/XiongKezhi/Sandbox/releases{/id}", 66 | "deployments_url": "https://api.github.com/repos/XiongKezhi/Sandbox/deployments", 67 | "created_at": "2020-05-19T13:58:09Z", 68 | "updated_at": "2020-05-19T14:01:45Z", 69 | "pushed_at": "2020-05-29T06:15:58Z", 70 | "git_url": "git://github.com/XiongKezhi/Sandbox.git", 71 | "ssh_url": "git@github.com:XiongKezhi/Sandbox.git", 72 | "clone_url": "https://github.com/XiongKezhi/Sandbox.git", 73 | "svn_url": "https://github.com/XiongKezhi/Sandbox", 74 | "homepage": null, 75 | "size": 2, 76 | "stargazers_count": 0, 77 | "watchers_count": 0, 78 | "language": null, 79 | "has_issues": true, 80 | "has_projects": true, 81 | "has_downloads": true, 82 | "has_wiki": true, 83 | "has_pages": false, 84 | "forks_count": 0, 85 | "mirror_url": null, 86 | "archived": false, 87 | "disabled": false, 88 | "open_issues_count": 1, 89 | "license": null, 90 | "visibility": "public", 91 | "forks": 0, 92 | "open_issues": 1, 93 | "watchers": 0, 94 | "default_branch": "master", 95 | "temp_clone_token": null, 96 | "network_count": 0, 97 | "subscribers_count": 1 98 | } -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitSCMChecksContext.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.net.URL; 6 | import java.util.Optional; 7 | 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | import edu.hm.hafner.util.FilteredLog; 11 | import edu.hm.hafner.util.VisibleForTesting; 12 | import edu.umd.cs.findbugs.annotations.CheckForNull; 13 | 14 | import hudson.model.Run; 15 | import hudson.model.TaskListener; 16 | import hudson.plugins.git.GitSCM; 17 | import hudson.plugins.git.Revision; 18 | import hudson.plugins.git.UserRemoteConfig; 19 | import hudson.plugins.git.util.BuildData; 20 | 21 | /** 22 | * Provides a {@link GitHubChecksContext} for a Jenkins job that uses a supported {@link GitSCM}. 23 | */ 24 | class GitSCMChecksContext extends GitHubChecksContext { 25 | private static final int VALID_REPOSITORY_PATH_SEGMENTS = 2; 26 | private final Run run; 27 | 28 | /** 29 | * Creates a {@link GitSCMChecksContext} according to the run. All attributes are computed during this period. 30 | * 31 | * @param run a run of a GitHub Branch Source project 32 | * @param runURL the URL to the Jenkins run 33 | */ 34 | GitSCMChecksContext(final Run run, final String runURL) { 35 | this(run, runURL, new SCMFacade()); 36 | } 37 | 38 | GitSCMChecksContext(final Run run, final String runURL, final SCMFacade scmFacade) { 39 | super(run.getParent(), runURL, scmFacade); 40 | 41 | this.run = run; 42 | } 43 | 44 | @Override 45 | protected Optional> getRun() { 46 | return Optional.of(run); 47 | } 48 | 49 | @Override 50 | public String getHeadSha() { 51 | try { 52 | String head = getGitCommitEnvironment(); 53 | if (StringUtils.isNotBlank(head)) { 54 | return head; 55 | } 56 | return getLastBuiltRevisionFromBuildData(); 57 | } 58 | catch (IOException | InterruptedException e) { 59 | // ignore and return a default 60 | } 61 | return StringUtils.EMPTY; 62 | } 63 | 64 | public String getGitCommitEnvironment() throws IOException, InterruptedException { 65 | return StringUtils.defaultString(run.getEnvironment(TaskListener.NULL).get("GIT_COMMIT")); 66 | } 67 | 68 | private String getLastBuiltRevisionFromBuildData() { 69 | BuildData gitBuildData = run.getAction(BuildData.class); 70 | if (gitBuildData != null) { 71 | Revision lastBuiltRevision = gitBuildData.getLastBuiltRevision(); 72 | if (lastBuiltRevision != null) { 73 | return lastBuiltRevision.getSha1().getName(); 74 | } 75 | } 76 | return StringUtils.EMPTY; 77 | } 78 | 79 | @Override 80 | public String getRepository() { 81 | String repositoryURL = getUserRemoteConfig().getUrl(); 82 | if (repositoryURL == null) { 83 | return StringUtils.EMPTY; 84 | } 85 | 86 | return getRepository(repositoryURL); 87 | } 88 | 89 | @VisibleForTesting 90 | String getRepository(final String repositoryUrl) { 91 | if (StringUtils.isBlank(repositoryUrl)) { 92 | return StringUtils.EMPTY; 93 | } 94 | 95 | if (repositoryUrl.startsWith("http")) { 96 | URL url; 97 | try { 98 | url = new URL(repositoryUrl); 99 | } 100 | catch (MalformedURLException e) { 101 | return StringUtils.EMPTY; 102 | } 103 | 104 | String[] pathParts = StringUtils.removeStart(url.getPath(), "/").split("/"); 105 | if (pathParts.length == VALID_REPOSITORY_PATH_SEGMENTS) { 106 | return pathParts[0] + "/" + StringUtils.removeEnd(pathParts[1], ".git"); 107 | } 108 | } 109 | else if (repositoryUrl.matches("git@.+:.+\\/.+")) { 110 | return StringUtils.removeEnd(repositoryUrl.split(":")[1], ".git"); 111 | } 112 | 113 | return StringUtils.EMPTY; 114 | } 115 | 116 | @Override 117 | @CheckForNull 118 | protected String getCredentialsId() { 119 | return getUserRemoteConfig().getCredentialsId(); 120 | } 121 | 122 | private UserRemoteConfig getUserRemoteConfig() { 123 | return getScmFacade().getUserRemoteConfig(resolveGitSCM()); 124 | } 125 | 126 | private GitSCM resolveGitSCM() { 127 | Optional gitSCM = getScmFacade().findGitSCM(run); 128 | if (gitSCM.isPresent()) { 129 | return gitSCM.get(); 130 | } 131 | throw new IllegalStateException( 132 | "Skipped publishing GitHub checks: no Git SCM source available for job: " + getJob().getName()); 133 | } 134 | 135 | @Override 136 | public boolean isValid(final FilteredLog logger) { 137 | logger.logError("Trying to resolve checks parameters from Git SCM..."); 138 | 139 | if (getScmFacade().findGitSCM(run).isEmpty()) { 140 | logger.logError("Job does not use Git SCM"); 141 | 142 | return false; 143 | } 144 | 145 | if (!hasValidCredentials(logger)) { 146 | logger.logError("Job does not have valid credentials"); 147 | 148 | return false; 149 | } 150 | 151 | String repository = getRepository(); 152 | if (StringUtils.isEmpty(repository)) { 153 | logger.logError("Repository url is not valid, requiring one of the following schemes:\n" 154 | + "\t1. \"git@git-server:repository-owner/repository(.git)\"\n" 155 | + "\t2. \"http(s)://git-server/repository-owner/repository(.git)\""); 156 | 157 | return false; 158 | } 159 | 160 | if (getHeadSha().isEmpty()) { 161 | logger.logError("No HEAD SHA found for '%s'", repository); 162 | 163 | return false; 164 | } 165 | 166 | logger.logInfo("Using GitSCM repository '%s' for GitHub checks", repository); 167 | 168 | return true; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/resources/__files/create-access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "XiongKezhi", 3 | "expires_at": "2019-08-10T05:54:58Z", 4 | "permissions": { 5 | "checks": "write", 6 | "pull_requests": "write", 7 | "contents": "read", 8 | "metadata": "read" 9 | }, 10 | "repository_selection": "selected", 11 | "repositories": [ 12 | { 13 | "id": 111111111, 14 | "node_id": "asdfasdf", 15 | "name": "XiongKezhi", 16 | "full_name": "XiongKezhi/XiongKezhi", 17 | "private": true, 18 | "owner": { 19 | "login": "XiongKezhi", 20 | "id": 11111111, 21 | "node_id": "asdfasdf", 22 | "avatar_url": "https://avatars2.githubusercontent.com/u/11111111?v=4", 23 | "gravatar_id": "", 24 | "url": "https://api.github.com/users/XiongKezhi", 25 | "html_url": "https://github.com/XiongKezhi", 26 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 27 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 28 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 29 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 30 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 31 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 32 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 33 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 34 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 35 | "type": "Organization", 36 | "site_admin": false 37 | }, 38 | "html_url": "https://github.com/XiongKezhi/XiongKezhi", 39 | "description": null, 40 | "fork": false, 41 | "url": "https://api.github.com/repos/XiongKezhi/XiongKezhi", 42 | "forks_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/forks", 43 | "keys_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/keys{/key_id}", 44 | "collaborators_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/collaborators{/collaborator}", 45 | "teams_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/teams", 46 | "hooks_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/hooks", 47 | "issue_events_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/issues/events{/number}", 48 | "events_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/events", 49 | "assignees_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/assignees{/user}", 50 | "branches_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/branches{/branch}", 51 | "tags_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/tags", 52 | "blobs_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/git/blobs{/sha}", 53 | "git_tags_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/git/tags{/sha}", 54 | "git_refs_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/git/refs{/sha}", 55 | "trees_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/git/trees{/sha}", 56 | "statuses_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/statuses/{sha}", 57 | "languages_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/languages", 58 | "stargazers_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/stargazers", 59 | "contributors_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/contributors", 60 | "subscribers_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/subscribers", 61 | "subscription_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/subscription", 62 | "commits_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/commits{/sha}", 63 | "git_commits_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/git/commits{/sha}", 64 | "comments_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/comments{/number}", 65 | "issue_comment_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/issues/comments{/number}", 66 | "contents_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/contents/{+path}", 67 | "compare_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/compare/{base}...{head}", 68 | "merges_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/merges", 69 | "archive_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/{archive_format}{/ref}", 70 | "downloads_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/downloads", 71 | "issues_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/issues{/number}", 72 | "pulls_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/pulls{/number}", 73 | "milestones_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/milestones{/number}", 74 | "notifications_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/notifications{?since,all,participating}", 75 | "labels_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/labels{/name}", 76 | "releases_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/releases{/id}", 77 | "deployments_url": "https://api.github.com/repos/XiongKezhi/XiongKezhi/deployments", 78 | "created_at": "2018-09-06T03:25:38Z", 79 | "updated_at": "2018-09-30T22:04:06Z", 80 | "pushed_at": "2019-08-08T22:22:34Z", 81 | "git_url": "git://github.com/XiongKezhi/XiongKezhi.git", 82 | "ssh_url": "git@github.com:XiongKezhi/XiongKezhi.git", 83 | "clone_url": "https://github.com/XiongKezhi/XiongKezhi.git", 84 | "svn_url": "https://github.com/XiongKezhi/XiongKezhi", 85 | "homepage": null, 86 | "size": 618, 87 | "stargazers_count": 0, 88 | "watchers_count": 0, 89 | "language": "Java", 90 | "has_issues": true, 91 | "has_projects": true, 92 | "has_downloads": true, 93 | "has_wiki": true, 94 | "has_pages": false, 95 | "forks_count": 0, 96 | "mirror_url": null, 97 | "archived": false, 98 | "disabled": false, 99 | "open_issues_count": 5, 100 | "license": null, 101 | "forks": 0, 102 | "open_issues": 5, 103 | "watchers": 0, 104 | "default_branch": "main" 105 | } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/status/GitHubSCMSourceStatusChecksTrait.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github.status; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | import org.kohsuke.stapler.DataBoundSetter; 7 | import org.kohsuke.stapler.QueryParameter; 8 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 9 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSourceContext; 10 | import org.jenkinsci.Symbol; 11 | import hudson.Extension; 12 | import hudson.util.FormValidation; 13 | import jenkins.scm.api.SCMSource; 14 | import jenkins.scm.api.trait.SCMSourceContext; 15 | import jenkins.scm.api.trait.SCMSourceTrait; 16 | import jenkins.scm.api.trait.SCMSourceTraitDescriptor; 17 | import jenkins.scm.impl.trait.Discovery; 18 | 19 | import io.jenkins.plugins.checks.status.AbstractStatusChecksProperties; 20 | 21 | /** 22 | * Traits to control {@link AbstractStatusChecksProperties} for jobs using 23 | * {@link GitHubSCMSource}. 24 | */ 25 | @SuppressWarnings("PMD.DataClass") 26 | public class GitHubSCMSourceStatusChecksTrait extends SCMSourceTrait implements GitHubStatusChecksConfigurations { 27 | private boolean skip = false; 28 | private boolean skipNotifications = false; 29 | private boolean unstableBuildNeutral = false; 30 | private String name = "Jenkins"; 31 | private boolean suppressLogs = false; 32 | private boolean skipProgressUpdates = false; 33 | 34 | /** 35 | * Constructor for stapler. 36 | */ 37 | @DataBoundConstructor 38 | public GitHubSCMSourceStatusChecksTrait() { 39 | super(); 40 | } 41 | 42 | /** 43 | * Defines the status checks name which is also used as identifier for GitHub checks. 44 | * 45 | * @return the name of status checks 46 | */ 47 | @Override 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | /** 53 | * Defines whether to skip publishing status checks. 54 | * 55 | * @return true to skip publishing checks 56 | */ 57 | @Override 58 | public boolean isSkip() { 59 | return skip; 60 | } 61 | 62 | @Override 63 | public boolean isUnstableBuildNeutral() { 64 | return unstableBuildNeutral; 65 | } 66 | 67 | /** 68 | * Defines whether to skip notifications from {@link org.jenkinsci.plugins.github_branch_source.GitHubBuildStatusNotification} 69 | * which utilizes the GitHub Status API. 70 | * 71 | * @return true to skip notifications 72 | */ 73 | public boolean isSkipNotifications() { 74 | return skipNotifications; 75 | } 76 | 77 | @Override 78 | public boolean isSuppressLogs() { 79 | return suppressLogs; 80 | } 81 | 82 | @Override 83 | public boolean isSkipProgressUpdates() { 84 | return skipProgressUpdates; 85 | } 86 | 87 | @DataBoundSetter 88 | public void setSkipProgressUpdates(final boolean skipProgressUpdates) { 89 | this.skipProgressUpdates = skipProgressUpdates; 90 | } 91 | 92 | /** 93 | * Set the name of the status checks. 94 | * 95 | * @param name 96 | * name of the checks 97 | */ 98 | @DataBoundSetter 99 | public void setName(final String name) { 100 | this.name = name; 101 | } 102 | 103 | /** 104 | * Set if skip publishing status checks. 105 | * 106 | * @param skip 107 | * true if skip 108 | */ 109 | @DataBoundSetter 110 | public void setSkip(final boolean skip) { 111 | this.skip = skip; 112 | } 113 | 114 | @DataBoundSetter 115 | public void setUnstableBuildNeutral(final boolean unstableBuildNeutral) { 116 | this.unstableBuildNeutral = unstableBuildNeutral; 117 | } 118 | 119 | @DataBoundSetter 120 | public void setSkipNotifications(final boolean skipNotifications) { 121 | this.skipNotifications = skipNotifications; 122 | } 123 | 124 | @DataBoundSetter 125 | public void setSuppressLogs(final boolean suppressLogs) { 126 | this.suppressLogs = suppressLogs; 127 | } 128 | 129 | @Override 130 | protected void decorateContext(final SCMSourceContext context) { 131 | if (isSkipNotifications() && context instanceof GitHubSCMSourceContext) { 132 | ((GitHubSCMSourceContext)context).withNotificationsDisabled(true); 133 | } 134 | } 135 | 136 | /** 137 | * Descriptor implementation for {@link GitHubSCMSourceStatusChecksTrait}. 138 | */ 139 | @Symbol("gitHubStatusChecks") 140 | @Extension 141 | @Discovery 142 | public static class DescriptorImpl extends SCMSourceTraitDescriptor { 143 | /** 144 | * Returns the display name. 145 | * 146 | * @return "Status Checks Properties" 147 | */ 148 | @Override 149 | public String getDisplayName() { 150 | return "Status Checks Properties"; 151 | } 152 | 153 | /** 154 | * The {@link GitHubSCMSourceStatusChecksTrait} is only applicable to {@link GitHubSCMSourceContext}. 155 | * 156 | * @return {@link GitHubSCMSourceContext}.class 157 | */ 158 | @Override 159 | public Class getContextClass() { 160 | return GitHubSCMSourceContext.class; 161 | } 162 | 163 | /** 164 | * The {@link GitHubSCMSourceStatusChecksTrait} is only applicable to {@link GitHubSCMSource}. 165 | * 166 | * @return {@link GitHubSCMSource}.class 167 | */ 168 | @Override 169 | public Class getSourceClass() { 170 | return GitHubSCMSource.class; 171 | } 172 | 173 | /** 174 | * Checks if the name of status checks is valid. 175 | * 176 | * @param name 177 | * name of status checks 178 | * @return ok if the name is not empty 179 | */ 180 | public FormValidation doCheckName(@QueryParameter final String name) { 181 | if (StringUtils.isBlank(name)) { 182 | return FormValidation.error("Name should not be empty!"); 183 | } 184 | 185 | return FormValidation.ok(); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubChecksPublisher.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import java.io.IOException; 4 | import java.time.Instant; 5 | import java.util.Date; 6 | import java.util.Optional; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | import edu.hm.hafner.util.VisibleForTesting; 13 | 14 | import org.kohsuke.github.GHCheckRun; 15 | import org.kohsuke.github.GHCheckRunBuilder; 16 | import org.kohsuke.github.GitHub; 17 | import org.jenkinsci.plugins.github_branch_source.Connector; 18 | import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; 19 | 20 | import io.jenkins.plugins.checks.api.ChecksDetails; 21 | import io.jenkins.plugins.checks.api.ChecksPublisher; 22 | import io.jenkins.plugins.util.PluginLogger; 23 | 24 | import static java.lang.String.*; 25 | 26 | /** 27 | * A publisher which publishes GitHub check runs. 28 | */ 29 | public class GitHubChecksPublisher extends ChecksPublisher { 30 | private static final String GITHUB_URL = "https://api.github.com"; 31 | private static final Logger SYSTEM_LOGGER = Logger.getLogger(GitHubChecksPublisher.class.getName()); 32 | 33 | private final GitHubChecksContext context; 34 | private final PluginLogger buildLogger; 35 | private final String gitHubUrl; 36 | 37 | /** 38 | * Creates a new instance of GitHubChecksPublisher. 39 | * 40 | * @param context 41 | * a context which contains SCM properties 42 | * @param buildLogger 43 | * the logger to use 44 | */ 45 | public GitHubChecksPublisher(final GitHubChecksContext context, final PluginLogger buildLogger) { 46 | this(context, buildLogger, GITHUB_URL); 47 | } 48 | 49 | GitHubChecksPublisher(final GitHubChecksContext context, final PluginLogger buildLogger, final String gitHubUrl) { 50 | super(); 51 | 52 | this.context = context; 53 | this.buildLogger = buildLogger; 54 | this.gitHubUrl = gitHubUrl; 55 | } 56 | 57 | /** 58 | * Publishes a GitHub check run. 59 | * 60 | * @param details the details of a check run 61 | */ 62 | @Override 63 | public void publish(final ChecksDetails details) { 64 | try { 65 | final var credentials = context.getCredentials(); 66 | 67 | // Prevent publication with unsupported credential types 68 | switch (credentials.getClass().getSimpleName()) { 69 | case "GitHubAppCredentials": 70 | case "VaultUsernamePasswordCredentialImpl": 71 | break; 72 | default: 73 | return; 74 | } 75 | 76 | String apiUri = null; 77 | if (credentials instanceof GitHubAppCredentials) { 78 | apiUri = ((GitHubAppCredentials) credentials).getApiUri(); 79 | } 80 | 81 | GitHub gitHub = Connector.connect(StringUtils.defaultIfBlank(apiUri, gitHubUrl), 82 | credentials); 83 | 84 | GitHubChecksDetails gitHubDetails = new GitHubChecksDetails(details); 85 | 86 | Optional existingId = context.getId(gitHubDetails.getName()); 87 | 88 | final GHCheckRun run; 89 | 90 | if (existingId.isPresent()) { 91 | run = getUpdater(gitHub, gitHubDetails, existingId.get()).create(); 92 | } 93 | else { 94 | run = getCreator(gitHub, gitHubDetails).create(); 95 | } 96 | 97 | context.addActionIfMissing(run.getId(), gitHubDetails.getName()); 98 | 99 | buildLogger.log("GitHub check (name: %s, status: %s) has been published.", gitHubDetails.getName(), 100 | gitHubDetails.getStatus()); 101 | SYSTEM_LOGGER.fine(format("Published check for repo: %s, sha: %s, job name: %s, name: %s, status: %s", 102 | context.getRepository(), 103 | context.getHeadSha(), 104 | context.getJob().getFullName(), 105 | gitHubDetails.getName(), 106 | gitHubDetails.getStatus()).replaceAll("[\r\n]", "")); 107 | } 108 | catch (IOException e) { 109 | String message = "Failed Publishing GitHub checks: "; 110 | SYSTEM_LOGGER.log(Level.WARNING, (message + details).replaceAll("[\r\n]", ""), e); 111 | buildLogger.log("%s", message + e); 112 | } 113 | } 114 | 115 | @VisibleForTesting 116 | GHCheckRunBuilder getUpdater(final GitHub github, final GitHubChecksDetails details, final long checkId) 117 | throws IOException { 118 | GHCheckRunBuilder builder = github.getRepository(context.getRepository()) 119 | .updateCheckRun(checkId); 120 | 121 | return applyDetails(builder, details); 122 | } 123 | 124 | @VisibleForTesting 125 | GHCheckRunBuilder getCreator(final GitHub gitHub, final GitHubChecksDetails details) throws IOException { 126 | GHCheckRunBuilder builder = gitHub.getRepository(context.getRepository()) 127 | .createCheckRun(details.getName(), context.getHeadSha()) 128 | .withStartedAt(details.getStartedAt().orElse(Date.from(Instant.now()))); 129 | 130 | return applyDetails(builder, details); 131 | } 132 | 133 | @VisibleForTesting 134 | GitHubChecksContext getContext() { 135 | return context; 136 | } 137 | 138 | private GHCheckRunBuilder applyDetails(final GHCheckRunBuilder builder, final GitHubChecksDetails details) { 139 | builder 140 | .withStatus(details.getStatus()) 141 | .withDetailsURL(details.getDetailsURL().orElse(context.getURL())); 142 | 143 | if (context.getRun().isPresent()) { 144 | final String externalId = context.getRun().get().getExternalizableId(); 145 | builder.withExternalID(externalId); 146 | } 147 | 148 | if (details.getConclusion().isPresent()) { 149 | builder.withConclusion(details.getConclusion().get()) 150 | .withCompletedAt(details.getCompletedAt().orElse(Date.from(Instant.now()))); 151 | } 152 | 153 | details.getOutput().ifPresent(builder::add); 154 | details.getActions().forEach(builder::add); 155 | 156 | return builder; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/GitHubChecksPublisherFactoryTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import hudson.EnvVars; 4 | import hudson.model.Job; 5 | import hudson.model.Run; 6 | import hudson.model.TaskListener; 7 | import hudson.plugins.git.GitSCM; 8 | import hudson.plugins.git.UserRemoteConfig; 9 | import jenkins.scm.api.SCMHead; 10 | import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; 11 | import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; 12 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 13 | import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import java.io.IOException; 17 | import java.util.Optional; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | class GitHubChecksPublisherFactoryTest { 24 | 25 | @Test 26 | void shouldCreateGitHubChecksPublisherFromRunForProjectWithValidGitHubSCMSource() { 27 | Run run = mock(Run.class); 28 | Job job = mock(Job.class); 29 | GitHubSCMSource source = mock(GitHubSCMSource.class); 30 | GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); 31 | PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); 32 | SCMFacade scmFacade = mock(SCMFacade.class); 33 | 34 | when(run.getParent()).thenReturn(job); 35 | when(job.getLastBuild()).thenReturn(run); 36 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); 37 | when(source.getCredentialsId()).thenReturn("credentials id"); 38 | when(scmFacade.findGitHubAppCredentials(job, "credentials id")).thenReturn(Optional.of(credentials)); 39 | when(scmFacade.findRevision(source, run)).thenReturn(Optional.of(revision)); 40 | when(scmFacade.findHash(revision)).thenReturn(Optional.of("a1b2c3")); 41 | 42 | GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, 43 | job)); 44 | assertThat(factory.createPublisher(run, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); 45 | } 46 | 47 | @Test 48 | void shouldReturnGitHubChecksPublisherFromJobProjectWithValidGitHubSCMSource() { 49 | Run run = mock(Run.class); 50 | Job job = mock(Job.class); 51 | GitHubSCMSource source = mock(GitHubSCMSource.class); 52 | GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); 53 | PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); 54 | SCMHead head = mock(SCMHead.class); 55 | SCMFacade scmFacade = mock(SCMFacade.class); 56 | 57 | when(run.getParent()).thenReturn(job); 58 | when(job.getLastBuild()).thenReturn(run); 59 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); 60 | when(source.getCredentialsId()).thenReturn("credentials id"); 61 | when(scmFacade.findGitHubAppCredentials(job, "credentials id")).thenReturn(Optional.of(credentials)); 62 | when(scmFacade.findHead(job)).thenReturn(Optional.of(head)); 63 | when(scmFacade.findRevision(source, head)).thenReturn(Optional.of(revision)); 64 | when(scmFacade.findHash(revision)).thenReturn(Optional.of("a1b2c3")); 65 | 66 | GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, 67 | job)); 68 | assertThat(factory.createPublisher(job, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); 69 | } 70 | 71 | @Test 72 | void shouldCreateGitHubChecksPublisherFromRunForProjectWithValidGitSCM() throws IOException, InterruptedException { 73 | Job job = mock(Job.class); 74 | Run run = mock(Run.class); 75 | GitSCM gitSCM = mock(GitSCM.class); 76 | UserRemoteConfig config = mock(UserRemoteConfig.class); 77 | GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); 78 | SCMFacade scmFacade = mock(SCMFacade.class); 79 | EnvVars envVars = mock(EnvVars.class); 80 | 81 | when(run.getParent()).thenReturn(job); 82 | when(run.getEnvironment(TaskListener.NULL)).thenReturn(envVars); 83 | when(envVars.get("GIT_COMMIT")).thenReturn("a1b2c3"); 84 | when(scmFacade.getScm(job)).thenReturn(gitSCM); 85 | when(scmFacade.findGitSCM(run)).thenReturn(Optional.of(gitSCM)); 86 | when(scmFacade.getUserRemoteConfig(gitSCM)).thenReturn(config); 87 | when(config.getCredentialsId()).thenReturn("1"); 88 | when(scmFacade.findGitHubAppCredentials(job, "1")).thenReturn(Optional.of(credentials)); 89 | when(config.getUrl()).thenReturn("https://github.com/jenkinsci/github-checks-plugin"); 90 | 91 | GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(scmFacade, createDisplayURLProvider(run, 92 | job)); 93 | assertThat(factory.createPublisher(run, TaskListener.NULL)).containsInstanceOf(GitHubChecksPublisher.class); 94 | } 95 | 96 | @Test 97 | void shouldReturnEmptyFromRunForInvalidProject() { 98 | Run run = mock(Run.class); 99 | SCMFacade facade = mock(SCMFacade.class); 100 | DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); 101 | 102 | GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(facade, urlProvider); 103 | assertThat(factory.createPublisher(run, TaskListener.NULL)).isNotPresent(); 104 | } 105 | 106 | @Test 107 | void shouldCreateNullPublisherFromJobForInvalidProject() { 108 | Job job = mock(Job.class); 109 | SCMFacade facade = mock(SCMFacade.class); 110 | DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); 111 | 112 | GitHubChecksPublisherFactory factory = new GitHubChecksPublisherFactory(facade, urlProvider); 113 | assertThat(factory.createPublisher(job, TaskListener.NULL)) 114 | .isNotPresent(); 115 | } 116 | 117 | private static DisplayURLProvider createDisplayURLProvider(final Run run, final Job job) { 118 | DisplayURLProvider urlProvider = mock(DisplayURLProvider.class); 119 | 120 | when(urlProvider.getRunURL(run)).thenReturn(null); 121 | when(urlProvider.getJobURL(job)).thenReturn(null); 122 | 123 | return urlProvider; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import java.io.IOException; 4 | import java.io.StringReader; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | import edu.hm.hafner.util.VisibleForTesting; 14 | import edu.umd.cs.findbugs.annotations.CheckForNull; 15 | 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | import org.kohsuke.github.GHEvent; 19 | import org.kohsuke.github.GHEventPayload; 20 | import org.kohsuke.github.GHRepository; 21 | import org.kohsuke.github.GitHub; 22 | import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; 23 | import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; 24 | import hudson.Extension; 25 | import hudson.model.Action; 26 | import hudson.model.Cause; 27 | import hudson.model.CauseAction; 28 | import hudson.model.Item; 29 | import hudson.model.Job; 30 | import hudson.model.ParametersAction; 31 | import hudson.model.Run; 32 | import hudson.security.ACL; 33 | import hudson.security.ACLContext; 34 | import jenkins.model.ParameterizedJobMixIn; 35 | 36 | import io.jenkins.plugins.util.JenkinsFacade; 37 | 38 | /** 39 | * This subscriber manages {@link GHEvent#CHECK_RUN} event and handles the re-run action request. 40 | */ 41 | @Extension 42 | public class CheckRunGHEventSubscriber extends GHEventsSubscriber { 43 | private static final Logger LOGGER = Logger.getLogger(CheckRunGHEventSubscriber.class.getName()); 44 | private static final String RERUN_ACTION = "rerequested"; 45 | 46 | private final JenkinsFacade jenkinsFacade; 47 | private final SCMFacade scmFacade; 48 | 49 | /** 50 | * Construct the subscriber. 51 | */ 52 | public CheckRunGHEventSubscriber() { 53 | this(new JenkinsFacade(), new SCMFacade()); 54 | } 55 | 56 | @VisibleForTesting 57 | CheckRunGHEventSubscriber(final JenkinsFacade jenkinsFacade, final SCMFacade scmFacade) { 58 | super(); 59 | 60 | this.jenkinsFacade = jenkinsFacade; 61 | this.scmFacade = scmFacade; 62 | } 63 | 64 | @Override 65 | protected boolean isApplicable(@CheckForNull final Item item) { 66 | if (item instanceof Job) { 67 | return scmFacade.findGitHubSCMSource((Job) item).isPresent(); 68 | } 69 | 70 | return false; 71 | } 72 | 73 | @Override 74 | protected Set events() { 75 | return Set.copyOf(Collections.singletonList(GHEvent.CHECK_RUN)); 76 | } 77 | 78 | @Override 79 | protected void onEvent(final GHSubscriberEvent event) { 80 | final String payload = event.getPayload(); 81 | try { 82 | GHEventPayload.CheckRun checkRun = GitHub.offline().parseEventPayload(new StringReader(payload), GHEventPayload.CheckRun.class); 83 | if (!RERUN_ACTION.equals(checkRun.getAction())) { 84 | LOGGER.log(Level.FINE, 85 | "Unsupported check run action: " + checkRun.getAction().replaceAll("[\r\n]", "")); 86 | return; 87 | } 88 | 89 | JSONObject payloadJSON = new JSONObject(payload); 90 | 91 | LOGGER.log(Level.INFO, "Received rerun request through GitHub checks API."); 92 | try (ACLContext ignored = ACL.as2(ACL.SYSTEM2)) { 93 | String branchName = payloadJSON.getJSONObject("check_run").getJSONObject("check_suite").optString("head_branch"); 94 | scheduleRerun(checkRun, branchName); 95 | } 96 | } 97 | catch (IOException | JSONException e) { 98 | throw new IllegalStateException("Could not parse check run event: " + payload.replaceAll("[\r\n]", ""), e); 99 | } 100 | } 101 | 102 | private void scheduleRerun(final GHEventPayload.CheckRun checkRun, final String branchName) { 103 | final GHRepository repository = checkRun.getRepository(); 104 | 105 | Optional> optionalRun = jenkinsFacade.getBuild(checkRun.getCheckRun().getExternalId()); 106 | if (optionalRun.isPresent()) { 107 | Run run = optionalRun.get(); 108 | Job job = run.getParent(); 109 | 110 | Cause cause = new GitHubChecksRerunActionCause(checkRun.getSender().getLogin(), branchName); 111 | 112 | List actions = new ArrayList<>(); 113 | actions.add(new CauseAction(cause)); 114 | 115 | ParametersAction paramAction = run.getAction(ParametersAction.class); 116 | if (paramAction != null) { 117 | actions.add(paramAction); 118 | } 119 | 120 | ParameterizedJobMixIn.scheduleBuild2(job, 0, actions.toArray(new Action[0])); 121 | 122 | LOGGER.log(Level.INFO, String.format("Scheduled rerun (build #%d) for job %s, requested by %s", 123 | job.getNextBuildNumber(), jenkinsFacade.getFullNameOf(job), 124 | checkRun.getSender().getLogin()).replaceAll("[\r\n]", "")); 125 | } 126 | else { 127 | LOGGER.log(Level.WARNING, String.format("No build found for rerun request from repository: %s and id: %s", 128 | repository.getFullName(), checkRun.getCheckRun().getExternalId()).replaceAll("[\r\n]", "")); 129 | } 130 | } 131 | 132 | /** 133 | * Declares that a build was started due to a user's rerun request through GitHub checks API. 134 | */ 135 | public static class GitHubChecksRerunActionCause extends Cause { 136 | private final String user; 137 | private final String branchName; 138 | 139 | /** 140 | * Construct the cause with user who requested the rerun. 141 | * 142 | * @param user 143 | * name of the user who made the request 144 | * @param branchName 145 | * name of the branch for which checks are to be run against 146 | */ 147 | public GitHubChecksRerunActionCause(final String user, final String branchName) { 148 | super(); 149 | 150 | this.user = user; 151 | this.branchName = branchName; 152 | } 153 | 154 | public String getBranchName() { 155 | return this.branchName; 156 | } 157 | 158 | @Override 159 | public String getShortDescription() { 160 | return String.format("Rerun request by %s through GitHub checks API, for branch %s", user, branchName); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/SCMFacade.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials; 4 | 5 | import edu.umd.cs.findbugs.annotations.CheckForNull; 6 | import hudson.model.AbstractProject; 7 | import hudson.model.Job; 8 | import hudson.model.Run; 9 | import hudson.plugins.git.GitSCM; 10 | import hudson.plugins.git.UserRemoteConfig; 11 | import hudson.scm.NullSCM; 12 | import hudson.scm.SCM; 13 | import jenkins.plugins.git.AbstractGitSCMSource; 14 | import jenkins.plugins.git.GitSCMSource; 15 | import jenkins.scm.api.SCMHead; 16 | import jenkins.scm.api.SCMRevision; 17 | import jenkins.scm.api.SCMRevisionAction; 18 | import jenkins.scm.api.SCMSource; 19 | import jenkins.triggers.SCMTriggerItem; 20 | 21 | import org.jenkinsci.plugins.github_branch_source.Connector; 22 | import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; 23 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 24 | import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; 25 | import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition; 26 | import org.jenkinsci.plugins.workflow.flow.FlowDefinition; 27 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 28 | 29 | import java.io.IOException; 30 | import java.util.Collection; 31 | import java.util.List; 32 | import java.util.Optional; 33 | 34 | /** 35 | * Facade to {@link GitHubSCMSource} and {@link GitSCM} in Jenkins. 36 | * Used for finding a supported SCM of a job. 37 | */ 38 | public class SCMFacade { 39 | /** 40 | * Find {@link GitHubSCMSource} (or GitHub repository) used by the {@code job}. 41 | * 42 | * @param job 43 | * the Jenkins project 44 | * @return the found GitHub SCM source used or empty 45 | */ 46 | @CheckForNull 47 | public SCMSource findSCMSource(final Job job) { 48 | return SCMSource.SourceByItem.findSource(job); 49 | } 50 | 51 | /** 52 | * Find {@link GitHubSCMSource} (or GitHub repository) used by the {@code job}. 53 | * 54 | * @param job 55 | * the Jenkins project 56 | * @return the found GitHub SCM source used or empty 57 | */ 58 | public Optional findGitHubSCMSource(final Job job) { 59 | SCMSource source = findSCMSource(job); 60 | return source instanceof GitHubSCMSource ? Optional.of((GitHubSCMSource) source) : Optional.empty(); 61 | } 62 | 63 | /** 64 | * Find {@link GitSCMSource} used by the {@code job}. 65 | * 66 | * @param job 67 | * the Jenkins project 68 | * @return the found Git SCN source or empty 69 | */ 70 | public Optional findGitSCMSource(final Job job) { 71 | SCMSource source = findSCMSource(job); 72 | return source instanceof GitSCMSource ? Optional.of((GitSCMSource) source) : Optional.empty(); 73 | } 74 | 75 | /** 76 | * Finds the {@link GitSCM} used by the {@code run}. 77 | * 78 | * @param run 79 | * the run to get the SCM from 80 | * @return the found GitSCM or empty 81 | */ 82 | public Optional findGitSCM(final Run run) { 83 | SCM scm = getScm(run); 84 | 85 | return toGitScm(scm); 86 | } 87 | 88 | /** 89 | * Finds the {@link GitSCM} used by the {@code job}. 90 | * @param job 91 | * the job to get the SCM from 92 | * @return the found GitSCM or empty 93 | */ 94 | public Optional findGitSCM(final Job job) { 95 | SCM scm = getScm(job); 96 | 97 | return toGitScm(scm); 98 | } 99 | 100 | private Optional toGitScm(final SCM scm) { 101 | if (scm instanceof GitSCM) { 102 | return Optional.of((GitSCM) scm); 103 | } 104 | 105 | return Optional.empty(); 106 | } 107 | 108 | UserRemoteConfig getUserRemoteConfig(final GitSCM scm) { 109 | List configs = scm.getUserRemoteConfigs(); 110 | if (configs.isEmpty()) { 111 | return new UserRemoteConfig(null, null, null, null); 112 | } 113 | return configs.get(0); 114 | } 115 | 116 | /** 117 | * Find {@link GitHubAppCredentials} with the {@code credentialsId} used by the {@code job}. 118 | * 119 | * @param job 120 | * the Jenkins project 121 | * @param credentialsId 122 | * the id of the target credentials 123 | * @return the found GitHub App credentials or empty 124 | */ 125 | public Optional findGitHubAppCredentials(final Job job, final String credentialsId) { 126 | final var source = findGitHubSCMSource(job); 127 | final var apiUri = source.map(GitHubSCMSource::getApiUri).orElse(null); 128 | final var owner = source.map(GitHubSCMSource::getRepoOwner).orElse(null); 129 | final var appCredentials = Connector.lookupScanCredentials(job, apiUri, credentialsId, owner); 130 | return Optional.ofNullable(appCredentials).filter(StandardUsernameCredentials.class::isInstance).map(StandardUsernameCredentials.class::cast); 131 | } 132 | 133 | /** 134 | * Find {@link SCMHead} (or branch) used by the {@code job}. 135 | * 136 | * @param job 137 | * the Jenkins project 138 | * @return the found SCM head or empty 139 | */ 140 | public Optional findHead(final Job job) { 141 | SCMHead head = SCMHead.HeadByItem.findHead(job); 142 | return Optional.ofNullable(head); 143 | } 144 | 145 | /** 146 | * Fetch the current {@link SCMRevision} used by the {@code head} of the {@code source}. 147 | * 148 | * @param source 149 | * the GitHub repository 150 | * @param head 151 | * the branch 152 | * @return the fetched revision or empty 153 | */ 154 | public Optional findRevision(final SCMSource source, final SCMHead head) { 155 | try { 156 | return Optional.ofNullable(source.fetch(head, null)); 157 | } 158 | catch (IOException | InterruptedException e) { 159 | throw new IllegalStateException(String.format("Could not fetch revision from repository: %s and branch: %s", 160 | source.getId(), head.getName()), e); 161 | } 162 | } 163 | 164 | /** 165 | * Find the current {@link SCMRevision} of the {@code source} and {@code run} locally through 166 | * {@link jenkins.scm.api.SCMRevisionAction}. 167 | * 168 | * @param source 169 | * the GitHub repository 170 | * @param run 171 | * the Jenkins run 172 | * @return the found revision or empty 173 | */ 174 | public Optional findRevision(final GitHubSCMSource source, final Run run) { 175 | return Optional.ofNullable(SCMRevisionAction.getRevision(source, run)); 176 | } 177 | 178 | /** 179 | * Find the hash value in {@code revision}. 180 | * 181 | * @param revision 182 | * the revision for a build 183 | * @return the found hash or empty 184 | */ 185 | public Optional findHash(final SCMRevision revision) { 186 | if (revision instanceof AbstractGitSCMSource.SCMRevisionImpl) { 187 | return Optional.of(((AbstractGitSCMSource.SCMRevisionImpl) revision).getHash()); 188 | } 189 | else if (revision instanceof PullRequestSCMRevision) { 190 | return Optional.of(((PullRequestSCMRevision) revision).getPullHash()); 191 | } 192 | else { 193 | return Optional.empty(); 194 | } 195 | } 196 | 197 | /** 198 | * Returns the SCM in a given build. If no SCM can be determined, then a {@link NullSCM} instance will be returned. 199 | * 200 | * @param run 201 | * the build to get the SCM from 202 | * 203 | * @return the SCM 204 | */ 205 | public SCM getScm(final Run run) { 206 | return getScm(run.getParent()); 207 | } 208 | 209 | /** 210 | * Returns the SCM in a given job. If no SCM can be determined, then a {@link NullSCM} instance will be returned. 211 | * 212 | * @param job 213 | * the job to get the SCM from 214 | * 215 | * @return the SCM 216 | */ 217 | public SCM getScm(final Job job) { 218 | if (job instanceof AbstractProject) { 219 | return extractFromProject((AbstractProject) job); 220 | } 221 | else if (job instanceof SCMTriggerItem) { 222 | return extractFromPipeline(job); 223 | } 224 | return new NullSCM(); 225 | } 226 | 227 | private SCM extractFromPipeline(final Job job) { 228 | Collection scms = ((SCMTriggerItem) job).getSCMs(); 229 | if (!scms.isEmpty()) { 230 | return scms.iterator().next(); // TODO: what should we do if more than one SCM has been used 231 | } 232 | 233 | if (job instanceof WorkflowJob) { 234 | FlowDefinition definition = ((WorkflowJob) job).getDefinition(); 235 | if (definition instanceof CpsScmFlowDefinition) { 236 | return ((CpsScmFlowDefinition) definition).getScm(); 237 | } 238 | } 239 | 240 | return new NullSCM(); 241 | } 242 | 243 | private SCM extractFromProject(final AbstractProject job) { 244 | if (job.getScm() != null) { 245 | return job.getScm(); 246 | } 247 | 248 | SCM scm = job.getRootProject().getScm(); 249 | if (scm != null) { 250 | return scm; 251 | } 252 | 253 | return new NullSCM(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriberTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import hudson.model.Item; 4 | import hudson.model.Job; 5 | import hudson.model.ParametersAction; 6 | import hudson.model.Run; 7 | import hudson.model.StringParameterValue; 8 | import io.jenkins.plugins.util.JenkinsFacade; 9 | import org.apache.commons.io.FileUtils; 10 | import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; 11 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 12 | import org.junit.jupiter.api.Test; 13 | import org.jvnet.hudson.test.LogRecorder; 14 | import org.kohsuke.github.GHEvent; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.Optional; 20 | import java.util.logging.Level; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.when; 26 | 27 | class CheckRunGHEventSubscriberTest { 28 | 29 | private static final String RERUN_REQUEST_JSON_FOR_PR = "check-run-event-with-rerun-action-for-pr.json"; 30 | private static final String RERUN_REQUEST_JSON_FOR_MASTER = "check-run-event-with-rerun-action-for-master.json"; 31 | private static final String RERUN_REQUEST_JSON_FOR_PR_MISSING_CHECKSUITE = "check-run-event-with-rerun-action-for-pr-missing-check-suite.json"; 32 | private static final String RERUN_REQUEST_JSON_FOR_PR_MISSING_CHECKSUITE_HEAD_BRANCH = "check-run-event-with-rerun-action-for-pr-missing-check-suite-head-branch.json"; 33 | 34 | @Test 35 | void shouldBeApplicableForJobWithGitHubSCMSource() { 36 | Job job = mock(Job.class); 37 | JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); 38 | SCMFacade scmFacade = mock(SCMFacade.class); 39 | GitHubSCMSource source = mock(GitHubSCMSource.class); 40 | 41 | when(scmFacade.findGitHubSCMSource(job)).thenReturn(Optional.of(source)); 42 | 43 | assertThat(new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade).isApplicable(job)) 44 | .isTrue(); 45 | } 46 | 47 | @Test 48 | void shouldNotBeApplicableForJobWithoutGitHubSCMSource() { 49 | Job job = mock(Job.class); 50 | assertThat(new CheckRunGHEventSubscriber().isApplicable(job)) 51 | .isFalse(); 52 | } 53 | 54 | @Test 55 | void shouldNotBeApplicableForItemThatNotInstanceOfJob() { 56 | Item item = mock(Item.class); 57 | assertThat(new CheckRunGHEventSubscriber().isApplicable(item)) 58 | .isFalse(); 59 | } 60 | 61 | @Test 62 | void shouldSubscribeToCheckRunEvent() { 63 | assertThat(new CheckRunGHEventSubscriber().events()).containsOnly(GHEvent.CHECK_RUN); 64 | } 65 | 66 | @Test 67 | void shouldProcessCheckRunEventWithRerequestedAction() throws IOException { 68 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1)) { 69 | new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class)) 70 | .onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR)); 71 | assertThat(logRecorder.getMessages().get(0)).contains("Received rerun request through GitHub checks API."); 72 | } 73 | } 74 | 75 | @Test 76 | void shouldThrowExceptionWhenCheckSuitesMissingFromPayload() { 77 | assertThatThrownBy( 78 | () -> new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class)) 79 | .onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR_MISSING_CHECKSUITE))) 80 | .isInstanceOf(IllegalStateException.class) 81 | .hasMessageContaining("Could not parse check run event:"); 82 | } 83 | 84 | @Test 85 | void shouldIgnoreHeadBranchMissingFromPayload() throws IOException { 86 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1)) { 87 | new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class)) 88 | .onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR_MISSING_CHECKSUITE_HEAD_BRANCH)); 89 | assertThat(logRecorder.getMessages().get(0)).contains("Received rerun request through GitHub checks API."); 90 | } 91 | } 92 | 93 | @Test 94 | void shouldIgnoreCheckRunEventWithoutRerequestedAction() throws IOException { 95 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.FINE).capture(1)) { 96 | new CheckRunGHEventSubscriber(mock(JenkinsFacade.class), mock(SCMFacade.class)) 97 | .onEvent(createEventWithRerunRequest("check-run-event-with-created-action.json")); 98 | assertThat(logRecorder.getMessages()).contains("Unsupported check run action: created"); 99 | } 100 | } 101 | 102 | @Test 103 | void shouldScheduleRerunForPR() throws IOException { 104 | Job job = mock(Job.class); 105 | Run run = mock(Run.class); 106 | JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); 107 | SCMFacade scmFacade = mock(SCMFacade.class); 108 | 109 | when(jenkinsFacade.getBuild("codingstyle/PR-1#2")).thenReturn(Optional.of(run)); 110 | when(jenkinsFacade.getFullNameOf(job)).thenReturn("codingstyle/PR-1"); 111 | when(run.getParent()).thenReturn(job); 112 | when(run.getAction(ParametersAction.class)).thenReturn( 113 | new ParametersAction(new StringParameterValue("test_key", "test_value")) 114 | ); 115 | when(job.getNextBuildNumber()).thenReturn(1); 116 | when(job.getName()).thenReturn("PR-1"); 117 | 118 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1)) { 119 | new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade) 120 | .onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR)); 121 | assertThat(logRecorder.getMessages()) 122 | .contains("Scheduled rerun (build #1) for job codingstyle/PR-1, requested by XiongKezhi"); 123 | } 124 | } 125 | 126 | @Test 127 | void shouldScheduleRerunForMaster() throws IOException { 128 | Job job = mock(Job.class); 129 | Run run = mock(Run.class); 130 | JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); 131 | SCMFacade scmFacade = mock(SCMFacade.class); 132 | 133 | when(jenkinsFacade.getBuild("codingstyle/master#8")).thenReturn(Optional.of(run)); 134 | when(jenkinsFacade.getFullNameOf(job)).thenReturn("codingstyle/master"); 135 | when(run.getParent()).thenReturn(job); 136 | when(run.getAction(ParametersAction.class)).thenReturn(null); 137 | when(job.getNextBuildNumber()).thenReturn(1); 138 | when(job.getName()).thenReturn("master"); 139 | 140 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.INFO).capture(1)) { 141 | new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade) 142 | .onEvent(createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_MASTER)); 143 | assertThat(logRecorder.getMessages()) 144 | .contains("Scheduled rerun (build #1) for job codingstyle/master, requested by XiongKezhi"); 145 | } 146 | } 147 | 148 | @Test 149 | void shouldNotScheduleRerunWhenNoProperBuildFound() throws IOException { 150 | JenkinsFacade jenkinsFacade = mock(JenkinsFacade.class); 151 | when(jenkinsFacade.getBuild("codingstyle/PR-1#2")).thenReturn(Optional.empty()); 152 | 153 | assertNoBuildIsScheduled(jenkinsFacade, mock(SCMFacade.class), 154 | createEventWithRerunRequest(RERUN_REQUEST_JSON_FOR_PR)); 155 | } 156 | 157 | @Test 158 | void shouldContainsUserAndBranchInShortDescriptionOfGitHubChecksRerunActionCause() { 159 | CheckRunGHEventSubscriber.GitHubChecksRerunActionCause cause = 160 | new CheckRunGHEventSubscriber.GitHubChecksRerunActionCause("jenkins", "some_branch"); 161 | 162 | assertThat(cause.getShortDescription()).isEqualTo("Rerun request by jenkins through GitHub checks API, for branch some_branch"); 163 | } 164 | 165 | @Test 166 | void shouldHaveAccessibleBranchNameInGitHubChecksRerunActionCause() { 167 | CheckRunGHEventSubscriber.GitHubChecksRerunActionCause cause = 168 | new CheckRunGHEventSubscriber.GitHubChecksRerunActionCause("jenkins", "some_branch"); 169 | 170 | assertThat(cause.getBranchName()).isEqualTo("some_branch"); 171 | } 172 | 173 | private static void assertNoBuildIsScheduled(final JenkinsFacade jenkinsFacade, final SCMFacade scmFacade, 174 | final GHSubscriberEvent event) { 175 | try (LogRecorder logRecorder = new LogRecorder().record(CheckRunGHEventSubscriber.class.getName(), Level.WARNING).capture(1)) { 176 | new CheckRunGHEventSubscriber(jenkinsFacade, scmFacade).onEvent(event); 177 | assertThat(logRecorder.getMessages()) 178 | .contains("No build found for rerun request from repository: XiongKezhi/codingstyle and id: codingstyle/PR-1#2"); 179 | } 180 | } 181 | 182 | private static GHSubscriberEvent createEventWithRerunRequest(final String jsonFile) throws IOException { 183 | return new GHSubscriberEvent("CheckRunGHEventSubscriberTest", GHEvent.CHECK_RUN, 184 | FileUtils.readFileToString(new File(CheckRunGHEventSubscriberTest.class.getResource( 185 | CheckRunGHEventSubscriberTest.class.getSimpleName() + "/" + jsonFile).getFile()), StandardCharsets.UTF_8)); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/checks/github/GitHubChecksDetails.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import java.net.URI; 4 | import java.time.ZoneOffset; 5 | import java.util.Date; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.stream.Collectors; 9 | 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | import org.kohsuke.github.GHCheckRun.AnnotationLevel; 13 | import org.kohsuke.github.GHCheckRun.Conclusion; 14 | import org.kohsuke.github.GHCheckRun.Status; 15 | import org.kohsuke.github.GHCheckRunBuilder; 16 | import org.kohsuke.github.GHCheckRunBuilder.Action; 17 | import org.kohsuke.github.GHCheckRunBuilder.Annotation; 18 | import org.kohsuke.github.GHCheckRunBuilder.Image; 19 | import org.kohsuke.github.GHCheckRunBuilder.Output; 20 | 21 | import io.jenkins.plugins.checks.api.ChecksAction; 22 | import io.jenkins.plugins.checks.api.ChecksAnnotation; 23 | import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel; 24 | import io.jenkins.plugins.checks.api.ChecksConclusion; 25 | import io.jenkins.plugins.checks.api.ChecksDetails; 26 | import io.jenkins.plugins.checks.api.ChecksImage; 27 | import io.jenkins.plugins.checks.api.ChecksOutput; 28 | import io.jenkins.plugins.checks.api.ChecksStatus; 29 | 30 | /** 31 | * An adaptor which adapts the generic checks objects of {@link ChecksDetails} to the specific GitHub checks run 32 | * objects of {@link GHCheckRunBuilder}. 33 | */ 34 | class GitHubChecksDetails { 35 | private final ChecksDetails details; 36 | 37 | private static final int MAX_MESSAGE_SIZE_TO_CHECKS_API = 65_535; 38 | 39 | /** 40 | * Construct with the given {@link ChecksDetails}. 41 | * 42 | * @param details the details of a generic check run 43 | */ 44 | GitHubChecksDetails(final ChecksDetails details) { 45 | if (details.getConclusion() == ChecksConclusion.NONE) { 46 | if (details.getStatus() == ChecksStatus.COMPLETED) { 47 | throw new IllegalArgumentException("No conclusion has been set when status is completed."); 48 | } 49 | 50 | if (details.getCompletedAt().isPresent()) { 51 | throw new IllegalArgumentException("No conclusion has been set when \"completedAt\" is provided."); 52 | } 53 | } 54 | 55 | this.details = details; 56 | } 57 | 58 | /** 59 | * Returns the name of a GitHub check run. 60 | * 61 | * @return the name of the check 62 | */ 63 | public String getName() { 64 | return details.getName() 65 | .filter(StringUtils::isNotBlank) 66 | .orElseThrow(() -> new IllegalArgumentException("The check name is blank.")); 67 | } 68 | 69 | /** 70 | * Returns the {@link Status} of a GitHub check run. 71 | * 72 | * 73 | * @return the status of a check run 74 | * @throws IllegalArgumentException if the status of the {@code details} is not one of {@link ChecksStatus} 75 | */ 76 | public Status getStatus() { 77 | return switch (details.getStatus()) { 78 | case NONE, QUEUED -> Status.QUEUED; 79 | case IN_PROGRESS -> Status.IN_PROGRESS; 80 | case COMPLETED -> Status.COMPLETED; 81 | }; 82 | } 83 | 84 | /** 85 | * Returns the URL of site which contains details of a GitHub check run. 86 | * 87 | * @return an URL of the site 88 | */ 89 | public Optional getDetailsURL() { 90 | if (details.getDetailsURL().filter(StringUtils::isBlank).isPresent()) { 91 | return Optional.empty(); 92 | } 93 | 94 | details.getDetailsURL().ifPresent(url -> { 95 | if (!StringUtils.equalsAny(URI.create(url).getScheme(), "http", "https")) { 96 | throw new IllegalArgumentException("The details url is not http or https scheme: " + url); 97 | } 98 | } 99 | ); 100 | return details.getDetailsURL(); 101 | } 102 | 103 | /** 104 | * Returns the UTC time when the check started. 105 | * 106 | * @return the start time of a check 107 | */ 108 | public Optional getStartedAt() { 109 | if (details.getStartedAt().isPresent()) { 110 | return Optional.of(Date.from( 111 | details.getStartedAt().get() 112 | .toInstant(ZoneOffset.UTC))); 113 | } 114 | return Optional.empty(); 115 | } 116 | 117 | /** 118 | * Returns the {@link Conclusion} of a completed GitHub check run. 119 | * 120 | * @return the conclusion of a completed check run 121 | * @throws IllegalArgumentException if the conclusion of the {@code details} is not one of {@link ChecksConclusion} 122 | */ 123 | @SuppressWarnings("PMD.CyclomaticComplexity") 124 | public Optional getConclusion() { 125 | return switch (details.getConclusion()) { 126 | case SKIPPED -> 127 | Optional.of(Conclusion.SKIPPED); // TODO use CANCELLED if https://github.com/github/feedback/discussions/10255 is fixed 128 | case FAILURE, CANCELED, TIME_OUT -> // TODO TIMED_OUT as above 129 | Optional.of(Conclusion.FAILURE); 130 | case NEUTRAL -> Optional.of(Conclusion.NEUTRAL); 131 | case SUCCESS -> Optional.of(Conclusion.SUCCESS); 132 | case ACTION_REQUIRED -> Optional.of(Conclusion.ACTION_REQUIRED); 133 | case NONE -> Optional.empty(); 134 | }; 135 | } 136 | 137 | /** 138 | * Returns the UTC time when the check completed. 139 | * 140 | * @return the completed time of a check 141 | */ 142 | public Optional getCompletedAt() { 143 | if (details.getCompletedAt().isPresent()) { 144 | return Optional.of(Date.from( 145 | details.getCompletedAt().get() 146 | .toInstant(ZoneOffset.UTC))); 147 | } 148 | return Optional.empty(); 149 | } 150 | 151 | /** 152 | * Returns the {@link Output} of a GitHub check run. 153 | * 154 | * @return the output of a check run 155 | */ 156 | public Optional getOutput() { 157 | if (details.getOutput().isPresent()) { 158 | ChecksOutput checksOutput = details.getOutput().get(); 159 | Output output = new Output( 160 | checksOutput.getTitle().orElseThrow( 161 | () -> new IllegalArgumentException("Title of output is required but not provided")), 162 | checksOutput.getSummary(MAX_MESSAGE_SIZE_TO_CHECKS_API).orElseThrow( 163 | () -> new IllegalArgumentException("Summary of output is required but not provided"))) 164 | .withText(checksOutput.getText(MAX_MESSAGE_SIZE_TO_CHECKS_API).orElse(null)); 165 | checksOutput.getChecksAnnotations().stream().map(this::getAnnotation).forEach(output::add); 166 | checksOutput.getChecksImages().stream().map(this::getImage).forEach(output::add); 167 | return Optional.of(output); 168 | } 169 | 170 | return Optional.empty(); 171 | } 172 | 173 | /** 174 | * Returns the {@link Action} of a GitHub check run. 175 | * 176 | * @return the actions list of a check run. 177 | */ 178 | public List getActions() { 179 | return details.getActions().stream() 180 | .map(this::getAction) 181 | .collect(Collectors.toList()); 182 | } 183 | 184 | private Action getAction(final ChecksAction checksAction) { 185 | return new Action( 186 | checksAction.getLabel() 187 | .orElseThrow(() -> 188 | new IllegalArgumentException("Label of action is required but not provided")), 189 | checksAction.getDescription() 190 | .orElseThrow(() -> 191 | new IllegalArgumentException("Description of action is required but not provided")), 192 | checksAction.getIdentifier() 193 | .orElseThrow(() -> 194 | new IllegalArgumentException("Identifier of action is required but not provided"))); 195 | } 196 | 197 | private Annotation getAnnotation(final ChecksAnnotation checksAnnotation) { 198 | return new Annotation( 199 | checksAnnotation.getPath() 200 | .orElseThrow(() -> new IllegalArgumentException("Path is required but not provided.")), 201 | checksAnnotation.getStartLine() 202 | .orElseThrow(() -> new IllegalArgumentException("Start line is required but not provided.")), 203 | checksAnnotation.getEndLine(). 204 | orElseThrow(() -> new IllegalArgumentException("End line is required but not provided.")), 205 | getAnnotationLevel(checksAnnotation.getAnnotationLevel()), 206 | checksAnnotation.getMessage() 207 | .orElseThrow(() -> new IllegalArgumentException("Message is required but not provided."))) 208 | .withTitle(checksAnnotation.getTitle().orElse(null)) 209 | .withRawDetails(checksAnnotation.getRawDetails().orElse(null)) 210 | .withStartColumn(checksAnnotation.getStartColumn().orElse(null)) 211 | .withEndColumn(checksAnnotation.getEndColumn().orElse(null)); 212 | } 213 | 214 | private Image getImage(final ChecksImage checksImage) { 215 | return new Image( 216 | checksImage.getAlt() 217 | .orElseThrow(() -> new IllegalArgumentException("alt of image is required but not provided.")), 218 | checksImage.getImageUrl() 219 | .orElseThrow(() -> new IllegalArgumentException("url of image is required but not provided."))) 220 | .withCaption(checksImage.getCaption().orElse(null)); 221 | } 222 | 223 | private AnnotationLevel getAnnotationLevel(final ChecksAnnotationLevel checksLevel) { 224 | return switch (checksLevel) { 225 | case NOTICE -> AnnotationLevel.NOTICE; 226 | case FAILURE -> AnnotationLevel.FAILURE; 227 | case WARNING -> AnnotationLevel.WARNING; 228 | case NONE -> throw new IllegalArgumentException("Annotation level is required but not set."); 229 | }; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriberTest/check-run-event-with-rerun-action-for-pr-missing-check-suite.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "rerequested", 3 | "check_run": { 4 | "id": 993923912, 5 | "node_id": "MDg6Q2hlY2tSdW45OTM5MjM5MTI=", 6 | "head_sha": "3c0ea12c02129ff4919afe087616d84547d93539", 7 | "external_id": "codingstyle/PR-1#2", 8 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993923912", 9 | "html_url": "https://github.com/XiongKezhi/codingstyle/runs/993923912", 10 | "details_url": "http://127.0.0.1:8080/job/pipeline-coding-style/job/PR-1/2/display/redirect", 11 | "status": "completed", 12 | "conclusion": "failure", 13 | "started_at": "2020-08-17T14:04:06Z", 14 | "completed_at": "2020-08-17T14:04:06Z", 15 | "output": { 16 | "title": null, 17 | "summary": null, 18 | "text": null, 19 | "annotations_count": 0, 20 | "annotations_url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993923912/annotations" 21 | }, 22 | "name": "Jenkins", 23 | "app": { 24 | "id": 76899, 25 | "slug": "jenkins-checks-api", 26 | "node_id": "MDM6QXBwNzY4OTk=", 27 | "owner": { 28 | "login": "XiongKezhi", 29 | "id": 30348893, 30 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 31 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 32 | "gravatar_id": "", 33 | "url": "https://api.github.com/users/XiongKezhi", 34 | "html_url": "https://github.com/XiongKezhi", 35 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 36 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 37 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 38 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 39 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 40 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 41 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 42 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 43 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 44 | "type": "User", 45 | "site_admin": false 46 | }, 47 | "name": "Jenkins Checks API", 48 | "description": "", 49 | "external_url": "https://smee.io/2C4VArPRXWl2zFg", 50 | "html_url": "https://github.com/apps/jenkins-checks-api", 51 | "created_at": "2020-08-14T08:47:10Z", 52 | "updated_at": "2020-08-14T09:04:57Z", 53 | "permissions": { 54 | "checks": "write", 55 | "contents": "write", 56 | "metadata": "read", 57 | "organization_hooks": "write", 58 | "pull_requests": "write", 59 | "repository_hooks": "write", 60 | "statuses": "write" 61 | }, 62 | "events": [ 63 | "check_run", 64 | "commit_comment", 65 | "create", 66 | "pull_request", 67 | "push", 68 | "status" 69 | ] 70 | }, 71 | "pull_requests": [ 72 | { 73 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls/1", 74 | "id": 447475694, 75 | "number": 1, 76 | "head": { 77 | "ref": "simplify-jenkinsfile", 78 | "sha": "3c0ea12c02129ff4919afe087616d84547d93539", 79 | "repo": { 80 | "id": 278546516, 81 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 82 | "name": "codingstyle" 83 | } 84 | }, 85 | "base": { 86 | "ref": "master", 87 | "sha": "b22fdfec1127073e509224a99a7704eeebdebe11", 88 | "repo": { 89 | "id": 278546516, 90 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 91 | "name": "codingstyle" 92 | } 93 | } 94 | } 95 | ] 96 | }, 97 | "repository": { 98 | "id": 278546516, 99 | "node_id": "MDEwOlJlcG9zaXRvcnkyNzg1NDY1MTY=", 100 | "name": "codingstyle", 101 | "full_name": "XiongKezhi/codingstyle", 102 | "private": false, 103 | "owner": { 104 | "login": "XiongKezhi", 105 | "id": 30348893, 106 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 107 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 108 | "gravatar_id": "", 109 | "url": "https://api.github.com/users/XiongKezhi", 110 | "html_url": "https://github.com/XiongKezhi", 111 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 112 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 113 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 114 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 115 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 116 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 117 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 118 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 119 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 120 | "type": "User", 121 | "site_admin": false 122 | }, 123 | "html_url": "https://github.com/XiongKezhi/codingstyle", 124 | "description": "Java coding style and template project used at Munich university of applied sciences ", 125 | "fork": true, 126 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 127 | "forks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/forks", 128 | "keys_url": "https://api.github.com/repos/XiongKezhi/codingstyle/keys{/key_id}", 129 | "collaborators_url": "https://api.github.com/repos/XiongKezhi/codingstyle/collaborators{/collaborator}", 130 | "teams_url": "https://api.github.com/repos/XiongKezhi/codingstyle/teams", 131 | "hooks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/hooks", 132 | "issue_events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/events{/number}", 133 | "events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/events", 134 | "assignees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/assignees{/user}", 135 | "branches_url": "https://api.github.com/repos/XiongKezhi/codingstyle/branches{/branch}", 136 | "tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/tags", 137 | "blobs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/blobs{/sha}", 138 | "git_tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/tags{/sha}", 139 | "git_refs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/refs{/sha}", 140 | "trees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/trees{/sha}", 141 | "statuses_url": "https://api.github.com/repos/XiongKezhi/codingstyle/statuses/{sha}", 142 | "languages_url": "https://api.github.com/repos/XiongKezhi/codingstyle/languages", 143 | "stargazers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/stargazers", 144 | "contributors_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contributors", 145 | "subscribers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscribers", 146 | "subscription_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscription", 147 | "commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/commits{/sha}", 148 | "git_commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/commits{/sha}", 149 | "comments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/comments{/number}", 150 | "issue_comment_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/comments{/number}", 151 | "contents_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contents/{+path}", 152 | "compare_url": "https://api.github.com/repos/XiongKezhi/codingstyle/compare/{base}...{head}", 153 | "merges_url": "https://api.github.com/repos/XiongKezhi/codingstyle/merges", 154 | "archive_url": "https://api.github.com/repos/XiongKezhi/codingstyle/{archive_format}{/ref}", 155 | "downloads_url": "https://api.github.com/repos/XiongKezhi/codingstyle/downloads", 156 | "issues_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues{/number}", 157 | "pulls_url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls{/number}", 158 | "milestones_url": "https://api.github.com/repos/XiongKezhi/codingstyle/milestones{/number}", 159 | "notifications_url": "https://api.github.com/repos/XiongKezhi/codingstyle/notifications{?since,all,participating}", 160 | "labels_url": "https://api.github.com/repos/XiongKezhi/codingstyle/labels{/name}", 161 | "releases_url": "https://api.github.com/repos/XiongKezhi/codingstyle/releases{/id}", 162 | "deployments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/deployments", 163 | "created_at": "2020-07-10T05:28:50Z", 164 | "updated_at": "2020-08-17T12:56:37Z", 165 | "pushed_at": "2020-08-17T14:02:28Z", 166 | "git_url": "git://github.com/XiongKezhi/codingstyle.git", 167 | "ssh_url": "git@github.com:XiongKezhi/codingstyle.git", 168 | "clone_url": "https://github.com/XiongKezhi/codingstyle.git", 169 | "svn_url": "https://github.com/XiongKezhi/codingstyle", 170 | "homepage": "", 171 | "size": 1003, 172 | "stargazers_count": 0, 173 | "watchers_count": 0, 174 | "language": "Java", 175 | "has_issues": false, 176 | "has_projects": true, 177 | "has_downloads": true, 178 | "has_wiki": true, 179 | "has_pages": false, 180 | "forks_count": 3, 181 | "mirror_url": null, 182 | "archived": false, 183 | "disabled": false, 184 | "open_issues_count": 5, 185 | "license": { 186 | "key": "other", 187 | "name": "Other", 188 | "spdx_id": "NOASSERTION", 189 | "url": null, 190 | "node_id": "MDc6TGljZW5zZTA=" 191 | }, 192 | "forks": 3, 193 | "open_issues": 5, 194 | "watchers": 0, 195 | "default_branch": "master" 196 | }, 197 | "sender": { 198 | "login": "XiongKezhi", 199 | "id": 30348893, 200 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 201 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 202 | "gravatar_id": "", 203 | "url": "https://api.github.com/users/XiongKezhi", 204 | "html_url": "https://github.com/XiongKezhi", 205 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 206 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 207 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 208 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 209 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 210 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 211 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 212 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 213 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 214 | "type": "User", 215 | "site_admin": false 216 | }, 217 | "installation": { 218 | "id": 11244691, 219 | "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTEyNDQ2OTE=" 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/java/io/jenkins/plugins/checks/github/GitHubSCMSourceChecksContextTest.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.checks.github; 2 | 3 | import edu.hm.hafner.util.FilteredLog; 4 | import hudson.model.Job; 5 | import hudson.model.Run; 6 | import jenkins.plugins.git.AbstractGitSCMSource; 7 | import jenkins.scm.api.SCMHead; 8 | import jenkins.scm.api.SCMRevision; 9 | import org.jenkinsci.plugins.displayurlapi.ClassicDisplayURLProvider; 10 | import org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials; 11 | import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource; 12 | import org.jenkinsci.plugins.github_branch_source.PullRequestSCMRevision; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.util.Optional; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.when; 21 | 22 | class GitHubSCMSourceChecksContextTest { 23 | private static final String URL = "URL"; 24 | 25 | @Test 26 | void shouldGetHeadShaFromMasterBranch() { 27 | Job job = mock(Job.class); 28 | SCMHead head = mock(SCMHead.class); 29 | AbstractGitSCMSource.SCMRevisionImpl revision = mock(AbstractGitSCMSource.SCMRevisionImpl.class); 30 | GitHubSCMSource source = mock(GitHubSCMSource.class); 31 | 32 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, 33 | createGitHubSCMFacadeWithRevision(job, source, head, revision, "a1b2c3")) 34 | .getHeadSha()) 35 | .isEqualTo("a1b2c3"); 36 | } 37 | 38 | @Test 39 | void shouldGetHeadShaFromPullRequest() { 40 | Job job = mock(Job.class); 41 | SCMHead head = mock(SCMHead.class); 42 | PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); 43 | GitHubSCMSource source = mock(GitHubSCMSource.class); 44 | 45 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, 46 | createGitHubSCMFacadeWithRevision(job, source, head, revision, "a1b2c3")) 47 | .getHeadSha()) 48 | .isEqualTo("a1b2c3"); 49 | } 50 | 51 | @Test 52 | void shouldGetHeadShaFromRun() { 53 | Job job = mock(Job.class); 54 | Run run = mock(Run.class); 55 | PullRequestSCMRevision revision = mock(PullRequestSCMRevision.class); 56 | GitHubSCMSource source = mock(GitHubSCMSource.class); 57 | 58 | when(run.getParent()).thenReturn(job); 59 | when(job.getLastBuild()).thenReturn(run); 60 | 61 | assertThat(GitHubSCMSourceChecksContext.fromRun(run, URL, 62 | createGitHubSCMFacadeWithRevision(run, source, revision, "a1b2c3")) 63 | .getHeadSha()) 64 | .isEqualTo("a1b2c3"); 65 | } 66 | 67 | @Test 68 | void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSCMHeadAvailable() { 69 | Job job = mock(Job.class); 70 | GitHubSCMSource source = mock(GitHubSCMSource.class); 71 | 72 | when(job.getName()).thenReturn("github-checks-plugin"); 73 | 74 | assertThatThrownBy(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, source)) 75 | ::getHeadSha) 76 | .isInstanceOf(IllegalStateException.class) 77 | .hasMessage("No SHA found for job: github-checks-plugin"); 78 | } 79 | 80 | @Test 81 | void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSCMRevisionAvailable() { 82 | Job job = mock(Job.class); 83 | SCMHead head = mock(SCMHead.class); 84 | GitHubSCMSource source = mock(GitHubSCMSource.class); 85 | 86 | when(job.getName()).thenReturn("github-checks-plugin"); 87 | when(source.getRepoOwner()).thenReturn("jenkinsci"); 88 | when(source.getRepository()).thenReturn("github-checks-plugin"); 89 | when(head.getName()).thenReturn("master"); 90 | 91 | assertThatThrownBy(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithRevision(job, source, 92 | head, null, null))::getHeadSha) 93 | .isInstanceOf(IllegalStateException.class) 94 | .hasMessage("No SHA found for job: github-checks-plugin"); 95 | } 96 | 97 | @Test 98 | void shouldThrowIllegalStateExceptionWhenGetHeadShaButNoSuitableSCMRevisionAvailable() { 99 | Job job = mock(Job.class); 100 | SCMHead head = mock(SCMHead.class); 101 | SCMRevision revision = mock(SCMRevision.class); 102 | GitHubSCMSource source = mock(GitHubSCMSource.class); 103 | 104 | when(job.getName()).thenReturn("github-checks-plugin"); 105 | 106 | assertThatThrownBy(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithRevision(job, source, 107 | head, revision, null))::getHeadSha) 108 | .isInstanceOf(IllegalStateException.class) 109 | .hasMessage("No SHA found for job: github-checks-plugin"); 110 | } 111 | 112 | @Test 113 | void shouldGetRepositoryName() { 114 | Job job = mock(Job.class); 115 | GitHubSCMSource source = mock(GitHubSCMSource.class); 116 | 117 | when(source.getRepoOwner()).thenReturn("jenkinsci"); 118 | when(source.getRepository()).thenReturn("github-checks-plugin"); 119 | 120 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, source)).getRepository()) 121 | .isEqualTo("jenkinsci/github-checks-plugin"); 122 | } 123 | 124 | @Test 125 | void shouldThrowIllegalStateExceptionWhenGetRepositoryButNoGitHubSCMSourceAvailable() { 126 | Job job = mock(Job.class); 127 | when(job.getName()).thenReturn("github-checks-plugin"); 128 | 129 | assertThatThrownBy(() -> GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, null)) 130 | .getRepository()) 131 | .isInstanceOf(IllegalStateException.class) 132 | .hasMessage("No GitHub SCM source found for job: github-checks-plugin"); 133 | } 134 | 135 | @Test 136 | void shouldGetCredentials() { 137 | Job job = mock(Job.class); 138 | GitHubSCMSource source = mock(GitHubSCMSource.class); 139 | GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); 140 | 141 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithCredentials(job, source, credentials, "1")) 142 | .getCredentials()) 143 | .isEqualTo(credentials); 144 | } 145 | 146 | @Test 147 | void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoCredentialsAvailable() { 148 | Job job = mock(Job.class); 149 | GitHubSCMSource source = mock(GitHubSCMSource.class); 150 | 151 | when(job.getName()).thenReturn("github-checks-plugin"); 152 | 153 | assertThatThrownBy(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithCredentials(job, source, 154 | null, null))::getCredentials) 155 | .isInstanceOf(IllegalStateException.class) 156 | .hasMessage("No GitHub APP credentials available for job: github-checks-plugin"); 157 | } 158 | 159 | @Test 160 | void shouldThrowIllegalStateExceptionWhenGetCredentialsButNoSourceAvailable() { 161 | Job job = mock(Job.class); 162 | SCMFacade scmFacade = mock(SCMFacade.class); 163 | 164 | when(job.getName()).thenReturn("github-checks-plugin"); 165 | 166 | assertThatThrownBy(GitHubSCMSourceChecksContext.fromJob(job, URL, scmFacade)::getCredentials) 167 | .isInstanceOf(IllegalStateException.class) 168 | .hasMessage("No GitHub APP credentials available for job: github-checks-plugin"); 169 | } 170 | 171 | @Test 172 | void shouldGetURLForJob() { 173 | Job job = mock(Job.class); 174 | 175 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, null)).getURL()) 176 | .isEqualTo(URL); 177 | } 178 | 179 | @Test 180 | void shouldGetURLForRun() { 181 | Run run = mock(Run.class); 182 | Job job = mock(Job.class); 183 | ClassicDisplayURLProvider urlProvider = mock(ClassicDisplayURLProvider.class); 184 | 185 | when(urlProvider.getRunURL(run)) 186 | .thenReturn("http://127.0.0.1:8080/job/github-checks-plugin/job/master/200"); 187 | 188 | assertThat(GitHubSCMSourceChecksContext.fromRun(run, urlProvider.getRunURL(run), 189 | createGitHubSCMFacadeWithSource(job, null)).getURL()) 190 | .isEqualTo("http://127.0.0.1:8080/job/github-checks-plugin/job/master/200"); 191 | } 192 | 193 | @Test 194 | void shouldReturnFalseWhenValidateContextButHasNoValidCredentials() { 195 | Job job = mock(Job.class); 196 | GitHubSCMSource source = mock(GitHubSCMSource.class); 197 | FilteredLog logger = new FilteredLog(""); 198 | 199 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, source)) 200 | .isValid(logger)) 201 | .isFalse(); 202 | assertThat(logger.getErrorMessages()).contains("No credentials found"); 203 | } 204 | 205 | @Test 206 | void shouldReturnFalseWhenValidateContextButHasNoValidGitHubAppCredentials() { 207 | Job job = mock(Job.class); 208 | GitHubSCMSource source = mock(GitHubSCMSource.class); 209 | FilteredLog logger = new FilteredLog(""); 210 | 211 | when(source.getCredentialsId()).thenReturn("oauth-credentials"); 212 | 213 | assertThat(GitHubSCMSourceChecksContext.fromJob(job, URL, createGitHubSCMFacadeWithSource(job, source)) 214 | .isValid(logger)) 215 | .isFalse(); 216 | assertThat(logger.getErrorMessages()) 217 | .contains("No GitHub app credentials found: 'oauth-credentials'") 218 | .contains("See: https://github.com/jenkinsci/github-branch-source-plugin/blob/master/docs/github-app.adoc"); 219 | } 220 | 221 | @Test 222 | void shouldReturnFalseWhenValidateContextButHasNoValidSHA() { 223 | Run run = mock(Run.class); 224 | Job job = mock(Job.class); 225 | GitHubSCMSource source = mock(GitHubSCMSource.class); 226 | GitHubAppCredentials credentials = mock(GitHubAppCredentials.class); 227 | FilteredLog logger = new FilteredLog(""); 228 | 229 | when(run.getParent()).thenReturn(job); 230 | 231 | when(source.getRepoOwner()).thenReturn("jenkinsci"); 232 | when(source.getRepository()).thenReturn("github-checks"); 233 | 234 | assertThat(GitHubSCMSourceChecksContext.fromRun(run, URL, createGitHubSCMFacadeWithCredentials(job, source, 235 | credentials, "1")).isValid(logger)) 236 | .isFalse(); 237 | assertThat(logger.getErrorMessages()).contains("No HEAD SHA found for jenkinsci/github-checks"); 238 | } 239 | 240 | private SCMFacade createGitHubSCMFacadeWithRevision(final Job job, final GitHubSCMSource source, 241 | final SCMHead head, final SCMRevision revision, 242 | final String hash) { 243 | SCMFacade facade = createGitHubSCMFacadeWithSource(job, source); 244 | 245 | when(facade.findHead(job)).thenReturn(Optional.ofNullable(head)); 246 | when(facade.findRevision(source, head)).thenReturn(Optional.ofNullable(revision)); 247 | when(facade.findHash(revision)).thenReturn(Optional.ofNullable(hash)); 248 | 249 | return facade; 250 | } 251 | 252 | private SCMFacade createGitHubSCMFacadeWithRevision(final Run run, final GitHubSCMSource source, 253 | final SCMRevision revision, final String hash) { 254 | SCMFacade facade = createGitHubSCMFacadeWithSource(run.getParent(), source); 255 | 256 | when(facade.findRevision(source, run)).thenReturn(Optional.of(revision)); 257 | when(facade.findHash(revision)).thenReturn(Optional.of(hash)); 258 | 259 | return facade; 260 | } 261 | 262 | private SCMFacade createGitHubSCMFacadeWithCredentials(final Job job, final GitHubSCMSource source, 263 | final GitHubAppCredentials credentials, 264 | final String credentialsId) { 265 | SCMFacade facade = createGitHubSCMFacadeWithSource(job, source); 266 | 267 | when(source.getCredentialsId()).thenReturn(credentialsId); 268 | when(facade.findGitHubAppCredentials(job, credentialsId)).thenReturn(Optional.ofNullable(credentials)); 269 | 270 | return facade; 271 | } 272 | 273 | private SCMFacade createGitHubSCMFacadeWithSource(final Job job, final GitHubSCMSource source) { 274 | SCMFacade facade = mock(SCMFacade.class); 275 | 276 | when(facade.findGitHubSCMSource(job)).thenReturn(Optional.ofNullable(source)); 277 | 278 | return facade; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriberTest/check-run-event-with-rerun-action-for-master.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "rerequested", 3 | "check_run": { 4 | "id": 993663296, 5 | "node_id": "MDg6Q2hlY2tSdW45OTM2NjMyOTY=", 6 | "head_sha": "b22fdfec1127073e509224a99a7704eeebdebe11", 7 | "external_id": "codingstyle/master#8", 8 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993663296", 9 | "html_url": "https://github.com/XiongKezhi/codingstyle/runs/993663296", 10 | "details_url": "http://127.0.0.1:8080/job/free-coding-style/8/display/redirect", 11 | "status": "completed", 12 | "conclusion": "failure", 13 | "started_at": "2020-08-17T13:01:07Z", 14 | "completed_at": "2020-08-17T13:01:07Z", 15 | "output": { 16 | "title": null, 17 | "summary": null, 18 | "text": null, 19 | "annotations_count": 0, 20 | "annotations_url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993663296/annotations" 21 | }, 22 | "name": "Jenkins", 23 | "check_suite": { 24 | "id": 1060102573, 25 | "node_id": "MDEwOkNoZWNrU3VpdGUxMDYwMTAyNTcz", 26 | "head_branch": "master", 27 | "head_sha": "b22fdfec1127073e509224a99a7704eeebdebe11", 28 | "status": "queued", 29 | "conclusion": null, 30 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-suites/1060102573", 31 | "before": "487d357398fc831d782b1fce1fef7606878f5560", 32 | "after": "b22fdfec1127073e509224a99a7704eeebdebe11", 33 | "pull_requests": [], 34 | "app": { 35 | "id": 76899, 36 | "slug": "jenkins-checks-api", 37 | "node_id": "MDM6QXBwNzY4OTk=", 38 | "owner": { 39 | "login": "XiongKezhi", 40 | "id": 30348893, 41 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 42 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 43 | "gravatar_id": "", 44 | "url": "https://api.github.com/users/XiongKezhi", 45 | "html_url": "https://github.com/XiongKezhi", 46 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 47 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 48 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 49 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 50 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 51 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 52 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 53 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 54 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 55 | "type": "User", 56 | "site_admin": false 57 | }, 58 | "name": "Jenkins Checks API", 59 | "description": "", 60 | "external_url": "https://smee.io/2C4VArPRXWl2zFg", 61 | "html_url": "https://github.com/apps/jenkins-checks-api", 62 | "created_at": "2020-08-14T08:47:10Z", 63 | "updated_at": "2020-08-14T09:04:57Z", 64 | "permissions": { 65 | "checks": "write", 66 | "contents": "write", 67 | "metadata": "read", 68 | "organization_hooks": "write", 69 | "pull_requests": "write", 70 | "repository_hooks": "write", 71 | "statuses": "write" 72 | }, 73 | "events": [ 74 | "check_run", 75 | "commit_comment", 76 | "create", 77 | "pull_request", 78 | "push", 79 | "status" 80 | ] 81 | }, 82 | "created_at": "2020-08-17T12:56:34Z", 83 | "updated_at": "2020-08-17T13:02:34Z" 84 | }, 85 | "app": { 86 | "id": 76899, 87 | "slug": "jenkins-checks-api", 88 | "node_id": "MDM6QXBwNzY4OTk=", 89 | "owner": { 90 | "login": "XiongKezhi", 91 | "id": 30348893, 92 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 93 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 94 | "gravatar_id": "", 95 | "url": "https://api.github.com/users/XiongKezhi", 96 | "html_url": "https://github.com/XiongKezhi", 97 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 98 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 99 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 100 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 101 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 102 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 103 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 104 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 105 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 106 | "type": "User", 107 | "site_admin": false 108 | }, 109 | "name": "Jenkins Checks API", 110 | "description": "", 111 | "external_url": "https://smee.io/2C4VArPRXWl2zFg", 112 | "html_url": "https://github.com/apps/jenkins-checks-api", 113 | "created_at": "2020-08-14T08:47:10Z", 114 | "updated_at": "2020-08-14T09:04:57Z", 115 | "permissions": { 116 | "checks": "write", 117 | "contents": "write", 118 | "metadata": "read", 119 | "organization_hooks": "write", 120 | "pull_requests": "write", 121 | "repository_hooks": "write", 122 | "statuses": "write" 123 | }, 124 | "events": [ 125 | "check_run", 126 | "commit_comment", 127 | "create", 128 | "pull_request", 129 | "push", 130 | "status" 131 | ] 132 | }, 133 | "pull_requests": [] 134 | }, 135 | "repository": { 136 | "id": 278546516, 137 | "node_id": "MDEwOlJlcG9zaXRvcnkyNzg1NDY1MTY=", 138 | "name": "codingstyle", 139 | "full_name": "XiongKezhi/codingstyle", 140 | "private": false, 141 | "owner": { 142 | "login": "XiongKezhi", 143 | "id": 30348893, 144 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 145 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 146 | "gravatar_id": "", 147 | "url": "https://api.github.com/users/XiongKezhi", 148 | "html_url": "https://github.com/XiongKezhi", 149 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 150 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 151 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 152 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 153 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 154 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 155 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 156 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 157 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 158 | "type": "User", 159 | "site_admin": false 160 | }, 161 | "html_url": "https://github.com/XiongKezhi/codingstyle", 162 | "description": "Java coding style and template project used at Munich university of applied sciences ", 163 | "fork": true, 164 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 165 | "forks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/forks", 166 | "keys_url": "https://api.github.com/repos/XiongKezhi/codingstyle/keys{/key_id}", 167 | "collaborators_url": "https://api.github.com/repos/XiongKezhi/codingstyle/collaborators{/collaborator}", 168 | "teams_url": "https://api.github.com/repos/XiongKezhi/codingstyle/teams", 169 | "hooks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/hooks", 170 | "issue_events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/events{/number}", 171 | "events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/events", 172 | "assignees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/assignees{/user}", 173 | "branches_url": "https://api.github.com/repos/XiongKezhi/codingstyle/branches{/branch}", 174 | "tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/tags", 175 | "blobs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/blobs{/sha}", 176 | "git_tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/tags{/sha}", 177 | "git_refs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/refs{/sha}", 178 | "trees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/trees{/sha}", 179 | "statuses_url": "https://api.github.com/repos/XiongKezhi/codingstyle/statuses/{sha}", 180 | "languages_url": "https://api.github.com/repos/XiongKezhi/codingstyle/languages", 181 | "stargazers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/stargazers", 182 | "contributors_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contributors", 183 | "subscribers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscribers", 184 | "subscription_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscription", 185 | "commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/commits{/sha}", 186 | "git_commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/commits{/sha}", 187 | "comments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/comments{/number}", 188 | "issue_comment_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/comments{/number}", 189 | "contents_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contents/{+path}", 190 | "compare_url": "https://api.github.com/repos/XiongKezhi/codingstyle/compare/{base}...{head}", 191 | "merges_url": "https://api.github.com/repos/XiongKezhi/codingstyle/merges", 192 | "archive_url": "https://api.github.com/repos/XiongKezhi/codingstyle/{archive_format}{/ref}", 193 | "downloads_url": "https://api.github.com/repos/XiongKezhi/codingstyle/downloads", 194 | "issues_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues{/number}", 195 | "pulls_url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls{/number}", 196 | "milestones_url": "https://api.github.com/repos/XiongKezhi/codingstyle/milestones{/number}", 197 | "notifications_url": "https://api.github.com/repos/XiongKezhi/codingstyle/notifications{?since,all,participating}", 198 | "labels_url": "https://api.github.com/repos/XiongKezhi/codingstyle/labels{/name}", 199 | "releases_url": "https://api.github.com/repos/XiongKezhi/codingstyle/releases{/id}", 200 | "deployments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/deployments", 201 | "created_at": "2020-07-10T05:28:50Z", 202 | "updated_at": "2020-08-17T12:56:37Z", 203 | "pushed_at": "2020-08-17T12:56:34Z", 204 | "git_url": "git://github.com/XiongKezhi/codingstyle.git", 205 | "ssh_url": "git@github.com:XiongKezhi/codingstyle.git", 206 | "clone_url": "https://github.com/XiongKezhi/codingstyle.git", 207 | "svn_url": "https://github.com/XiongKezhi/codingstyle", 208 | "homepage": "", 209 | "size": 999, 210 | "stargazers_count": 0, 211 | "watchers_count": 0, 212 | "language": "Java", 213 | "has_issues": false, 214 | "has_projects": true, 215 | "has_downloads": true, 216 | "has_wiki": true, 217 | "has_pages": false, 218 | "forks_count": 3, 219 | "mirror_url": null, 220 | "archived": false, 221 | "disabled": false, 222 | "open_issues_count": 5, 223 | "license": { 224 | "key": "other", 225 | "name": "Other", 226 | "spdx_id": "NOASSERTION", 227 | "url": null, 228 | "node_id": "MDc6TGljZW5zZTA=" 229 | }, 230 | "forks": 3, 231 | "open_issues": 5, 232 | "watchers": 0, 233 | "default_branch": "master" 234 | }, 235 | "sender": { 236 | "login": "XiongKezhi", 237 | "id": 30348893, 238 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 239 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 240 | "gravatar_id": "", 241 | "url": "https://api.github.com/users/XiongKezhi", 242 | "html_url": "https://github.com/XiongKezhi", 243 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 244 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 245 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 246 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 247 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 248 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 249 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 250 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 251 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 252 | "type": "User", 253 | "site_admin": false 254 | }, 255 | "installation": { 256 | "id": 11244691, 257 | "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTEyNDQ2OTE=" 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/test/resources/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriberTest/check-run-event-with-rerun-action-for-pr.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "rerequested", 3 | "check_run": { 4 | "id": 993923912, 5 | "node_id": "MDg6Q2hlY2tSdW45OTM5MjM5MTI=", 6 | "head_sha": "3c0ea12c02129ff4919afe087616d84547d93539", 7 | "external_id": "codingstyle/PR-1#2", 8 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993923912", 9 | "html_url": "https://github.com/XiongKezhi/codingstyle/runs/993923912", 10 | "details_url": "http://127.0.0.1:8080/job/pipeline-coding-style/job/PR-1/2/display/redirect", 11 | "status": "completed", 12 | "conclusion": "failure", 13 | "started_at": "2020-08-17T14:04:06Z", 14 | "completed_at": "2020-08-17T14:04:06Z", 15 | "output": { 16 | "title": null, 17 | "summary": null, 18 | "text": null, 19 | "annotations_count": 0, 20 | "annotations_url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-runs/993923912/annotations" 21 | }, 22 | "name": "Jenkins", 23 | "check_suite": { 24 | "id": 1060407784, 25 | "node_id": "MDEwOkNoZWNrU3VpdGUxMDYwNDA3Nzg0", 26 | "head_branch": "simplify-jenkinsfile", 27 | "head_sha": "3c0ea12c02129ff4919afe087616d84547d93539", 28 | "status": "queued", 29 | "conclusion": null, 30 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/check-suites/1060407784", 31 | "before": "2a5453f64dc2dc2433411afbbbfaf757e8ff1793", 32 | "after": "3c0ea12c02129ff4919afe087616d84547d93539", 33 | "pull_requests": [ 34 | { 35 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls/1", 36 | "id": 447475694, 37 | "number": 1, 38 | "head": { 39 | "ref": "simplify-jenkinsfile", 40 | "sha": "3c0ea12c02129ff4919afe087616d84547d93539", 41 | "repo": { 42 | "id": 278546516, 43 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 44 | "name": "codingstyle" 45 | } 46 | }, 47 | "base": { 48 | "ref": "master", 49 | "sha": "b22fdfec1127073e509224a99a7704eeebdebe11", 50 | "repo": { 51 | "id": 278546516, 52 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 53 | "name": "codingstyle" 54 | } 55 | } 56 | } 57 | ], 58 | "app": { 59 | "id": 76899, 60 | "slug": "jenkins-checks-api", 61 | "node_id": "MDM6QXBwNzY4OTk=", 62 | "owner": { 63 | "login": "XiongKezhi", 64 | "id": 30348893, 65 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 66 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 67 | "gravatar_id": "", 68 | "url": "https://api.github.com/users/XiongKezhi", 69 | "html_url": "https://github.com/XiongKezhi", 70 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 71 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 72 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 73 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 74 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 75 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 76 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 77 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 78 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 79 | "type": "User", 80 | "site_admin": false 81 | }, 82 | "name": "Jenkins Checks API", 83 | "description": "", 84 | "external_url": "https://smee.io/2C4VArPRXWl2zFg", 85 | "html_url": "https://github.com/apps/jenkins-checks-api", 86 | "created_at": "2020-08-14T08:47:10Z", 87 | "updated_at": "2020-08-14T09:04:57Z", 88 | "permissions": { 89 | "checks": "write", 90 | "contents": "write", 91 | "metadata": "read", 92 | "organization_hooks": "write", 93 | "pull_requests": "write", 94 | "repository_hooks": "write", 95 | "statuses": "write" 96 | }, 97 | "events": [ 98 | "check_run", 99 | "commit_comment", 100 | "create", 101 | "pull_request", 102 | "push", 103 | "status" 104 | ] 105 | }, 106 | "created_at": "2020-08-17T14:02:27Z", 107 | "updated_at": "2020-08-17T14:04:50Z" 108 | }, 109 | "app": { 110 | "id": 76899, 111 | "slug": "jenkins-checks-api", 112 | "node_id": "MDM6QXBwNzY4OTk=", 113 | "owner": { 114 | "login": "XiongKezhi", 115 | "id": 30348893, 116 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 117 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 118 | "gravatar_id": "", 119 | "url": "https://api.github.com/users/XiongKezhi", 120 | "html_url": "https://github.com/XiongKezhi", 121 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 122 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 123 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 124 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 125 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 126 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 127 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 128 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 129 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 130 | "type": "User", 131 | "site_admin": false 132 | }, 133 | "name": "Jenkins Checks API", 134 | "description": "", 135 | "external_url": "https://smee.io/2C4VArPRXWl2zFg", 136 | "html_url": "https://github.com/apps/jenkins-checks-api", 137 | "created_at": "2020-08-14T08:47:10Z", 138 | "updated_at": "2020-08-14T09:04:57Z", 139 | "permissions": { 140 | "checks": "write", 141 | "contents": "write", 142 | "metadata": "read", 143 | "organization_hooks": "write", 144 | "pull_requests": "write", 145 | "repository_hooks": "write", 146 | "statuses": "write" 147 | }, 148 | "events": [ 149 | "check_run", 150 | "commit_comment", 151 | "create", 152 | "pull_request", 153 | "push", 154 | "status" 155 | ] 156 | }, 157 | "pull_requests": [ 158 | { 159 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls/1", 160 | "id": 447475694, 161 | "number": 1, 162 | "head": { 163 | "ref": "simplify-jenkinsfile", 164 | "sha": "3c0ea12c02129ff4919afe087616d84547d93539", 165 | "repo": { 166 | "id": 278546516, 167 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 168 | "name": "codingstyle" 169 | } 170 | }, 171 | "base": { 172 | "ref": "master", 173 | "sha": "b22fdfec1127073e509224a99a7704eeebdebe11", 174 | "repo": { 175 | "id": 278546516, 176 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 177 | "name": "codingstyle" 178 | } 179 | } 180 | } 181 | ] 182 | }, 183 | "repository": { 184 | "id": 278546516, 185 | "node_id": "MDEwOlJlcG9zaXRvcnkyNzg1NDY1MTY=", 186 | "name": "codingstyle", 187 | "full_name": "XiongKezhi/codingstyle", 188 | "private": false, 189 | "owner": { 190 | "login": "XiongKezhi", 191 | "id": 30348893, 192 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 193 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 194 | "gravatar_id": "", 195 | "url": "https://api.github.com/users/XiongKezhi", 196 | "html_url": "https://github.com/XiongKezhi", 197 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 198 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 199 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 200 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 201 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 202 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 203 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 204 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 205 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 206 | "type": "User", 207 | "site_admin": false 208 | }, 209 | "html_url": "https://github.com/XiongKezhi/codingstyle", 210 | "description": "Java coding style and template project used at Munich university of applied sciences ", 211 | "fork": true, 212 | "url": "https://api.github.com/repos/XiongKezhi/codingstyle", 213 | "forks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/forks", 214 | "keys_url": "https://api.github.com/repos/XiongKezhi/codingstyle/keys{/key_id}", 215 | "collaborators_url": "https://api.github.com/repos/XiongKezhi/codingstyle/collaborators{/collaborator}", 216 | "teams_url": "https://api.github.com/repos/XiongKezhi/codingstyle/teams", 217 | "hooks_url": "https://api.github.com/repos/XiongKezhi/codingstyle/hooks", 218 | "issue_events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/events{/number}", 219 | "events_url": "https://api.github.com/repos/XiongKezhi/codingstyle/events", 220 | "assignees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/assignees{/user}", 221 | "branches_url": "https://api.github.com/repos/XiongKezhi/codingstyle/branches{/branch}", 222 | "tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/tags", 223 | "blobs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/blobs{/sha}", 224 | "git_tags_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/tags{/sha}", 225 | "git_refs_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/refs{/sha}", 226 | "trees_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/trees{/sha}", 227 | "statuses_url": "https://api.github.com/repos/XiongKezhi/codingstyle/statuses/{sha}", 228 | "languages_url": "https://api.github.com/repos/XiongKezhi/codingstyle/languages", 229 | "stargazers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/stargazers", 230 | "contributors_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contributors", 231 | "subscribers_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscribers", 232 | "subscription_url": "https://api.github.com/repos/XiongKezhi/codingstyle/subscription", 233 | "commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/commits{/sha}", 234 | "git_commits_url": "https://api.github.com/repos/XiongKezhi/codingstyle/git/commits{/sha}", 235 | "comments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/comments{/number}", 236 | "issue_comment_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues/comments{/number}", 237 | "contents_url": "https://api.github.com/repos/XiongKezhi/codingstyle/contents/{+path}", 238 | "compare_url": "https://api.github.com/repos/XiongKezhi/codingstyle/compare/{base}...{head}", 239 | "merges_url": "https://api.github.com/repos/XiongKezhi/codingstyle/merges", 240 | "archive_url": "https://api.github.com/repos/XiongKezhi/codingstyle/{archive_format}{/ref}", 241 | "downloads_url": "https://api.github.com/repos/XiongKezhi/codingstyle/downloads", 242 | "issues_url": "https://api.github.com/repos/XiongKezhi/codingstyle/issues{/number}", 243 | "pulls_url": "https://api.github.com/repos/XiongKezhi/codingstyle/pulls{/number}", 244 | "milestones_url": "https://api.github.com/repos/XiongKezhi/codingstyle/milestones{/number}", 245 | "notifications_url": "https://api.github.com/repos/XiongKezhi/codingstyle/notifications{?since,all,participating}", 246 | "labels_url": "https://api.github.com/repos/XiongKezhi/codingstyle/labels{/name}", 247 | "releases_url": "https://api.github.com/repos/XiongKezhi/codingstyle/releases{/id}", 248 | "deployments_url": "https://api.github.com/repos/XiongKezhi/codingstyle/deployments", 249 | "created_at": "2020-07-10T05:28:50Z", 250 | "updated_at": "2020-08-17T12:56:37Z", 251 | "pushed_at": "2020-08-17T14:02:28Z", 252 | "git_url": "git://github.com/XiongKezhi/codingstyle.git", 253 | "ssh_url": "git@github.com:XiongKezhi/codingstyle.git", 254 | "clone_url": "https://github.com/XiongKezhi/codingstyle.git", 255 | "svn_url": "https://github.com/XiongKezhi/codingstyle", 256 | "homepage": "", 257 | "size": 1003, 258 | "stargazers_count": 0, 259 | "watchers_count": 0, 260 | "language": "Java", 261 | "has_issues": false, 262 | "has_projects": true, 263 | "has_downloads": true, 264 | "has_wiki": true, 265 | "has_pages": false, 266 | "forks_count": 3, 267 | "mirror_url": null, 268 | "archived": false, 269 | "disabled": false, 270 | "open_issues_count": 5, 271 | "license": { 272 | "key": "other", 273 | "name": "Other", 274 | "spdx_id": "NOASSERTION", 275 | "url": null, 276 | "node_id": "MDc6TGljZW5zZTA=" 277 | }, 278 | "forks": 3, 279 | "open_issues": 5, 280 | "watchers": 0, 281 | "default_branch": "master" 282 | }, 283 | "sender": { 284 | "login": "XiongKezhi", 285 | "id": 30348893, 286 | "node_id": "MDQ6VXNlcjMwMzQ4ODkz", 287 | "avatar_url": "https://avatars1.githubusercontent.com/u/30348893?v=4", 288 | "gravatar_id": "", 289 | "url": "https://api.github.com/users/XiongKezhi", 290 | "html_url": "https://github.com/XiongKezhi", 291 | "followers_url": "https://api.github.com/users/XiongKezhi/followers", 292 | "following_url": "https://api.github.com/users/XiongKezhi/following{/other_user}", 293 | "gists_url": "https://api.github.com/users/XiongKezhi/gists{/gist_id}", 294 | "starred_url": "https://api.github.com/users/XiongKezhi/starred{/owner}{/repo}", 295 | "subscriptions_url": "https://api.github.com/users/XiongKezhi/subscriptions", 296 | "organizations_url": "https://api.github.com/users/XiongKezhi/orgs", 297 | "repos_url": "https://api.github.com/users/XiongKezhi/repos", 298 | "events_url": "https://api.github.com/users/XiongKezhi/events{/privacy}", 299 | "received_events_url": "https://api.github.com/users/XiongKezhi/received_events", 300 | "type": "User", 301 | "site_admin": false 302 | }, 303 | "installation": { 304 | "id": 11244691, 305 | "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTEyNDQ2OTE=" 306 | } 307 | } 308 | --------------------------------------------------------------------------------