├── src ├── test │ ├── resources │ │ └── logging.properties │ ├── java │ │ └── au │ │ │ └── com │ │ │ └── centrumsystems │ │ │ └── hudson │ │ │ └── plugin │ │ │ ├── buildpipeline │ │ │ ├── testsupport │ │ │ │ ├── Page.java │ │ │ │ ├── Condition.java │ │ │ │ ├── LoginLogoutPage.java │ │ │ │ ├── PipelinePage.java │ │ │ │ ├── PipelineWebDriverTestBase.java │ │ │ │ ├── TestUtils.java │ │ │ │ └── BuildCardComponent.java │ │ │ ├── DownstreamProjectGridBuilderTest.java │ │ │ ├── ProjectFormTest.java │ │ │ ├── BuildFormTest.java │ │ │ ├── trigger │ │ │ │ ├── DownstreamDependencyTest.java │ │ │ │ └── BuildPipelineTriggerTest.java │ │ │ ├── BuildPipelineViewConstructorTest.java │ │ │ └── functionaltest │ │ │ │ ├── ParameterPassingTest.java │ │ │ │ └── BuildSecurityTest.java │ │ │ └── util │ │ │ ├── ProjectUtilTest.java │ │ │ └── BuildUtilTest.java │ └── groovy │ │ └── au │ │ └── com │ │ └── centrumsystems │ │ └── hudson │ │ └── plugin │ │ └── buildpipeline │ │ └── dashboard │ │ └── BuildPipelineDashboardTest.groovy └── main │ ├── webapp │ ├── images │ │ ├── gear.png │ │ ├── clock-small.png │ │ ├── gear-small.png │ │ ├── hourglass.png │ │ ├── user-small.png │ │ ├── has-parameter.png │ │ ├── fancybox │ │ │ ├── blank.gif │ │ │ ├── fancybox.png │ │ │ ├── fancy_close.png │ │ │ ├── fancybox-x.png │ │ │ ├── fancybox-y.png │ │ │ ├── fancy_loading.png │ │ │ ├── fancy_nav_left.png │ │ │ ├── fancy_shadow_e.png │ │ │ ├── fancy_shadow_n.png │ │ │ ├── fancy_shadow_s.png │ │ │ ├── fancy_shadow_w.png │ │ │ ├── fancy_nav_right.png │ │ │ ├── fancy_shadow_ne.png │ │ │ ├── fancy_shadow_nw.png │ │ │ ├── fancy_shadow_se.png │ │ │ ├── fancy_shadow_sw.png │ │ │ ├── fancy_title_left.png │ │ │ ├── fancy_title_main.png │ │ │ ├── fancy_title_over.png │ │ │ └── fancy_title_right.png │ │ └── application-small-list-blue.png │ ├── css │ │ ├── redmond │ │ │ └── images │ │ │ │ ├── ui-icons_217bc0_256x240.png │ │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ │ ├── ui-icons_469bdd_256x240.png │ │ │ │ ├── ui-icons_6da8d5_256x240.png │ │ │ │ ├── ui-icons_cd0a0a_256x240.png │ │ │ │ ├── ui-icons_d8e7f3_256x240.png │ │ │ │ ├── ui-icons_f9bd01_256x240.png │ │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ │ ├── ui-bg_flat_55_fbec88_40x100.png │ │ │ │ ├── ui-bg_glass_75_d0e5f5_1x400.png │ │ │ │ ├── ui-bg_glass_85_dfeffc_1x400.png │ │ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ │ │ ├── ui-bg_gloss-wave_55_5c9ccc_500x100.png │ │ │ │ ├── ui-bg_inset-hard_100_f5f8f9_1x100.png │ │ │ │ └── ui-bg_inset-hard_100_fcfdfd_1x100.png │ │ ├── jquery.tooltip.css │ │ ├── PIE.php │ │ ├── main_dashboard.css │ │ ├── main.css │ │ └── jquery.fancybox-1.3.4.css │ └── js │ │ ├── jquery.tooltip.min.js │ │ └── build-pipeline.js │ ├── resources │ ├── au │ │ └── com │ │ │ └── centrumsystems │ │ │ └── hudson │ │ │ └── plugin │ │ │ └── buildpipeline │ │ │ ├── BuildPipelineView │ │ │ ├── newViewDetail.jelly │ │ │ ├── help-noOfDisplayedBuilds.html │ │ │ ├── help-refreshFrequency.html │ │ │ ├── help-showRevisionBox.html │ │ │ ├── newViewDetail.properties │ │ │ ├── help-showPipelineParameters.html │ │ │ ├── help-showPipelineParametersInHeaders.html │ │ │ ├── main.jelly │ │ │ ├── help-alwaysAllowManualTrigger.html │ │ │ ├── help-showPipelineDefinitionHeader.html │ │ │ ├── help-triggerOnlyLatestJob.html │ │ │ ├── main_dashboard.jelly │ │ │ └── configure-entries.jelly │ │ │ ├── dashboard │ │ │ ├── Messages.properties │ │ │ └── BuildPipelineDashboard │ │ │ │ ├── portlet.jelly │ │ │ │ └── config.jelly │ │ │ ├── DownstreamProjectGridBuilder │ │ │ ├── help-firstJob.html │ │ │ └── config.jelly │ │ │ ├── trigger │ │ │ └── BuildPipelineTrigger │ │ │ │ ├── help-isManualBuild.html │ │ │ │ ├── help-buildPipeline.html │ │ │ │ └── config.jelly │ │ │ └── messages.properties │ └── index.jelly │ ├── java │ └── au │ │ └── com │ │ └── centrumsystems │ │ └── hudson │ │ └── plugin │ │ ├── buildpipeline │ │ ├── BuildGrid.java │ │ ├── BuildNotFoundException.java │ │ ├── ProjectGrid.java │ │ ├── Strings.java │ │ ├── ProjectGridBuilderDescriptor.java │ │ ├── DefaultBuildGridImpl.java │ │ ├── DefaultProjectGridImpl.java │ │ ├── BuildPipelineForm.java │ │ ├── dashboard │ │ │ ├── ReadOnlyBuildPipelineView.java │ │ │ └── BuildPipelineDashboard.java │ │ ├── ProjectGridBuilder.java │ │ ├── Grid.java │ │ ├── trigger │ │ │ └── DownstreamDependency.java │ │ ├── BuildForm.java │ │ ├── ProjectForm.java │ │ └── DownstreamProjectGridBuilder.java │ │ └── util │ │ ├── HudsonResult.java │ │ ├── ProjectUtil.java │ │ └── BuildUtil.java │ └── groovy │ └── au │ └── com │ └── centrumsystems │ └── hudson │ └── plugin │ └── buildpipeline │ ├── ProjectJSONBuilder.groovy │ └── BuildJSONBuilder.groovy ├── .gitignore ├── .hgignore ├── findbugs-exclude.xml ├── checkstyle_suppressions.xml ├── LICENSE.txt ├── README.md ├── checkstyle_rules.xml └── pom.xml /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | .level = FINE 2 | -------------------------------------------------------------------------------- /src/main/webapp/images/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/gear.png -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/newViewDetail.jelly: -------------------------------------------------------------------------------- 1 |
2 | ${%blurb} 3 |
-------------------------------------------------------------------------------- /src/main/webapp/images/clock-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/clock-small.png -------------------------------------------------------------------------------- /src/main/webapp/images/gear-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/gear-small.png -------------------------------------------------------------------------------- /src/main/webapp/images/hourglass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/hourglass.png -------------------------------------------------------------------------------- /src/main/webapp/images/user-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/user-small.png -------------------------------------------------------------------------------- /src/main/webapp/images/has-parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/has-parameter.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/blank.gif -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancybox.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_close.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancybox-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancybox-x.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancybox-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancybox-y.png -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/Messages.properties: -------------------------------------------------------------------------------- 1 | Portlet.BuildPipelineDashboardDescriptor=Build Pipeline Dashboard View -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_loading.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_nav_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_nav_left.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_e.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_n.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_s.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_w.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_nav_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_nav_right.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_ne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_ne.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_nw.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_se.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_shadow_sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_shadow_sw.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_title_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_title_left.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_title_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_title_main.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_title_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_title_over.png -------------------------------------------------------------------------------- /src/main/webapp/images/fancybox/fancy_title_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/fancybox/fancy_title_right.png -------------------------------------------------------------------------------- /src/main/webapp/images/application-small-list-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/images/application-small-list-blue.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_217bc0_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_217bc0_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_469bdd_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_469bdd_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_6da8d5_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_6da8d5_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_d8e7f3_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_d8e7f3_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-icons_f9bd01_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-icons_f9bd01_256x240.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-noOfDisplayedBuilds.html: -------------------------------------------------------------------------------- 1 |
2 | Select the number of build pipelines to display in the view. 3 |
-------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/DownstreamProjectGridBuilder/help-firstJob.html: -------------------------------------------------------------------------------- 1 |
2 | Select the initial or parent Job in the build pipeline view. 3 |
-------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | 5 | *.iml 6 | 7 | **/*.iml 8 | 9 | target 10 | work 11 | 12 | .checkstyle 13 | 14 | # IntelliJ 15 | .idea/ 16 | *.iml 17 | *.ipr 18 | *.iws 19 | -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png -------------------------------------------------------------------------------- /src/main/webapp/css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magnars/build-pipeline-plugin/master/src/main/webapp/css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-refreshFrequency.html: -------------------------------------------------------------------------------- 1 |
2 | Frequency at which the Build Pipeline Plugin updates the build cards in seconds 3 |
-------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | ^target$ 2 | ^work$ 3 | ^bin 4 | ^.classpath 5 | ^.project 6 | ^.settings 7 | ^.checkstyle 8 | ^.DS_Store 9 | ^.idea$ 10 | build-pipeline-plugin.iml 11 | build-pipeline-plugin.ipr 12 | build-pipeline-plugin.iws -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-showRevisionBox.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option if you want to display the revision box for each pipeline instance. 3 |
-------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/BuildPipelineTrigger/help-isManualBuild.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option "Require manual build execution" if you want to enable manual build promotion. 3 |
-------------------------------------------------------------------------------- /findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/newViewDetail.properties: -------------------------------------------------------------------------------- 1 | blurb=\ 2 | Shows the jobs in a build pipeline view. The complete pipeline of jobs that a version propagates through are shown as a row in the view. -------------------------------------------------------------------------------- /src/main/webapp/css/jquery.tooltip.css: -------------------------------------------------------------------------------- 1 | #tooltip { 2 | position: absolute; 3 | z-index: 3000; 4 | border: 1px solid #111; 5 | background-color: #eee; 6 | padding: 5px; 7 | opacity: 0.85; 8 | } 9 | #tooltip h3, #tooltip div { margin: 0; } 10 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-showPipelineParameters.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option if you want to display the parameters used 3 | to run the first job in each pipeline's revision box. 4 |
-------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/Page.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import java.net.URI; 4 | 5 | public interface Page { 6 | 7 | String getRelativeUrl(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-showPipelineParametersInHeaders.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option if you want to display the parameters used 3 | to run the latest successful job in the pipeline's project headers. 4 |
-------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/Condition.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | public interface Condition { 4 | 5 | boolean isSatisfied() throws Exception; 6 | String describe(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/main.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 |
2 | This plugin renders upstream and downstream connected jobs that typically form a build pipeline. In addition, it offers the ability to define manual triggers for jobs that require intervention prior to execution, e.g. an approval process outside of Jenkins. 3 |
-------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-alwaysAllowManualTrigger.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option if you want to be able to execute again a successful pipeline step. If the build is parameterized, this will re-execute the step using the same parameter values that were used when it was previously executed. 3 |
-------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildGrid.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | /** 4 | * {@link Grid} of {@link BuildForm}s, which represents one instance 5 | * of a pipeline execution. 6 | * 7 | * @author Kohsuke Kawaguchi 8 | */ 9 | public abstract class BuildGrid extends Grid { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-showPipelineDefinitionHeader.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option if you want to show the pipeline definition header in the pipeline view. If this option is not selected, then a pipeline that has never been run will not show any details about its jobs and appear like a blank form. Job details will only appear after the pipeline has been run at least once. 3 |
-------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/BuildPipelineTrigger/help-buildPipeline.html: -------------------------------------------------------------------------------- 1 |
2 | The Build Pipeline View plugin uses the relationship between upstream and downstream projects to map out a build pipeline. 3 | As default, the downstream project requires a manual trigger when the upstream job is completed. 4 | Specify the Downstream triggered projects in the "Downstream Project Name" field. Multiple projects can be specified by using comma, like "abc, def". 5 |
-------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/DownstreamProjectGridBuilder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${%This layout mode derives the pipeline structure based on the upstream/downstream trigger relationship between jobs.} 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildNotFoundException.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | /** 4 | * Build not found 5 | * 6 | * @author marcin 7 | * 8 | */ 9 | public class BuildNotFoundException extends RuntimeException { 10 | 11 | /** 12 | * serialVersionUID 13 | */ 14 | private static final long serialVersionUID = 1L; 15 | 16 | /** 17 | * constructor 18 | * 19 | * @param message 20 | * message 21 | */ 22 | public BuildNotFoundException(final String message) { 23 | super(message); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/help-triggerOnlyLatestJob.html: -------------------------------------------------------------------------------- 1 |
2 | Select this option to restrict the display of a Trigger button to only the most recent successful build pipelines.
3 |
4 | This option will also limit retries to just unsuccessful builds of the most recent build pipelines.
5 |
6 | Yes: Only the most recent successful builds displayed on the view will have 7 | a manual trigger button for the next build in the pipeline.
8 | No: All successful builds displayed on the view will have a manual trigger 9 | button for the next build in the pipeline.
10 |
-------------------------------------------------------------------------------- /checkstyle_suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webapp/css/PIE.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/messages.properties: -------------------------------------------------------------------------------- 1 | BuildPipelineTrigger.DisplayText=Build other projects (manual step) 2 | BuildPipelineTrigger.FailedPersistDuringRemoval=Failed to persist project BuildPipelineTrigger setting during removal of 3 | BuildPipelineTrigger.FailedPersistDuringRename_FMT=Failed to persist project BuildPipelineTrigger setting during rename from $1%s to $2%s 4 | BuildPipelineView.DisplayText=Build Pipeline View 5 | 6 | PipelineBuild.PendingBuildOfProject=Pending build of project: 7 | PipelineBuild.ToString_FMT=Project: %s : Build: %s 8 | PipelineBuild.RevisionNotAvailable=Revision not available 9 | 10 | Portlet.BuildPipelineDashboardDescriptor=Build Pipeline Dashboard View -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectGrid.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | /** 4 | * Two-dimensional placement of {@link ProjectForm}s into a grid/matrix layout. 5 | * 6 | * This class is also responsible for producing a sequence of {@link BuildGrid}s 7 | * that are the instances of the pipelines. 8 | * 9 | * @author Kohsuke Kawaguchi 10 | */ 11 | public abstract class ProjectGrid extends Grid { 12 | /** 13 | * Iterates instances of the pipeline grid view from this project layout. 14 | * 15 | * The caller is only going to iterate {@link BuildGrid}s up to a certain number 16 | * that the user has configured. 17 | * 18 | * 19 | * @return never null. 20 | */ 21 | public abstract Iterable builds(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/main_dashboard.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/groovy/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectJSONBuilder.groovy: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline 2 | 3 | import groovy.json.JsonBuilder 4 | 5 | class ProjectJSONBuilder { 6 | 7 | static String asJSON(ProjectForm projectForm) { 8 | def entries = new ArrayList() 9 | projectForm.lastSuccessfulBuildParams?.each() { key, value -> 10 | entries.add({ 11 | paramName(key) 12 | paramValue(value) 13 | }) 14 | } 15 | 16 | def builder = new JsonBuilder() 17 | def root = builder { 18 | id(projectForm.id) 19 | name(projectForm.name) 20 | health(projectForm.health) 21 | url(projectForm.url) 22 | lastSuccessfulBuildNumber(projectForm.lastSuccessfulBuildNumber) 23 | lastSuccessfulBuildParams(entries.toArray()) 24 | } 25 | return builder.toString() 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/BuildPipelineTrigger/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/DownstreamProjectGridBuilderTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import org.jvnet.hudson.test.HudsonTestCase; 4 | 5 | /** 6 | * @author Kohsuke Kawaguchi 7 | */ 8 | public class DownstreamProjectGridBuilderTest extends HudsonTestCase { 9 | /** 10 | * Makes sure that the config form will keep the settings intact. 11 | */ 12 | public void testConfigRoundtrip() throws Exception { 13 | DownstreamProjectGridBuilder gridBuilder = new DownstreamProjectGridBuilder("something"); 14 | BuildPipelineView v = new BuildPipelineView("foo","Title", gridBuilder, "5", true, null); 15 | jenkins.addView(v); 16 | configRoundtrip(v); 17 | BuildPipelineView av = (BuildPipelineView)jenkins.getView(v.getViewName()); 18 | assertSame(v,av); 19 | // assertNotSame(gridBuilder,(DownstreamProjectGridBuilder)av.getGridBuilder()); //FIXME: this is making the test fail, and it's not obvious why this should be true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/BuildPipelineDashboard/portlet.jelly: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-Present, Centrum Systems Pty Ltd 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/Strings.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import java.util.MissingResourceException; 4 | import java.util.ResourceBundle; 5 | 6 | /** 7 | * Message resource bundle utility 8 | * 9 | * @author Unknown 10 | */ 11 | public final class Strings { 12 | /** 13 | * bundle name 14 | */ 15 | private static final String BUNDLE_NAME = "au.com.centrumsystems.hudson.plugin.buildpipeline.messages"; //$NON-NLS-1$ 16 | 17 | /** 18 | * message resource bundle 19 | */ 20 | private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); 21 | 22 | /** 23 | * 24 | */ 25 | private Strings() { 26 | } 27 | 28 | /** 29 | * 30 | * @param key 31 | * key to resource bundle 32 | * @return resource of the key. 33 | */ 34 | public static String getString(final String key) { 35 | try { 36 | return RESOURCE_BUNDLE.getString(key); 37 | } catch (final MissingResourceException e) { 38 | return '!' + key + '!'; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Build Pipeline Plugin 2 | ===================== 3 | * [Wiki][wiki] 4 | * [Issue Tracking][issues] 5 | * [How to Contribute][contributing] 6 | 7 | Building the Project 8 | -------------------- 9 | 10 | ### Dependencies 11 | * [Apache Maven][maven] 3.0.4 or later 12 | 13 | ### Targets 14 | ```shell 15 | $ mvn clean install 16 | ``` 17 | 18 | Installing Plugin Locally 19 | ------------------------- 20 | 1. Build the project to produce `target/build-pipeline-plugin.hpi` 21 | 2. Remove any installation of the build-pipeline-plugin in `$user.home/.jenkins/plugins/` 22 | 3. Copy `target/build-pipeline-plugin.hpi` to `$user.home/.jenkins/plugins/` 23 | 4. Start/Restart Jenkins 24 | 25 | 26 | Continuous Integration 27 | ---------------------- 28 | After a pull request is accepted, it is run through a [Jenkins job][job] hosted on CloudBees. 29 | 30 | 31 | [wiki]: https://wiki.jenkins-ci.org/display/JENKINS/Build+Pipeline+Plugin 32 | [issues]: http://issues.jenkins-ci.org/secure/IssueNavigator.jspa?mode=hide&reset=true&jqlQuery=project+%3D+JENKINS+AND+status+in+%28Open%2C+%22In+Progress%22%2C+Reopened%29+AND+component+%3D+%27build-pipeline%27 33 | [contributing]: https://wiki.jenkins-ci.org/display/JENKINS/Build+Pipeline+Plugin+-+How+to+Contribute 34 | [maven]: https://maven.apache.org/ 35 | [job]: https://jenkins.ci.cloudbees.com/job/plugins/job/build-pipeline-plugin/ 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectGridBuilderDescriptor.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import hudson.DescriptorExtensionList; 4 | import hudson.model.Descriptor; 5 | import jenkins.model.Jenkins; 6 | 7 | /** 8 | * {@link Descriptor} for {@link ProjectGridBuilder}. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public abstract class ProjectGridBuilderDescriptor extends Descriptor { 13 | /** 14 | * For {@link Descriptor}s that explicitly specify {@link ProjectGridBuilder} 15 | * 16 | * @param clazz 17 | * The type of the {@link ProjectGridBuilder}. 18 | */ 19 | public ProjectGridBuilderDescriptor(Class clazz) { 20 | super(clazz); 21 | } 22 | 23 | /** 24 | * For {@link Descriptor}s that are enclosed in their {@link ProjectGridBuilder}s. 25 | */ 26 | public ProjectGridBuilderDescriptor() { 27 | } 28 | 29 | /** 30 | * Returns all the registered {@link ProjectGridBuilder} descriptors. 31 | * 32 | * @return 33 | * always non-null 34 | */ 35 | public static DescriptorExtensionList all() { 36 | return Jenkins.getInstance().getDescriptorList(ProjectGridBuilder.class); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/groovy/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/BuildPipelineDashboardTest.groovy: -------------------------------------------------------------------------------- 1 | import au.com.centrumsystems.hudson.plugin.buildpipeline.DownstreamProjectGridBuilder 2 | import spock.lang.* 3 | 4 | import au.com.centrumsystems.hudson.plugin.buildpipeline.dashboard.BuildPipelineDashboard 5 | import au.com.centrumsystems.hudson.plugin.buildpipeline.dashboard.ReadOnlyBuildPipelineView 6 | 7 | class BuildPipelineDashboardTest extends Specification { 8 | def cut 9 | def setup() { 10 | cut = new BuildPipelineDashboard('TestProject', 'Test Description', new DownstreamProjectGridBuilder('Job10'), '5') 11 | } 12 | 13 | def "should return a new BuildPipelineView"() { 14 | def bpv = cut.getBuildPipelineView() 15 | 16 | expect: 17 | bpv != null 18 | bpv instanceof ReadOnlyBuildPipelineView 19 | bpv.getGridBuilder().getFirstJob() == 'Job10' 20 | bpv.getNoOfDisplayedBuilds() == '5' 21 | bpv.getBuildViewTitle() == 'TestProject' 22 | } 23 | 24 | def "should not have build permissions"() { 25 | def bpv = cut.getBuildPipelineView() 26 | 27 | expect: 28 | !bpv.hasBuildPermission() 29 | } 30 | 31 | def "should not have any permission"() { 32 | def bpv = cut.getBuildPipelineView() 33 | 34 | expect: 35 | !bpv.hasPermission(null) 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/BuildPipelineDashboard/config.jelly: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/util/HudsonResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrum Systems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.util; 26 | 27 | /** 28 | * Hudson Result 29 | * 30 | * @author Centrum Systems 31 | */ 32 | public enum HudsonResult { 33 | /** 34 | * Hudson Result 35 | */ 36 | SUCCESS, UNSTABLE, FAILURE, NOT_BUILT, ABORT, BUILDING, PENDING, MANUAL 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/DefaultBuildGridImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * {@link BuildGrid} implementation backed by a sparse array. 8 | * 9 | * @author Kohsuke Kawaguchi 10 | */ 11 | public class DefaultBuildGridImpl extends BuildGrid { 12 | /** 13 | * Actual data. 14 | */ 15 | private final Map> data = new HashMap>(); 16 | 17 | /** 18 | * Dimension of the {@link #data} 19 | */ 20 | private int rows, cols; 21 | 22 | /** 23 | * Mutable, but only for the code that instantiates {@link DefaultBuildGridImpl}. 24 | * 25 | * @param row 26 | * position of the form 27 | * @param col 28 | * position of the form 29 | * @param p 30 | * The build to add. null to remove the value. 31 | */ 32 | public void set(int row, int col, BuildForm p) { 33 | Map c = data.get(row); 34 | if (c == null) { 35 | c = new HashMap(); 36 | data.put(row, c); 37 | } 38 | c.put(col, p); 39 | 40 | rows = Math.max(rows, row + 1); 41 | cols = Math.max(cols, col + 1); 42 | } 43 | 44 | @Override 45 | public BuildForm get(int row, int col) { 46 | final Map cols = data.get(row); 47 | if (cols == null) { 48 | return null; 49 | } 50 | return cols.get(col); 51 | } 52 | 53 | @Override 54 | public int getColumns() { 55 | return cols; 56 | } 57 | 58 | @Override 59 | public int getRows() { 60 | return rows; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineView/configure-entries.jelly: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 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 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/LoginLogoutPage.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import java.net.URL; 8 | import java.net.URLEncoder; 9 | 10 | import static au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.TestUtils.waitForElement; 11 | 12 | public class LoginLogoutPage implements Page { 13 | 14 | private final URL baseUrl; 15 | private final WebDriver driver; 16 | 17 | public LoginLogoutPage(WebDriver driver, URL baseUrl) { 18 | this.driver = driver; 19 | this.baseUrl = baseUrl; 20 | } 21 | 22 | public void login(String username) { 23 | driver.get(baseUrl + "login"); 24 | 25 | usernameField().sendKeys(username); 26 | passwordField().sendKeys(username); 27 | passwordField().submit(); 28 | } 29 | 30 | private WebElement usernameField() { 31 | return waitForElement(By.name("j_username"), driver); 32 | } 33 | 34 | private WebElement passwordField() { 35 | return waitForElement(By.name("j_password"), driver); 36 | } 37 | 38 | public void logout() { 39 | driver.get(baseUrl + "logout"); 40 | } 41 | 42 | public String getRelativeUrl() { 43 | return "login"; 44 | } 45 | 46 | public String getUrl(T nextPage) { 47 | return baseUrl + "login?from=" + encodeSafely(nextPage.getRelativeUrl()); 48 | } 49 | 50 | private String encodeSafely(String s) { 51 | try { 52 | return URLEncoder.encode(s, "utf-8"); 53 | } catch (Exception e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/DefaultProjectGridImpl.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * {@link ProjectGrid} backed by map. 8 | * 9 | * @author Kohsuke Kawaguchi 10 | * @author Centrum Systems 11 | */ 12 | public abstract class DefaultProjectGridImpl extends ProjectGrid { 13 | /** 14 | * Actual data store is a sparse map. 15 | */ 16 | private final Map> data = new HashMap>(); 17 | 18 | /** 19 | * Dimension of the {@link #data} 20 | */ 21 | private int rows, cols; 22 | 23 | /** 24 | * Mutable, but only for {@link ProjectGridBuilder} 25 | * 26 | * @param row 27 | * position of the form 28 | * @param col 29 | * position of the form 30 | * @param p 31 | * The project to add. null to remove the value. 32 | */ 33 | public void set(int row, int col, ProjectForm p) { 34 | Map c = data.get(row); 35 | if (c == null) { 36 | c = new HashMap(); 37 | data.put(row, c); 38 | } 39 | c.put(col, p); 40 | 41 | rows = Math.max(rows, row + 1); 42 | cols = Math.max(cols, col + 1); 43 | } 44 | 45 | /** 46 | * Gets the project at the specified location. 47 | * 48 | * @param row 49 | * position of the form 50 | * @param col 51 | * position of the form 52 | * @return 53 | * possibly null. 54 | */ 55 | @Override 56 | public ProjectForm get(int row, int col) { 57 | final Map cols = data.get(row); 58 | if (cols == null) { 59 | return null; 60 | } 61 | return cols.get(col); 62 | } 63 | 64 | @Override 65 | public int getColumns() { 66 | return cols; 67 | } 68 | 69 | @Override 70 | public int getRows() { 71 | return rows; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineForm.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import com.google.common.collect.Iterables; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.logging.Logger; 8 | 9 | /** 10 | * @author Centrum Systems 11 | * 12 | * Representation of the projects and their related builds making up the build pipeline view 13 | * 14 | */ 15 | public class BuildPipelineForm { 16 | /** 17 | * logger 18 | */ 19 | private static final Logger LOGGER = Logger.getLogger(BuildPipelineForm.class.getName()); 20 | 21 | /** 22 | * projects laid out in a grid using maps to ease accessing (or maybe I made it way too complicated by not using a 2-dimensional array) 23 | * Outside map holds rows and inner map has ProjectForm at a particular position (defined with key) 24 | */ 25 | private final ProjectGrid projectGrid; 26 | /** 27 | * a list of maps of map represents build pipelines laid out in grids, similar to projectGrid, but we have many of these grids 28 | */ 29 | private final List buildGrids; 30 | 31 | /** 32 | * 33 | * @param grid 34 | * Project to be laid out in a grid 35 | * @param builds 36 | * builds to be laid out in a grid 37 | */ 38 | public BuildPipelineForm(final ProjectGrid grid, final Iterable builds) { 39 | projectGrid = grid; 40 | buildGrids = Arrays.asList(Iterables.toArray(builds, BuildGrid.class)); 41 | } 42 | 43 | public ProjectGrid getProjectGrid() { 44 | return projectGrid; 45 | } 46 | 47 | /** 48 | * grid width is the longest column map counting empties (keys represent position, so they are used to determine width) 49 | * 50 | * @return width 51 | */ 52 | public Integer getGridWidth() { 53 | return projectGrid.getColumns(); 54 | } 55 | 56 | public Integer getGridHeight() { 57 | return projectGrid.getRows(); 58 | } 59 | 60 | public List getBuildGrids() { 61 | return buildGrids; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/PipelinePage.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import java.net.URL; 8 | 9 | import static au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.TestUtils.elementIsPresent; 10 | import static au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.TestUtils.waitForElement; 11 | 12 | public class PipelinePage implements Page { 13 | 14 | private static final String TRIGGER_PIPELINE_BUTTON = "trigger-pipeline-button"; 15 | 16 | private final String pipelineName; 17 | private final WebDriver webDriver; 18 | private final URL baseUrl; 19 | 20 | public PipelinePage(WebDriver webDriver, String pipelineName, URL baseUrl) { 21 | this.webDriver = webDriver; 22 | this.pipelineName = pipelineName; 23 | this.baseUrl = baseUrl; 24 | } 25 | 26 | public PipelinePage open() { 27 | webDriver.get(baseUrl + getRelativeUrl()); 28 | return this; 29 | } 30 | 31 | public boolean runButtonIsPresent() { 32 | return triggerPipelineButton() != null; 33 | } 34 | 35 | public boolean runButtonIsAbsent() { 36 | waitForElement(By.xpath("//img[@alt='Pipeline History']"), webDriver); 37 | return !elementIsPresent(By.id(TRIGGER_PIPELINE_BUTTON), webDriver); 38 | } 39 | 40 | public PipelinePage clickRunButton() { 41 | triggerPipelineButton().click(); 42 | return this; 43 | } 44 | 45 | public void reload() throws Exception { 46 | webDriver.navigate().refresh(); 47 | } 48 | 49 | public BuildCardComponent buildCard(int pipelineGroup, int pipeline, int card) { 50 | return new BuildCardComponent(webDriver, pipelineGroup, pipeline, card).waitFor(); 51 | } 52 | 53 | private WebElement triggerPipelineButton() { 54 | return waitForElement(By.id(TRIGGER_PIPELINE_BUTTON), webDriver); 55 | } 56 | 57 | @Override 58 | public String getRelativeUrl() { 59 | return "view/" + pipelineName; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/PipelineWebDriverTestBase.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import au.com.centrumsystems.hudson.plugin.buildpipeline.BuildPipelineView; 4 | import au.com.centrumsystems.hudson.plugin.buildpipeline.DownstreamProjectGridBuilder; 5 | import hudson.model.FreeStyleProject; 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.jvnet.hudson.test.FailureBuilder; 10 | import org.jvnet.hudson.test.JenkinsRule; 11 | import org.openqa.selenium.WebDriver; 12 | import org.openqa.selenium.firefox.FirefoxDriver; 13 | 14 | public class PipelineWebDriverTestBase { 15 | 16 | protected static final String INITIAL_JOB = "initial-job"; 17 | protected static final String SECOND_JOB = "second-job"; 18 | 19 | @Rule 20 | public JenkinsRule jr = new JenkinsRule(); 21 | 22 | protected FreeStyleProject initialJob; 23 | 24 | protected JenkinsRule.DummySecurityRealm realm; 25 | protected BuildPipelineView pipelineView; 26 | protected LoginLogoutPage loginLogoutPage; 27 | protected PipelinePage pipelinePage; 28 | protected WebDriver webDriver; 29 | 30 | @Before 31 | public void initSharedComponents() throws Exception { 32 | realm = jr.createDummySecurityRealm(); 33 | jr.jenkins.setSecurityRealm(realm); 34 | pipelineView = new BuildPipelineView("pipeline", "Pipeline", new DownstreamProjectGridBuilder(INITIAL_JOB), "5", false, true, false, false, false, 1, null, null); 35 | jr.jenkins.addView(pipelineView); 36 | 37 | initialJob = jr.createFreeStyleProject(INITIAL_JOB); 38 | 39 | webDriver = new FirefoxDriver(); 40 | loginLogoutPage = new LoginLogoutPage(webDriver, jr.getURL()); 41 | pipelinePage = new PipelinePage(webDriver, pipelineView.getViewName(), jr.getURL()); 42 | } 43 | 44 | @After 45 | public void cleanUpWebDriver() { 46 | webDriver.close(); 47 | webDriver.quit(); 48 | } 49 | 50 | protected FreeStyleProject createFailingJob(String name) throws Exception{ 51 | FreeStyleProject failingJob = jr.createFreeStyleProject(name); 52 | failingJob.getBuildersList().add(new FailureBuilder()); 53 | return failingJob; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/TestUtils.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import com.google.common.base.Function; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.NoSuchElementException; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.WebElement; 8 | import org.openqa.selenium.support.ui.FluentWait; 9 | import org.openqa.selenium.support.ui.WebDriverWait; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static java.util.concurrent.TimeUnit.SECONDS; 14 | 15 | public class TestUtils { 16 | 17 | public static WebElement waitForElement(final By findBy, WebDriver driver) { 18 | return waitForElement(findBy, driver, 10, SECONDS); 19 | } 20 | 21 | public static WebElement waitForElement(final By findBy, WebDriver driver, long timeout, TimeUnit timeUnit) { 22 | return new WebDriverWait(driver, 10) 23 | .withTimeout(timeout, timeUnit) 24 | .until(new Function() { 25 | public WebElement apply(WebDriver driver) { 26 | return driver.findElement(findBy); 27 | } 28 | }); 29 | } 30 | 31 | public static WebElement waitForElement(final By findBy, WebElement parentElement) { 32 | return waitForElement(findBy, parentElement, 10, SECONDS); 33 | } 34 | 35 | public static WebElement waitForElement(final By findBy, WebElement parentElement, long timeout, TimeUnit timeUnit) { 36 | return new FluentWait(parentElement) 37 | .withTimeout(timeout, timeUnit) 38 | .ignoring(NoSuchElementException.class) 39 | .until(new Function() { 40 | public WebElement apply(WebElement element) { 41 | return element.findElement(findBy); 42 | } 43 | }); 44 | } 45 | 46 | public static void checkState(boolean condition, String message) { 47 | if (!condition) { 48 | throw new IllegalStateException(message); 49 | } 50 | } 51 | 52 | public static boolean elementIsPresent(By locator, WebDriver driver) { 53 | try { 54 | driver.findElement(locator); 55 | } catch (NoSuchElementException e) { 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/webapp/css/main_dashboard.css: -------------------------------------------------------------------------------- 1 | #side-panel { 2 | display: block; 3 | width: 280px; 4 | padding: 4px; 5 | } 6 | 7 | .build-detail { 8 | padding: 5px 10px; 9 | background-color:#fafafa; 10 | } 11 | 12 | .build-body, .build-actions { 13 | display: None; 14 | } 15 | 16 | .rounded { 17 | height: 100%; 18 | vertical-align: middle; 19 | } 20 | 21 | tbody.pipelineGroup { 22 | background-color: rgb(175, 225, 255); 23 | background-color: rgba(175, 225, 255, 0.5); 24 | } 25 | 26 | #build-pipeline-plugin-content { 27 | background: white; 28 | } 29 | 30 | #view-message { 31 | display: block; 32 | } 33 | 34 | .revision { 35 | /* generated from http://www.colorzilla.com/gradient-editor/*/ 36 | display: table-cell; 37 | vertical-align: middle; 38 | height: 70px; 39 | margin: 2px; 40 | padding: 5px; 41 | text-align: center; 42 | 43 | background: rgb(255,116,0); /* Old browsers */ 44 | /* IE9 SVG, needs conditional override of 'filter' to 'none' */ 45 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmNzQwMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZjc0MDAiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); 46 | background: -moz-linear-gradient(top, rgba(255,116,0,1) 0%, rgba(255,116,0,1) 100%); /* FF3.6+ */ 47 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,116,0,1)), color-stop(100%,rgba(255,116,0,1))); /* Chrome,Safari4+ */ 48 | background: -webkit-linear-gradient(top, rgba(255,116,0,1) 0%,rgba(255,116,0,1) 100%); /* Chrome10+,Safari5.1+ */ 49 | background: -o-linear-gradient(top, rgba(255,116,0,1) 0%,rgba(255,116,0,1) 100%); /* Opera 11.10+ */ 50 | background: -ms-linear-gradient(top, rgba(255,116,0,1) 0%,rgba(255,116,0,1) 100%); /* IE10+ */ 51 | background: linear-gradient(top, rgba(255,116,0,1) 0%,rgba(255,116,0,1) 100%); /* W3C */ 52 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff7400', endColorstr='#ff7400',GradientType=0 ); /* IE6-8 */ 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/ReadOnlyBuildPipelineView.java: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // ADOBE SYSTEMS INCORPORATED 4 | // Copyright 2012 Adobe Systems Incorporated 5 | // All Rights Reserved. 6 | // 7 | // NOTICE: Adobe permits you to use, modify, and distribute this file 8 | // in accordance with the terms of the license agreement accompanying it. 9 | // 10 | //////////////////////////////////////////////////////////////////////////////// 11 | package au.com.centrumsystems.hudson.plugin.buildpipeline.dashboard; 12 | 13 | import au.com.centrumsystems.hudson.plugin.buildpipeline.BuildPipelineView; 14 | import au.com.centrumsystems.hudson.plugin.buildpipeline.ProjectGridBuilder; 15 | import hudson.security.Permission; 16 | 17 | /** 18 | * This class provides a read-only view for the existing build-pipeline view. All calls checking permissions return false. The other reason 19 | * for this class is that it's used in a different context and not as a child of the view tab. 20 | * 21 | * @author Ingo Richter (irichter@adobe.com) 22 | * @since 04/01/2012 23 | */ 24 | public class ReadOnlyBuildPipelineView extends BuildPipelineView { 25 | /** 26 | * 27 | * @param displayName 28 | * display name of build pipeline view 29 | * @param description 30 | * description of build pipeline view 31 | * @param gridBuilder 32 | * controls the data to be displayed. 33 | * @param noOfDisplayedBuilds 34 | * number of displayed build of build pipeline view 35 | * @param triggerOnlyLatestJob 36 | * is trigger only latest job? 37 | * @param cssUrl 38 | * URL for the custom CSS file. 39 | */ 40 | public ReadOnlyBuildPipelineView(final String displayName, final String description, final ProjectGridBuilder gridBuilder, 41 | final String noOfDisplayedBuilds, final boolean triggerOnlyLatestJob, final String cssUrl) { 42 | super(displayName, displayName, gridBuilder, noOfDisplayedBuilds, triggerOnlyLatestJob, cssUrl); 43 | // this is ugly, but there is no other way to set the description of the view 44 | super.description = description; 45 | } 46 | 47 | @Override 48 | public boolean hasBuildPermission() { 49 | // we are not a 'real view' in this case and we don't care in R/O mode 50 | return false; 51 | } 52 | 53 | @Override 54 | public boolean hasPermission(final Permission p) { 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/testsupport/BuildCardComponent.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.TestUtils.elementIsPresent; 10 | import static au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.TestUtils.waitForElement; 11 | import static java.util.concurrent.TimeUnit.SECONDS; 12 | 13 | public class BuildCardComponent { 14 | 15 | private static final String TRIGGER_SPAN_XPATH = "//span[@class='pointer trigger']"; 16 | private static final String RETRY_IMG_XPATH = "//span[@class='pointer trigger']/img[@alt='retry']"; 17 | 18 | private final WebDriver webDriver; 19 | private final int pipelineGroup; 20 | private final int pipeline; 21 | private final int card; 22 | 23 | private WebElement cardWebElement; 24 | 25 | public BuildCardComponent(WebDriver webDriver, int pipelineGroup, int pipeline, int card) { 26 | this.webDriver = webDriver; 27 | this.pipelineGroup = pipelineGroup; 28 | this.pipeline = pipeline; 29 | this.card = card; 30 | } 31 | 32 | public BuildCardComponent waitFor() { 33 | cardWebElement = waitForElement(By.xpath(cardXPath(pipelineGroup, pipeline, card)), webDriver); 34 | return this; 35 | } 36 | 37 | public BuildCardComponent waitForBuildToStart() throws Exception { 38 | waitForElement(By.xpath("//table[@class='progress-bar']"), webDriver); 39 | return this; 40 | } 41 | 42 | public BuildCardComponent waitForFailure() { 43 | return waitForStatus("FAILURE"); 44 | } 45 | 46 | public BuildCardComponent waitForStatus(String status) { 47 | waitForElement( 48 | By.xpath("//table[contains(@class, '" + status + "')]"), 49 | cardWebElement, 50 | 20, SECONDS); 51 | return this; 52 | } 53 | 54 | public boolean hasManualTriggerButton() { 55 | return elementIsPresent(By.xpath(TRIGGER_SPAN_XPATH), webDriver); 56 | } 57 | 58 | public boolean hasRetryButton() { 59 | return elementIsPresent(By.xpath(RETRY_IMG_XPATH), webDriver); 60 | } 61 | 62 | public BuildCardComponent clickTriggerButton() throws Exception { 63 | triggerButtonHtmlElement().click(); 64 | return this; 65 | } 66 | 67 | private WebElement triggerButtonHtmlElement() { 68 | return cardWebElement.findElement(By.xpath(TRIGGER_SPAN_XPATH)); 69 | } 70 | 71 | private String cardXPath(int pipelineGroup, int pipeline, int card) { 72 | return String.format("//table[@id = 'pipelines']/tbody[%d]/tr[@class='build-pipeline'][%d]/td[starts-with(@id,'build-')][%d]", 73 | pipelineGroup, pipeline, card); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildJSONBuilder.groovy: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline 2 | 3 | import groovy.json.JsonBuilder 4 | import hudson.model.Cause 5 | import hudson.model.Item 6 | import hudson.model.ItemGroup 7 | 8 | class BuildJSONBuilder { 9 | 10 | static String asJSON(ItemGroup context, PipelineBuild pipelineBuild, Integer formId, Integer projectId, List buildDependencyIds, ArrayList params) { 11 | def builder = new JsonBuilder() 12 | def buildStatus = pipelineBuild.currentBuildResult 13 | def root = builder { 14 | id(formId) 15 | build { 16 | dependencyIds(buildDependencyIds) 17 | displayName(pipelineBuild.currentBuild?.displayName) 18 | duration(pipelineBuild.buildDuration) 19 | extId(pipelineBuild.currentBuild?.externalizableId) 20 | hasPermission(pipelineBuild.project?.hasPermission(Item.BUILD)); 21 | hasUpstreamBuild(null != pipelineBuild.upstreamBuild) 22 | isBuilding(buildStatus == 'BUILDING') 23 | isComplete(buildStatus != 'BUILDING' && buildStatus != 'PENDING' && buildStatus != 'MANUAL') 24 | isPending(buildStatus == 'PENDING') 25 | isSuccess(buildStatus == 'SUCCESS') 26 | isReadyToBeManuallyBuilt(pipelineBuild.isReadyToBeManuallyBuilt()) 27 | isManualTrigger(pipelineBuild.isManualTrigger()) 28 | isRerunnable(pipelineBuild.isRerunnable()) 29 | isLatestBuild(null != pipelineBuild.currentBuild?.number && pipelineBuild.currentBuild?.number == pipelineBuild.project.getLastBuild()?.number) 30 | isUpstreamBuildLatest(null != pipelineBuild.upstreamBuild?.number && pipelineBuild.upstreamBuild?.number == pipelineBuild.upstreamPipelineBuild?.project?.getLastBuild()?.number) 31 | isUpstreamBuildLatestSuccess(null != pipelineBuild.upstreamBuild?.number && pipelineBuild.upstreamBuild?.number == pipelineBuild.upstreamPipelineBuild?.project?.lastSuccessfulBuild?.number) 32 | number(pipelineBuild.currentBuild?.number) 33 | progress(pipelineBuild.buildProgress) 34 | progressLeft(100 - pipelineBuild.buildProgress) 35 | startDate(pipelineBuild.formattedStartDate) 36 | startTime(pipelineBuild.formattedStartTime) 37 | status(buildStatus) 38 | url(pipelineBuild.buildResultURL ? pipelineBuild.buildResultURL : pipelineBuild.projectURL) 39 | userId(pipelineBuild.currentBuild?.getCause(Cause.UserIdCause.class)?.getUserId()) 40 | estimatedRemainingTime(pipelineBuild.currentBuild?.executor?.estimatedRemainingTime) 41 | } 42 | project { 43 | disabled(pipelineBuild.projectDisabled) 44 | name(pipelineBuild.project.getRelativeNameFromGroup(context)) 45 | url(pipelineBuild.projectURL) 46 | health(pipelineBuild.projectHealth) 47 | id(projectId) 48 | parameters(params) 49 | } 50 | upstream { 51 | projectName(pipelineBuild.upstreamPipelineBuild?.project?.getRelativeNameFromGroup(context)) 52 | buildNumber(pipelineBuild.upstreamBuild?.number) 53 | } 54 | } 55 | return builder.toString() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectGridBuilder.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import hudson.model.AbstractDescribableImpl; 4 | import hudson.model.Item; 5 | import org.kohsuke.stapler.AncestorInPath; 6 | import org.kohsuke.stapler.HttpResponse; 7 | import org.kohsuke.stapler.StaplerRequest; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * Encapsulates the definition of how to layout projects into a {@link ProjectGrid}. 13 | * 14 | * @author Kohsuke Kawaguchi 15 | */ 16 | public abstract class ProjectGridBuilder extends AbstractDescribableImpl { 17 | 18 | /** 19 | * Builds the grid. 20 | * 21 | * @param owner 22 | * The view for which this builder is working. Never null. 23 | * If the {@link ProjectGridBuilder} takes user-supplied job name, 24 | * this parameter should be used as a context to resolve relative names. 25 | * See {@link jenkins.model.Jenkins#getItem(String, hudson.model.ItemGroup)} (where you obtain 26 | * {@link hudson.model.ItemGroup} by {@link BuildPipelineView#getOwnerItemGroup()}. 27 | * @return 28 | * Never null, although the obtained {@link ProjectGrid} can be empty. 29 | */ 30 | public abstract ProjectGrid build(BuildPipelineView owner); 31 | 32 | /** 33 | * Called by {@link BuildPipelineView} when one of its members are renamed. 34 | * 35 | * @param owner 36 | * View that this builder is operating under. 37 | * @param oldName 38 | * Old short name of the job 39 | * @param newName 40 | * New short name of the job 41 | * @param item 42 | * Job being renamed. 43 | */ 44 | public void onJobRenamed(BuildPipelineView owner, Item item, String oldName, String newName) throws IOException { 45 | // no-op 46 | } 47 | 48 | /** 49 | * If the grid produced by this builder supports the notion of "starting a new pipeline instance", 50 | * and if the current user has a permission to do so, then return true. 51 | * 52 | * @param owner 53 | * View that this builder is operating under. 54 | * @return 55 | * True if the user has a permission. 56 | */ 57 | public abstract boolean hasBuildPermission(BuildPipelineView owner); 58 | 59 | /** 60 | * If the first job of the grid produced by this builder has parameters 61 | * 62 | * @param owner 63 | * View that this builder is operating under. 64 | * @return 65 | * True if the first job has parameters. 66 | */ 67 | public abstract boolean startsWithParameters(BuildPipelineView owner); 68 | 69 | /** 70 | * Called to start a new pipeline instance 71 | * (normally by triggering some job.) 72 | * 73 | * @param req 74 | * Current HTTP request 75 | * @param owner 76 | * View that this builder is operating under. 77 | * @return 78 | * The HTTP response. 79 | */ 80 | public abstract HttpResponse doBuild(StaplerRequest req, @AncestorInPath BuildPipelineView owner) throws IOException; 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | @Override 86 | public ProjectGridBuilderDescriptor getDescriptor() { 87 | return (ProjectGridBuilderDescriptor) super.getDescriptor(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/Grid.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | /** 4 | * Two-dimensional finite sparse placement of things (such as projects and builds) into a grid/matrix layout. 5 | * 6 | * @param 7 | * The type of the data that gets placed in a two dimensional table. 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public abstract class Grid { 11 | /** 12 | * Height of the grid. Total number of rows. 13 | * 14 | * @return positive integer 15 | */ 16 | public abstract int getRows(); 17 | 18 | /** 19 | * Width of the grid. Total number of columns. 20 | * 21 | * @return positive integer 22 | */ 23 | public abstract int getColumns(); 24 | 25 | /** 26 | * Obtains the project placed at the specific position. 27 | * 28 | * @param row 29 | * {@code 0<=row<getRows()} 30 | * @param col 31 | * {@code 0<=col<getColumns()} 32 | * @return 33 | * null if there's nothing placed in that position. 34 | */ 35 | public abstract T get(int row, int col); 36 | 37 | /** 38 | * Tests if the layout is empty. 39 | * 40 | * @return 41 | * true if this grid contains no {@link ProjectForm} at all. 42 | */ 43 | public boolean isEmpty() { 44 | return getRows() == 0; // && getColumns()==0; -- testing one is enough 45 | } 46 | 47 | /** 48 | * Determines the next row of the grid that should be populated. 49 | * 50 | * Given (currentRow,currentColumn), find a row R>=currentRow such that 51 | * the row R contains no project to any column to the right of current column. 52 | * That is, find the row in which we can place a sibling of the project 53 | * placed in (currentRow,currentColumn). 54 | * 55 | * This method is useful for determining the position to insert a {@link ProjectForm} 56 | * when the layout is tree-like. 57 | * 58 | * @param currentRow 59 | * - The current row of the grid being used 60 | * @param currentColumn 61 | * - The current column of the grid being used 62 | * @return - The row number to be used 63 | */ 64 | public int getNextAvailableRow(final int currentRow, final int currentColumn) { 65 | final int rows = getRows(); 66 | for (int nextRow = currentRow; nextRow < rows; nextRow++) { 67 | if (hasDataToRight(nextRow, currentColumn)) { 68 | nextRow++; 69 | } else { 70 | return nextRow; 71 | } 72 | } 73 | return rows; 74 | } 75 | 76 | /** 77 | * Tests if the row of the grid already contains entries in the columns greater than the entered column. 78 | * 79 | * @param row 80 | * - The row of the grid 81 | * @param col 82 | * - The current column of the grid 83 | * @return - true: The row does contain data in the columns greater than col, false: The row does not contain data in the columns 84 | * greater than col 85 | */ 86 | private boolean hasDataToRight(final int row, final int col) { 87 | final int cols = getColumns(); 88 | for (int i = col; i < cols; i++) { 89 | if (get(row, i) != null) { 90 | return true; 91 | } 92 | } 93 | return false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectFormTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 5 | import static org.junit.Assert.assertThat; 6 | import hudson.model.FreeStyleBuild; 7 | import hudson.model.FreeStyleProject; 8 | import hudson.tasks.BuildTrigger; 9 | 10 | import java.io.IOException; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.jvnet.hudson.test.HudsonTestCase; 15 | 16 | public class ProjectFormTest extends HudsonTestCase { 17 | @Override 18 | @Before 19 | public void setUp() throws Exception { 20 | super.setUp(); 21 | } 22 | 23 | @Test 24 | public void testConstructor() throws Exception { 25 | final String proj1 = "Project1"; 26 | final String proj2 = "Project2"; 27 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 28 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 29 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 30 | hudson.rebuildDependencyGraph(); 31 | final FreeStyleBuild build1 = buildAndAssertSuccess(project1); 32 | waitUntilNoActivity(); 33 | 34 | final PipelineBuild pb = new PipelineBuild(build1, project1, null); 35 | final ProjectForm pf = new ProjectForm(project1); 36 | assertEquals(project1.getName(), pf.getName()); 37 | assertEquals(pb.getCurrentBuildResult(), pf.getResult()); 38 | assertEquals(pb.getProjectURL(), pf.getUrl()); 39 | assertEquals(pb.getProject().getBuildHealth().getIconUrl().replaceAll("\\.gif", "\\.png"), pf.getHealth()); 40 | assertThat(pf.getDependencies().get(0).getName(), is(project2.getName())); 41 | } 42 | 43 | @Test 44 | public void testEquals() throws IOException { 45 | final String proj1 = "Project1"; 46 | final String proj2 = "Project2"; 47 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 48 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 49 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 50 | hudson.rebuildDependencyGraph(); 51 | 52 | final ProjectForm pf = new ProjectForm(project1); 53 | final ProjectForm pf1 = new ProjectForm(project1); 54 | final ProjectForm pf2 = new ProjectForm(project2); 55 | final String proj3 = null; 56 | final ProjectForm pf3 = new ProjectForm(proj3); 57 | 58 | assertTrue(pf.equals(pf1)); 59 | assertFalse(pf.equals(pf2)); 60 | assertNotNull(pf); 61 | assertFalse(pf.equals(pf3)); 62 | 63 | } 64 | 65 | @Test 66 | public void testNoInfiniteRecursion() throws IOException { 67 | final String proj1 = "Project1"; 68 | final String proj2 = "Project2"; 69 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 70 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 71 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 72 | project2.getPublishersList().add(new BuildTrigger(proj1, false)); 73 | hudson.rebuildDependencyGraph(); 74 | 75 | final ProjectForm form1 = new ProjectForm(project1); 76 | assertThat(form1.getDependencies(), hasSize(1)); 77 | assertThat(form1.getDependencies().get(0).getDependencies(), hasSize(0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/DownstreamDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrum Systems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.buildpipeline.trigger; 26 | 27 | import hudson.model.Action; 28 | import hudson.model.DependencyGraph.Dependency; 29 | import hudson.model.Result; 30 | import hudson.model.TaskListener; 31 | import hudson.model.AbstractBuild; 32 | import hudson.model.AbstractProject; 33 | 34 | import java.util.List; 35 | import java.util.logging.Logger; 36 | 37 | import au.com.centrumsystems.hudson.plugin.util.ProjectUtil; 38 | 39 | /** 40 | * Defines downstream dependency for the build pipeline trigger 41 | * 42 | * @author Centrum Systems 43 | */ 44 | public class DownstreamDependency extends Dependency { 45 | /** 46 | * logger 47 | */ 48 | private static final Logger LOGGER = Logger.getLogger(DownstreamDependency.class.getName()); 49 | 50 | /** 51 | * Downstream Dependency 52 | * 53 | * @param upstream 54 | * the upstream job 55 | * @param downstream 56 | * the downstream job 57 | */ 58 | public DownstreamDependency(final AbstractProject upstream, final AbstractProject downstream) { 59 | super(upstream, downstream); 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | @SuppressWarnings("rawtypes") 66 | @Override 67 | public boolean shouldTriggerBuild(final AbstractBuild build, final TaskListener listener, final List actions) { 68 | LOGGER.fine("Checking if build should be triggered."); 69 | 70 | // If the upstream project has an automatic trigger to the downstream project 71 | // and the current build result was SUCCESS then return true. 72 | if (ProjectUtil.isManualTrigger(build.getProject(), getDownstreamProject())) { 73 | LOGGER.fine("(shouldn't trigger: manual)"); 74 | return false; 75 | } 76 | 77 | final Result result = build.getResult(); 78 | if (result == null) { 79 | throw new IllegalStateException("Build with a null result in DownstreamDependency#shouldTriggerBuilder"); 80 | } 81 | if (result.isWorseThan(Result.SUCCESS)) { 82 | LOGGER.fine("(shouldn't trigger: unsuccessful build)"); 83 | return false; 84 | } 85 | LOGGER.fine("(should trigger)"); 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildFormTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 5 | import static org.junit.Assert.assertThat; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import hudson.model.FreeStyleBuild; 11 | import hudson.model.FreeStyleProject; 12 | import hudson.model.ParameterDefinition; 13 | import hudson.model.ParameterValue; 14 | import hudson.model.ParametersDefinitionProperty; 15 | import hudson.model.StringParameterDefinition; 16 | import hudson.tasks.BuildTrigger; 17 | import net.sf.json.JSONObject; 18 | 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.jvnet.hudson.test.HudsonTestCase; 22 | import org.kohsuke.stapler.StaplerRequest; 23 | 24 | public class BuildFormTest extends HudsonTestCase { 25 | @Override 26 | @Before 27 | public void setUp() throws Exception { 28 | super.setUp(); 29 | } 30 | 31 | @Test 32 | public void testConstructor() throws Exception { 33 | final String proj1 = "Project1"; 34 | final String proj2 = "Project2"; 35 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 36 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 37 | hudson.rebuildDependencyGraph(); 38 | final FreeStyleBuild build1 = buildAndAssertSuccess(project1); 39 | waitUntilNoActivity(); 40 | 41 | final PipelineBuild pb = new PipelineBuild(build1, project1, null); 42 | final BuildForm bf = new BuildForm(jenkins, pb); 43 | 44 | assertThat(bf.getStatus(), is(pb.getCurrentBuildResult())); 45 | } 46 | 47 | @Test 48 | public void testGetParameterList() throws Exception { 49 | final String proj1 = "Project1"; 50 | final String proj2 = "Project2"; 51 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 52 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 53 | 54 | final List pds = new ArrayList(); 55 | pds.add(new StringParameterDefinition("tag","")); 56 | pds.add(new StringParameterDefinition("branch","")); 57 | 58 | project1.addProperty(new ParametersDefinitionProperty(pds)); 59 | hudson.rebuildDependencyGraph(); 60 | final FreeStyleBuild build1 = buildAndAssertSuccess(project1); 61 | waitUntilNoActivity(); 62 | final ArrayList paramList = new ArrayList(); 63 | paramList.add("tag"); 64 | paramList.add("branch"); 65 | 66 | final PipelineBuild pb = new PipelineBuild(build1, project1, null); 67 | final BuildForm bf = new BuildForm(jenkins, pb); 68 | 69 | assertEquals(paramList, bf.getParameterList()); 70 | } 71 | 72 | @Test 73 | public void testNoInfiniteRecursion() throws Exception { 74 | final String proj1 = "Project1"; 75 | final String proj2 = "Project2"; 76 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 77 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 78 | project1.getPublishersList().add(new BuildTrigger(proj2, false)); 79 | project2.getPublishersList().add(new BuildTrigger(proj1, false)); 80 | hudson.rebuildDependencyGraph(); 81 | 82 | final BuildForm form1 = new BuildForm(jenkins, new PipelineBuild(null, project1, null)); 83 | assertThat(form1.getDependencies(), hasSize(1)); 84 | assertThat(form1.getDependencies().get(0).getDependencies(), hasSize(0)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/DownstreamDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrumsystems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */package au.com.centrumsystems.hudson.plugin.buildpipeline.trigger; 25 | 26 | import hudson.model.FreeStyleProject; 27 | import hudson.model.Hudson; 28 | import hudson.tasks.BuildTrigger; 29 | 30 | import java.io.IOException; 31 | 32 | import org.junit.Before; 33 | import org.junit.Test; 34 | import org.jvnet.hudson.test.HudsonTestCase; 35 | 36 | /** 37 | * @author Centrum Systems 38 | * 39 | */ 40 | public class DownstreamDependencyTest extends HudsonTestCase { 41 | 42 | @Override 43 | @Before 44 | public void setUp() throws Exception { 45 | super.setUp(); 46 | } 47 | 48 | @Test 49 | public void testDownstreamDependency() throws IOException { 50 | final String proj1 = "Proj1"; 51 | final String proj2 = "Proj2"; 52 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 53 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 54 | 55 | final DownstreamDependency myDD = new DownstreamDependency(project1, project2); 56 | assertEquals("Upstream project should be " + proj1, project1, myDD.getUpstreamProject()); 57 | assertEquals("Downstream project should be " + proj2, project2, myDD.getDownstreamProject()); 58 | } 59 | 60 | @Test 61 | public void testShouldTriggerBuild() throws Exception { 62 | final String proj1 = "Proj1"; 63 | final String proj2 = "Proj2"; 64 | final String proj3 = "Proj3"; 65 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 66 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 67 | final FreeStyleProject project3 = createFreeStyleProject(proj3); 68 | 69 | // Add TEST_PROJECT2 as a Manually executed pipeline project 70 | // Add TEST_PROJECT3 as a Post-build action -> build other projects 71 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2, null)); 72 | project1.getPublishersList().add(new BuildTrigger(proj3, true)); 73 | 74 | // Important; we must do this step to ensure that the dependency graphs are updated 75 | Hudson.getInstance().rebuildDependencyGraph(); 76 | 77 | // Build project1 and wait until completion 78 | buildAndAssertSuccess(project1); 79 | waitUntilNoActivity(); 80 | 81 | assertNull(project2.getLastBuild()); 82 | assertNotNull(project3.getLastBuild()); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildPipelineViewConstructorTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertThat; 6 | import static org.junit.Assert.assertTrue; 7 | import hudson.model.Hudson; 8 | import hudson.model.ItemGroup; 9 | import hudson.model.TopLevelItem; 10 | 11 | import java.io.IOException; 12 | 13 | import org.junit.Test; 14 | 15 | public class BuildPipelineViewConstructorTest { 16 | 17 | final String bpViewName = "MyTestView"; 18 | final String bpViewTitle = "MyTestViewTitle"; 19 | final String proj1 = "Proj1"; 20 | final DownstreamProjectGridBuilder gridBuilder = new DownstreamProjectGridBuilder(proj1); 21 | final DownstreamProjectGridBuilder nullGridBuilder = new DownstreamProjectGridBuilder(""); 22 | final String noOfBuilds = "5"; 23 | 24 | @Test 25 | public void testAlwaysAllowManualTrigger() throws IOException { 26 | // True 27 | BuildPipelineView testView = new BuildPipelineView(bpViewName, bpViewTitle, gridBuilder, noOfBuilds, true, true, false, false, false, 2, null, null); 28 | assertTrue(testView.isAlwaysAllowManualTrigger()); 29 | 30 | // False 31 | testView = new BuildPipelineView(bpViewName, bpViewTitle, nullGridBuilder, noOfBuilds, true, false, false, false, false, 2, null, null); 32 | assertFalse(testView.isAlwaysAllowManualTrigger()); 33 | } 34 | 35 | @Test 36 | public void testShowPipelineParameters() throws IOException { 37 | 38 | // True 39 | BuildPipelineView testView = new BuildPipelineView(bpViewName, bpViewTitle, gridBuilder, noOfBuilds, true, false, true, false, false, 2, null, null); 40 | assertTrue(testView.isShowPipelineParameters()); 41 | 42 | // False 43 | testView = new BuildPipelineView(bpViewName, bpViewTitle, nullGridBuilder, noOfBuilds, true, false, false, false, false, 2, null, null); 44 | assertFalse(testView.isShowPipelineParameters()); 45 | } 46 | 47 | @Test 48 | public void testShowPipelineParametersInHeaders() throws IOException { 49 | 50 | // True 51 | BuildPipelineView testView = new BuildPipelineView(bpViewName, bpViewTitle, gridBuilder, noOfBuilds, true, false, true, true, false, 2, null, null); 52 | assertTrue(testView.isShowPipelineParametersInHeaders()); 53 | 54 | // False 55 | testView = new BuildPipelineView(bpViewName, bpViewTitle, nullGridBuilder, noOfBuilds, true, false, false, false, false, 2, null, null); 56 | assertFalse(testView.isShowPipelineParametersInHeaders()); 57 | } 58 | 59 | @Test 60 | public void testRefreshFrequency() throws IOException { 61 | 62 | // False 63 | final BuildPipelineView testView = new BuildPipelineView(bpViewName, bpViewTitle, nullGridBuilder, noOfBuilds, true, false, false, false, false, 2, null, null); 64 | assertThat(testView.getRefreshFrequency(), is(2)); 65 | assertThat(testView.getRefreshFrequencyInMillis(), is(2000)); 66 | } 67 | 68 | /** 69 | * This is a factory to create an instance of the class under test. This helps to avoid a NPE in View.java when calling 70 | * getOwnerItemGroup and it's not set. This doesn't solve the root cause and it't only intended to make our tests succeed. 71 | */ 72 | static class BuildPipelineViewFactory { 73 | public static BuildPipelineView getBuildPipelineView(final String bpViewName, final String bpViewTitle, final ProjectGridBuilder gridBuilder, 74 | final String noOfBuilds, final boolean triggerOnlyLatestJob) { 75 | return new BuildPipelineView(bpViewName, bpViewTitle, gridBuilder, noOfBuilds, triggerOnlyLatestJob, null) { 76 | 77 | @Override 78 | public ItemGroup getOwnerItemGroup() { 79 | return Hudson.getInstance(); 80 | } 81 | }; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/functionaltest/ParameterPassingTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.functionaltest; 2 | 3 | import au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.PipelineWebDriverTestBase; 4 | import au.com.centrumsystems.hudson.plugin.buildpipeline.trigger.BuildPipelineTrigger; 5 | import com.google.common.base.Optional; 6 | import com.google.common.base.Predicate; 7 | import hudson.model.FreeStyleBuild; 8 | import hudson.model.FreeStyleProject; 9 | import hudson.model.ParametersAction; 10 | import hudson.model.StringParameterValue; 11 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameters; 12 | import hudson.plugins.parameterizedtrigger.PredefinedBuildParameters; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.openqa.selenium.support.ui.FluentWait; 16 | 17 | import java.util.Arrays; 18 | 19 | import static hudson.model.Result.FAILURE; 20 | import static java.util.concurrent.TimeUnit.SECONDS; 21 | import static org.hamcrest.core.Is.is; 22 | import static org.junit.Assert.assertThat; 23 | 24 | public class ParameterPassingTest extends PipelineWebDriverTestBase { 25 | 26 | FreeStyleProject secondJob; 27 | 28 | @Before 29 | public void init() throws Exception { 30 | secondJob = createFailingJob(SECOND_JOB); 31 | initialJob.getPublishersList().add( 32 | new BuildPipelineTrigger(secondJob.getName(), 33 | Arrays.asList(new PredefinedBuildParameters("myProp=some-value")))); 34 | jr.jenkins.rebuildDependencyGraph(); 35 | } 36 | 37 | @Test 38 | public void shouldPassParametersFromFirstJobToSecond() throws Exception { 39 | jr.buildAndAssertSuccess(initialJob); 40 | pipelinePage.open() 41 | .buildCard(1, 1, 2) 42 | .clickTriggerButton() 43 | .waitForFailure(); 44 | 45 | assertParameterValueIsPresentInBuild(secondJob.getBuilds().getFirstBuild()); 46 | } 47 | 48 | @Test 49 | public void secondJobShouldRetainParameterWhenRetried() throws Exception { 50 | jr.buildAndAssertSuccess(initialJob); 51 | pipelinePage.open() 52 | .buildCard(1, 1, 2) 53 | .clickTriggerButton() 54 | .waitForFailure() 55 | .clickTriggerButton(); 56 | 57 | waitForBuild2ToFail(); 58 | 59 | assertParameterValueIsPresentInBuild(secondJob.getBuilds().getLastBuild()); 60 | } 61 | 62 | private void waitForBuild2ToFail() { 63 | new FluentWait(secondJob) 64 | .ignoring(IllegalStateException.class) 65 | .withTimeout(10, SECONDS) 66 | .until(new Predicate() { 67 | public boolean apply(FreeStyleProject input) { 68 | return buildNumbered(2, input).getResult() == FAILURE; 69 | } 70 | }); 71 | } 72 | 73 | private void assertParameterValueIsPresentInBuild(FreeStyleBuild build) { 74 | assertThat(getMyPropParameterFrom(build).or(absentParameter()).value, is("some-value")); 75 | } 76 | 77 | private Optional getMyPropParameterFrom(FreeStyleBuild build) { 78 | ParametersAction parametersAction = build.getAction(ParametersAction.class); 79 | if (parametersAction != null) { 80 | return Optional.fromNullable((StringParameterValue) parametersAction.getParameter("myProp")); 81 | } 82 | 83 | return Optional.absent(); 84 | } 85 | 86 | private FreeStyleBuild buildNumbered(int number, FreeStyleProject job) { 87 | for (FreeStyleBuild build: job.getBuilds()) { 88 | if (build.getNumber() == number) { 89 | return build; 90 | } 91 | } 92 | 93 | throw new IllegalStateException("No build numbered " + number + " in " + job); 94 | } 95 | 96 | private StringParameterValue absentParameter() { 97 | return new StringParameterValue("myProp", "[absent]"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/functionaltest/BuildSecurityTest.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline.functionaltest; 2 | 3 | import au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.BuildCardComponent; 4 | import au.com.centrumsystems.hudson.plugin.buildpipeline.testsupport.PipelineWebDriverTestBase; 5 | import au.com.centrumsystems.hudson.plugin.buildpipeline.trigger.BuildPipelineTrigger; 6 | import hudson.model.FreeStyleProject; 7 | import hudson.model.Item; 8 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameters; 9 | import hudson.security.GlobalMatrixAuthorizationStrategy; 10 | import hudson.security.Permission; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | 14 | import java.util.Collections; 15 | 16 | import static org.junit.Assert.assertFalse; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | public class BuildSecurityTest extends PipelineWebDriverTestBase { 20 | 21 | static final String UNPRIVILEGED_USER = "unprivilegeduser"; 22 | static final String PRIVILEGED_USER = "privilegeduser"; 23 | 24 | FreeStyleProject secondJob; 25 | 26 | @Before 27 | public void init() throws Exception { 28 | GlobalMatrixAuthorizationStrategy authorizationStrategy = new GlobalMatrixAuthorizationStrategy(); 29 | jr.jenkins.setAuthorizationStrategy(authorizationStrategy); 30 | authorizationStrategy.add(Permission.READ, UNPRIVILEGED_USER); 31 | authorizationStrategy.add(Permission.READ, PRIVILEGED_USER); 32 | authorizationStrategy.add(Item.BUILD, PRIVILEGED_USER); 33 | authorizationStrategy.add(Item.CONFIGURE, PRIVILEGED_USER); 34 | 35 | secondJob = createFailingJob(SECOND_JOB); 36 | initialJob.getPublishersList().add(new BuildPipelineTrigger(secondJob.getName(), Collections.emptyList())); 37 | jr.jenkins.rebuildDependencyGraph(); 38 | } 39 | 40 | @Test 41 | public void pipelineShouldNotShowRunButtonIfUserNotPermittedToTriggerBuild() throws Exception { 42 | loginLogoutPage.login(UNPRIVILEGED_USER); 43 | pipelinePage.open(); 44 | 45 | assertTrue("The Run button should not be present", 46 | pipelinePage.runButtonIsAbsent()); 47 | } 48 | 49 | @Test 50 | public void pipelineShouldShowRunButtonIfUserPermittedToTriggerBuild() throws Exception { 51 | loginLogoutPage.login(PRIVILEGED_USER); 52 | pipelinePage.open(); 53 | 54 | assertTrue("The Run button should be present", 55 | pipelinePage.runButtonIsPresent()); 56 | } 57 | 58 | @Test 59 | public void manualBuildTriggerShouldNotBeShownIfNotPeritted() throws Exception { 60 | jr.buildAndAssertSuccess(initialJob); 61 | 62 | loginLogoutPage.login(UNPRIVILEGED_USER); 63 | pipelinePage.open(); 64 | 65 | assertFalse("Second card in pipeline should not have a trigger button", 66 | pipelinePage.buildCard(1, 1, 2).hasManualTriggerButton()); 67 | } 68 | 69 | @Test 70 | public void manualBuildTriggerShouldBeShownIfPermitted() throws Exception { 71 | jr.buildAndAssertSuccess(initialJob); 72 | 73 | loginLogoutPage.login(PRIVILEGED_USER); 74 | pipelinePage.open(); 75 | 76 | assertTrue("Second card in pipeline should have a trigger button", 77 | pipelinePage.buildCard(1, 1, 2).hasManualTriggerButton()); 78 | } 79 | 80 | @Test 81 | public void retryButtonShouldNotBeShownIfNotPermitted() throws Exception { 82 | jr.buildAndAssertSuccess(initialJob); 83 | loginLogoutPage.login(PRIVILEGED_USER); 84 | pipelinePage.open(); 85 | BuildCardComponent secondBuildCard = pipelinePage.buildCard(1, 1, 2); 86 | secondBuildCard.clickTriggerButton(); 87 | secondBuildCard.waitForFailure(); 88 | 89 | loginLogoutPage.logout(); 90 | loginLogoutPage.login(UNPRIVILEGED_USER); 91 | pipelinePage.open(); 92 | 93 | assertFalse("Second card in pipeline should not have a retry button", 94 | pipelinePage.buildCard(1, 1, 2).hasRetryButton()); 95 | } 96 | 97 | @Test 98 | public void retryButtonShouldBeShownIfPermitted() throws Exception { 99 | jr.buildAndAssertSuccess(initialJob); 100 | 101 | loginLogoutPage.login(PRIVILEGED_USER); 102 | pipelinePage.open(); 103 | 104 | BuildCardComponent secondBuildCard = pipelinePage.buildCard(1, 1, 2); 105 | secondBuildCard.clickTriggerButton(); 106 | secondBuildCard.waitForFailure(); 107 | 108 | assertTrue("Second card in pipeline should have a retry button", 109 | pipelinePage.buildCard(1, 1, 2).hasRetryButton()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/webapp/js/jquery.tooltip.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Tooltip plugin 1.3 3 | * 4 | * http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/ 5 | * http://docs.jquery.com/Plugins/Tooltip 6 | * 7 | * Copyright (c) 2006 - 2008 Jörn Zaefferer 8 | * 9 | * $Id: jquery.tooltip.js 5741 2008-06-21 15:22:16Z joern.zaefferer $ 10 | * 11 | * Dual licensed under the MIT and GPL licenses: 12 | * http://www.opensource.org/licenses/mit-license.php 13 | * http://www.gnu.org/licenses/gpl.html 14 | */;(function($){var helper={},current,title,tID,IE=$.browser.msie&&/MSIE\s(5\.5|6\.)/.test(navigator.userAgent),track=false;$.tooltip={blocked:false,defaults:{delay:200,fade:false,showURL:true,extraClass:"",top:15,left:15,id:"tooltip"},block:function(){$.tooltip.blocked=!$.tooltip.blocked;}};$.fn.extend({tooltip:function(settings){settings=$.extend({},$.tooltip.defaults,settings);createHelper(settings);return this.each(function(){$.data(this,"tooltip",settings);this.tOpacity=helper.parent.css("opacity");this.tooltipText=this.title;$(this).removeAttr("title");this.alt="";}).mouseover(save).mouseout(hide).click(hide);},fixPNG:IE?function(){return this.each(function(){var image=$(this).css('backgroundImage');if(image.match(/^url\(["']?(.*\.png)["']?\)$/i)){image=RegExp.$1;$(this).css({'backgroundImage':'none','filter':"progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='"+image+"')"}).each(function(){var position=$(this).css('position');if(position!='absolute'&&position!='relative')$(this).css('position','relative');});}});}:function(){return this;},unfixPNG:IE?function(){return this.each(function(){$(this).css({'filter':'',backgroundImage:''});});}:function(){return this;},hideWhenEmpty:function(){return this.each(function(){$(this)[$(this).html()?"show":"hide"]();});},url:function(){return this.attr('href')||this.attr('src');}});function createHelper(settings){if(helper.parent)return;helper.parent=$('

').appendTo(document.body).hide();if($.fn.bgiframe)helper.parent.bgiframe();helper.title=$('h3',helper.parent);helper.body=$('div.body',helper.parent);helper.url=$('div.url',helper.parent);}function settings(element){return $.data(element,"tooltip");}function handle(event){if(settings(this).delay)tID=setTimeout(show,settings(this).delay);else 15 | show();track=!!settings(this).track;$(document.body).bind('mousemove',update);update(event);}function save(){if($.tooltip.blocked||this==current||(!this.tooltipText&&!settings(this).bodyHandler))return;current=this;title=this.tooltipText;if(settings(this).bodyHandler){helper.title.hide();var bodyContent=settings(this).bodyHandler.call(this);if(bodyContent.nodeType||bodyContent.jquery){helper.body.empty().append(bodyContent)}else{helper.body.html(bodyContent);}helper.body.show();}else if(settings(this).showBody){var parts=title.split(settings(this).showBody);helper.title.html(parts.shift()).show();helper.body.empty();for(var i=0,part;(part=parts[i]);i++){if(i>0)helper.body.append("
");helper.body.append(part);}helper.body.hideWhenEmpty();}else{helper.title.html(title).show();helper.body.hide();}if(settings(this).showURL&&$(this).url())helper.url.html($(this).url().replace('http://','')).show();else 16 | helper.url.hide();helper.parent.addClass(settings(this).extraClass);if(settings(this).fixPNG)helper.parent.fixPNG();handle.apply(this,arguments);}function show(){tID=null;if((!IE||!$.fn.bgiframe)&&settings(current).fade){if(helper.parent.is(":animated"))helper.parent.stop().show().fadeTo(settings(current).fade,current.tOpacity);else 17 | helper.parent.is(':visible')?helper.parent.fadeTo(settings(current).fade,current.tOpacity):helper.parent.fadeIn(settings(current).fade);}else{helper.parent.show();}update();}function update(event){if($.tooltip.blocked)return;if(event&&event.target.tagName=="OPTION"){return;}if(!track&&helper.parent.is(":visible")){$(document.body).unbind('mousemove',update)}if(current==null){$(document.body).unbind('mousemove',update);return;}helper.parent.removeClass("viewport-right").removeClass("viewport-bottom");var left=helper.parent[0].offsetLeft;var top=helper.parent[0].offsetTop;if(event){left=event.pageX+settings(current).left;top=event.pageY+settings(current).top;var right='auto';if(settings(current).positionLeft){right=$(window).width()-left;left='auto';}helper.parent.css({left:left,right:right,top:top});}var v=viewport(),h=helper.parent[0];if(v.x+v.cx 2 | 3 | 4 | 7 | 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 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 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 | -------------------------------------------------------------------------------- /src/main/webapp/js/build-pipeline.js: -------------------------------------------------------------------------------- 1 | var BuildPipeline = function(viewProxy, buildCardTemplate, projectCardTemplate, refreshFrequency){ 2 | this.buildCardTemplate = buildCardTemplate; 3 | this.projectCardTemplate = projectCardTemplate; 4 | this.buildProxies = {}; 5 | this.projectProxies = {}; 6 | this.viewProxy = viewProxy; 7 | this.refreshFrequency = refreshFrequency; 8 | }; 9 | 10 | BuildPipeline.prototype = { 11 | showProgress : function(id, dependencies) { 12 | var buildPipeline = this; 13 | var intervalId = setInterval(function(){ 14 | if (isPageVisible()) { 15 | buildPipeline.buildProxies[id].asJSON(function(data){ 16 | var buildData = jQuery.parseJSON(data.responseObject()); 17 | if (buildData.build.progress > 0) { 18 | buildPipeline.updateBuildCardFromJSON(buildData, false); 19 | } else { 20 | buildPipeline.updateBuildCardFromJSON(buildData, true); 21 | if (!jQuery.isEmptyObject(buildPipeline.projectProxies)) { 22 | buildPipeline.updateProjectCard(buildData.project.id); 23 | } 24 | clearInterval(intervalId); 25 | //refresh all build cards since some statuses will be invalid for older builds 26 | buildPipeline.updateAllBuildCards(dependencies); 27 | // trigger all dependency tracking 28 | jQuery.each(dependencies, function(){ 29 | jQuery("#pipelines").trigger("show-status-" + this); 30 | }); 31 | } 32 | }); 33 | } 34 | }, buildPipeline.refreshFrequency); 35 | }, 36 | updateBuildCard : function(id) { 37 | var buildPipeline = this; 38 | buildPipeline.buildProxies[id].asJSON(function(data){ 39 | buildPipeline.updateBuildCardFromJSON(jQuery.parseJSON(data.responseObject()), true); 40 | }); 41 | }, 42 | updateAllBuildCards : function(dependenciesNotToUpdate) { 43 | var buildPipeline = this; 44 | jQuery.each(buildPipeline.buildProxies, function(key, value){ 45 | if (jQuery.inArray(parseInt(key), dependenciesNotToUpdate) < 0) { 46 | buildPipeline.updateBuildCard(key); 47 | } 48 | }); 49 | }, 50 | updateProjectCard : function(id) { 51 | var buildPipeline = this; 52 | buildPipeline.projectProxies[id].asJSON(function(data){ 53 | buildPipeline.updateProjectCardFromJSON(jQuery.parseJSON(data.responseObject()), true); 54 | }); 55 | }, 56 | updateBuildCardFromJSON : function(buildAsJSON, fadeIn) { 57 | var buildPipeline = this; 58 | jQuery("#build-" + buildAsJSON.id).empty(); 59 | jQuery(buildPipeline.buildCardTemplate(buildAsJSON)).hide().appendTo("#build-" + buildAsJSON.id).fadeIn(fadeIn ? 1000 : 0); 60 | }, 61 | updateProjectCardFromJSON : function(projectAsJSON, fadeIn) { 62 | var buildPipeline = this; 63 | jQuery("#project-" + projectAsJSON.id).empty(); 64 | jQuery(buildPipeline.projectCardTemplate(projectAsJSON)).hide().appendTo("#project-" + projectAsJSON.id).fadeIn(fadeIn ? 1000 : 0); 65 | }, 66 | updateNextBuildAndShowProgress : function(id, nextBuildNumber, dependencies) { 67 | var buildPipeline = this; 68 | //try to get the updated build, that's not pending 69 | var intervalId = setInterval(function(){ 70 | buildPipeline.buildProxies[id].updatePipelineBuild(nextBuildNumber, function(updated){ 71 | if (updated.responseObject()) { 72 | buildPipeline.showProgress(id, dependencies); 73 | clearInterval(intervalId); 74 | } 75 | }); 76 | }, buildPipeline.refreshFrequency); 77 | }, 78 | triggerBuild : function(id, upstreamProjectName, upstreamBuildNumber, triggerProjectName, dependencyIds) { 79 | var buildPipeline = this; 80 | buildPipeline.viewProxy.triggerManualBuild(upstreamBuildNumber, triggerProjectName, upstreamProjectName, function(data){ 81 | buildPipeline.updateNextBuildAndShowProgress(id, data.responseObject(), dependencyIds); 82 | }); 83 | }, 84 | retryBuild : function(id, triggerProjectName, dependencyIds) { 85 | var buildPipeline = this; 86 | buildPipeline.viewProxy.retryBuild(triggerProjectName, function(data){ 87 | buildPipeline.updateNextBuildAndShowProgress(id, data.responseObject(), dependencyIds); 88 | }); 89 | }, 90 | rerunBuild : function(id, buildExternalizableId, dependencyIds) { 91 | var buildPipeline = this; 92 | buildPipeline.viewProxy.rerunBuild(buildExternalizableId, function(data){ 93 | buildPipeline.updateNextBuildAndShowProgress(id, data.responseObject(), dependencyIds); 94 | }); 95 | }, 96 | showSpinner : function(id){ 97 | jQuery("#status-bar-" + id).html('
'); 98 | jQuery("#icons-" + id).empty(); 99 | }, 100 | fillDialog : function(href, title) { 101 | jQuery.fancybox({ 102 | type: 'iframe', 103 | title: title, 104 | titlePosition: 'outside', 105 | href: '/' + href, 106 | transitionIn : 'elastic', 107 | transitionOut : 'elastic', 108 | width: '90%', 109 | height: '80%' 110 | }); 111 | }, 112 | closeDialog : function() { 113 | jQuery.fancybox.close(); 114 | }, 115 | showModalSpinner : function() { 116 | jQuery.fancybox.showActivity(); 117 | }, 118 | hideModalSpinner : function() { 119 | jQuery.fancybox.hideActivity(); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/BuildForm.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import hudson.model.AbstractBuild; 4 | import hudson.model.AbstractProject; 5 | import hudson.model.ItemGroup; 6 | import hudson.model.ParametersDefinitionProperty; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.LinkedHashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.logging.Logger; 15 | 16 | import org.kohsuke.stapler.bind.JavaScriptMethod; 17 | 18 | /** 19 | * @author Centrum Systems 20 | * 21 | * Representation of a build results pipeline 22 | * 23 | */ 24 | public class BuildForm { 25 | /** 26 | * logger 27 | */ 28 | private static final Logger LOGGER = Logger.getLogger(BuildForm.class.getName()); 29 | 30 | /** 31 | * status 32 | */ 33 | private String status = ""; 34 | 35 | /** 36 | * pipeline build 37 | */ 38 | private PipelineBuild pipelineBuild; 39 | 40 | /** 41 | * id 42 | */ 43 | private final Integer id; 44 | 45 | /** 46 | * project id used to update project cards 47 | */ 48 | // TODO refactor to get rid of this coupling 49 | private final Integer projectId; 50 | 51 | /** 52 | * downstream builds 53 | */ 54 | private List dependencies = new ArrayList(); 55 | 56 | 57 | /** 58 | * project stringfied list of parameters for the project 59 | * */ 60 | private final ArrayList parameters; 61 | 62 | /** 63 | * The item group pipeline view belongs to 64 | */ 65 | private final ItemGroup context; 66 | 67 | /** 68 | * @param pipelineBuild 69 | * pipeline build domain used to see the form 70 | * @param context 71 | * item group pipeline view belongs to, used to compute relative item names 72 | */ 73 | public BuildForm(ItemGroup context, final PipelineBuild pipelineBuild) { 74 | this(context, pipelineBuild, new LinkedHashSet>(Arrays.asList(pipelineBuild.getProject()))); 75 | } 76 | 77 | /** 78 | * @param pipelineBuild 79 | * pipeline build domain used to see the form 80 | * @param context 81 | * item group pipeline view belongs to, used to compute relative item names 82 | * @param parentPath 83 | * already traversed projects 84 | */ 85 | private BuildForm(ItemGroup context, final PipelineBuild pipelineBuild, final Collection> parentPath) { 86 | this.context = context; 87 | this.pipelineBuild = pipelineBuild; 88 | status = pipelineBuild.getCurrentBuildResult(); 89 | dependencies = new ArrayList(); 90 | for (final PipelineBuild downstream : pipelineBuild.getDownstreamPipeline()) { 91 | final Collection> forkedPath = new LinkedHashSet>(parentPath); 92 | if (forkedPath.add(downstream.getProject())) { 93 | dependencies.add(new BuildForm(context, downstream, forkedPath)); 94 | } 95 | } 96 | id = hashCode(); 97 | final AbstractProject project = pipelineBuild.getProject(); 98 | projectId = project.getFullName().hashCode(); 99 | final ParametersDefinitionProperty params = project.getProperty(ParametersDefinitionProperty.class); 100 | final ArrayList paramList = new ArrayList(); 101 | if (params != null) { 102 | for (String p : params.getParameterDefinitionNames()) { 103 | paramList.add(p); 104 | } 105 | } 106 | parameters = paramList; 107 | } 108 | 109 | public String getStatus() { 110 | return status; 111 | } 112 | 113 | public List getDependencies() { 114 | return dependencies; 115 | } 116 | 117 | /** 118 | * @return All ids for existing depencies. 119 | */ 120 | public List getDependencyIds() { 121 | final List ids = new ArrayList(); 122 | for (final BuildForm dependency : dependencies) { 123 | ids.add(dependency.getId()); 124 | } 125 | return ids; 126 | } 127 | 128 | /** 129 | * @return convert pipelineBuild as json format. 130 | */ 131 | @JavaScriptMethod 132 | public String asJSON() { 133 | return BuildJSONBuilder.asJSON(context, pipelineBuild, id, projectId, getDependencyIds(), getParameterList()); 134 | } 135 | 136 | public int getId() { 137 | return id; 138 | } 139 | 140 | /** 141 | * 142 | * @param nextBuildNumber 143 | * nextBuildNumber 144 | * @return is the build pipeline updated. 145 | */ 146 | @JavaScriptMethod 147 | public boolean updatePipelineBuild(final int nextBuildNumber) { 148 | boolean updated = false; 149 | final AbstractBuild newBuild = pipelineBuild.getProject().getBuildByNumber(nextBuildNumber); 150 | if (newBuild != null) { 151 | updated = true; 152 | pipelineBuild = new PipelineBuild(newBuild, newBuild.getProject(), pipelineBuild.getUpstreamBuild()); 153 | } 154 | return updated; 155 | } 156 | 157 | public int getNextBuildNumber() { 158 | return pipelineBuild.getProject().getNextBuildNumber(); 159 | } 160 | 161 | public String getRevision() { 162 | return pipelineBuild.getPipelineVersion(); 163 | } 164 | 165 | @JavaScriptMethod 166 | public boolean isManualTrigger() { 167 | return pipelineBuild.isManualTrigger(); 168 | } 169 | 170 | public Map getParameters() { 171 | return pipelineBuild.getBuildParameters(); 172 | } 173 | 174 | public ArrayList getParameterList() { 175 | return parameters; 176 | } 177 | 178 | public Integer getProjectId() { 179 | return projectId; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/util/ProjectUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrum Systems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.util; 26 | 27 | import hudson.model.DependencyGraph; 28 | import hudson.model.ParameterValue; 29 | import hudson.model.AbstractProject; 30 | import hudson.model.Descriptor; 31 | import hudson.model.Hudson; 32 | import hudson.model.ParameterDefinition; 33 | import hudson.model.ParametersAction; 34 | import hudson.model.ParametersDefinitionProperty; 35 | import hudson.tasks.Publisher; 36 | import hudson.util.DescribableList; 37 | 38 | import java.util.ArrayList; 39 | import java.util.List; 40 | 41 | import au.com.centrumsystems.hudson.plugin.buildpipeline.trigger.BuildPipelineTrigger; 42 | import jenkins.model.Jenkins; 43 | 44 | /** 45 | * Provides helper methods for #hudson.model.AbstractProject 46 | * 47 | * @author Centrum Systems 48 | * 49 | */ 50 | public final class ProjectUtil { 51 | 52 | /** 53 | * Default constructor 54 | */ 55 | public ProjectUtil() { 56 | 57 | } 58 | 59 | /** 60 | * Given a Project get a List of all of its Downstream projects 61 | * 62 | * @param currentProject 63 | * Current project 64 | * @return List of Downstream projects 65 | */ 66 | public static List> getDownstreamProjects(final AbstractProject currentProject) { 67 | final DependencyGraph myDependencyGraph = Hudson.getInstance().getDependencyGraph(); 68 | 69 | final List> downstreamProjectsList = new ArrayList>(); 70 | for (final AbstractProject proj : myDependencyGraph.getDownstream(currentProject)) { 71 | downstreamProjectsList.add(proj); 72 | } 73 | return downstreamProjectsList; 74 | } 75 | 76 | /** 77 | * Determines whether a project has any downstream projects. 78 | * 79 | * @param currentProject 80 | * - The project in question 81 | * @return - true: Current project has downstream projects; false: Current project does not have any downstream projects 82 | */ 83 | public static boolean hasDownstreamProjects(final AbstractProject currentProject) { 84 | return (getDownstreamProjects(currentProject).size() > 0); 85 | } 86 | 87 | /** 88 | * Determines if a manual trigger of the downstream project from the current upstream project is required. 89 | * 90 | * @param upstreamProject 91 | * - The upstream project 92 | * @param downstreamProject 93 | * - The downstream project 94 | * 95 | * @return - true: Manual trigger required; false: Manual trigger not required 96 | */ 97 | public static boolean isManualTrigger(final AbstractProject upstreamProject, final AbstractProject downstreamProject) { 98 | 99 | boolean manualTrigger = false; 100 | if ((upstreamProject != null) && (downstreamProject != null)) { 101 | final DescribableList> upstreamPublishersLists = upstreamProject.getPublishersList(); 102 | 103 | for (final Publisher upstreamPub : upstreamPublishersLists) { 104 | if (upstreamPub instanceof BuildPipelineTrigger) { 105 | final String manualDownstreamProjects = ((BuildPipelineTrigger) upstreamPub).getDownstreamProjectNames(); 106 | final String[] downstreamProjs = manualDownstreamProjects.split(","); 107 | for (final String nextProj : downstreamProjs) { 108 | if (Jenkins.getInstance().getItem(nextProj.trim(), upstreamProject) == downstreamProject) { 109 | manualTrigger = true; 110 | break; 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | return manualTrigger; 118 | 119 | } 120 | 121 | /** 122 | * Gets the ParametersAction of an AbstractProject 123 | * 124 | * @param project 125 | * - The AbstractProject 126 | * @return The ParametersAction of the AbstractProject 127 | */ 128 | public static ParametersAction getProjectParametersAction(final AbstractProject project) { 129 | if (project != null) { 130 | final ParametersDefinitionProperty property = project.getProperty(ParametersDefinitionProperty.class); 131 | if (property == null) { 132 | return null; 133 | } 134 | 135 | final List parameters = new ArrayList(); 136 | for (final ParameterDefinition pd : property.getParameterDefinitions()) { 137 | final ParameterValue param = pd.getDefaultParameterValue(); 138 | if (param != null) { 139 | parameters.add(param); 140 | } 141 | } 142 | return new ParametersAction(parameters); 143 | } else { 144 | return null; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/dashboard/BuildPipelineDashboard.java: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // ADOBE SYSTEMS INCORPORATED 4 | // Copyright 2012 Adobe Systems Incorporated 5 | // All Rights Reserved. 6 | // 7 | // NOTICE: Adobe permits you to use, modify, and distribute this file 8 | // in accordance with the terms of the license agreement accompanying it. 9 | // 10 | //////////////////////////////////////////////////////////////////////////////// 11 | package au.com.centrumsystems.hudson.plugin.buildpipeline.dashboard; 12 | 13 | import au.com.centrumsystems.hudson.plugin.buildpipeline.DownstreamProjectGridBuilder; 14 | import au.com.centrumsystems.hudson.plugin.buildpipeline.ProjectGridBuilder; 15 | import hudson.Extension; 16 | import hudson.model.Descriptor; 17 | import hudson.model.Hudson; 18 | import hudson.plugins.view.dashboard.DashboardPortlet; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import org.kohsuke.stapler.DataBoundConstructor; 24 | 25 | import au.com.centrumsystems.hudson.plugin.buildpipeline.BuildPipelineView; 26 | import au.com.centrumsystems.hudson.plugin.buildpipeline.Strings; 27 | 28 | 29 | /** 30 | * This class provides the entry point to use this plugin in the dashboard-plugin 31 | * 32 | * @author Ingo Richter (irichter@adobe.com) 33 | * @since 03/30/2012 34 | */ 35 | public class BuildPipelineDashboard extends DashboardPortlet { 36 | /** 37 | * @deprecated 38 | * For backward compatibility. Back when we didn't have {@link #gridBuilder}, 39 | * this field stored the first job to display. 40 | */ 41 | @Deprecated 42 | private String selectedJob; 43 | 44 | /** Controls the layout. */ 45 | private ProjectGridBuilder gridBuilder; 46 | 47 | /** 48 | * noOfDisplayedBuilds. 49 | */ 50 | private String noOfDisplayedBuilds; 51 | 52 | /** 53 | * a brief description of this portlet. 54 | */ 55 | private String description; 56 | 57 | /** 58 | * URL for custom CSS file. 59 | */ 60 | private String cssUrl; 61 | 62 | /** 63 | * Constructor 64 | * 65 | * @param name 66 | * the name of this view 67 | * @param description 68 | * a brief description of this view 69 | * @param gridBuilder 70 | * controls the layout 71 | * @param noOfDisplayedBuilds 72 | * how many builds will be displayed for this job 73 | */ 74 | @DataBoundConstructor 75 | public BuildPipelineDashboard(final String name, final String description, 76 | final ProjectGridBuilder gridBuilder, final String noOfDisplayedBuilds) { 77 | super(name); 78 | this.description = description; 79 | this.gridBuilder = gridBuilder; 80 | this.noOfDisplayedBuilds = noOfDisplayedBuilds; 81 | } 82 | 83 | public ProjectGridBuilder getGridBuilder() { 84 | return gridBuilder; 85 | } 86 | 87 | public void setGridBuilder(ProjectGridBuilder gridBuilder) { 88 | this.gridBuilder = gridBuilder; 89 | } 90 | 91 | /** 92 | * @return 93 | * always this. 94 | */ 95 | protected Object readResolve() { 96 | if (gridBuilder == null && selectedJob != null) { 97 | gridBuilder = new DownstreamProjectGridBuilder(selectedJob); 98 | selectedJob = null; 99 | } 100 | return this; 101 | 102 | 103 | } 104 | 105 | public String getNoOfDisplayedBuilds() { 106 | return noOfDisplayedBuilds; 107 | } 108 | 109 | public void setNoOfDisplayedBuilds(final String noOfDisplayedBuilds) { 110 | this.noOfDisplayedBuilds = noOfDisplayedBuilds; 111 | } 112 | 113 | public String getSelectedJob() { 114 | return selectedJob; 115 | } 116 | 117 | public void setSelectedJob(final String selectedJob) { 118 | this.selectedJob = selectedJob; 119 | } 120 | 121 | public String getDescription() { 122 | return description; 123 | } 124 | 125 | public void setDescription(final String description) { 126 | this.description = description; 127 | } 128 | 129 | public void setCssUrl(final String cssUrl) { 130 | this.cssUrl = cssUrl; 131 | } 132 | 133 | public String getCssUrl() { 134 | return cssUrl; 135 | } 136 | 137 | public BuildPipelineView getBuildPipelineView() { 138 | return new ReadOnlyBuildPipelineView(getDisplayName(), getDescription(), getGridBuilder(), 139 | getNoOfDisplayedBuilds(), false, getCssUrl()); 140 | } 141 | 142 | /** 143 | * Extension point registration. 144 | */ 145 | // TODO: create a class and use this code also in BuildPipelineView 146 | @Extension(optional = true) 147 | public static class BuildPipelineDashboardDescriptor extends Descriptor { 148 | 149 | @Override 150 | public String getDisplayName() { 151 | return Strings.getString("Portlet.BuildPipelineDashboardDescriptor"); 152 | } 153 | 154 | /** 155 | * Display Job List Item in the Edit View Page 156 | * 157 | * @return ListBoxModel 158 | */ 159 | public hudson.util.ListBoxModel doFillSelectedJobItems() { 160 | final hudson.util.ListBoxModel options = new hudson.util.ListBoxModel(); 161 | for (final String jobName : Hudson.getInstance().getJobNames()) { 162 | options.add(jobName); 163 | } 164 | return options; 165 | } 166 | 167 | /** 168 | * Display No Of Builds Items in the Edit View Page 169 | * 170 | * @return ListBoxModel 171 | */ 172 | public hudson.util.ListBoxModel doFillNoOfDisplayedBuildsItems() { 173 | final hudson.util.ListBoxModel options = new hudson.util.ListBoxModel(); 174 | final List noOfBuilds = new ArrayList(); 175 | noOfBuilds.add("1"); 176 | noOfBuilds.add("2"); 177 | noOfBuilds.add("3"); 178 | noOfBuilds.add("5"); 179 | noOfBuilds.add("10"); 180 | noOfBuilds.add("20"); 181 | noOfBuilds.add("50"); 182 | noOfBuilds.add("100"); 183 | noOfBuilds.add("200"); 184 | noOfBuilds.add("500"); 185 | 186 | for (final String noOfBuild : noOfBuilds) { 187 | options.add(noOfBuild); 188 | } 189 | return options; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/util/ProjectUtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrumsystems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.util; 26 | 27 | import hudson.model.AbstractProject; 28 | import hudson.model.FreeStyleProject; 29 | import hudson.model.Hudson; 30 | import hudson.model.ParametersAction; 31 | import hudson.model.ParametersDefinitionProperty; 32 | import hudson.model.StringParameterDefinition; 33 | import hudson.tasks.BuildTrigger; 34 | 35 | import java.io.IOException; 36 | import java.net.URISyntaxException; 37 | import java.util.List; 38 | import java.util.concurrent.ExecutionException; 39 | 40 | import org.junit.Before; 41 | import org.junit.Test; 42 | import org.jvnet.hudson.test.HudsonTestCase; 43 | 44 | import au.com.centrumsystems.hudson.plugin.buildpipeline.PipelineBuild; 45 | import au.com.centrumsystems.hudson.plugin.buildpipeline.trigger.BuildPipelineTrigger; 46 | 47 | public class ProjectUtilTest extends HudsonTestCase { 48 | 49 | @Override 50 | @Before 51 | public void setUp() throws Exception { 52 | super.setUp(); 53 | } 54 | 55 | @Test 56 | public void testGetDownstreamProjects() throws IOException { 57 | final String proj1 = "Proj1"; 58 | final String proj2 = "Proj2"; 59 | final String proj3 = "Proj3"; 60 | 61 | // Create a test project 62 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 63 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 64 | 65 | // Add project2 as a post build action: build other project 66 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2, null)); 67 | project1.getPublishersList().add(new BuildPipelineTrigger(proj3, null)); 68 | 69 | // Important; we must do this step to ensure that the dependency graphs are updated 70 | Hudson.getInstance().rebuildDependencyGraph(); 71 | 72 | // Test the method 73 | final List> dsProjects = ProjectUtil.getDownstreamProjects(project1); 74 | assertEquals(project1.getName() + " should have a downstream project " + project2.getName(), project2, dsProjects.get(0)); 75 | } 76 | 77 | @Test 78 | public void testIsManualTrigger() throws IOException { 79 | final String proj1 = "Proj1"; 80 | final String proj2 = "Proj2"; 81 | final String proj3 = "Proj3"; 82 | 83 | // Create a test project 84 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 85 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 86 | final FreeStyleProject project3 = createFreeStyleProject(proj3); 87 | 88 | // Add TEST_PROJECT2 as a Manually executed pipeline project 89 | // Add TEST_PROJECT3 as a Post-build action -> build other projects 90 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2, null)); 91 | project1.getPublishersList().add(new BuildTrigger(proj3, true)); 92 | 93 | // Important; we must do this step to ensure that the dependency graphs are updated 94 | Hudson.getInstance().rebuildDependencyGraph(); 95 | 96 | // Test the method 97 | assertTrue(proj2 + " should be a manual trigger", ProjectUtil.isManualTrigger(project1, project2)); 98 | assertFalse(proj3 + " should be an automatic trigger", ProjectUtil.isManualTrigger(project1, project3)); 99 | 100 | assertFalse(ProjectUtil.isManualTrigger(null, null)); 101 | } 102 | 103 | @Test 104 | public void testHasDownstreamProjects() throws IOException { 105 | final String proj1 = "Proj1"; 106 | final String proj2 = "Proj2"; 107 | final String proj3 = "Proj3"; 108 | 109 | // Create a test project 110 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 111 | createFreeStyleProject(proj2); 112 | createFreeStyleProject(proj3); 113 | 114 | // Add project2 as a post build action: build other project 115 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2, null)); 116 | project1.getPublishersList().add(new BuildTrigger(proj3, true)); 117 | 118 | // Important; we must do this step to ensure that the dependency graphs are updated 119 | Hudson.getInstance().rebuildDependencyGraph(); 120 | 121 | // Test the method 122 | assertTrue(project1.getName() + " should have downstream projects", ProjectUtil.hasDownstreamProjects(project1)); 123 | } 124 | 125 | @Test 126 | public void testGetProjectURL() throws URISyntaxException, IOException { 127 | final String proj1 = "Proj 1"; 128 | final String proj1Url = "job/Proj%201/"; 129 | 130 | // Create a test project 131 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 132 | final PipelineBuild pipelineBuild = new PipelineBuild(project1); 133 | 134 | assertEquals("The project URL should have been " + proj1Url, proj1Url, pipelineBuild.getProjectURL()); 135 | } 136 | 137 | @Test 138 | public void testGetProjectParametersAction() throws IOException, InterruptedException, ExecutionException { 139 | final String proj1 = "Proj1"; 140 | final String proj2 = "Proj2"; 141 | final String paramKey = "testKey"; 142 | final String paramValue = "testValue"; 143 | 144 | // Create a test project 145 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 146 | // Add a String parameter 147 | project1.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(paramKey, paramValue)))); 148 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 149 | 150 | // Important; we must do this step to ensure that the dependency graphs are updated 151 | Hudson.getInstance().rebuildDependencyGraph(); 152 | 153 | // Test the method 154 | ParametersAction params = ProjectUtil.getProjectParametersAction(project1); 155 | assertEquals(params.getParameter(paramKey).getName(), paramKey); 156 | params = ProjectUtil.getProjectParametersAction(project2); 157 | assertNull(params); 158 | 159 | assertNull(ProjectUtil.getProjectParametersAction(null)); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/util/BuildUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrum Systems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.util; 26 | 27 | import hudson.model.Action; 28 | import hudson.model.ParameterValue; 29 | import hudson.model.AbstractBuild; 30 | import hudson.model.AbstractProject; 31 | import hudson.model.Cause; 32 | import hudson.model.Cause.UpstreamCause; 33 | import hudson.model.CauseAction; 34 | import hudson.model.FileParameterValue; 35 | import hudson.model.ParametersAction; 36 | 37 | import java.util.ArrayList; 38 | import java.util.HashMap; 39 | import java.util.LinkedHashMap; 40 | import java.util.List; 41 | import java.util.Map; 42 | import java.util.Set; 43 | 44 | /** 45 | * Provides helper methods for #hudson.model.AbstractBuild 46 | * 47 | * @author Centrum Systems 48 | * 49 | */ 50 | public final class BuildUtil { 51 | 52 | /** 53 | * Gets the next downstream build based on the upstream build and downstream project. 54 | * 55 | * @param downstreamProject 56 | * - The downstream project 57 | * @param upstreamBuild 58 | * - The upstream build 59 | * @return - The next downstream build based on the upstream build and downstream project, or null if there is no downstream project. 60 | */ 61 | public static AbstractBuild getDownstreamBuild(final AbstractProject downstreamProject, 62 | final AbstractBuild upstreamBuild) { 63 | if ((downstreamProject != null) && (upstreamBuild != null)) { 64 | @SuppressWarnings("unchecked") 65 | final List> downstreamBuilds = (List>) downstreamProject.getBuilds(); 66 | for (final AbstractBuild innerBuild : downstreamBuilds) { 67 | for (final CauseAction action : innerBuild.getActions(CauseAction.class)) { 68 | for (final Cause cause : action.getCauses()) { 69 | if (cause instanceof UpstreamCause) { 70 | final UpstreamCause upstreamCause = (UpstreamCause) cause; 71 | if (upstreamCause.getUpstreamProject().equals(upstreamBuild.getProject().getFullName()) 72 | && (upstreamCause.getUpstreamBuild() == upstreamBuild.getNumber())) { 73 | return innerBuild; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | /** 84 | * Given an Upstream AbstractBuild and a Downstream AbstractProject will retrieve the associated ParametersAction. This will result in 85 | * parameters from the upstream build not overriding parameters on the downstream project. 86 | * 87 | * @param upstreamBuild 88 | * - The AbstractBuild 89 | * @param downstreamProject 90 | * - The AbstractProject 91 | * @return - AbstractBuild's ParametersAction 92 | */ 93 | public static Action getAllBuildParametersAction(// 94 | final AbstractBuild upstreamBuild, final AbstractProject downstreamProject) { // 95 | // Retrieve the List of Actions from the downstream project 96 | final ParametersAction dsProjectParametersAction = ProjectUtil.getProjectParametersAction(downstreamProject); 97 | 98 | // Retrieve the List of Actions from the upstream build 99 | final ParametersAction usBuildParametersAction = BuildUtil.getBuildParametersAction(upstreamBuild); 100 | 101 | return mergeParameters(usBuildParametersAction, dsProjectParametersAction); 102 | } 103 | 104 | /** 105 | * Gets the ParametersAction of an AbstractBuild 106 | * 107 | * @param build 108 | * - AbstractBuild 109 | * @return - ParametersAction of AbstractBuild 110 | */ 111 | public static ParametersAction getBuildParametersAction(final AbstractBuild build) { 112 | ParametersAction buildParametersAction = null; 113 | if (build != null) { 114 | // If a ParametersAction is found 115 | for (final Action nextAction : build.getActions()) { 116 | if (nextAction instanceof ParametersAction) { 117 | buildParametersAction = (ParametersAction) nextAction; 118 | 119 | final List parameters = new ArrayList(); 120 | for (ParameterValue parameter : buildParametersAction.getParameters()) { 121 | // FileParameterValue is currently not reusable, so omit these: 122 | if (!(parameter instanceof FileParameterValue)) { 123 | parameters.add(parameter); 124 | } 125 | } 126 | buildParametersAction = new ParametersAction(parameters); 127 | } 128 | } 129 | } 130 | 131 | return buildParametersAction; 132 | } 133 | 134 | /** 135 | * Merges two sets of ParametersAction 136 | * 137 | * @param base 138 | * ParametersAction set 1 139 | * @param overlay 140 | * ParametersAction set 2 141 | * @return - Single set of ParametersAction 142 | */ 143 | public static ParametersAction mergeParameters(final ParametersAction base, final ParametersAction overlay) { 144 | final LinkedHashMap params = new LinkedHashMap(); 145 | if (base != null) { 146 | for (final ParameterValue param : base.getParameters()) { 147 | params.put(param.getName(), param); 148 | } 149 | } 150 | 151 | if (overlay != null) { 152 | for (final ParameterValue param : overlay.getParameters()) { 153 | params.put(param.getName(), param); 154 | } 155 | } 156 | 157 | return new ParametersAction(params.values().toArray(new ParameterValue[params.size()])); 158 | } 159 | 160 | /** 161 | * Retrieve build parameters in String format without sensitive parameters (passwords, ...) 162 | * 163 | * @param build the build we retrieve the parameters from 164 | * @return a map of parameters names and values 165 | */ 166 | public static Map getUnsensitiveParameters(final AbstractBuild build) { 167 | final Map retval = new HashMap(); 168 | if (build != null) { 169 | retval.putAll(build.getBuildVariables()); 170 | final Set sensitiveBuildVariables = build.getSensitiveBuildVariables(); 171 | if (sensitiveBuildVariables != null) { 172 | for (String paramName : sensitiveBuildVariables) { 173 | if (retval.containsKey(paramName)) { 174 | // We have the choice to hide the parameter or to replace it with special characters 175 | retval.put(paramName, "********"); 176 | //retval.remove(paramName); 177 | } 178 | } 179 | } 180 | } 181 | 182 | return retval; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/main/webapp/css/main.css: -------------------------------------------------------------------------------- 1 | #build-pipeline-plugin-content { 2 | background-attachment: initial; 3 | background-clip: initial; 4 | background-color: rgba(6, 72, 131, 0.0976563); 5 | background-image: initial; 6 | background-origin: initial; 7 | background-position: initial initial; 8 | background-repeat: initial initial; 9 | border-bottom-left-radius: 20px; 10 | border-bottom-right-radius: 20px; 11 | border-top-left-radius: 20px; 12 | border-top-right-radius: 20px; 13 | margin-left: auto; 14 | margin-right: auto; 15 | padding: 10px; 16 | 17 | } 18 | 19 | /******************** popup ***************************/ 20 | #popup-iframe{ 21 | width: 100%; 22 | height: 95%; 23 | frameborder: 0; 24 | bottom: 0; 25 | } 26 | 27 | #close-popup { 28 | float:right; 29 | width:50px; 30 | color: red; 31 | font-weight:bold; 32 | } 33 | 34 | #popup { 35 | height: 100%; 36 | width: 100%; 37 | background: #000000; 38 | position: absolute; 39 | top: 0; 40 | -khtml-opacity: 0.6; 41 | opacity: 0.6; 42 | filter:alpha(opacity=75); 43 | z-index:1; 44 | } 45 | 46 | #window { 47 | width: 800px; 48 | height: 500px; 49 | margin: 0 auto; 50 | border: 1px solid #000000; 51 | background: #ffffff; 52 | position: absolute; 53 | top: 50px; 54 | left: 19%; 55 | z-index:1; 56 | } 57 | 58 | /******************** icon bar ***************************/ 59 | #icon-bar { 60 | margin-bottom: 4px; 61 | height: 48px; 62 | margin-left: auto; 63 | margin-right: auto; 64 | width: 340px; 65 | } 66 | 67 | .icon-container { 68 | float: left; 69 | margin: 4px; 70 | width: 48px; 71 | } 72 | 73 | .icon-container div { 74 | font-size: 9px; 75 | text-align: center; 76 | color: #777; 77 | text-shadow:1px 0px 0px #333333; 78 | } 79 | 80 | .overflow-hidden { 81 | -o-text-overflow: ellipsis; 82 | -icab-text-overflow: ellipsis; 83 | -khtml-text-overflow: ellipsis; 84 | -webkit-text-overflow: ellipsis; 85 | text-overflow: ellipsis; 86 | overflow: hidden; 87 | white-space: nowrap; 88 | } 89 | 90 | 91 | /*************************** build card *******************************/ 92 | .rounded { 93 | height: 70px; 94 | width: 141px; 95 | -webkit-opacity: 0.9; 96 | opacity: 0.9; 97 | 98 | -webkit-border-radius: 4px; 99 | border-radius: 4px; 100 | 101 | -webkit-box-shadow: 4px 4px 4px #555; 102 | box-shadow: 4px 4px 4px #555; 103 | 104 | font-size: 10px; 105 | font-weight: bolder; 106 | color: white; 107 | margin-left: auto; 108 | margin-right: auto; 109 | } 110 | 111 | .header div { 112 | background: rgba(255, 255, 255, 0.7); 113 | color: #333; 114 | padding: 3px; 115 | 116 | -webkit-border-top-right-radius: 4px; 117 | border-top-right-radius: 4px; 118 | 119 | -webkit-border-top-left-radius: 4px; 120 | border-top-left-radius: 4px; 121 | } 122 | 123 | .build-number { 124 | font-style: italic; 125 | background: url("../images/gear-small.png") no-repeat left center; 126 | padding-left: 14px; 127 | } 128 | 129 | .build-duration { 130 | background: url("../images/clock-small.png") no-repeat left center; 131 | padding-left: 14px; 132 | } 133 | 134 | .build-duration-container { 135 | width: 135px; 136 | } 137 | 138 | .build-time { 139 | background: url("../images/application-small-list-blue.png") no-repeat left center; 140 | padding-left: 14px; 141 | text-align: left; 142 | text-overflow: clip; 143 | overflow: hidden; 144 | white-space: nowrap; 145 | font-size: 0.9em; 146 | width: 125px; 147 | } 148 | 149 | .build-user { 150 | background: url("../images/user-small.png") no-repeat left center; 151 | padding-left: 14px; 152 | width: 102px; 153 | } 154 | 155 | .build-card { 156 | float: left; 157 | width: 100%; 158 | } 159 | 160 | .build-card .build-body { 161 | height: 38px; 162 | } 163 | 164 | .build-card .secondary-info { 165 | font-size: 1em; 166 | } 167 | 168 | .build-card .build-actions { 169 | height: 18px; 170 | } 171 | 172 | .build-card .build-actions .status-bar { 173 | float: left; 174 | height: 100%; 175 | padding-left: 4px; 176 | } 177 | 178 | .build-card .build-actions .status-bar table { 179 | width: 110px; 180 | position: relative; 181 | top: 6px; 182 | } 183 | 184 | .build-card .build-actions .icons { 185 | float: right; 186 | } 187 | 188 | .build-card .build-actions img { 189 | width: 16px; 190 | height: 16px; 191 | padding-right: 2px; 192 | } 193 | 194 | .param-name { 195 | text-align: right; 196 | font-weight: bold; 197 | font-size: .9em; 198 | } 199 | 200 | .param-value { 201 | text-align: left; 202 | font-weight: normal; 203 | font-size: .9em; 204 | } 205 | 206 | #pipelines { 207 | width: 100%; 208 | text-align: center; 209 | border-collapse: collapse; 210 | } 211 | 212 | #legendView td { 213 | vertical-align: middle; 214 | text-align: center; 215 | } 216 | 217 | td.next { 218 | width: 40px; 219 | vertical-align: top; 220 | } 221 | 222 | tbody.pipelineGroup { 223 | background-color: #fcae3f; 224 | background-color: rgba(252, 174, 63, 0.7); 225 | } 226 | 227 | tbody > tr:first-child > td:first-child { 228 | -webkit-border-top-left-radius: 4px; 229 | border-top-left-radius: 4px; 230 | } 231 | 232 | tbody > tr:first-child > td:last-child { 233 | -webkit-border-top-right-radius: 4px; 234 | border-top-right-radius: 4px; 235 | } 236 | 237 | tbody > tr:last-child > td:first-child { 238 | -webkit-border-bottom-left-radius: 4px; 239 | border-bottom-left-radius: 4px; 240 | } 241 | 242 | tbody > tr:last-child > td:last-child { 243 | -webkit-border-bottom-right-radius: 4px; 244 | border-bottom-right-radius: 4px; 245 | } 246 | 247 | 248 | tr.build-pipeline > td, tr#project-pipeline > td { 249 | padding: 6px 6px 8px 6px; 250 | } 251 | 252 | tr.spacerRow td { 253 | padding: 3px; 254 | } 255 | 256 | .legend { 257 | background-color: #8b8989; 258 | } 259 | 260 | .revision { 261 | background-color: #8b8989; 262 | margin-left: 0; 263 | } 264 | 265 | .revision .build-number-pipeline { 266 | padding-top: 0.25em; 267 | font-size: 2em; 268 | } 269 | 270 | .revision .header div { 271 | overflow: hidden; 272 | } 273 | 274 | .disabled { 275 | background-color: #c8c6c7 !important; 276 | color: #222; 277 | } 278 | 279 | .SUCCESS { 280 | background-color: #1bd130; 281 | color: #222; 282 | } 283 | 284 | .FAILURE { 285 | background-color: #ef2929; 286 | } 287 | 288 | .UNSTABLE { 289 | background-color: #fcd116; 290 | color: #222; 291 | } 292 | 293 | .BUILDING { 294 | background-color: #f7f94b; 295 | color: #222; 296 | } 297 | 298 | .NOT_BUILT { 299 | background-color: #f7f94b; 300 | color: #222; 301 | } 302 | 303 | .ABORT { 304 | background-color: #bab4b4; 305 | } 306 | 307 | .PENDING { 308 | background-color: #00ccff; 309 | color: #222; 310 | } 311 | 312 | .MANUAL { 313 | background-color: #00ccff; 314 | color: #222; 315 | } 316 | 317 | .PROJECT { 318 | background-color: #aba8a8; 319 | font-size: 12px; 320 | font-weight: bold; 321 | display: table; 322 | height: 20px; 323 | } 324 | 325 | .PROJECT .header-name { 326 | display: table-row; 327 | color: #333; 328 | } 329 | 330 | .PROJECT .header-name div.header-wrapper { 331 | text-align: left; 332 | } 333 | 334 | .PROJECT .status-images { 335 | display: table-row; 336 | } 337 | 338 | .BuildViewMargin { 339 | margin-left: 30px; 340 | } 341 | 342 | #projectHeader { 343 | border-radius: 10px; 344 | -webkit-border-radius: 10px; 345 | border-width: 10px; 346 | font-size: 12px; 347 | font-weight: bold; 348 | } 349 | 350 | #side-panel { 351 | display: none; 352 | } 353 | 354 | #view-message { 355 | display: none; 356 | } 357 | 358 | a:link,a:visited,a:active { 359 | text-decoration: none; 360 | color: inherit; 361 | color: expression(this.parentNode.currentStyle [ 'color' ]); 362 | /* IE fix */ 363 | font-weight: inherit; 364 | } 365 | 366 | a:hover { 367 | text-decoration: underline; 368 | color: inherit; 369 | color: expression(this.parentNode.currentStyle['color']); 370 | font-weight: inherit; 371 | } 372 | .pointer { 373 | cursor: pointer; 374 | } 375 | 376 | .hidden { 377 | display: none; 378 | } 379 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/util/BuildUtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrumsystems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.util; 26 | 27 | import hudson.model.FreeStyleBuild; 28 | import hudson.model.AbstractBuild; 29 | import hudson.model.Cause.UserCause; 30 | import hudson.model.FreeStyleProject; 31 | import hudson.model.Hudson; 32 | import hudson.model.ParametersAction; 33 | import hudson.model.ParametersDefinitionProperty; 34 | import hudson.model.StringParameterDefinition; 35 | import hudson.model.StringParameterValue; 36 | import hudson.tasks.BuildTrigger; 37 | 38 | import org.junit.Before; 39 | import org.junit.Test; 40 | import org.jvnet.hudson.test.HudsonTestCase; 41 | 42 | public class BuildUtilTest extends HudsonTestCase { 43 | 44 | @Override 45 | @Before 46 | public void setUp() throws Exception { 47 | super.setUp(); 48 | } 49 | 50 | @Test 51 | public void testGetDownstreamBuild() throws Exception { 52 | final String proj1 = "Proj1"; 53 | final String proj2 = "Proj2"; 54 | final String proj3 = "Proj3"; 55 | 56 | FreeStyleProject project1, project2, project3; 57 | FreeStyleBuild build1, build2, build3; 58 | final FreeStyleProject project4 = null; 59 | final FreeStyleBuild build4 = null; 60 | 61 | // Create test projects and associated builders 62 | project1 = createFreeStyleProject(proj1); 63 | project2 = createFreeStyleProject(proj2); 64 | project3 = createFreeStyleProject(proj3); 65 | 66 | // Add project2 as a post build action: build other project 67 | project1.getPublishersList().add(new BuildTrigger(proj2, true)); 68 | project2.getPublishersList().add(new BuildTrigger(proj3, true)); 69 | 70 | // Important; we must do this step to ensure that the dependency graphs are updated 71 | Hudson.getInstance().rebuildDependencyGraph(); 72 | 73 | // Build project1, upon completion project2 will be built 74 | build1 = buildAndAssertSuccess(project1); 75 | // When all building is complete retrieve the last build from project2 76 | waitUntilNoActivity(); 77 | build2 = project2.getLastBuild(); 78 | build3 = project3.getLastBuild(); 79 | 80 | AbstractBuild nextBuild = BuildUtil.getDownstreamBuild(project2, build1); 81 | assertEquals("The next build should be " + proj1 + build2.number, build2, nextBuild); 82 | 83 | nextBuild = BuildUtil.getDownstreamBuild(project3, nextBuild); 84 | assertEquals("The next build should be " + proj1 + build3.number, build3, nextBuild); 85 | 86 | nextBuild = BuildUtil.getDownstreamBuild(project4, build4); 87 | assertNull(nextBuild); 88 | } 89 | 90 | @Test 91 | public void testGetAllBuildParametersAction() throws Exception { 92 | final String proj1 = "Proj1"; 93 | final String proj2 = "Proj2"; 94 | final String key1 = "testKey"; 95 | final String key2 = "testKey2"; 96 | final String value1 = "testValue"; 97 | final String value2 = "testValue2"; 98 | final String value3 = "testValue3"; 99 | 100 | FreeStyleProject project1, project2; 101 | FreeStyleBuild build1; 102 | 103 | // Create test projects and associated builders 104 | project1 = createFreeStyleProject(proj1); 105 | project2 = createFreeStyleProject(proj2); 106 | // Add a String parameter 107 | project1.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(key1, value1)))); 108 | project1.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(key2, value3)))); 109 | project2.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(key1, value2)))); 110 | 111 | // Add project2 as a post build action: build other project 112 | project1.getPublishersList().add(new BuildTrigger(proj2, true)); 113 | 114 | // Important; we must do this step to ensure that the dependency graphs are updated 115 | Hudson.getInstance().rebuildDependencyGraph(); 116 | 117 | // Build project1, upon completion project2 will be built 118 | // build1 = buildAndAssertSuccess(project1); 119 | build1 = project1.scheduleBuild2(0, new UserCause(), 120 | new ParametersAction(new StringParameterValue(key1, value1), new StringParameterValue(key2, value3))).get(); 121 | // When all building is complete retrieve the last build from project2 122 | waitUntilNoActivity(); 123 | 124 | final ParametersAction params = (ParametersAction) BuildUtil.getAllBuildParametersAction(build1, project2); 125 | assertEquals(((StringParameterValue) params.getParameter(key1)).value, value2); 126 | assertEquals(((StringParameterValue) params.getParameter(key2)).value, value3); 127 | } 128 | 129 | @Test 130 | public void testGetBuildParametersAction() throws Exception { 131 | final String proj1 = "Proj1"; 132 | final String key1 = "testKey"; 133 | final String key2 = "testKey2"; 134 | final String value1 = "testValue"; 135 | final String value3 = "testValue3"; 136 | 137 | FreeStyleProject project1; 138 | FreeStyleBuild build1; 139 | final FreeStyleBuild build2 = null; 140 | 141 | // Create test projects and associated builders 142 | project1 = createFreeStyleProject(proj1); 143 | 144 | // Add a String parameter 145 | project1.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(key1, value1)))); 146 | project1.addProperty((new ParametersDefinitionProperty(new StringParameterDefinition(key2, value3)))); 147 | 148 | // Important; we must do this step to ensure that the dependency graphs are updated 149 | Hudson.getInstance().rebuildDependencyGraph(); 150 | 151 | // Build project1 with the two StringParameterValues 152 | build1 = project1.scheduleBuild2(0, new UserCause(), 153 | new ParametersAction(new StringParameterValue(key1, value1), new StringParameterValue(key2, value3))).get(); 154 | waitUntilNoActivity(); 155 | 156 | ParametersAction params = BuildUtil.getBuildParametersAction(build1); 157 | assertEquals(((StringParameterValue) params.getParameter(key1)).value, value1); 158 | assertEquals(((StringParameterValue) params.getParameter(key2)).value, value3); 159 | 160 | params = BuildUtil.getBuildParametersAction(build2); 161 | assertNull(params); 162 | } 163 | 164 | @Test 165 | public void testMergeParameters() throws Exception { 166 | final String key1 = "testKey"; 167 | final String key2 = "testKey2"; 168 | final String value1 = "testValue"; 169 | final String value2 = "testValue2"; 170 | final String value3 = "testValue3"; 171 | 172 | ParametersAction baseParams = new ParametersAction(new StringParameterValue(key1, value1), new StringParameterValue(key2, value3)); 173 | ParametersAction extraParams = new ParametersAction(new StringParameterValue(key2, value2)); 174 | 175 | ParametersAction params = BuildUtil.mergeParameters(baseParams, extraParams); 176 | assertEquals(((StringParameterValue) params.getParameter(key1)).value, value1); 177 | assertEquals(((StringParameterValue) params.getParameter(key2)).value, value2); 178 | 179 | baseParams = null; 180 | extraParams = null; 181 | params = BuildUtil.mergeParameters(baseParams, extraParams); 182 | assertEquals(params.getParameters().size(), 0); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/ProjectForm.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import au.com.centrumsystems.hudson.plugin.util.BuildUtil; 4 | import hudson.Util; 5 | import hudson.model.AbstractBuild; 6 | import hudson.model.AbstractProject; 7 | import hudson.model.Hudson; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.LinkedHashSet; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import hudson.plugins.parameterizedtrigger.SubProjectsAction; 18 | import org.kohsuke.stapler.bind.JavaScriptMethod; 19 | 20 | /** 21 | * @author Centrum Systems 22 | * 23 | * Representation of a set of projects 24 | * 25 | */ 26 | public class ProjectForm { 27 | /** 28 | * project name 29 | */ 30 | private final String name; 31 | /** 32 | * last build result 33 | */ 34 | private final String result; 35 | /** 36 | * overall health 37 | */ 38 | private final String health; 39 | /** 40 | * project url 41 | */ 42 | private final String url; 43 | /** 44 | * downstream projects 45 | */ 46 | private final List dependencies; 47 | /** 48 | * display manual build 49 | */ 50 | private Boolean displayTrigger; 51 | 52 | /** 53 | * the latest successful build number 54 | */ 55 | private final String lastSuccessfulBuildNumber; 56 | 57 | /** 58 | * the parameters used in the last successful build 59 | */ 60 | private final Map lastSuccessfulBuildParams; 61 | 62 | /** 63 | * keep reference to the project so that we can update it 64 | */ 65 | private final AbstractProject project; 66 | 67 | /** 68 | * @param name 69 | * project name 70 | */ 71 | public ProjectForm(final String name) { 72 | this.name = name; 73 | result = ""; 74 | health = ""; 75 | url = ""; 76 | lastSuccessfulBuildNumber = ""; 77 | lastSuccessfulBuildParams = new HashMap(); 78 | dependencies = new ArrayList(); 79 | this.displayTrigger = true; 80 | project = null; 81 | } 82 | 83 | /** 84 | * @param project 85 | * project 86 | */ 87 | public ProjectForm(final AbstractProject project) { 88 | this(project, new LinkedHashSet>(Arrays.asList(project))); 89 | } 90 | 91 | /** 92 | * @param project 93 | * project 94 | * @param parentPath 95 | * already traversed projects 96 | */ 97 | private ProjectForm(final AbstractProject project, final Collection> parentPath) { 98 | final PipelineBuild pipelineBuild = new PipelineBuild(project.getLastBuild(), project, null); 99 | 100 | name = pipelineBuild.getProject().getFullName(); 101 | result = pipelineBuild.getCurrentBuildResult(); 102 | health = pipelineBuild.getProject().getBuildHealth().getIconUrl().replaceAll("\\.gif", "\\.png"); 103 | url = pipelineBuild.getProjectURL(); 104 | dependencies = new ArrayList(); 105 | for (final AbstractProject dependency : project.getDownstreamProjects()) { 106 | final Collection> forkedPath = new LinkedHashSet>(parentPath); 107 | if (forkedPath.add(dependency)) { 108 | dependencies.add(new ProjectForm(dependency, forkedPath)); 109 | } 110 | } 111 | if (Hudson.getInstance().getPlugin("parameterized-trigger") != null) { 112 | for (SubProjectsAction action : Util.filter(project.getActions(), SubProjectsAction.class)) { 113 | for (hudson.plugins.parameterizedtrigger.BlockableBuildTriggerConfig config : action.getConfigs()) { 114 | for (final AbstractProject dependency : config.getProjectList(project.getParent(), null)) { 115 | final Collection> forkedPath = new LinkedHashSet>(parentPath); 116 | if (forkedPath.add(dependency)) { 117 | final ProjectForm candidate = new ProjectForm(dependency, forkedPath); 118 | // if subprojects come back as downstreams someday, no duplicates wanted 119 | if (!dependencies.contains(candidate)) { 120 | dependencies.add(candidate); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | this.displayTrigger = true; 128 | 129 | final AbstractBuild lastSuccessfulBuild = pipelineBuild.getProject().getLastSuccessfulBuild(); 130 | lastSuccessfulBuildNumber = (null == lastSuccessfulBuild) ? "" : "" + lastSuccessfulBuild.getNumber(); 131 | lastSuccessfulBuildParams = BuildUtil.getUnsensitiveParameters(lastSuccessfulBuild); 132 | 133 | this.project = project; 134 | } 135 | 136 | /** 137 | * Wraps possibly null {@link AbstractProject} into {@link ProjectForm}. 138 | * 139 | * @param p 140 | * project to be wrapped. 141 | * @return 142 | * possibly null. 143 | */ 144 | public static ProjectForm as(AbstractProject p) { 145 | return p != null ? new ProjectForm(p) : null; 146 | } 147 | 148 | public String getName() { 149 | return name; 150 | } 151 | 152 | public String getHealth() { 153 | return health; 154 | } 155 | 156 | public String getResult() { 157 | return result; 158 | } 159 | 160 | public String getUrl() { 161 | return url; 162 | } 163 | 164 | public String getLastSuccessfulBuildNumber() { 165 | return lastSuccessfulBuildNumber; 166 | } 167 | 168 | public Map getLastSuccessfulBuildParams() { 169 | return lastSuccessfulBuildParams; 170 | } 171 | 172 | public List getDependencies() { 173 | return dependencies; 174 | } 175 | 176 | /** 177 | * Gets a display value to determine whether a manual jobs 'trigger' button will be shown. This is used along with 178 | * isTriggerOnlyLatestJob property allow only the latest version of a job to run. 179 | * 180 | * Works by: Initially always defaulted to true. If isTriggerOnlyLatestJob is set to true then as the html code is rendered the first 181 | * job which should show the trigger button will render and then a call will be made to 'setDisplayTrigger' to change the value to both 182 | * so all future jobs will not display the trigger. see main.jelly 183 | * 184 | * @return boolean whether to display or not 185 | */ 186 | public Boolean getDisplayTrigger() { 187 | return displayTrigger; 188 | } 189 | 190 | /** 191 | * Sets a display value to determine whether a manual jobs 'trigger' button will be shown. This is used along with 192 | * isTriggerOnlyLatestJob property allow only the latest version of a job to run. 193 | * 194 | * Works by: Initially always defaulted to true. If isTriggerOnlyLatestJob is set to true then as the html code is rendered the first 195 | * job which should show the trigger button will render and then a call will be made to 'setDisplayTrigger' to change the value to both 196 | * so all future jobs will not display the trigger. see main.jelly 197 | * 198 | * @param display 199 | * - boolean to indicate whether the trigger button should be shown 200 | */ 201 | public void setDisplayTrigger(final Boolean display) { 202 | displayTrigger = display; 203 | } 204 | 205 | @Override 206 | public int hashCode() { 207 | final int prime = 31; 208 | int result = 1; 209 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 210 | return result; 211 | } 212 | 213 | @Override 214 | public boolean equals(final Object obj) { 215 | if (this == obj) { 216 | return true; 217 | } 218 | if (obj == null) { 219 | return false; 220 | } 221 | if (getClass() != obj.getClass()) { 222 | return false; 223 | } 224 | final ProjectForm other = (ProjectForm) obj; 225 | if (name == null) { 226 | if (other.name != null) { 227 | return false; 228 | } 229 | } else if (!name.equals(other.name)) { 230 | return false; 231 | } 232 | return true; 233 | } 234 | 235 | public int getId() { 236 | return name.hashCode(); 237 | } 238 | 239 | /** 240 | * Project as JSON 241 | * 242 | * @return JSON string 243 | */ 244 | @JavaScriptMethod 245 | public String asJSON() { 246 | return ProjectJSONBuilder.asJSON(new ProjectForm(project)); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /src/main/java/au/com/centrumsystems/hudson/plugin/buildpipeline/DownstreamProjectGridBuilder.java: -------------------------------------------------------------------------------- 1 | package au.com.centrumsystems.hudson.plugin.buildpipeline; 2 | 3 | import hudson.Extension; 4 | import hudson.model.AbstractBuild; 5 | import hudson.model.AbstractProject; 6 | import hudson.model.Item; 7 | import hudson.model.ItemGroup; 8 | import hudson.model.ParametersDefinitionProperty; 9 | import hudson.util.AdaptedIterator; 10 | import hudson.util.HttpResponses; 11 | import hudson.util.ListBoxModel; 12 | 13 | import jenkins.model.Jenkins; 14 | import jenkins.util.TimeDuration; 15 | 16 | import org.kohsuke.stapler.AncestorInPath; 17 | import org.kohsuke.stapler.DataBoundConstructor; 18 | import org.kohsuke.stapler.HttpResponse; 19 | import org.kohsuke.stapler.StaplerRequest; 20 | import org.kohsuke.stapler.StaplerResponse; 21 | import org.kohsuke.stapler.interceptor.RequirePOST; 22 | 23 | import javax.servlet.ServletException; 24 | 25 | import java.io.IOException; 26 | import java.util.Collections; 27 | import java.util.Iterator; 28 | 29 | import org.acegisecurity.AccessDeniedException; 30 | 31 | /** 32 | * {@link ProjectGridBuilder} based on the upstream/downstream relationship. 33 | * 34 | * @author Kohsuke Kawaguchi 35 | */ 36 | public class DownstreamProjectGridBuilder extends ProjectGridBuilder { 37 | /** 38 | * Name of the first job in the grid, relative to the owner view. 39 | */ 40 | private String firstJob; 41 | 42 | /** 43 | * @param firstJob Name of the job to lead the piepline. 44 | */ 45 | @DataBoundConstructor 46 | public DownstreamProjectGridBuilder(String firstJob) { 47 | this.firstJob = firstJob; 48 | } 49 | 50 | /** 51 | * {@link ProjectGrid} that lays out things via upstream/downstream. 52 | */ 53 | private static final class GridImpl extends DefaultProjectGridImpl { 54 | /** 55 | * Project at the top-left corner. Initiator of the pipeline. 56 | */ 57 | private final AbstractProject start; 58 | 59 | /** 60 | * The item group pipeline view belongs to 61 | */ 62 | private final ItemGroup context; 63 | 64 | /** 65 | * @param context 66 | * item group pipeline view belongs to, used to compute relative item names 67 | * @param start The first project to lead the pipeline. 68 | */ 69 | private GridImpl(ItemGroup context, AbstractProject start) { 70 | this.context = context; 71 | this.start = start; 72 | placeProjectInGrid(0, 0, ProjectForm.as(start)); 73 | } 74 | 75 | /** 76 | * Function called recursively to place a project form in a grid 77 | * 78 | * @param startingRow project will be placed in the starting row and 1st child as well. Each subsequent 79 | * child will be placed in a row below the previous. 80 | * @param startingColumn project will be placed in starting column. All children will be placed in next column. 81 | * @param projectForm project to be placed 82 | */ 83 | private void placeProjectInGrid(final int startingRow, final int startingColumn, final ProjectForm projectForm) { 84 | if (projectForm == null) { 85 | return; 86 | } 87 | 88 | int row = getNextAvailableRow(startingRow, startingColumn); 89 | set(row, startingColumn, projectForm); 90 | 91 | final int childrensColumn = startingColumn + 1; 92 | for (final ProjectForm downstreamProject : projectForm.getDependencies()) { 93 | placeProjectInGrid(row, childrensColumn, downstreamProject); 94 | row++; 95 | } 96 | } 97 | 98 | /** 99 | * Factory for {@link Iterator}. 100 | */ 101 | private final Iterable builds = new Iterable() { 102 | @Override 103 | public Iterator iterator() { 104 | if (start == null) { 105 | return Collections.emptyList().iterator(); // no dat 106 | } 107 | 108 | final Iterator> base = start.getBuilds().iterator(); 109 | return new AdaptedIterator, BuildGrid>(base) { 110 | @Override 111 | protected BuildGrid adapt(AbstractBuild item) { 112 | return new BuildGridImpl(new BuildForm(context, new PipelineBuild(item))); 113 | } 114 | }; 115 | } 116 | }; 117 | 118 | @Override 119 | public Iterable builds() { 120 | return builds; 121 | } 122 | } 123 | 124 | /** 125 | * {@link BuildGrid} implementation that lays things out via its upstream/downstream relationship. 126 | */ 127 | private static final class BuildGridImpl extends DefaultBuildGridImpl { 128 | /** 129 | * @param start The first build to lead the pipeline instance. 130 | */ 131 | private BuildGridImpl(final BuildForm start) { 132 | placeBuildInGrid(0, 0, start); 133 | } 134 | 135 | /** 136 | * Function called recursively to place a build form in a grid 137 | * 138 | * @param startingRow build will be placed in the starting row and 1st child as well. Each subsequent child 139 | * will be placed in a row below the previous. 140 | * @param startingColumn build will be placed in starting column. All children will be placed in next column. 141 | * @param buildForm build to be placed 142 | */ 143 | private void placeBuildInGrid(final int startingRow, final int startingColumn, final BuildForm buildForm) { 144 | int row = getNextAvailableRow(startingRow, startingColumn); 145 | set(row, startingColumn, buildForm); 146 | 147 | final int childrensColumn = startingColumn + 1; 148 | for (final BuildForm downstreamProject : buildForm.getDependencies()) { 149 | placeBuildInGrid(row, childrensColumn, downstreamProject); 150 | row++; 151 | } 152 | } 153 | } 154 | 155 | public String getFirstJob() { 156 | return firstJob; 157 | } 158 | 159 | /** 160 | * The job that's configured as the head of the pipeline. 161 | * 162 | * @param owner View that this builder is operating under. 163 | * @return possibly null 164 | */ 165 | public AbstractProject getFirstJob(BuildPipelineView owner) { 166 | try { 167 | return Jenkins.getInstance().getItem(firstJob, owner.getOwnerItemGroup(), AbstractProject.class); 168 | } catch (final AccessDeniedException ex) { 169 | return null; 170 | } 171 | } 172 | 173 | @Override 174 | public boolean hasBuildPermission(BuildPipelineView owner) { 175 | final AbstractProject job = getFirstJob(owner); 176 | return job != null && job.hasPermission(Item.BUILD); 177 | } 178 | 179 | @Override 180 | public boolean startsWithParameters(BuildPipelineView owner) { 181 | final AbstractProject firstJob = this.getFirstJob(owner); 182 | final ParametersDefinitionProperty pdp = firstJob.getProperty(ParametersDefinitionProperty.class); 183 | return pdp != null; 184 | } 185 | 186 | @Override 187 | @RequirePOST 188 | public HttpResponse doBuild(StaplerRequest req, @AncestorInPath BuildPipelineView owner) throws IOException { 189 | final AbstractProject p = getFirstJob(owner); 190 | if (p == null) { 191 | return HttpResponses.error(StaplerResponse.SC_BAD_REQUEST, "No such project: " + getFirstJob()); 192 | } 193 | 194 | return new HttpResponse() { 195 | @Override 196 | public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { 197 | rsp.sendRedirect(".."); 198 | rsp.setStatus(200); 199 | try { 200 | p.doBuild(req, rsp, new TimeDuration(0)); 201 | } catch (IllegalStateException e) { 202 | ; 203 | // Ignore because sendRedirect(String) gets called twice. We do not want to hit the top 204 | // level of the project but instead we want to be redirected back 1 directory. 205 | } 206 | } 207 | }; 208 | } 209 | 210 | @Override 211 | public ProjectGrid build(BuildPipelineView owner) { 212 | return new GridImpl(owner.getOwnerItemGroup(), getFirstJob(owner)); 213 | } 214 | 215 | @Override 216 | public void onJobRenamed(BuildPipelineView owner, Item item, String oldName, String newName) throws IOException { 217 | if (item instanceof AbstractProject) { 218 | if ((oldName != null) && (oldName.equals(this.firstJob))) { 219 | this.firstJob = newName; 220 | owner.save(); 221 | } 222 | } 223 | } 224 | 225 | /** 226 | * Descriptor. 227 | */ 228 | @Extension(ordinal = 1000) // historical default behavior, so give it a higher priority 229 | public static class DescriptorImpl extends ProjectGridBuilderDescriptor { 230 | @Override 231 | public String getDisplayName() { 232 | return "Based on upstream/downstream relationship"; 233 | } 234 | 235 | /** 236 | * Display Job List Item in the Edit View Page 237 | * 238 | * @param context What to resolve relative job names against? 239 | * @return ListBoxModel 240 | */ 241 | public ListBoxModel doFillFirstJobItems(@AncestorInPath ItemGroup context) { 242 | final hudson.util.ListBoxModel options = new hudson.util.ListBoxModel(); 243 | for (final AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class)) { 244 | options.add(/* TODO 1.515: p.getRelativeDisplayNameFrom(context) */p.getFullDisplayName(), 245 | p.getRelativeNameFrom(context)); 246 | } 247 | return options; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/test/java/au/com/centrumsystems/hudson/plugin/buildpipeline/trigger/BuildPipelineTriggerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2011, Centrumsystems Pty Ltd 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | package au.com.centrumsystems.hudson.plugin.buildpipeline.trigger; 26 | 27 | import static org.hamcrest.CoreMatchers.is; 28 | import static org.hamcrest.Matchers.not; 29 | import static org.junit.Assert.assertThat; 30 | 31 | import hudson.model.AbstractProject; 32 | import hudson.model.Descriptor; 33 | import hudson.model.FreeStyleProject; 34 | import hudson.model.Hudson; 35 | import hudson.plugins.parameterizedtrigger.AbstractBuildParameters; 36 | import hudson.tasks.Publisher; 37 | import hudson.util.DescribableList; 38 | import hudson.util.FormValidation; 39 | 40 | import java.io.IOException; 41 | import java.util.Collections; 42 | 43 | import org.junit.Before; 44 | import org.junit.Test; 45 | import org.jvnet.hudson.test.HudsonTestCase; 46 | 47 | /** 48 | * BuildPipelineTrigger test class 49 | * 50 | * @author Centrum Systems 51 | * 52 | */ 53 | public class BuildPipelineTriggerTest extends HudsonTestCase { 54 | 55 | @Override 56 | @Before 57 | protected void setUp() throws Exception { 58 | super.setUp(); 59 | } 60 | 61 | @Test(expected = IllegalArgumentException.class) 62 | public void testInvalidConstructor() { 63 | try { 64 | new BuildPipelineTrigger(null, null); 65 | fail("An IllegalArgumentException should have been thrown."); 66 | } catch (final IllegalArgumentException e) { 67 | 68 | } 69 | } 70 | 71 | @Test 72 | public void testBuildPipelineTrigger() throws IOException { 73 | final String proj1 = "Proj1"; 74 | final String proj2 = "Proj2"; 75 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 76 | // Add TEST_PROJECT2 as a post build action: build other project 77 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2, null)); 78 | // Important; we must do this step to ensure that the dependency graphs are updated 79 | Hudson.getInstance().rebuildDependencyGraph(); 80 | 81 | final BuildPipelineTrigger myBPTrigger = new BuildPipelineTrigger(proj1, null); 82 | 83 | assertNotNull("A valid BuildPipelineTrigger should have been created.", myBPTrigger); 84 | 85 | assertEquals("BuildPipelineTrigger downstream project is " + proj1, proj1, myBPTrigger.getDownstreamProjectNames()); 86 | } 87 | 88 | @Test 89 | public void testOnDownstreamProjectRenamed() throws IOException { 90 | final String proj1 = "Proj1"; 91 | final String proj2 = "Proj2"; 92 | final String proj3 = "Proj3"; 93 | final BuildPipelineTrigger bpTrigger = new BuildPipelineTrigger(proj1, null); 94 | bpTrigger.setDownstreamProjectNames(proj2 + ", " + proj3); 95 | assertTrue(bpTrigger.onDownstreamProjectRenamed(proj2, proj2 + "NEW")); 96 | 97 | assertEquals(proj2 + "NEW," + proj3, bpTrigger.getDownstreamProjectNames()); 98 | 99 | // Null case 100 | bpTrigger.setDownstreamProjectNames(null); 101 | assertFalse(bpTrigger.onDownstreamProjectRenamed(proj2, proj3)); 102 | } 103 | 104 | @Test 105 | public void testOnDownstreamProjectDeleted() { 106 | final String proj1 = "Proj1"; 107 | final String proj2 = "Proj2"; 108 | final String proj3 = "Proj3"; 109 | final BuildPipelineTrigger bpTrigger = new BuildPipelineTrigger(proj1, null); 110 | bpTrigger.setDownstreamProjectNames(proj2 + ", " + proj3); 111 | assertTrue(bpTrigger.onDownstreamProjectDeleted(proj2)); 112 | 113 | assertEquals(proj3, bpTrigger.getDownstreamProjectNames()); 114 | 115 | // Null case 116 | bpTrigger.setDownstreamProjectNames(null); 117 | assertFalse(bpTrigger.onDownstreamProjectDeleted(proj2)); 118 | } 119 | 120 | @Test 121 | public void testOnRenamed() throws IOException { 122 | final String proj1 = "Proj1"; 123 | final String proj2 = "Proj2"; 124 | final String proj3 = "Proj3"; 125 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 126 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 127 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2 + "," + proj3, null)); 128 | Hudson.getInstance().rebuildDependencyGraph(); 129 | 130 | project2.renameTo(proj2 + "NEW"); 131 | 132 | final DescribableList> downstreamPublishersList = project1.getPublishersList(); 133 | for (final Publisher downstreamPub : downstreamPublishersList) { 134 | if (downstreamPub instanceof BuildPipelineTrigger) { 135 | final String manualDownstreamProjects = ((BuildPipelineTrigger) downstreamPub).getDownstreamProjectNames(); 136 | assertEquals(proj2 + "NEW," + proj3, manualDownstreamProjects); 137 | } 138 | } 139 | } 140 | 141 | @Test 142 | public void testOnDeleted() throws IOException, InterruptedException { 143 | final String proj1 = "Proj1"; 144 | final String proj2 = "Proj2"; 145 | final String proj3 = "Proj3"; 146 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 147 | final FreeStyleProject project2 = createFreeStyleProject(proj2); 148 | project1.getPublishersList().add(new BuildPipelineTrigger(proj2 + "," + proj3, null)); 149 | Hudson.getInstance().rebuildDependencyGraph(); 150 | 151 | project2.delete(); 152 | 153 | final DescribableList> downstreamPublishersList = project1.getPublishersList(); 154 | for (final Publisher downstreamPub : downstreamPublishersList) { 155 | if (downstreamPub instanceof BuildPipelineTrigger) { 156 | final String manualDownstreamProjects = ((BuildPipelineTrigger) downstreamPub).getDownstreamProjectNames(); 157 | assertEquals(proj3, manualDownstreamProjects); 158 | } 159 | } 160 | } 161 | 162 | @Test 163 | public void testDoCheckDownstreamProjectNames() throws IOException, InterruptedException { 164 | final AbstractProject upstreamProject = createFreeStyleProject("Upstream"); 165 | 166 | final String proj1 = "Proj1"; 167 | final String proj2 = "Proj2"; 168 | createFreeStyleProject(proj1); 169 | 170 | final BuildPipelineTrigger.DescriptorImpl di = new BuildPipelineTrigger.DescriptorImpl(); 171 | 172 | assertEquals(FormValidation.ok(), di.doCheckDownstreamProjectNames(upstreamProject, proj1)); 173 | assertThat(FormValidation.error("No such project ‘" + proj2 + "’. Did you mean ‘" + proj1 + "’?").toString(), is(di 174 | .doCheckDownstreamProjectNames(upstreamProject, proj2).toString())); 175 | } 176 | 177 | @Test 178 | public void testRemoveDownstreamTrigger() throws IOException, InterruptedException { 179 | final String proj1 = "Proj1"; 180 | final String proj2 = "Proj2"; 181 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 182 | final BuildPipelineTrigger buildPipelineTrigger = new BuildPipelineTrigger(proj2, null); 183 | project1.getPublishersList().add(buildPipelineTrigger); 184 | Hudson.getInstance().rebuildDependencyGraph(); 185 | 186 | buildPipelineTrigger.removeDownstreamTrigger(buildPipelineTrigger, project1, proj2); 187 | 188 | final DescribableList> downstreamPublishersList = project1.getPublishersList(); 189 | for (final Publisher downstreamPub : downstreamPublishersList) { 190 | if (downstreamPub instanceof BuildPipelineTrigger) { 191 | final String manualDownstreamProjects = ((BuildPipelineTrigger) downstreamPub).getDownstreamProjectNames(); 192 | assertEquals("", manualDownstreamProjects); 193 | } 194 | } 195 | } 196 | 197 | @Test 198 | public void testCyclicDownstreamTrigger() throws IOException, InterruptedException { 199 | final String proj1 = "Proj1"; 200 | final FreeStyleProject project1 = createFreeStyleProject(proj1); 201 | final BuildPipelineTrigger cyclicPipelineTrigger = new BuildPipelineTrigger(proj1, null); 202 | project1.getPublishersList().add(cyclicPipelineTrigger); 203 | Hudson.getInstance().rebuildDependencyGraph(); 204 | 205 | final DescribableList> downstreamPublishersList = project1.getPublishersList(); 206 | for (final Publisher downstreamPub : downstreamPublishersList) { 207 | if (downstreamPub instanceof BuildPipelineTrigger) { 208 | final String manualDownstreamProjects = ((BuildPipelineTrigger) downstreamPub).getDownstreamProjectNames(); 209 | assertEquals("", manualDownstreamProjects); 210 | } 211 | } 212 | 213 | } 214 | 215 | @Test 216 | public void testGetBuilderConfigDescriptors() throws Exception { 217 | final BuildPipelineTrigger.DescriptorImpl di = new BuildPipelineTrigger.DescriptorImpl(); 218 | 219 | assertThat(di.getBuilderConfigDescriptors(), is(not(Collections.>emptyList()))); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/main/webapp/css/jquery.fancybox-1.3.4.css: -------------------------------------------------------------------------------- 1 | /* 2 | * FancyBox - jQuery Plugin 3 | * Simple and fancy lightbox alternative 4 | * 5 | * Examples and documentation at: http://fancybox.net 6 | * 7 | * Copyright (c) 2008 - 2010 Janis Skarnelis 8 | * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. 9 | * 10 | * Version: 1.3.4 (11/11/2010) 11 | * Requires: jQuery v1.3+ 12 | * 13 | * Dual licensed under the MIT and GPL licenses: 14 | * http://www.opensource.org/licenses/mit-license.php 15 | * http://www.gnu.org/licenses/gpl.html 16 | */ 17 | 18 | #fancybox-loading { 19 | position: fixed; 20 | top: 50%; 21 | left: 50%; 22 | width: 40px; 23 | height: 40px; 24 | margin-top: -20px; 25 | margin-left: -20px; 26 | cursor: pointer; 27 | overflow: hidden; 28 | z-index: 1104; 29 | display: none; 30 | } 31 | 32 | #fancybox-loading div { 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | width: 40px; 37 | height: 480px; 38 | background-image: url('../images/fancybox/fancybox.png'); 39 | } 40 | 41 | #fancybox-overlay { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | z-index: 1100; 47 | display: none; 48 | } 49 | 50 | #fancybox-tmp { 51 | padding: 0; 52 | margin: 0; 53 | border: 0; 54 | overflow: auto; 55 | display: none; 56 | } 57 | 58 | #fancybox-wrap { 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | padding: 20px; 63 | z-index: 1101; 64 | outline: none; 65 | display: none; 66 | } 67 | 68 | #fancybox-outer { 69 | position: relative; 70 | width: 100%; 71 | height: 100%; 72 | background: #fff; 73 | } 74 | 75 | #fancybox-content { 76 | width: 0; 77 | height: 0; 78 | padding: 0; 79 | outline: none; 80 | position: relative; 81 | overflow: hidden; 82 | z-index: 1102; 83 | border: 0px solid #fff; 84 | } 85 | 86 | #fancybox-hide-sel-frame { 87 | position: absolute; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | height: 100%; 92 | background: transparent; 93 | z-index: 1101; 94 | } 95 | 96 | #fancybox-close { 97 | position: absolute; 98 | top: -15px; 99 | right: -15px; 100 | width: 30px; 101 | height: 30px; 102 | background: transparent url('../images/fancybox/fancybox.png') -40px 0px; 103 | cursor: pointer; 104 | z-index: 1103; 105 | display: none; 106 | } 107 | 108 | #fancybox-error { 109 | color: #444; 110 | font: normal 12px/20px Arial; 111 | padding: 14px; 112 | margin: 0; 113 | } 114 | 115 | #fancybox-img { 116 | width: 100%; 117 | height: 100%; 118 | padding: 0; 119 | margin: 0; 120 | border: none; 121 | outline: none; 122 | line-height: 0; 123 | vertical-align: top; 124 | } 125 | 126 | #fancybox-frame { 127 | width: 100%; 128 | height: 100%; 129 | border: none; 130 | display: block; 131 | } 132 | 133 | #fancybox-left, #fancybox-right { 134 | position: absolute; 135 | bottom: 0px; 136 | height: 100%; 137 | width: 35%; 138 | cursor: pointer; 139 | outline: none; 140 | background: transparent url('../images/fancybox/blank.gif'); 141 | z-index: 1102; 142 | display: none; 143 | } 144 | 145 | #fancybox-left { 146 | left: 0px; 147 | } 148 | 149 | #fancybox-right { 150 | right: 0px; 151 | } 152 | 153 | #fancybox-left-ico, #fancybox-right-ico { 154 | position: absolute; 155 | top: 50%; 156 | left: -9999px; 157 | width: 30px; 158 | height: 30px; 159 | margin-top: -15px; 160 | cursor: pointer; 161 | z-index: 1102; 162 | display: block; 163 | } 164 | 165 | #fancybox-left-ico { 166 | background-image: url('../images/fancybox/fancybox.png'); 167 | background-position: -40px -30px; 168 | } 169 | 170 | #fancybox-right-ico { 171 | background-image: url('../images/fancybox/fancybox.png'); 172 | background-position: -40px -60px; 173 | } 174 | 175 | #fancybox-left:hover, #fancybox-right:hover { 176 | visibility: visible; /* IE6 */ 177 | } 178 | 179 | #fancybox-left:hover span { 180 | left: 20px; 181 | } 182 | 183 | #fancybox-right:hover span { 184 | left: auto; 185 | right: 20px; 186 | } 187 | 188 | .fancybox-bg { 189 | position: absolute; 190 | padding: 0; 191 | margin: 0; 192 | border: 0; 193 | width: 20px; 194 | height: 20px; 195 | z-index: 1001; 196 | } 197 | 198 | #fancybox-bg-n { 199 | top: -20px; 200 | left: 0; 201 | width: 100%; 202 | background-image: url('../images/fancybox/fancybox-x.png'); 203 | } 204 | 205 | #fancybox-bg-ne { 206 | top: -20px; 207 | right: -20px; 208 | background-image: url('../images/fancybox/fancybox.png'); 209 | background-position: -40px -162px; 210 | } 211 | 212 | #fancybox-bg-e { 213 | top: 0; 214 | right: -20px; 215 | height: 100%; 216 | background-image: url('../images/fancybox/fancybox-y.png'); 217 | background-position: -20px 0px; 218 | } 219 | 220 | #fancybox-bg-se { 221 | bottom: -20px; 222 | right: -20px; 223 | background-image: url('../images/fancybox/fancybox.png'); 224 | background-position: -40px -182px; 225 | } 226 | 227 | #fancybox-bg-s { 228 | bottom: -20px; 229 | left: 0; 230 | width: 100%; 231 | background-image: url('../images/fancybox/fancybox-x.png'); 232 | background-position: 0px -20px; 233 | } 234 | 235 | #fancybox-bg-sw { 236 | bottom: -20px; 237 | left: -20px; 238 | background-image: url('../images/fancybox/fancybox.png'); 239 | background-position: -40px -142px; 240 | } 241 | 242 | #fancybox-bg-w { 243 | top: 0; 244 | left: -20px; 245 | height: 100%; 246 | background-image: url('../images/fancybox/fancybox-y.png'); 247 | } 248 | 249 | #fancybox-bg-nw { 250 | top: -20px; 251 | left: -20px; 252 | background-image: url('../images/fancybox/fancybox.png'); 253 | background-position: -40px -122px; 254 | } 255 | 256 | #fancybox-title { 257 | font-family: Helvetica; 258 | font-size: 12px; 259 | z-index: 1102; 260 | } 261 | 262 | .fancybox-title-inside { 263 | padding-bottom: 10px; 264 | text-align: center; 265 | color: #333; 266 | background: #fff; 267 | position: relative; 268 | } 269 | 270 | .fancybox-title-outside { 271 | padding-top: 10px; 272 | color: #fff; 273 | } 274 | 275 | .fancybox-title-over { 276 | position: absolute; 277 | bottom: 0; 278 | left: 0; 279 | color: #FFF; 280 | text-align: left; 281 | } 282 | 283 | #fancybox-title-over { 284 | padding: 10px; 285 | background-image: url('../images/fancybox/fancy_title_over.png'); 286 | display: block; 287 | } 288 | 289 | .fancybox-title-float { 290 | position: absolute; 291 | left: 0; 292 | bottom: -20px; 293 | height: 32px; 294 | } 295 | 296 | #fancybox-title-float-wrap { 297 | border: none; 298 | border-collapse: collapse; 299 | width: auto; 300 | } 301 | 302 | #fancybox-title-float-wrap td { 303 | border: none; 304 | white-space: nowrap; 305 | } 306 | 307 | #fancybox-title-float-left { 308 | padding: 0 0 0 15px; 309 | background: url('../images/fancybox/fancybox.png') -40px -90px no-repeat; 310 | } 311 | 312 | #fancybox-title-float-main { 313 | color: #FFF; 314 | line-height: 29px; 315 | font-weight: bold; 316 | padding: 0 0 3px 0; 317 | background: url('../images/fancybox/fancybox-x.png') 0px -40px; 318 | } 319 | 320 | #fancybox-title-float-right { 321 | padding: 0 0 0 15px; 322 | background: url('../images/fancybox/fancybox.png') -55px -90px no-repeat; 323 | } 324 | 325 | /* IE6 */ 326 | 327 | .fancybox-ie6 #fancybox-close { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_close.png', sizingMethod='scale'); } 328 | 329 | .fancybox-ie6 #fancybox-left-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_nav_left.png', sizingMethod='scale'); } 330 | .fancybox-ie6 #fancybox-right-ico { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_nav_right.png', sizingMethod='scale'); } 331 | 332 | .fancybox-ie6 #fancybox-title-over { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_title_over.png', sizingMethod='scale'); zoom: 1; } 333 | .fancybox-ie6 #fancybox-title-float-left { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_title_left.png', sizingMethod='scale'); } 334 | .fancybox-ie6 #fancybox-title-float-main { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_title_main.png', sizingMethod='scale'); } 335 | .fancybox-ie6 #fancybox-title-float-right { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_title_right.png', sizingMethod='scale'); } 336 | 337 | .fancybox-ie6 #fancybox-bg-w, .fancybox-ie6 #fancybox-bg-e, .fancybox-ie6 #fancybox-left, .fancybox-ie6 #fancybox-right, #fancybox-hide-sel-frame { 338 | height: expression(this.parentNode.clientHeight + "px"); 339 | } 340 | 341 | #fancybox-loading.fancybox-ie6 { 342 | position: absolute; margin-top: 0; 343 | top: expression( (-20 + (document.documentElement.clientHeight ? document.documentElement.clientHeight/2 : document.body.clientHeight/2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop )) + 'px'); 344 | } 345 | 346 | #fancybox-loading.fancybox-ie6 div { background: transparent; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_loading.png', sizingMethod='scale'); } 347 | 348 | /* IE6, IE7, IE8 */ 349 | 350 | .fancybox-ie .fancybox-bg { background: transparent !important; } 351 | 352 | .fancybox-ie #fancybox-bg-n { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_n.png', sizingMethod='scale'); } 353 | .fancybox-ie #fancybox-bg-ne { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_ne.png', sizingMethod='scale'); } 354 | .fancybox-ie #fancybox-bg-e { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_e.png', sizingMethod='scale'); } 355 | .fancybox-ie #fancybox-bg-se { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_se.png', sizingMethod='scale'); } 356 | .fancybox-ie #fancybox-bg-s { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_s.png', sizingMethod='scale'); } 357 | .fancybox-ie #fancybox-bg-sw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_sw.png', sizingMethod='scale'); } 358 | .fancybox-ie #fancybox-bg-w { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_w.png', sizingMethod='scale'); } 359 | .fancybox-ie #fancybox-bg-nw { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../images/fancybox/fancy_shadow_nw.png', sizingMethod='scale'); } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 1.554.2 7 | 8 | build-pipeline-plugin 9 | 1.4.5-SNAPSHOT 10 | hpi 11 | Build Pipeline Plugin 12 | This plugin provides build pipeline functionality to Hudson and Jenkins. This allows a chain of jobs to be visualised in a new view. Manual jobs in the pipeline can be triggered by a user with the appropriate permissions manually confirming. 13 | https://wiki.jenkins-ci.org/display/JENKINS/Build+Pipeline+Plugin 14 | 15 | 16 | MIT License 17 | http://www.opensource.org/licenses/mit-license.php 18 | 19 | 20 | 21 | scm:git:https://github.com/jenkinsci/${project.artifactId}.git 22 | scm:git:https://github.com/jenkinsci/${project.artifactId}.git 23 | https://github.com/jenkinsci/build-pipeline-plugin 24 | HEAD 25 | 26 | 27 | 50 28 | checkstyle_rules.xml 29 | pmd_rules.xml 30 | UTF-8 31 | 1.8.6 32 | 2.31.0 33 | **/functionaltest/*.java 34 | 35 | 36 | 37 | centrum 38 | Centrum Systems 39 | plugins@centrumsystems.com.au 40 | http://www.centrumsystems.com.au 41 | 42 | 43 | 44 | 45 | junit 46 | junit 47 | 4.9 48 | test 49 | 50 | 51 | org.mockito 52 | mockito-all 53 | 1.9.0 54 | test 55 | 56 | 57 | org.spockframework 58 | spock-core 59 | 0.6-groovy-1.8 60 | test 61 | 62 | 63 | org.codehaus.groovy 64 | groovy-all 65 | 66 | 67 | 68 | 69 | org.codehaus.groovy 70 | groovy-all 71 | ${groovy.version} 72 | provided 73 | 74 | 75 | org.jenkins-ci.plugins 76 | jquery 77 | 1.7.2-1 78 | 79 | 80 | 81 | 82 | cglib 83 | cglib-nodep 84 | 2.2 85 | test 86 | 87 | 88 | 90 | org.objenesis 91 | objenesis 92 | 1.2 93 | test 94 | 95 | 96 | 97 | org.hamcrest 98 | hamcrest-core 99 | 1.2 100 | test 101 | 102 | 103 | org.jenkins-ci.plugins 104 | dashboard-view 105 | 2.2 106 | true 107 | 108 | 109 | org.jenkins-ci.plugins 110 | parameterized-trigger 111 | 2.17 112 | 113 | 114 | 115 | org.seleniumhq.selenium 116 | selenium-firefox-driver 117 | ${selenium.version} 118 | 119 | 120 | commons-io 121 | commons-io 122 | 123 | 124 | 125 | 126 | org.seleniumhq.selenium 127 | selenium-chrome-driver 128 | ${selenium.version} 129 | 130 | 131 | org.seleniumhq.selenium 132 | selenium-ie-driver 133 | ${selenium.version} 134 | 135 | 136 | org.seleniumhq.selenium 137 | selenium-support 138 | ${selenium.version} 139 | 140 | 141 | 142 | commons-beanutils 143 | commons-beanutils 144 | 1.8.3 145 | test 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-compiler-plugin 155 | 2.4 156 | 157 | 158 | 1.6 159 | 1.6 160 | 161 | 162 | 163 | org.codehaus.gmaven 164 | gmaven-plugin 165 | 1.4 166 | 167 | 168 | 1.8 169 | 170 | 171 | 172 | org.codehaus.gmaven.runtime 173 | gmaven-runtime-1.8 174 | 1.4 175 | 176 | 177 | org.codehaus.groovy 178 | groovy-all 179 | 180 | 181 | 182 | 183 | org.codehaus.groovy 184 | groovy-all 185 | ${groovy.version} 186 | 187 | 188 | 189 | 190 | 191 | generateStubs 192 | compile 193 | generateTestStubs 194 | testCompile 195 | 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-checkstyle-plugin 202 | 2.6 203 | 204 | 205 | ${checkstyle.rules.file} 206 | true 207 | warning 208 | basedir=${basedir} 209 | true 210 | 211 | 212 | 213 | 214 | check 215 | 216 | 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-pmd-plugin 222 | 2.7.1 223 | 224 | 225 | 226 | ${pmd.config.file} 227 | 228 | 229 | **/generated-sources/** 230 | 231 | false 232 | 233 | 234 | 235 | 236 | check 237 | 238 | 239 | 240 | 241 | 242 | org.codehaus.mojo 243 | findbugs-maven-plugin 244 | 2.3.2 245 | 246 | 247 | Max 248 | Medium 249 | true 250 | findbugs-exclude.xml 251 | 252 | 253 | 254 | 255 | check 256 | 257 | 258 | 259 | 260 | 261 | org.apache.maven.plugins 262 | maven-release-plugin 263 | 2.5 264 | 265 | 266 | forked-path 267 | deploy 268 | 269 | 270 | 271 | org.apache.maven.plugins 272 | maven-enforcer-plugin 273 | 1.0 274 | 275 | 276 | 277 | 278 | org.apache.maven.plugins 279 | maven-surefire-plugin 280 | 2.16 281 | 282 | 283 | ${exclude.tests} 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 294 | 295 | org.eclipse.m2e 296 | lifecycle-mapping 297 | 1.0.0 298 | 299 | 300 | 301 | 302 | 303 | org.jenkins-ci.tools 304 | maven-hpi-plugin 305 | [1.7,) 306 | 307 | insert-test 308 | resolve-test-dependencies 309 | test-hpl 310 | validate 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | org.apache.maven.plugins 320 | maven-resources-plugin 321 | [2.0,) 322 | 323 | default-resources 324 | default-test-resources 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | web-tests 342 | 343 | none 344 | 345 | 346 | 347 | 348 | 349 | 350 | maven.jenkins-ci.org 351 | http://maven.jenkins-ci.org/content/repositories/releases/ 352 | 353 | 354 | 355 | 356 | 357 | maven-central 358 | http://repo1.maven.org/maven2 359 | 360 | 361 | maven.jenkins-ci.org 362 | http://repo.jenkins-ci.org/public 363 | 364 | 365 | 366 | 367 | maven-central 368 | http://repo1.maven.org/maven2 369 | 370 | 371 | maven.jenkins-ci.org 372 | http://repo.jenkins-ci.org/public 373 | 374 | 375 | 376 | 377 | --------------------------------------------------------------------------------