├── .github ├── CODEOWNERS ├── auto_assign.yml ├── dependabot.yml ├── labels.yml └── workflows │ ├── assign-pr.yml │ ├── cd.yaml │ └── sync-labels.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── CHANGELOG.md ├── Jenkinsfile ├── LICENSE ├── README.md ├── codacy └── checkstyle.xml ├── etc ├── Jenkinsfile └── images │ ├── add-portlet.png │ ├── demo-1.3.0-beta.gif │ ├── example-monitor-overview.png │ ├── missing-portlet.png │ ├── portlets │ ├── code-coverage-api.png │ └── warnings-next-generation.png │ └── settings.png ├── pom.xml └── src ├── main ├── java │ └── io │ │ └── jenkins │ │ └── plugins │ │ └── monitoring │ │ ├── DemoPortlet.java │ │ ├── Monitor.java │ │ ├── MonitorConfigurationProperty.java │ │ ├── MonitorPortlet.java │ │ ├── MonitorPortletFactory.java │ │ ├── MonitoringCustomAction.java │ │ ├── MonitoringDefaultAction.java │ │ ├── MonitoringDefaultActionFactory.java │ │ ├── MonitoringMultibranchProjectAction.java │ │ ├── MonitoringMultibranchProjectActionFactory.java │ │ ├── MonitoringWorkflowJobAction.java │ │ ├── MonitoringWorkflowJobActionFactory.java │ │ └── util │ │ ├── PortletUtils.java │ │ └── PullRequestUtils.java ├── resources │ ├── alerts │ │ ├── taglib │ │ ├── warning.jelly │ │ └── warning.properties │ ├── index.jelly │ ├── io │ │ └── jenkins │ │ │ └── plugins │ │ │ └── monitoring │ │ │ ├── DemoPortlet │ │ │ └── monitor.jelly │ │ │ ├── Messages.properties │ │ │ ├── MonitorConfigurationProperty │ │ │ ├── config.jelly │ │ │ └── config.properties │ │ │ ├── MonitoringDefaultAction │ │ │ ├── index.jelly │ │ │ └── index.properties │ │ │ ├── MonitoringMultibranchProjectAction │ │ │ ├── index.jelly │ │ │ └── index.properties │ │ │ └── MonitoringWorkflowJobAction │ │ │ └── index.jelly │ ├── metadata │ │ ├── metadata.jelly │ │ └── taglib │ ├── modals │ │ ├── add-portlet.jelly │ │ ├── add-portlet.properties │ │ ├── config.jelly │ │ ├── config.properties │ │ └── taglib │ ├── portlet │ │ ├── portlet.jelly │ │ ├── portlet.properties │ │ └── taglib │ └── schema.json └── webapp │ ├── css │ ├── jsontree.css │ └── pull-request-monitoring.css │ ├── icons │ ├── JCasC.svg │ ├── line-graph-32x32.png │ ├── line-graph-48x48.png │ └── line-graph-64x64.png │ └── js │ ├── jsontree.js │ └── pull-request-monitoring.js └── test ├── java └── io │ └── jenkins │ └── plugins │ └── monitoring │ └── MonitorTest.java └── resources └── io └── jenkins └── plugins └── monitoring ├── Jenkinsfile.custom ├── Jenkinsfile.customEmpty ├── Jenkinsfile.emptyStage ├── Jenkinsfile.error └── Jenkinsfile.error2 /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/pull-request-monitoring-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: false 2 | addAssignees: true 3 | 4 | assignees: 5 | - simonsymhoven 6 | 7 | skipKeywords: 8 | - wip 9 | 10 | numberOfAssignees: 0 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | target-branch: master 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: bug 2 | description: Bugs or performance problems 3 | color: CC0000 4 | - name: feature 5 | color: 1d76db 6 | description: New features 7 | - name: enhancement 8 | description: Enhancement of existing functionality 9 | color: 0366d6 10 | - name: deprecated 11 | description: Deprecating API 12 | color: f4c21d 13 | - name: removed 14 | description: Removing API 15 | color: e4b21d 16 | - name: tests 17 | description: Enhancement of tests 18 | color: 0e8a16 19 | - name: documentation 20 | description: Enhancement of documentation 21 | color: bfafea 22 | - name: internal 23 | description: Internal changes without user or API impact 24 | color: e6e6e6 25 | - name: dependencies 26 | description: Update of dependencies 27 | color: e6e6e6 28 | - name: java 29 | desccription: Pull requests that update Maven Java dependencies 30 | color: b6b6b6 31 | - name: github_actions 32 | desccription: Pull requests that update Github Actions code 33 | color: 909090 -------------------------------------------------------------------------------- /.github/workflows/assign-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign PR' 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | add-reviews: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: kentaro-m/auto-assign-action@v2.0.0 10 | with: 11 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 12 | -------------------------------------------------------------------------------- /.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 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: micnncim/action-label-syncer@v1 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # IDEA Files 26 | *.iml 27 | .idea 28 | 29 | target 30 | work 31 | *.DS_Store 32 | 33 | node_modules 34 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes of this project will be automatically logged by release drafter in 4 | [GitHub releases](https://github.com/jenkinsci/pull-request-monitoring-plugin/releases). 5 | 6 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* 2 | See the documentation for more options: 3 | https://github.com/jenkins-infra/pipeline-library/ 4 | */ 5 | buildPlugin( 6 | forkCount: '1C', // run this number of tests in parallel for faster feedback. If the number terminates with a 'C', the value will be multiplied by the number of available CPU cores 7 | useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests 8 | configurations: [ 9 | [platform: 'linux', jdk: 21], 10 | [platform: 'windows', jdk: 17], 11 | ]) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Simon Symhoven 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/jenkinsci/pull-request-monitoring.svg)](https://gitter.im/jenkinsci/pull-request-monitoring?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | ![GitHub](https://img.shields.io/github/license/jenkinsci/pull-request-monitoring-plugin) 3 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/jenkinsci/pull-request-monitoring-plugin) 4 | ![Open GitHub issues](https://img.shields.io/github/issues/jenkinsci/pull-request-monitoring-plugin) 5 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/jenkinsci/pull-request-monitoring-plugin/GitHub%20CI/master?label=GitHub%20CI) 6 | [![Build Status](https://ci.jenkins.io/buildStatus/icon?subject=Jenkins%20CI&job=Plugins%2Fpull-request-monitoring-plugin%2Fmaster)](https://ci.jenkins.io/job/Plugins/job/pull-request-monitoring-plugin/job/master/) 7 | ![Contributions](https://img.shields.io/badge/contributions-welcome-orange) 8 | ![Jenkins Plugins](https://img.shields.io/jenkins/plugin/v/pull-request-monitoring?label=latest%20version) 9 | ![Jenkins Plugin installs](https://img.shields.io/jenkins/plugin/i/pull-request-monitoring) 10 | [![codecov](https://codecov.io/gh/jenkinsci/pull-request-monitoring-plugin/branch/master/graph/badge.svg?token=8tAFvX7eSJ)](https://codecov.io/gh/jenkinsci/pull-request-monitoring-plugin) 11 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/07246f679cbc4bc280b92dabc4425a17)](https://www.codacy.com/gh/jenkinsci/pull-request-monitoring-plugin/dashboard?utm_source=github.com&utm_medium=referral&utm_content=jenkinsci/pull-request-monitoring-plugin&utm_campaign=Badge_Grade) 12 | 13 | 14 |
15 |

16 | 17 | Logo 18 | 19 | 20 |

Pull Request Monitoring

21 | 22 |

23 | Jenkins plugin to monitor pull requests with a customizable dashboard. 24 |
25 | Explore the docs » 26 |
27 |
28 | Report Bug 29 | · 30 | Request Feature 31 |

32 |

33 | 34 | At the [Jenkins UX SIG Meeting](https://www.youtube.com/watch?v=F1ISpA7K0YA) on 28. April 2021, there was a live demo of the first beta version (1.0.3-beta). 35 | 36 | 37 |
38 |

Table of Contents

39 |
    40 |
  1. 41 | About The Project 42 | 45 |
  2. 46 |
  3. 47 | Getting Started 48 | 62 |
  4. 63 |
  5. Usage 64 | 75 |
  6. 76 |
  7. Available Portlets
  8. 77 |
  9. Demo 78 |
  10. Roadmap
  11. 79 |
  12. Contributing
  13. 80 |
  14. License
  15. 81 |
  16. Credits
  17. 82 |
  18. Contact
  19. 83 |
84 |
85 | 86 | # About The Project 87 | 88 | Many software teams have changed their development processes to lightweight pull requests. 89 | Changes to the software are packed into such a pull request, 90 | which is then manually reviewed and also automatically built in the CI/CD server. 91 | In Jenkins, however, these pull requests are not "first-class citizens"; the results of a pull request 92 | are currently simulated via branches. 93 | 94 | For developers, this representation is insufficient: instead of having Git diffs, tests, static analysis, etc. 95 | as an overview in the overall project, a filtered representation of these results on the changes actually made 96 | would be much more helpful. 97 | 98 | This plugin offers a possibility to display and aggregate the results (in the form of individual views) of a pull 99 | request in a configurable dashboard. Views can only be accessed or displayed if the corresponding plugin fulfils 100 | certain requirements and already provides a view. 101 | 102 | ## Built With 103 | 104 | * [Muuri](https://github.com/haltu/muuri) wrapped in [Jenkins Muuri.js API Plugin](https://github.com/jenkinsci/muuri-api-plugin) 105 | * [Select2](https://select2.org) wrapped in [Jenkins Select2.js API Plugin](https://github.com/jenkinsci/select2-api-plugin) 106 | 107 | # Getting Started 108 | 109 | ## Prerequisites 110 | 111 | Currently, only **multibranch pipelines** projects are supported to use this plugin. Therefore, you have to 112 | install the corresponding Jenkins plugin [Multibranch: Pipeline](https://plugins.jenkins.io/workflow-multibranch/) 113 | and connect to own of your SCM Repositories to use the **Pull Request Monitoring** Jenkins plugin. 114 | 115 | ## Provide a portlet 116 | 117 | This plugin relies on other plugins to provide a view that aggregates and provides delta metrics for a pull request. 118 | 119 | ### The code behind 120 | 121 | The [MonitorPortlet](src/main/java/io/jenkins/plugins/monitoring/MonitorPortlet.java) class defines the base class 122 | for each portlet that is to be displayed. In order to register the portlet for the plugin, 123 | a factory class is required, which must be provided with the annotation `@Extension`. 124 | The factory has to extend the [MonitorPortletFactory](src/main/java/io/jenkins/plugins/monitoring/MonitorPortletFactory.java) 125 | abstract class, receives the current `Run`, delivers a set of `MonitorPortlets` and defines a `displayedName`, 126 | which appears as `optgroup` in the dashboard in the dropdown list of all available portlets and their corresponding 127 | factory. 128 | 129 | ![Add portlet](etc/images/add-portlet.png) 130 | 131 | #### One Instance Of One Portlet 132 | 133 | Normally, one plugin delivers one portlet. A minimal example could look as follows: 134 | 135 | ```java 136 | /** 137 | * An example Monitor Portlet implementation with one portlet delivered by factory. 138 | */ 139 | public class DemoPortlet extends MonitorPortlet { 140 | private final Run build; 141 | 142 | /** 143 | * Create a new {@link DemoPortlet}. 144 | * 145 | * @param run 146 | * the {@link Run} 147 | */ 148 | public DemoPortlet(Run run) { 149 | this.build = run; 150 | } 151 | 152 | /** 153 | * Defines the title of portlet. It will be shown in the 154 | * upper left corner of the portlet. 155 | * 156 | * @return 157 | * the title as string. 158 | */ 159 | @Override 160 | public String getTitle() { 161 | return "Demo Portlet"; 162 | } 163 | 164 | /** 165 | * Defines the id for the portlet. 166 | * 167 | * @return 168 | * the id. 169 | */ 170 | @Override 171 | public String getId() { 172 | return "demo-portlet-id"; 173 | } 174 | 175 | /** 176 | * Defines whether the portlet is shown per default in the user dashboard or not. 177 | * 178 | * @return 179 | * true if portlet should be shown, false else. 180 | */ 181 | @Override 182 | public boolean isDefault() { 183 | return true; 184 | } 185 | 186 | /** 187 | * Defines the preferred width of the portlet. It's 188 | * possible to override the default width by user. 189 | * 190 | * @return 191 | * the width as int. (range from 100 to 1000) 192 | */ 193 | @Override 194 | public int getPreferredWidth() { 195 | return 300; 196 | } 197 | 198 | /** 199 | * Defines the preferred height of the portlet. It's 200 | * possible to override the default height by user. 201 | * 202 | * @return 203 | * the height as int. (range from 100 to 1000) 204 | */ 205 | @Override 206 | public int getPreferredHeight() { 207 | return 200; 208 | } 209 | 210 | /** 211 | * Defines the icon, which will be shown in the dropdown of 212 | * all available portlets in the dashboard. 213 | * 214 | * @return 215 | * the icon url as {@link java.util.Optional} of string, or an 216 | * empty Optional, if a default icon should be added. 217 | */ 218 | @Override 219 | public Optional getIconUrl() { 220 | return Optional.of(""); 221 | } 222 | 223 | /** 224 | * Defines a link to a detail view, if its needed. Links the title 225 | * of the portlet to this url. 226 | * 227 | * @return 228 | * {@link java.util.Optional} of the url, or an empty Optional, 229 | * if no link should be provided by portlet. 230 | */ 231 | @Override 232 | public Optional getDetailViewUrl() { 233 | return Optional.of(""); 234 | } 235 | 236 | /** 237 | * Creates a new {@link ExamplePortletFactory}. 238 | */ 239 | @Extension 240 | public static class ExamplePortletFactory extends MonitorPortletFactory { 241 | @Override 242 | public Collection getPortlets(Run build) { 243 | return Collections.singleton(new DemoPortlet(build)); 244 | } 245 | 246 | @Override 247 | public String getDisplayName() { 248 | return "Demo Portlet Factory"; 249 | } 250 | } 251 | } 252 | ``` 253 | 254 | > ⭕ **Null Check for used actions** 255 | > 256 | > Since an empty dashboard is always added by default, it is possible that the method `getPortlets(Run)` will 257 | > be called (e.g. open the dashboard of actual run) even though the corresponding run may not be finished. 258 | > It is possible that actions have not yet been added to the run. 259 | > It is therefore advisable to perform a null check on the actions of the run required by your portlet and return 260 | > an empty list if necessary. 261 | > (Example: code-coverage-api)

262 | 263 | #### Multiple Instances Of One Portlet 264 | 265 | The factory can also deliver several portlets of one class. 266 | 267 | > ⚠️ **Unique portlet ID**: 268 | > 269 | > The id must be unique. Please make sure that the id is related to the plugin so that there are no conflicts with 270 | > other plugins. It is recommended to use the artifact id of the plugin or parts of it as prefix. 271 | > If several portlets of the same class are created in the factory, it must be ensured that the ID is always unique 272 | > for each portlet! 273 | 274 | Here is an example of a factory that delivers two instances of a class: 275 | 276 | ```java 277 | /** 278 | * An example Monitor Portlet implementation with multiple portlets delivered by factory. 279 | */ 280 | public class DemoPortlet extends MonitorPortlet { 281 | private final Run run; 282 | private final String id; 283 | 284 | /** 285 | * Create a new {@link DemoPortlet}. 286 | * 287 | * @param run 288 | * the {@link Run} 289 | * @param id 290 | * the id. 291 | */ 292 | public DemoPortlet(Run run, String id) { 293 | this.run = run; 294 | this.id = id; 295 | } 296 | 297 | @Override 298 | public String getTitle() { 299 | return "Demo Portlet " + getId(); 300 | } 301 | 302 | @Override 303 | public String getId() { 304 | return id; 305 | } 306 | 307 | // other interface methods, see example above. 308 | 309 | /** 310 | * Creates a new {@link ExamplePortletFactory}. 311 | */ 312 | @Extension 313 | public static class ExamplePortletFactory extends MonitorPortletFactory { 314 | @Override 315 | public Collection getPortlets(Run build) { 316 | List portlets = new ArrayList<>(); 317 | portlets.add(new DemoPortlet(build, "example-portlet-first")); 318 | portlets.add(new DemoPortlet(build, "example-portlet-second")); 319 | return monitors; 320 | } 321 | 322 | @Override 323 | public String getDisplayName() { 324 | return "Demo Portlet Factory"; 325 | } 326 | } 327 | } 328 | ``` 329 | 330 | ### The corresponding jelly file 331 | 332 | Each portlet have to have a corresponding `monitor.jelly` file, which is responsible for the content of the 333 | plugins portlet delivered on the dashboard later. Therefore you have to create a new `monitor.jelly` file 334 | in the directory, which corresponds to the `MonitorView` class. 335 | 336 | **Example**: 337 | The code behind is defined in `src/main/java/io/jenkins/plugins/sample/DemoPortlet.java`. The related sources 338 | (e.g. the `monitor.jelly` file) have to be defined in 339 | `src/main/resources/io/jenkins/plugins/sample/DemoPortlet/monitory.jelly`. 340 | 341 | Now the portlet, which can be added later in the dashboard, can be filled individually. 342 | Of course, all obligatory functions of the Jelly files, such as [JEXL](https://commons.apache.org/proper/commons-jexl/) 343 | calls, can be used. To do this, please refer to the official Jenkins documentation. 344 | A minimal example: 345 | 346 | ```xml 347 | 348 | 349 | 350 | 351 |

Portlet content goes here!

352 | 353 |
354 | ``` 355 | 356 | # Usage Of Monitoring 357 | 358 | ## Introduction 359 | This plugin offers the following monitoring options: 360 | 361 | * project level: [MonitoringMultibranchProjectAction](src/main/java/io/jenkins/plugins/monitoring/MonitoringMultibranchProjectAction.java) 362 | * build level: [MonitoringBuildAction](src/main/java/io/jenkins/plugins/monitoring/MonitoringBuildAction.java) 363 | 364 | ## Project Level 365 | 366 | The monitoring on the project level is very basic and is limited to the summary of all open pull requests of the associated SCM. 367 | From here, you can access the corresponding monitoring dashboards, view the pull request information and navigate 368 | to the respective pull request in the repository. 369 | 370 | ![Example Overview](etc/images/example-monitor-overview.png) 371 | 372 | ## Build Level 373 | 374 | The monitoring at build level is the core of the plugin and offers the possibility 375 | to observe various delta metrics of a pull request provided by other portlets. 376 | 377 | The dashboard is only added to those builds whose branch 378 | [SCMHead](https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/SCMHead.html) of the parent job is an instance of 379 | [ChangeRequestSCMHead](https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/mixin/ChangeRequestSCMHead.html). 380 | Otherwise, the corresponding [MonitoringBuildAction](src/main/java/io/jenkins/plugins/monitoring/MonitoringBuildAction.java) 381 | will not be added and no dashboard will be provided for the build. 382 | For more details, please refer to the logging in the console output of the respective build. 383 | 384 | ### Persistence & Permissions 385 | 386 | The configuration, which is set via the Jenkinsfile or via the Dahsboard, is saved per user as a 387 | `hudson.model.UserProperty` and is browser independent. The plugin therefore requires that you are logged in 388 | and have the appropriate permissions. Otherwise, the action will not be displayed! 389 | 390 | ### Default Dashboard 391 | 392 | > 💡 **Reference build search**: 393 | > 394 | > The [git-forensics-api](https://www.jenkins.io/doc/pipeline/steps/git-forensics/) plugin allows to find a reference build for a pull request build. 395 | > It is highly recommended using this plugin and 396 | > to search for the reference build in the pipeline with `discoverGitReferenceBuild()` (or configure it via the Jenkins UI). 397 | > For more information please visit the [plugin page](https://www.jenkins.io/doc/pipeline/steps/git-forensics/) or have a 398 | > look into an example [pipeline](etc/Jenkinsfile). 399 | 400 | To start with the default dashboard, you have to do nothing. If the run is part of a pull request, the corresponding 401 | [MonitoringDefaultAction](src/main/java/io/jenkins/plugins/monitoring/MonitoringDefaultAction.java) is automatically 402 | added to the `Run` and later available on the sidepanel of the run. 403 | 404 | Now you are able to add portlets to the dashboard, change the layout or remove it again. 405 | The configuration will be saved for each project per user. If you want to save the configuration permanently, 406 | it is best to copy and paste it into the Jenkinsfile and overwrite the default dashboard and use a custom dashboard. 407 | 408 | > 🧩 **Default Portlets for Dashboard**: 409 | > 410 | > Since version 1.7.0, the portlets can decide whether they should be displayed by default or not in the 411 | > user dashboard. So when you start with the default dashboard, all available portlets 412 | > that are marked as default are automatically loaded into your dashboard. 413 | 414 | ### Custom Dashboard 415 | 416 | The other way to add a dashboard is to set the configuration of the dashboard via the Jenkinsfile to pre-define 417 | your dashboard. It is recommended to add the monitoring at the end of your pipeline, to ensure that other 418 | plugins such as static code analysis are performed first and that the actions that may be required are 419 | available: 420 | 421 | ```text 422 | stage ('Pull Request Monitoring - Dashboard Configuration') { 423 | monitoring ( 424 | ''' 425 | [ 426 | { 427 | // Minimal usage of one portlet 428 | "id": "portlet-id" 429 | }, 430 | { 431 | // Feel free to customize the portlets 432 | "id": "another-portlet-id", 433 | "width": 200, 434 | "height": 100, 435 | "color": "#FF5733" 436 | } 437 | ] 438 | ''' 439 | ) 440 | } 441 | ``` 442 | 443 | Therefore, the monitoring stage expects a JSONArray of JSONObjects. To validate your 444 | configured json, you could use a [JSON Schema Validator](https://www.jsonschemavalidator.net) and the 445 | corresponding [JSON schema](src/main/resources/schema.json) used for this plugin. 446 | Each JSONObject needs at least the `id` of the portlet to add. For `width` and `height`, the default 447 | `preferredWidth` and `preferredHeight` of the `MonitorPortlet` is used. `#000000` is used as default `color`. 448 | 449 | The dashboard will be added to your `Run` and the pre-defined monitor should be available. 450 | 451 | > 🔥 **Duplicate or missing portlet IDs**: 452 | > 453 | > Duplicates will be removed as well as missing portlet ids. The `Run` will not fail unless 454 | > the provided json does not follow the scheme! 455 | 456 | If there are unavailable portlets (e.g. corresponding plugin is uninstalled) in stored user configuration, 457 | and the dashboard tries to load this portlet, an alert will be displayed with the ids of the unavailable portlets: 458 | 459 | ![Unavailable Portlet](etc/images/missing-portlet.png) 460 | 461 | ### Settings 462 | 463 | Under the settings of each `Run`, various things can be tracked: 464 | 465 | 1. Are there changes of the pre-defined dashboard since the last build? 466 | 467 | 2. The current activated source of configuration (Default or User-specific). 468 | * Default means Jenkinsfile if `monitoring` is provided, else all available default portlets. 469 | 470 | * User-specific means the local changes of dashboard. 471 | 472 | 3. Configuration synced with the default one? If needed, you can synchronize the 473 | actual configuration with the default one. 474 | 475 | 4. The actual configuration 476 | 477 | 5. The default configuration. 478 | 479 | > ❗**Prioritising of the different configurations**: 480 | > 481 | > By default, all available default portlets are loaded. If there is a user-specific configuration for one 482 | > dashboard (per project), the user-specific configuration will be loaded per default. If you sync the current 483 | > configuration with the default one, the user-specific configuration for current dashboard will be deleted, and the 484 | > default configuration will be loaded. Deletion cannot be undone! 485 | 486 | ![Settings](etc/images/settings.png) 487 | 488 | # Available Portlets 489 | 490 | If your plugin is not in the list but provides a portlet, feel free to add it with a pull request! 491 | 492 | | | Plugin | Number of delivered portlets | Portlet ID | Default? | 493 | | --- | --- | --- | --- | ---| 494 | | ![Pull Request Monitoring](src/main/webapp/icons/line-graph-48x48.png) | [Pull Request Monitoring](https://plugins.jenkins.io/pull-request-monitoring/) | 2 | `first-demo-portlet`
`second-demo-portlet` | ❌ 495 | | ![Warnings Next Generation](etc/images/portlets/warnings-next-generation.png) | [Warnings Next Generation](https://plugins.jenkins.io/warnings-ng/) | ≥ 100 | depends on [tool](https://github.com/jenkinsci/warnings-ng-plugin/blob/master/SUPPORTED-FORMATS.md) | ✅ | 496 | | ![Code Coverage API](etc/images/portlets/code-coverage-api.png) | [Code Coverage API](https://plugins.jenkins.io/code-coverage-api/) | 1 | `code-coverage` | ✅ 497 | 498 | # Demo 499 | 500 | For the demo (v1.3.0-beta) I added two recorder (javadoc and pmd) of the 501 | [Warnings Ng Plugin](https://plugins.jenkins.io/warnings-ng/) to the pipeline and added both as 502 | default portlet to the `monitoring`. After the run finished, the portlets will be shown in 503 | the dashboard. 504 | 505 | ![Demo](etc/images/demo-1.3.0-beta.gif) 506 | 507 | # Roadmap 508 | 509 | See the [open issues](https://github.com/jenkinsci/pull-request-monitoring-plugin/issues) 510 | for a list of proposed features (and known issues). 511 | 512 | # Contributing 513 | 514 | Contributions are what make the open source community such an amazing place to be learn, 515 | inspire, and create. Any contributions you make are **greatly appreciated**. 516 | 517 | 1. Fork the Project 518 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 519 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 520 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 521 | 5. Open a Pull Request 522 | 523 | # License 524 | 525 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 526 | 527 | # Credits 528 | 529 | The following icons, which are used by this plugin 530 | 531 | * [Line Graph Icon 32x32 px](src/main/webapp/icons/line-graph-32x32.png) 532 | * [Line Graph Icon 64x64 px](src/main/webapp/icons/line-graph-64x64.png) 533 | 534 | made by [Freepik](https://www.freepik.com) from [Flaticon](https://www.flaticon.com/). 535 | 536 | # Contact 537 | 538 | Simon Symhoven - post@simon-symhoven.de 539 | 540 | Project Link: [https://github.com/jenkinsci/pull-request-monitoring-plugin](https://github.com/jenkinsci/pull-request-monitoring-plugin) -------------------------------------------------------------------------------- /codacy/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /etc/Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | 3 | stage ('Checkout') { 4 | checkout scm 5 | } 6 | 7 | stage ('Git mining') { 8 | discoverGitReferenceBuild() 9 | mineRepository() 10 | } 11 | 12 | stage ('Build and Static Analysis') { 13 | withMaven { 14 | sh 'mvn clean verify' 15 | } 16 | 17 | recordIssues tools: [javaDoc(), java()], aggregatingResults: 'true', id: 'java', name: 'Java' 18 | recordIssues tool: errorProne(), healthy: 1, unhealthy: 20 19 | recordIssues tools: [checkStyle(pattern: 'target/checkstyle-result.xml'), 20 | spotBugs(pattern: 'target/spotbugsXml.xml'), 21 | pmdParser(pattern: 'target/pmd.xml'), 22 | taskScanner(highTags:'FIXME', normalTags:'TODO', includePattern: '**/*.java', excludePattern: 'target/**/*')], 23 | qualityGates: [[threshold: 1, type: 'TOTAL', unstable: true]] 24 | recordIssues tool: mavenConsole() 25 | } 26 | 27 | 28 | stage ('Line and Branch Coverage') { 29 | withMaven { 30 | sh 'mvn jacoco:prepare-agent test jacoco:report -Dmaven.test.failure.ignore' 31 | } 32 | publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')], 33 | calculateDiffForChangeRequests: true 34 | } 35 | 36 | /* 37 | stage ('Pull Request Monitoring - Dashboard Configuration') { 38 | monitoring ( 39 | ''' 40 | [ 41 | { 42 | "id": "first-demo-portlet", 43 | "width": 400, 44 | "height": 400, 45 | "color": "#FF5733" 46 | } 47 | ] 48 | ''' 49 | ) 50 | } 51 | */ 52 | } 53 | -------------------------------------------------------------------------------- /etc/images/add-portlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/add-portlet.png -------------------------------------------------------------------------------- /etc/images/demo-1.3.0-beta.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/demo-1.3.0-beta.gif -------------------------------------------------------------------------------- /etc/images/example-monitor-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/example-monitor-overview.png -------------------------------------------------------------------------------- /etc/images/missing-portlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/missing-portlet.png -------------------------------------------------------------------------------- /etc/images/portlets/code-coverage-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/portlets/code-coverage-api.png -------------------------------------------------------------------------------- /etc/images/portlets/warnings-next-generation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/portlets/warnings-next-generation.png -------------------------------------------------------------------------------- /etc/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/etc/images/settings.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 4.87 9 | 10 | 11 | 12 | io.jenkins.plugins 13 | pull-request-monitoring 14 | hpi 15 | Pull Request Monitoring 16 | ${changelist} 17 | https://github.com/jenkinsci/${project.artifactId}-plugin 18 | 19 | 20 | 999999-SNAPSHOT 21 | 2.414 22 | ${jenkins.baseline}.3 23 | jenkinsci/${project.artifactId}-plugin 24 | 25 | 0.9.5-3 26 | 4.0.13-8 27 | 1.5.1 28 | 0.22.0 29 | 30 | 31 | 32 | 33 | 34 | io.jenkins.tools.bom 35 | bom-${jenkins.baseline}.x 36 | 2982.vdce2153031a_0 37 | pom 38 | import 39 | 40 | 41 | 42 | 43 | 44 | 45 | MIT license 46 | All source code is under the MIT license. 47 | 48 | 49 | 50 | 51 | 52 | Simon Symhoven 53 | simonsymhoven 54 | post@simon-symhoven.de 55 | 56 | 57 | 58 | 59 | scm:git:https://github.com/${gitHubRepo}.git 60 | scm:git:git@github.com:${gitHubRepo}.git 61 | https://github.com/${gitHubRepo} 62 | ${scmTag} 63 | 64 | 65 | 66 | 67 | io.jenkins.plugins 68 | plugin-util-api 69 | 70 | 71 | io.jenkins.plugins 72 | bootstrap5-api 73 | 74 | 75 | io.jenkins.plugins 76 | font-awesome-api 77 | 78 | 79 | io.jenkins.plugins 80 | jquery3-api 81 | 82 | 83 | io.jenkins.plugins 84 | muuri-api 85 | ${muuri-api.version} 86 | 87 | 88 | io.jenkins.plugins 89 | echarts-api 90 | 91 | 92 | io.jenkins.plugins 93 | select2-api 94 | ${select2-api.version} 95 | 96 | 97 | io.jenkins.plugins 98 | forensics-api 99 | 100 | 101 | org.everit.json 102 | org.everit.json.schema 103 | ${json-schema.version} 104 | 105 | 106 | json 107 | org.json 108 | 109 | 110 | 111 | 112 | org.jenkins-ci.plugins 113 | scm-api 114 | 115 | 116 | org.commonmark 117 | commonmark 118 | ${commonmark.version} 119 | 120 | 121 | io.jenkins.plugins 122 | commons-lang3-api 123 | 124 | 125 | 126 | 127 | org.jenkins-ci.plugins.workflow 128 | workflow-multibranch 129 | 130 | 131 | org.jenkins-ci.plugins.workflow 132 | workflow-api 133 | 134 | 135 | org.jenkins-ci.plugins.workflow 136 | workflow-job 137 | 138 | 139 | 140 | 141 | org.jenkins-ci.plugins.workflow 142 | workflow-basic-steps 143 | test 144 | 145 | 146 | org.jenkins-ci.plugins 147 | pipeline-stage-step 148 | test 149 | 150 | 151 | org.jenkins-ci.plugins 152 | scm-api 153 | tests 154 | test 155 | 156 | 157 | io.jenkins.plugins 158 | commons-text-api 159 | test 160 | 161 | 162 | 163 | 164 | 165 | 166 | maven-dependency-plugin 167 | 3.8.0 168 | 169 | 170 | org.jacoco 171 | jacoco-maven-plugin 172 | 173 | 174 | io/jenkins/plugins/**/* 175 | 176 | 177 | **/*Action* 178 | **/*Factory* 179 | **/*Property* 180 | **/*Portlet* 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | repo.jenkins-ci.org 190 | https://repo.jenkins-ci.org/public/ 191 | 192 | 193 | 194 | 195 | 196 | repo.jenkins-ci.org 197 | https://repo.jenkins-ci.org/public/ 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/DemoPortlet.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import hudson.Extension; 4 | import hudson.model.Run; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | 10 | /** 11 | * An example of {@link MonitorPortlet}. 12 | */ 13 | public class DemoPortlet extends MonitorPortlet { 14 | private final String id; 15 | private final String title; 16 | 17 | /** 18 | * Creates a new {@link DemoPortlet}. 19 | * 20 | * @param title 21 | * the title of the portlet. 22 | * 23 | * @param id 24 | * the id of the portlet. 25 | */ 26 | public DemoPortlet(final String title, final String id) { 27 | super(); 28 | this.id = id; 29 | this.title = title; 30 | } 31 | 32 | @Override 33 | public String getTitle() { 34 | return title; 35 | } 36 | 37 | @Override 38 | public String getId() { 39 | return id; 40 | } 41 | 42 | @Override 43 | public int getPreferredWidth() { 44 | return 300; 45 | } 46 | 47 | @Override 48 | public int getPreferredHeight() { 49 | return 200; 50 | } 51 | 52 | /** 53 | * An example of {@link MonitorPortletFactory}. 54 | */ 55 | @Extension 56 | public static class ExampleMonitorFactory extends MonitorPortletFactory { 57 | 58 | @Override 59 | public Collection getPortlets(final Run build) { 60 | List portlets = new ArrayList<>(); 61 | portlets.add(new DemoPortlet("Good First Portlet", "first-demo-portlet")); 62 | portlets.add(new DemoPortlet("Another Portlet", "second-demo-portlet")); 63 | return portlets; 64 | } 65 | 66 | @Override 67 | public String getDisplayName() { 68 | return "Pull Request Monitoring (Demo)"; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/Monitor.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import edu.umd.cs.findbugs.annotations.NonNull; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import hudson.Extension; 7 | import hudson.model.Run; 8 | import hudson.model.TaskListener; 9 | import io.jenkins.plugins.monitoring.util.PortletUtils; 10 | import io.jenkins.plugins.monitoring.util.PullRequestUtils; 11 | import org.apache.commons.collections.CollectionUtils; 12 | import org.apache.commons.lang.StringUtils; 13 | import org.jenkinsci.plugins.workflow.steps.*; 14 | import org.json.JSONArray; 15 | import org.json.JSONObject; 16 | import org.kohsuke.stapler.DataBoundConstructor; 17 | 18 | import java.io.IOException; 19 | import java.io.Serializable; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.Set; 24 | import java.util.stream.Collectors; 25 | 26 | /** 27 | * This {@link Step} is responsible for the configuration of the monitoring dashboard 28 | * via the corresponding Jenkinsfile. 29 | * 30 | * @author Simon Symhoven 31 | */ 32 | public final class Monitor extends Step implements Serializable { 33 | private static final long serialVersionUID = -1329798203887148860L; 34 | private String portlets; 35 | 36 | /** 37 | * Creates a new instance of {@link Monitor}. 38 | * 39 | * @param portlets 40 | * the monitor configuration as json array string. 41 | */ 42 | @DataBoundConstructor 43 | public Monitor(final String portlets) { 44 | super(); 45 | this.portlets = portlets; 46 | } 47 | 48 | private void setPortlets(String portlets) { 49 | this.portlets = portlets; 50 | } 51 | 52 | public String getPortlets() { 53 | return portlets; 54 | } 55 | 56 | @Override 57 | public StepExecution start(final StepContext stepContext) { 58 | return new Execution(stepContext, this); 59 | } 60 | 61 | /** 62 | * The {@link Execution} routine for the monitoring step. 63 | */ 64 | @SuppressFBWarnings(value = "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION") 65 | static class Execution extends SynchronousStepExecution { 66 | 67 | private static final long serialVersionUID = 1300005476208035751L; 68 | private final Monitor monitor; 69 | 70 | Execution(final StepContext stepContext, final Monitor monitor) { 71 | super(stepContext); 72 | this.monitor = monitor; 73 | } 74 | 75 | @Override 76 | public Void run() throws IOException, InterruptedException { 77 | final Run run = getContext().get(Run.class); 78 | 79 | if (run == null) { 80 | log("[Monitor] Run not found!"); 81 | return null; 82 | } 83 | 84 | if (!PortletUtils.isValidConfiguration(monitor.getPortlets())) { 85 | log("[Monitor] Portlet Configuration is invalid!"); 86 | return null; 87 | } 88 | 89 | JSONArray portlets = new JSONArray(monitor.getPortlets()); 90 | log("[Monitor] Portlet Configuration: " + portlets.toString(3)); 91 | 92 | List classes = PortletUtils.getAvailablePortlets(run) 93 | .stream() 94 | .map(MonitorPortlet::getId) 95 | .collect(Collectors.toList()); 96 | 97 | log("[Monitor] Available portlets: [" 98 | + StringUtils.join(classes, ", ") + "]"); 99 | 100 | List usedPortlets = new ArrayList<>(); 101 | 102 | for (Object o : portlets) { 103 | if (o instanceof JSONObject) { 104 | JSONObject portlet = (JSONObject) o; 105 | String id = portlet.getString("id"); 106 | 107 | if (usedPortlets.contains(id)) { 108 | log("[Monitor] Portlet with ID '" + id 109 | + "' already defined in list of portlets. Skip adding this portlet!"); 110 | } 111 | else { 112 | usedPortlets.add(id); 113 | } 114 | } 115 | } 116 | 117 | List missedPortletIds = new ArrayList(CollectionUtils.removeAll(usedPortlets, classes)); 118 | 119 | if (!missedPortletIds.isEmpty()) { 120 | log("[Monitor] Can't find the following portlets " 121 | + missedPortletIds + " in list of available portlets! Will remove from current configuration."); 122 | 123 | JSONArray cleanedPortlets = new JSONArray(); 124 | for (Object o : portlets) { 125 | JSONObject portlet = (JSONObject) o; 126 | if (!missedPortletIds.contains(portlet.getString("id"))) { 127 | cleanedPortlets.put(portlet); 128 | } 129 | } 130 | 131 | monitor.setPortlets(cleanedPortlets.toString(3)); 132 | 133 | log("[Monitor] Cleaned Portlets: " + cleanedPortlets.toString(3)); 134 | } 135 | 136 | if (PullRequestUtils.isPullRequest(run.getParent())) { 137 | log("[Monitor] Build is part of a pull request. Add 'MonitoringCustomAction' now."); 138 | 139 | run.addAction(new MonitoringCustomAction(monitor.getPortlets())); 140 | } 141 | else { 142 | log("[Monitor] Build is not part of a pull request. Skip adding 'MonitoringCustomAction'."); 143 | } 144 | 145 | return null; 146 | } 147 | 148 | private void log(String message) throws IOException, InterruptedException { 149 | Objects.requireNonNull(getContext().get(TaskListener.class)).getLogger() 150 | .println(message); 151 | } 152 | 153 | } 154 | 155 | /** 156 | * A {@link Descriptor} for the monitoring step. 157 | */ 158 | @Extension 159 | public static class Descriptor extends StepDescriptor { 160 | 161 | @Override 162 | public Set> getRequiredContext() { 163 | return ImmutableSet.of(TaskListener.class, Run.class); 164 | } 165 | 166 | @Override 167 | public String getFunctionName() { 168 | return "monitoring"; 169 | } 170 | 171 | @Override 172 | @NonNull 173 | public String getDisplayName() { 174 | return Messages.Step_DisplayName(); 175 | } 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitorConfigurationProperty.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Saveable; 6 | import hudson.model.User; 7 | import hudson.model.UserProperty; 8 | import hudson.model.UserPropertyDescriptor; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.logging.Level; 16 | import java.util.logging.Logger; 17 | 18 | /** 19 | * A {@link UserProperty} to store the json configuration per user as property. 20 | * 21 | * @author Simon Symhoven 22 | */ 23 | public class MonitorConfigurationProperty extends UserProperty implements Saveable { 24 | private static final Logger LOGGER = Logger.getLogger(MonitorConfigurationProperty.class.getName()); 25 | private final Collection configurations; 26 | 27 | /** 28 | * The id for the default configuration. 29 | */ 30 | public static final String DEFAULT_ID = "default"; 31 | 32 | /** 33 | * Creates a new {@link MonitorConfigurationProperty}. 34 | * 35 | * @param configurations 36 | * the list of configurations to add to the {@link MonitorConfigurationProperty}. 37 | */ 38 | public MonitorConfigurationProperty(final List configurations) { 39 | super(); 40 | this.configurations = configurations; 41 | } 42 | 43 | public Collection getConfigurations() { 44 | return configurations; 45 | } 46 | 47 | /** 48 | * Get a {@link MonitorConfiguration} by its id. 49 | * 50 | * @param id 51 | * the id of the {@link MonitorConfiguration} to get. 52 | * 53 | * @return 54 | * the {@link MonitorConfiguration} or default if id does not exist on {@link MonitorConfigurationProperty}. 55 | */ 56 | public MonitorConfiguration getConfiguration(final String id) { 57 | return getConfigurations().stream() 58 | .filter(monitorConfiguration -> monitorConfiguration.getId().equals(id)) 59 | .findFirst() 60 | .orElse(getConfigurations() 61 | .stream() 62 | .filter(monitorConfiguration -> monitorConfiguration.getId().equals(DEFAULT_ID)) 63 | .findFirst().get()); 64 | } 65 | 66 | /** 67 | * Updates an existing {@link MonitorConfiguration}. 68 | * 69 | * @param id 70 | * the id of the {@link MonitorConfiguration} to update. 71 | * 72 | * @param config 73 | * the config string to update. 74 | */ 75 | public void createOrUpdateConfiguration(final String id, final String config) { 76 | Optional property = getConfigurations().stream() 77 | .filter(monitorConfiguration -> monitorConfiguration.getId().equals(id)) 78 | .findFirst(); 79 | 80 | if (property.isPresent()) { 81 | property.get().setConfig(config); 82 | } 83 | else { 84 | getConfigurations().add(new MonitorConfiguration(id, config)); 85 | } 86 | 87 | save(); 88 | } 89 | 90 | /** 91 | * Removes a configuration from configurations list. 92 | * 93 | * @param id 94 | * the id of configuration to remove. 95 | */ 96 | public void removeConfiguration(final String id) { 97 | getConfigurations().remove(getConfiguration(id)); 98 | save(); 99 | } 100 | 101 | /** 102 | * Gets the {@link MonitorConfigurationProperty} for current user. 103 | * 104 | * @return 105 | * the {@link MonitorConfigurationProperty} as {@link Optional}. 106 | */ 107 | public static Optional forCurrentUser() { 108 | final User current = User.current(); 109 | return current == null ? Optional.empty() : Optional.ofNullable(current.getProperty(MonitorConfigurationProperty.class)); 110 | } 111 | 112 | @Override 113 | public void save() { 114 | try { 115 | user.save(); 116 | } 117 | catch (IOException exception) { 118 | LOGGER.log(Level.SEVERE, "User could not be saved: ", exception); 119 | } 120 | } 121 | 122 | /** 123 | * The property class to store. Each {@link MonitorConfiguration} has an id and a config (json string). 124 | */ 125 | public static class MonitorConfiguration { 126 | 127 | private final String id; 128 | private String config; 129 | 130 | /** 131 | * Creates a {@link MonitorConfiguration}. 132 | * 133 | * @param id 134 | * the id of the {@link MonitorConfiguration}. 135 | * 136 | * @param config 137 | * the config of the {@link MonitorConfiguration}. 138 | */ 139 | public MonitorConfiguration(final String id, final String config) { 140 | this.id = id; 141 | this.config = config; 142 | } 143 | 144 | public String getId() { 145 | return id; 146 | } 147 | 148 | public String getConfig() { 149 | return config; 150 | } 151 | 152 | public void setConfig(final String config) { 153 | this.config = config; 154 | } 155 | 156 | } 157 | 158 | /** 159 | * A {@link UserPropertyDescriptor} for the {@link MonitorConfigurationProperty}. 160 | */ 161 | @Extension 162 | public static class MonitorPropertyDescriptor extends UserPropertyDescriptor { 163 | 164 | @Override 165 | public UserProperty newInstance(final User user) { 166 | return new MonitorConfigurationProperty(new ArrayList<>()); 167 | } 168 | 169 | @NonNull 170 | @Override 171 | public String getDisplayName() { 172 | return Messages.Property_DisplayName(); 173 | } 174 | 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitorPortlet.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | *

Defines a single portlet for the pull request monitoring dashboard.

7 | * 8 | *

The id must be unique. Please make sure that the id is related to the plugin so that 9 | * there are no conflicts with other plugins. It is recommended to use the artifact id 10 | * of the plugin or parts of it as prefix. If several portlets of the same class are created in the 11 | * {@link MonitorPortletFactory}, it must be ensured that {@link #getId()} is always unique for each portlet!

12 | * 13 | * @since 1.6.0 14 | * @author Simon Symhoven 15 | */ 16 | public abstract class MonitorPortlet { 17 | 18 | /** 19 | * Defines the title to be shown. 20 | * 21 | * @return 22 | * the title. 23 | */ 24 | public abstract String getTitle(); 25 | 26 | /** 27 | * Defines the id for the portlet. 28 | * 29 | * @return 30 | * the id. 31 | */ 32 | public abstract String getId(); 33 | 34 | /** 35 | * Defines whether the portlet is shown per default in the dashboard or not. 36 | * The functionality for this is not yet given and the flag is ignored, but the API for it is 37 | * already prepared. 38 | * 39 | * @return 40 | * true if portlet should be shown, false else. 41 | * 42 | * @since 1.6.0 43 | */ 44 | public boolean isDefault() { 45 | return false; 46 | } 47 | 48 | /** 49 | * Defines the preferred width of the portlet. 50 | * 51 | * @return 52 | * the width in pixels. 53 | */ 54 | public abstract int getPreferredWidth(); 55 | 56 | /** 57 | * Defines the preferred height of the portlet. 58 | * 59 | * @return 60 | * the height in pixels. 61 | */ 62 | public abstract int getPreferredHeight(); 63 | 64 | /** 65 | * Defines the icon to show in the dropdown list of available portlets. 66 | * 67 | * @return 68 | * the app relative icon url depending on ${resURL} (/static/cache-id), 69 | * or {@code Optional.empty()}, if a default icon should be added. 70 | * Supported file formats (depending on browser): jpeg, gif, png, apng, svg, bmp, bmp ico, png ico. 71 | */ 72 | public Optional getIconUrl() { 73 | return Optional.empty(); 74 | } 75 | 76 | /** 77 | * Defines the relative url to a detail view of the portlet. 78 | * 79 | * @return 80 | * the relative link to the detail view depending on the current build url, 81 | * e.g. current build url: "http://localhost:8080/jenkins/job/job-name/view/change-requests/job/PR-1/1/", 82 | * then the relative url could be "pull-request-monitoring". 83 | * or {@code Optional.empty()}, if no link should be added to portlet. 84 | */ 85 | public Optional getDetailViewUrl() { 86 | return Optional.empty(); 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | @Override 93 | public String toString() { 94 | return "MonitorPortlet{'" + getId() + "'}"; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitorPortletFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import hudson.ExtensionPoint; 4 | import hudson.model.Run; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | *

Defines the {@link ExtensionPoint} to register one or more new pull request monitoring portlets.

10 | * 11 | *

The {@link #getDisplayName()} is shown in a dropdown list as optgroup. The children of the optgroup 12 | * are the registered portlets of {@link #getPortlets(Run)}.

13 | * 14 | *

Since an empty dashboard is always added by default, it is possible that the method {@link #getPortlets(Run)} will 15 | * be called even though the current run may not be finished. It is therefore advisable to perform a null check on 16 | * the actions of the run required by your portlet and return an empty list if necessary. 17 | * (Example: code-coverage-api)

18 | * 19 | * @since 1.6.0 20 | * @author Simon Symhoven 21 | */ 22 | public abstract class MonitorPortletFactory implements ExtensionPoint { 23 | 24 | /** 25 | * Get a collection of {@link MonitorPortlet} to display. 26 | * 27 | * @param build 28 | * the reference {@link Run}. 29 | * 30 | * @return 31 | * a collection of {@link MonitorPortlet}. 32 | */ 33 | public abstract Collection getPortlets(Run build); 34 | 35 | /** 36 | * Defines the name of the factory. 37 | * 38 | * @return 39 | * the name to display for the factory. 40 | */ 41 | public abstract String getDisplayName(); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringCustomAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import hudson.model.InvisibleAction; 4 | import hudson.model.Run; 5 | 6 | /** 7 | * This action is added to {@link Run}, if configuration is set in the Jenkinsfile. Therefore the portlets of this 8 | * action {@link MonitoringCustomAction} overwrites the one from the {@link MonitoringDefaultAction} added by the 9 | * {@link MonitoringDefaultActionFactory}. 10 | * 11 | * @author Simon Symhoven 12 | */ 13 | public class MonitoringCustomAction extends InvisibleAction { 14 | private final String portlets; 15 | 16 | /** 17 | * Creates a new instance of {@link MonitoringCustomAction}. 18 | * 19 | * @param portlets 20 | * the portlets as json array string to be add. 21 | */ 22 | public MonitoringCustomAction(final String portlets) { 23 | super(); 24 | this.portlets = portlets; 25 | } 26 | 27 | public String getPortlets() { 28 | return portlets; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringDefaultAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import edu.hm.hafner.util.FilteredLog; 7 | import hudson.model.Run; 8 | import io.jenkins.plugins.forensics.reference.ReferenceFinder; 9 | import io.jenkins.plugins.monitoring.util.PortletUtils; 10 | import j2html.tags.DomContent; 11 | import jenkins.model.Jenkins; 12 | import jenkins.model.RunAction2; 13 | import jenkins.scm.api.metadata.ContributorMetadataAction; 14 | import jenkins.scm.api.metadata.ObjectMetadataAction; 15 | import jenkins.scm.api.mixin.ChangeRequestSCMHead2; 16 | import org.apache.commons.collections.CollectionUtils; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.commonmark.node.Node; 19 | import org.commonmark.parser.Parser; 20 | import org.commonmark.renderer.html.HtmlRenderer; 21 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; 22 | import org.json.JSONArray; 23 | import org.json.JSONObject; 24 | import org.kohsuke.stapler.StaplerProxy; 25 | import org.kohsuke.stapler.bind.JavaScriptMethod; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Optional; 30 | import java.util.logging.Level; 31 | import java.util.logging.Logger; 32 | import java.util.stream.Collectors; 33 | 34 | import static j2html.TagCreator.*; 35 | 36 | /** 37 | * This action displays a link on the side panel of a {@link Run}. The action is only displayed if the parent job 38 | * is a pull request. 39 | * The action is responsible to render the summary via its associated 'summary.jelly' view and render the 40 | * main plugin page, where the user can configure the dashboard with all supported plugins via its associated 41 | * 'index.jelly view. 42 | * 43 | * @author Simon Symhoven 44 | */ 45 | public class MonitoringDefaultAction implements RunAction2, StaplerProxy { 46 | private static final Logger LOGGER = Logger.getLogger(MonitoringDefaultAction.class.getName()); 47 | private transient Run run; 48 | 49 | /** 50 | * Creates a new instance of {@link MonitoringDefaultAction}. 51 | * 52 | * @param run 53 | * the run that owns this action. 54 | */ 55 | public MonitoringDefaultAction(final Run run) { 56 | this.run = run; 57 | } 58 | 59 | @Override 60 | public String getIconFileName() { 61 | return MonitoringMultibranchProjectAction.getIconSmall(); 62 | } 63 | 64 | @Override 65 | public String getDisplayName() { 66 | return String.format("%s '%s'", Messages.BuildAction_Name(), getRun().getDisplayName()); 67 | } 68 | 69 | @Override 70 | public String getUrlName() { 71 | return MonitoringMultibranchProjectAction.getURI(); 72 | } 73 | 74 | @Override 75 | public void onAttached(final Run build) { 76 | this.run = build; 77 | } 78 | 79 | @Override 80 | public void onLoad(final Run build) { 81 | this.run = build; 82 | } 83 | 84 | public Run getRun() { 85 | return run; 86 | } 87 | 88 | public String getPortlets() { 89 | return PortletUtils.getDefaultPortletsAsConfiguration(getRun()); 90 | } 91 | 92 | /** 93 | * Get the {@link ObjectMetadataAction} for the current build. 94 | * 95 | * @return 96 | * the {@link ObjectMetadataAction}. 97 | */ 98 | public Optional getObjectMetadataAction() { 99 | return Optional.ofNullable(getRun().getParent().getProperty(BranchJobProperty.class) 100 | .getBranch().getAction(ObjectMetadataAction.class)); 101 | } 102 | 103 | /** 104 | * Get the {@link ContributorMetadataAction} for the current build. 105 | * 106 | * @return 107 | * the {@link ContributorMetadataAction}. 108 | */ 109 | public Optional getContributorMetadataAction() { 110 | return Optional.ofNullable(getRun().getParent().getProperty(BranchJobProperty.class) 111 | .getBranch().getAction(ContributorMetadataAction.class)); 112 | } 113 | 114 | /** 115 | * Get the {@link ChangeRequestSCMHead2} for a specific {@link Run}. 116 | * 117 | * @return 118 | * the {@link ChangeRequestSCMHead2} of run. 119 | */ 120 | public ChangeRequestSCMHead2 getScmHead() { 121 | return (ChangeRequestSCMHead2) getRun().getParent().getProperty(BranchJobProperty.class).getBranch().getHead(); 122 | } 123 | 124 | /** 125 | * Creates the heading for the pull request metadata accordion. 126 | * 127 | * @return 128 | * the title as html element. 129 | */ 130 | public String getPullRequestMetadataTitle() { 131 | 132 | Optional contributorMetadataAction = getContributorMetadataAction(); 133 | 134 | return span(join( 135 | strong(iffElse(contributorMetadataAction.isPresent(), 136 | getContributorMetadataAction().get().getContributor(), 137 | "unknown")), "wants to merge", 138 | strong(getScmHead().getOriginName()), "into", 139 | strong(getScmHead().getTarget().getName()))).render(); 140 | } 141 | 142 | /** 143 | * Creates the reference build link for the pull request metadata title. 144 | * 145 | * @return 146 | * the reference build as html element. 147 | */ 148 | public String getPullRequestReferenceBuildDescription() { 149 | Optional> referenceBuild = getReferenceBuild(); 150 | 151 | String url = referenceBuild.map(value -> Jenkins.get().getRootUrl() + value.getUrl()).orElse("#"); 152 | String name = referenceBuild.map(Run::getDisplayName).orElse("#?"); 153 | 154 | String target = getScmHead().getTarget().getName(); 155 | 156 | DomContent reference = join("Reference Build:", a().withId("reference-build-link") 157 | .withHref(url) 158 | .withText(target + " " + name)); 159 | 160 | return span(reference).render(); 161 | } 162 | 163 | /** 164 | * Get the reference build to current build. 165 | * 166 | * @return 167 | * the reference build as {@link Optional}. 168 | */ 169 | private Optional> getReferenceBuild() { 170 | FilteredLog log = new FilteredLog(""); 171 | ReferenceFinder referenceFinder = new ReferenceFinder(); 172 | return referenceFinder.findReference(getRun(), log); 173 | } 174 | 175 | /** 176 | * Creates the body for the pull request metadata accordion. 177 | * 178 | * @return 179 | * the body as html element. 180 | */ 181 | public String getPullRequestMetadataBody() { 182 | if (!getObjectMetadataAction().isPresent()) { 183 | return span(i("No metadata found.")).render(); 184 | } 185 | 186 | String description = getObjectMetadataAction().get().getObjectDescription(); 187 | 188 | if (StringUtils.isEmpty(description)) { 189 | return span(i("No description provided.")).render(); 190 | } 191 | 192 | Parser parser = Parser.builder().build(); 193 | Node document = parser.parse(description); 194 | HtmlRenderer renderer = HtmlRenderer.builder().build(); 195 | 196 | return renderer.render(document); 197 | } 198 | 199 | /** 200 | * Get the project it based on the current {@link Run}. 201 | * 202 | * @return 203 | * the display name of the current project as id. 204 | */ 205 | public String getConfigurationId() { 206 | String id = getRun().getParent().getParent().getDisplayName(); 207 | return StringUtils.toRootLowerCase(id).replaceAll(" ", "-"); 208 | } 209 | 210 | /** 211 | * Sets the default for {@link MonitorConfigurationProperty}. 212 | */ 213 | private void setDefaultMonitorConfiguration() { 214 | MonitorConfigurationProperty 215 | .forCurrentUser() 216 | .ifPresent(monitorConfigurationProperty -> monitorConfigurationProperty 217 | .createOrUpdateConfiguration(MonitorConfigurationProperty.DEFAULT_ID, resolvePortlets())); 218 | } 219 | 220 | /** 221 | * Get all portlets, which are not available anymore. 222 | * 223 | * @return 224 | * a list of all unavailable portlet ids. 225 | */ 226 | @SuppressWarnings("unchecked") 227 | public List getUnavailablePortlets() { 228 | JSONArray portlets = new JSONArray(getConfiguration()); 229 | 230 | List usedPlugins = new ArrayList<>(); 231 | 232 | for (Object o : portlets) { 233 | JSONObject portlet = (JSONObject) o; 234 | usedPlugins.add(portlet.getString("id")); 235 | } 236 | 237 | List availablePlugins = PortletUtils.getAvailablePortlets(getRun()) 238 | .stream().map(MonitorPortlet::getId).collect(Collectors.toList()); 239 | return new ArrayList(CollectionUtils.removeAll(usedPlugins, availablePlugins)); 240 | } 241 | 242 | /** 243 | * Checks if there are changes in the configuration since the last build. 244 | * 245 | * @return 246 | * true if both configurations are equal, else false. 247 | */ 248 | public boolean hasChanges() { 249 | MonitoringCustomAction action = getRun().getAction(MonitoringCustomAction.class); 250 | 251 | if (action == null) { 252 | return false; 253 | } 254 | 255 | Run previous = getRun().getPreviousBuild(); 256 | 257 | if (previous == null) { 258 | return false; 259 | } 260 | 261 | MonitoringCustomAction prevAction = previous.getAction(MonitoringCustomAction.class); 262 | 263 | if (prevAction == null) { 264 | return false; 265 | } 266 | 267 | return !areJsonNodesEquals(action.getPortlets(), prevAction.getPortlets()); 268 | } 269 | 270 | /** 271 | * Compares to json nodes, if they are equals. 272 | * 273 | * @param s1 274 | * the first json node as string. 275 | * 276 | * @param s2 277 | * the second json node as string. 278 | * 279 | * @return 280 | * true, if both are equals, else false. 281 | */ 282 | public boolean areJsonNodesEquals(final String s1, final String s2) { 283 | 284 | try { 285 | ObjectMapper mapper = new ObjectMapper(); 286 | JsonNode node1 = mapper.readTree(s1); 287 | JsonNode node2 = mapper.readTree(s2); 288 | return node1.equals(node2); 289 | } 290 | catch (JsonProcessingException exception) { 291 | LOGGER.log(Level.SEVERE, "Json could not be parsed: ", exception); 292 | } 293 | 294 | return false; 295 | } 296 | 297 | /* 298 | JavaScriptMethod block. All methods are called from a jelly file. 299 | */ 300 | 301 | /** 302 | * Saves the actual dashboard configuration to {@link MonitorConfigurationProperty}. 303 | * 304 | * @param config 305 | * the config string to update. 306 | * 307 | */ 308 | @JavaScriptMethod 309 | public void updateMonitorConfiguration(final String config) { 310 | MonitorConfigurationProperty 311 | .forCurrentUser() 312 | .ifPresent(monitorConfigurationProperty -> 313 | monitorConfigurationProperty.createOrUpdateConfiguration(getConfigurationId(), config)); 314 | } 315 | 316 | /** 317 | * Get the current dashboard configuration of user. 318 | * 319 | * @return 320 | * the config of the corresponding {@link MonitorConfigurationProperty} of the actual project 321 | * or the default configuration of Jenkinsfile if no config exists in {@link MonitorConfigurationProperty} 322 | * for actual project. 323 | */ 324 | @JavaScriptMethod 325 | public String getConfiguration() { 326 | 327 | MonitorConfigurationProperty monitorConfigurationProperty = MonitorConfigurationProperty 328 | .forCurrentUser().orElse(null); 329 | 330 | return monitorConfigurationProperty == null 331 | ? resolvePortlets() : monitorConfigurationProperty.getConfiguration(getConfigurationId()).getConfig(); 332 | } 333 | 334 | /** 335 | * Checks if the actual configuration is synced with the default one (Jenkinsfile or empty one). 336 | * 337 | * @return 338 | * true, if synced, else false. 339 | */ 340 | @JavaScriptMethod 341 | public boolean isMonitorConfigurationSynced() { 342 | 343 | MonitorConfigurationProperty monitorConfigurationProperty = MonitorConfigurationProperty.forCurrentUser() 344 | .orElse(null); 345 | 346 | if (monitorConfigurationProperty == null) { 347 | return true; 348 | } 349 | 350 | MonitorConfigurationProperty.MonitorConfiguration projectConfiguration = 351 | monitorConfigurationProperty.getConfiguration(getConfigurationId()); 352 | 353 | if (projectConfiguration == null) { 354 | return true; 355 | } 356 | 357 | MonitorConfigurationProperty.MonitorConfiguration defaultConfiguration = 358 | monitorConfigurationProperty.getConfiguration(MonitorConfigurationProperty.DEFAULT_ID); 359 | 360 | return areJsonNodesEquals(projectConfiguration.getConfig(), defaultConfiguration.getConfig()); 361 | } 362 | 363 | /** 364 | * Resolves the portlet string of the {@link Monitor}. 365 | * 366 | * @return 367 | * the portlet string of {@link MonitoringCustomAction} if exists, 368 | * else the one of {@link MonitoringDefaultAction}. 369 | */ 370 | @JavaScriptMethod 371 | public String resolvePortlets() { 372 | MonitoringCustomAction action = getRun().getAction(MonitoringCustomAction.class); 373 | return action == null ? getPortlets() : action.getPortlets(); 374 | } 375 | 376 | /** 377 | * Reset the current project configuration to default. 378 | */ 379 | @JavaScriptMethod 380 | public void resetMonitorConfiguration() { 381 | MonitorConfigurationProperty 382 | .forCurrentUser() 383 | .ifPresent(monitorConfigurationProperty -> monitorConfigurationProperty.removeConfiguration(getConfigurationId())); 384 | } 385 | 386 | @Override 387 | public Object getTarget() { 388 | setDefaultMonitorConfiguration(); 389 | return this; 390 | } 391 | 392 | /** 393 | * Gets all {@link MonitorPortlet} for corresponding {@link MonitorPortletFactory}. 394 | * 395 | * @param build 396 | * the reference build. 397 | * 398 | * @return 399 | * all available {@link MonitorPortlet}. 400 | */ 401 | public static List getAvailablePortlets(final Run build) { 402 | return PortletUtils.getAvailablePortlets(build); 403 | } 404 | 405 | /** 406 | * Get all portlet factories, type of {@link MonitorPortletFactory}. 407 | * 408 | * @return 409 | * all factories as list. 410 | */ 411 | public static List getFactories() { 412 | return PortletUtils.getFactories(); 413 | } 414 | 415 | /** 416 | * Gets all {@link MonitorPortlet} for one {@link MonitorPortletFactory}. 417 | * 418 | * @param build 419 | * the build to get the portlets for. 420 | * 421 | * @param factory 422 | * the factory to get the portlets for. 423 | * 424 | * @return 425 | * the filtered portlets. 426 | */ 427 | public static List getAvailablePortletsForFactory( 428 | final Run build, final MonitorPortletFactory factory) { 429 | return PortletUtils.getAvailablePortletsForFactory(build, factory); 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringDefaultActionFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Action; 6 | import hudson.model.Job; 7 | import hudson.model.Run; 8 | import io.jenkins.plugins.monitoring.util.PullRequestUtils; 9 | import jenkins.model.TransientActionFactory; 10 | 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | 14 | /** 15 | * A {@link TransientActionFactory} to add an action to specific {@link Run}. 16 | * 17 | * @author Simon Symhoven 18 | */ 19 | @Extension 20 | public class MonitoringDefaultActionFactory extends TransientActionFactory { 21 | @Override 22 | public Class type() { 23 | return Run.class; 24 | } 25 | 26 | @NonNull 27 | @Override 28 | public Collection createFor(@NonNull final Run run) { 29 | 30 | if (run.isBuilding()) { 31 | return Collections.emptyList(); 32 | } 33 | 34 | final Job job = run.getParent(); 35 | 36 | if (PullRequestUtils.isPullRequest(job)) { 37 | return Collections.singletonList(new MonitoringDefaultAction(run)); 38 | } 39 | 40 | return Collections.emptyList(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringMultibranchProjectAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import hudson.model.Action; 4 | import hudson.model.Job; 5 | import hudson.model.ProminentProjectAction; 6 | import io.jenkins.plugins.monitoring.util.PullRequestUtils; 7 | import jenkins.branch.MultiBranchProject; 8 | import jenkins.scm.api.metadata.ContributorMetadataAction; 9 | import jenkins.scm.api.metadata.ObjectMetadataAction; 10 | import jenkins.scm.api.mixin.ChangeRequestSCMHead2; 11 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; 12 | 13 | import java.util.List; 14 | import java.util.Optional; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * This action displays a link on the side panel of a {@link MultiBranchProject}. 19 | * The action is responsible to render the basic overview of all open pull requests 20 | * via its associated 'index.jelly' view. 21 | * 22 | * @author Simon Symhoven 23 | */ 24 | public class MonitoringMultibranchProjectAction implements ProminentProjectAction, Action { 25 | 26 | private static final String URI = "pull-request-monitoring"; 27 | private static final String ICONS_PREFIX = "/plugin/pull-request-monitoring/icons/"; 28 | private static final String ICON_SMALL = ICONS_PREFIX + "line-graph-32x32.png"; 29 | private static final String ICON_BIG = ICONS_PREFIX + "line-graph-64x64.png"; 30 | 31 | private final transient MultiBranchProject multiBranchProject; 32 | 33 | /** 34 | * Creates a new instance of {@link MonitoringMultibranchProjectAction}. 35 | * 36 | * @param multiBranchProject 37 | * the project that owns this action. 38 | */ 39 | public MonitoringMultibranchProjectAction(final MultiBranchProject multiBranchProject) { 40 | this.multiBranchProject = multiBranchProject; 41 | } 42 | 43 | @Override 44 | public String getIconFileName() { 45 | return ICON_BIG; 46 | } 47 | 48 | @Override 49 | public String getDisplayName() { 50 | return Messages.ProjectAction_Name(); 51 | } 52 | 53 | @Override 54 | public String getUrlName() { 55 | return URI; 56 | } 57 | 58 | public MultiBranchProject getProject() { 59 | return multiBranchProject; 60 | } 61 | 62 | /** 63 | * Filters all jobs of selected {@link MultiBranchProject} by "Pull Request". 64 | * 65 | * @return 66 | * filtered list of all {@link #getJobs() jobs} by "Pull Request". 67 | */ 68 | public List> getPullRequests() { 69 | return getJobs().stream().filter(PullRequestUtils::isPullRequest).collect(Collectors.toList()); 70 | } 71 | 72 | /** 73 | * Get the {@link ChangeRequestSCMHead2} for a specific {@link Job}. 74 | * 75 | * @param job 76 | * the job to get {@link ChangeRequestSCMHead2} for. 77 | * 78 | * @return 79 | * the {@link ChangeRequestSCMHead2} of job. 80 | */ 81 | public ChangeRequestSCMHead2 getScmHead(final Job job) { 82 | return (ChangeRequestSCMHead2) job.getProperty(BranchJobProperty.class).getBranch().getHead(); 83 | } 84 | 85 | /** 86 | * Fetch all jobs (items) of current {@link MultiBranchProject}. 87 | * 88 | * @return 89 | * {@link List} of all jobs of current {@link MultiBranchProject}. 90 | */ 91 | private List> getJobs() { 92 | return multiBranchProject.getItems().stream().map(item -> (Job) item).collect(Collectors.toList()); 93 | } 94 | 95 | /** 96 | * Get the {@link ObjectMetadataAction} for a given job, e.g. the name of 97 | * the pull request and the link to the repository. 98 | * 99 | * @param job 100 | * the job to get {@link ObjectMetadataAction} for. 101 | * @return 102 | * the {@link ObjectMetadataAction} for the given job as {@link Optional}. 103 | */ 104 | public Optional getObjectMetaData(final Job job) { 105 | return Optional.ofNullable( 106 | job.getProperty(BranchJobProperty.class).getBranch().getAction(ObjectMetadataAction.class)); 107 | } 108 | 109 | /** 110 | * Get the {@link ContributorMetadataAction} for a given job, e.g. the name of the contributor. 111 | * 112 | * @param job 113 | * the job to get {@link ContributorMetadataAction} for. 114 | * @return 115 | * the {@link ContributorMetadataAction} for the given job as {@link Optional}. 116 | */ 117 | public Optional getContributorMetaData(final Job job) { 118 | return Optional.ofNullable( 119 | job.getProperty(BranchJobProperty.class).getBranch().getAction(ContributorMetadataAction.class)); 120 | } 121 | 122 | public static String getURI() { 123 | return URI; 124 | } 125 | 126 | public static String getIconSmall() { 127 | return ICON_SMALL; 128 | } 129 | 130 | public static String getIconBig() { 131 | return ICON_BIG; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringMultibranchProjectActionFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Action; 6 | import jenkins.branch.MultiBranchProject; 7 | import jenkins.model.TransientActionFactory; 8 | 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | 12 | /** 13 | * A {@link TransientActionFactory} to add an action to specific {@link MultiBranchProject}. 14 | * 15 | * @author Simon Symhoven 16 | */ 17 | @Extension 18 | public class MonitoringMultibranchProjectActionFactory extends TransientActionFactory { 19 | 20 | /** 21 | * Specifies the {@link Class} of the job ({@link MultiBranchProject}) to add the action to. 22 | * 23 | * @return 24 | * the {@link Class} of the job to add the action to. 25 | */ 26 | @Override 27 | public Class type() { 28 | return MultiBranchProject.class; 29 | } 30 | 31 | /** 32 | * Add the action to the selected {@link MultiBranchProject}. 33 | * 34 | * @param multiBranchProject 35 | * the job to add the action to. 36 | * @return 37 | * {@link Collections} of {@link MonitoringMultibranchProjectAction}. 38 | */ 39 | @Override 40 | @NonNull 41 | public Collection createFor(@NonNull final MultiBranchProject multiBranchProject) { 42 | return Collections.singletonList(new MonitoringMultibranchProjectAction(multiBranchProject)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringWorkflowJobAction.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import hudson.model.Action; 4 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 5 | 6 | /** 7 | * This action displays a link on the side panel of a {@link WorkflowJob}. The action is only displayed if the job 8 | * is a pull request, which is described in the associated {@link MonitoringWorkflowJobActionFactory}. 9 | * The action is responsible to reference the latest build of the job and navigates to the corresponding 10 | * {@link MonitoringDefaultAction}. 11 | * 12 | * @author Simon Symhoven 13 | */ 14 | public class MonitoringWorkflowJobAction implements Action { 15 | 16 | private final transient WorkflowJob workflowJob; 17 | 18 | /** 19 | * Creates a new instance of {@link MonitoringWorkflowJobAction}. 20 | * 21 | * @param workflowJob 22 | * the job that owns owns this action. 23 | */ 24 | public MonitoringWorkflowJobAction(final WorkflowJob workflowJob) { 25 | this.workflowJob = workflowJob; 26 | } 27 | 28 | @Override 29 | public String getIconFileName() { 30 | return MonitoringMultibranchProjectAction.getIconSmall(); 31 | } 32 | 33 | @Override 34 | public String getDisplayName() { 35 | return String.format("%s '%s'", Messages.JobAction_Name(), workflowJob.getLastBuild().getDisplayName()); 36 | } 37 | 38 | @Override 39 | public String getUrlName() { 40 | return workflowJob.getLastBuild().getNumber() + "/" + MonitoringMultibranchProjectAction.getURI(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/MonitoringWorkflowJobActionFactory.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.Extension; 5 | import hudson.model.Action; 6 | import io.jenkins.plugins.monitoring.util.PullRequestUtils; 7 | import jenkins.model.TransientActionFactory; 8 | import org.jenkinsci.plugins.workflow.job.WorkflowJob; 9 | 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | 13 | /** 14 | * A {@link TransientActionFactory} to add an action to specific {@link WorkflowJob}. 15 | * 16 | * @author Simon Symhoven 17 | */ 18 | @Extension 19 | public class MonitoringWorkflowJobActionFactory extends TransientActionFactory { 20 | 21 | /** 22 | * Specifies the {@link Class} of the job {@link WorkflowJob} to add the action to. 23 | * 24 | * @return 25 | * the {@link Class} of job to add the action to. 26 | */ 27 | @Override 28 | public Class type() { 29 | return WorkflowJob.class; 30 | } 31 | 32 | /** 33 | * Add the action to the selected {@link WorkflowJob} if its a Pull Request. 34 | * 35 | * @param workflowJob 36 | * the job to add the action to. 37 | * @return 38 | * {@link Collections} of {@link MonitoringWorkflowJobAction} if 39 | * {@link WorkflowJob} is a Pull Request, else a empty collection. 40 | */ 41 | @NonNull 42 | @Override 43 | public Collection createFor(@NonNull final WorkflowJob workflowJob) { 44 | 45 | if (workflowJob.getLastBuild() == null) { 46 | return Collections.emptyList(); 47 | } 48 | 49 | if (PullRequestUtils.isPullRequest(workflowJob)) { 50 | return Collections.singletonList(new MonitoringWorkflowJobAction(workflowJob)); 51 | } 52 | 53 | return Collections.emptyList(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/util/PortletUtils.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring.util; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import hudson.ExtensionList; 5 | import hudson.model.Run; 6 | import io.jenkins.plugins.monitoring.MonitorPortlet; 7 | import io.jenkins.plugins.monitoring.MonitorPortletFactory; 8 | import org.everit.json.schema.Schema; 9 | import org.everit.json.schema.loader.SchemaLoader; 10 | import org.json.JSONArray; 11 | import org.json.JSONObject; 12 | import org.json.JSONTokener; 13 | 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.util.Collection; 17 | import java.util.List; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | import java.util.stream.Collectors; 21 | 22 | /** 23 | * A utility class for the portlets. 24 | */ 25 | public final class PortletUtils { 26 | private static final Logger LOGGER = Logger.getLogger(PortletUtils.class.getName()); 27 | 28 | private PortletUtils() { 29 | // make checkstyle happy. 30 | } 31 | 32 | /** 33 | * Gets all {@link MonitorPortlet} for corresponding {@link MonitorPortletFactory}. 34 | * 35 | * @param build 36 | * the reference build. 37 | * 38 | * @return 39 | * all available {@link MonitorPortlet}. 40 | */ 41 | public static List getAvailablePortlets(final Run build) { 42 | return getFactories() 43 | .stream() 44 | .map(factory -> factory.getPortlets(build)) 45 | .flatMap(Collection::stream) 46 | .collect(Collectors.toList()); 47 | } 48 | 49 | /** 50 | * Get all portlet factories, type of {@link MonitorPortletFactory}. 51 | * 52 | * @return 53 | * all factories as list. 54 | */ 55 | public static List getFactories() { 56 | return ExtensionList.lookup(MonitorPortletFactory.class); 57 | } 58 | 59 | /** 60 | * Gets all {@link MonitorPortlet} for one {@link MonitorPortletFactory}. 61 | * 62 | * @param build 63 | * the build to get the portlets for. 64 | * 65 | * @param factory 66 | * the factory to get the portlets for. 67 | * 68 | * @return 69 | * the filtered portlets. 70 | */ 71 | public static List getAvailablePortletsForFactory( 72 | final Run build, final MonitorPortletFactory factory) { 73 | return getFactories() 74 | .stream() 75 | .filter(fac -> fac.equals(factory)) 76 | .map(fac -> fac.getPortlets(build)) 77 | .flatMap(Collection::stream) 78 | .collect(Collectors.toList()); 79 | } 80 | 81 | /** 82 | * Get all the default portlets as configuration. 83 | * 84 | * @param build 85 | * the build to get the portlets for. 86 | * 87 | * @return 88 | * the json array configuration as string. 89 | */ 90 | public static String getDefaultPortletsAsConfiguration(Run build) { 91 | return new JSONArray(getAvailablePortlets(build) 92 | .stream() 93 | .filter(MonitorPortlet::isDefault) 94 | .map(portlet -> new JSONObject(String.format("{\"id\": \"%s\"}", portlet.getId()))) 95 | .toArray()) 96 | .toString(); 97 | } 98 | 99 | /** 100 | * Validate the json configuration. 101 | * 102 | * @param configuration 103 | * the configuration as json 104 | * 105 | * @return 106 | * true, if the configuration is valid, else false. 107 | */ 108 | public static boolean isValidConfiguration(@NonNull final String configuration) { 109 | try (InputStream schemaStream = PortletUtils.class.getResourceAsStream("/schema.json")) { 110 | JSONObject jsonSchema = new JSONObject(new JSONTokener(schemaStream)); 111 | JSONArray jsonSubject = new JSONArray(configuration); 112 | Schema schema = SchemaLoader.load(jsonSchema); 113 | schema.validate(jsonSubject); 114 | return true; 115 | } 116 | catch (IOException exception) { 117 | LOGGER.log(Level.SEVERE, "Invalid Configuration found: ", exception); 118 | return false; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/io/jenkins/plugins/monitoring/util/PullRequestUtils.java: -------------------------------------------------------------------------------- 1 | package io.jenkins.plugins.monitoring.util; 2 | 3 | import hudson.model.Job; 4 | import jenkins.scm.api.SCMHead; 5 | import jenkins.scm.api.mixin.ChangeRequestSCMHead2; 6 | import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; 7 | 8 | /** 9 | * A utility class for pull requests. 10 | */ 11 | public final class PullRequestUtils { 12 | 13 | private PullRequestUtils() { 14 | // make checkstyle happy. 15 | } 16 | 17 | /** 18 | * Checks whether a given {@link Job} is a pull request or not. 19 | * 20 | * @param job 21 | * the job to analyse. 22 | * 23 | * @return 24 | * true if the job is a pull request, else false. 25 | */ 26 | public static boolean isPullRequest(Job job) { 27 | BranchJobProperty branchJobProperty = job.getProperty(BranchJobProperty.class); 28 | 29 | if (branchJobProperty == null) { 30 | return false; 31 | } 32 | 33 | SCMHead head = branchJobProperty.getBranch().getHead(); 34 | return head instanceof ChangeRequestSCMHead2; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/alerts/taglib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pull-request-monitoring-plugin/d602c91131a8112eef8cc9d00aee013f4443a307/src/main/resources/alerts/taglib -------------------------------------------------------------------------------- /src/main/resources/alerts/warning.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creates a warning if portlets are not available anymore. 6 | 7 | 8 | Owner of the page. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/alerts/warning.properties: -------------------------------------------------------------------------------- 1 | title=Some portlets seem to be no longer available! 2 | description=It looks like the portlets with the following IDs are no longer available. \ 3 | They have been automatically removed from the current user configuration. -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin offers a possibility to display and aggregate the results (in the form of individual views) of a pull 4 | request in a configurable dashboard. Views can only be accessed or displayed if the corresponding plugin fulfils 5 | certain requirements and already provides a view. 6 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/monitoring/DemoPortlet/monitor.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

${it.title}

6 | 7 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/monitoring/Messages.properties: -------------------------------------------------------------------------------- 1 | Step.DisplayName=Configure Monitoring Dashboard 2 | 3 | Property.DisplayName=Pull Request Monitoring 4 | 5 | ProjectAction.Name=Pull Request Monitoring 6 | 7 | BuildAction.Name=Pull Request Monitoring 8 | 9 | JobAction.Name=Pull Request Monitoring -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/monitoring/MonitorConfigurationProperty/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

${%description}

6 | 7 |
-------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/monitoring/MonitorConfigurationProperty/config.properties: -------------------------------------------------------------------------------- 1 | description=You cannot change anything here. Please edit the configuration in the Jenkinsfile or in the dashboard itself. -------------------------------------------------------------------------------- /src/main/resources/io/jenkins/plugins/monitoring/MonitoringDefaultAction/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

${it.getObjectMetadataAction().get().getObjectDisplayName()}

25 |
26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |