├── 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 |
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 extends SCMSourceContext> 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 extends SCMSource> 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 | [](https://gitter.im/jenkinsci/github-checks-api)
4 | [](https://github.com/XiongKezhi/checks-api-plugin/issues)
5 | [](https://ci.jenkins.io/job/Plugins/job/github-checks-plugin/job/master/)
6 | [](https://github.com/jenkinsci/github-checks-plugin/actions)
7 | [](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 | [](https://codecov.io/gh/jenkinsci/github-checks-plugin)
9 |
10 | 
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 | 
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 | 
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 | 
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 | 
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 extends SCMSourceContext> 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 extends SCMSource> 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 extends SCM> 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