├── yaml_junit.png ├── junit_pipeline.png ├── pipeline ├── templates │ ├── steps │ │ ├── template-script.yml │ │ ├── template-mock.yml │ │ └── template-steps.yml │ ├── jobs │ │ └── template-jobs.yml │ └── stages │ │ └── template-stages.yml ├── simple-pipeline.yml ├── simple-deployment.yml ├── external-resources-pipeline.yml ├── powershell-mock.yml ├── bash-mock.yml ├── pipeline-test.yml └── junit-pipeline.yml ├── src ├── main │ ├── java │ │ └── azdo │ │ │ ├── hook │ │ │ ├── Hook.java │ │ │ ├── DeleteTargetFile.java │ │ │ ├── FindReplaceInFile.java │ │ │ └── DeleteJUnitPipelineDependency.java │ │ │ ├── yaml │ │ │ ├── ActionResult.java │ │ │ ├── RepositoryResource.java │ │ │ └── YamlTemplate.java │ │ │ ├── action │ │ │ ├── Action.java │ │ │ ├── ActionDeleteSection.java │ │ │ ├── ActionUpdateSectionByProperty.java │ │ │ ├── ActionUpdateSection.java │ │ │ ├── ActionDeleteSectionByProperty.java │ │ │ ├── ActionInsertSection.java │ │ │ ├── ActionInsertSectionByProperty.java │ │ │ ├── ActionResetTrigger.java │ │ │ ├── ActionReturnPropertyValue.java │ │ │ ├── ActionInsertLineInSection.java │ │ │ ├── ActionAddPropertyToSection.java │ │ │ ├── ActionOverrideElement.java │ │ │ ├── ActionOnSection.java │ │ │ ├── ActionInsertLineInInnerSection.java │ │ │ └── ActionOnSectionByProperty.java │ │ │ ├── utils │ │ │ ├── Constants.java │ │ │ ├── Log.java │ │ │ ├── PomUtils.java │ │ │ ├── GitUtils.java │ │ │ ├── Utils.java │ │ │ └── PropertyUtils.java │ │ │ └── junit │ │ │ ├── TimelineRecord.java │ │ │ └── RunResult.java │ └── resources │ │ ├── log4j.properties │ │ ├── log4j2.xml │ │ └── junit_pipeline.properties └── test │ └── java │ └── PipelineUnit.java ├── LICENSE └── pom.xml /yaml_junit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hvmerode/junit-pipeline/HEAD/yaml_junit.png -------------------------------------------------------------------------------- /junit_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hvmerode/junit-pipeline/HEAD/junit_pipeline.png -------------------------------------------------------------------------------- /pipeline/templates/steps/template-script.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Henry van Merode. 2 | # Licensed under the MIT License. 3 | 4 | steps: 5 | - script: | 6 | echo 'This is script step' 7 | displayName: This is script step 8 | -------------------------------------------------------------------------------- /src/main/java/azdo/hook/Hook.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.hook; 5 | 6 | public class Hook { 7 | public void executeHook () 8 | { 9 | throw new UnsupportedOperationException(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pipeline/templates/steps/template-mock.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Henry van Merode. 2 | # Licensed under the MIT License. 3 | 4 | parameters: 5 | - name: param_1 6 | type: string 7 | - name: param_2 8 | type: string 9 | 10 | steps: 11 | - script: | 12 | echo 'This is mock template file' 13 | displayName: template-mock.yml script 14 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=DEBUG, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /pipeline/templates/steps/template-steps.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: param_1 3 | type: string 4 | default: xxx 5 | - name: param_2 6 | type: string 7 | default: param_2_value 8 | steps: 9 | - script: | 10 | echo 'This is parameter ${{ parameters.param_1 }}' 11 | displayName: template-steps.yml script 12 | - script: | 13 | echo 'This is step 2 of file template-steps.yml' 14 | displayName: template-steps.yml script 15 | -------------------------------------------------------------------------------- /src/main/java/azdo/yaml/ActionResult.java: -------------------------------------------------------------------------------- 1 | package azdo.yaml; 2 | 3 | /****************************************************************************************** 4 | Use in the execution of an Action instance. 5 | *******************************************************************************************/ 6 | public class ActionResult { 7 | public Object l1 = null; 8 | public Object l2 = null; 9 | public Object l3 = null; 10 | public boolean actionExecuted = false; 11 | } 12 | -------------------------------------------------------------------------------- /pipeline/templates/jobs/template-jobs.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: param_1 3 | type: string 4 | default: xxx 5 | jobs: 6 | - job: Job_A 7 | displayName: template-jobs.yml job 8 | variables: 9 | - name: jobVar 10 | value: 'jobVarInTemplate' 11 | pool: 12 | vmImage: ubuntu-latest 13 | steps: 14 | - script: | 15 | echo 'This is job: Job_A with parameter ${{ parameters.param_1 }}' 16 | displayName: template-jobs.yml script 17 | - template: ../steps/template-steps.yml 18 | -------------------------------------------------------------------------------- /pipeline/templates/stages/template-stages.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: aNiceParam 3 | type: string 4 | default: aNiceDefault 5 | - name: template 6 | type: string 7 | default: "default_name_of_template_param" 8 | stages: 9 | - stage: Stage_B 10 | displayName: template-stages.yml stage 11 | jobs: 12 | - job: Job_B 13 | displayName: template-stages.yml job 14 | pool: 15 | vmImage: ubuntu-latest 16 | steps: 17 | - script: | 18 | echo 'This is job: Job_B with parameter ${{ parameters.aNiceParam }}' 19 | displayName: template-stages.yml script 20 | - template: ../jobs/template-jobs.yml 21 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/Action.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.yaml.ActionResult; 4 | 5 | import java.util.Map; 6 | 7 | public interface Action { 8 | public enum ACTION {INSERT_SECTION, UPDATE_SECTION, DELETE_SECTION, INSERT_SECTION_LINE, GET_PROPERTY}; 9 | // Executes the action 10 | void execute (ActionResult actionResult); 11 | 12 | // You can ask a question to the action; does it need a section name to be executed or is a section type sufficient 13 | boolean needsSectionIdentifier (); 14 | 15 | // A custom action does not follow the pattern of regular actions 16 | boolean isCustomAction (); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/azdo/yaml/RepositoryResource.java: -------------------------------------------------------------------------------- 1 | package azdo.yaml; 2 | 3 | /****************************************************************************************** 4 | Details of a repository. 5 | *******************************************************************************************/ 6 | public class RepositoryResource { 7 | public String repository; 8 | public String endpoint; 9 | public String trigger; 10 | public String name; 11 | public String project; 12 | public String ref; 13 | public String type; 14 | public String localBase; // Base location on the local file system 15 | public static final String LOCAL_SOURCE_POSTFIX = "_source"; // Prefix added to the cloned source directory to prevent mixing up with the local target directory 16 | } 17 | -------------------------------------------------------------------------------- /pipeline/simple-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Build numbering format 2 | name: $(Date:yyyyMMdd)$(Rev:.r) 3 | 4 | trigger: 5 | branches: 6 | include: 7 | - master 8 | - myFeature 9 | exclude: 10 | - releases/* 11 | 12 | variables: 13 | - name: testVar 14 | value: test 15 | 16 | parameters: 17 | - name: param_1 18 | type: string 19 | default: 'default' 20 | 21 | stages: 22 | - stage: simpleStage 23 | displayName: simple_stage 24 | jobs: 25 | - job: simpleJob 26 | displayName: simple_job 27 | pool: 28 | vmImage: 'ubuntu-latest' 29 | steps: 30 | - script: | 31 | echo 'This is a simple test' 32 | displayName: 'Testing, testing' 33 | - template: templates/steps/template-script.yml 34 | -------------------------------------------------------------------------------- /pipeline/simple-deployment.yml: -------------------------------------------------------------------------------- 1 | # Build numbering format 2 | name: $(Date:yyyyMMdd)$(Rev:.r) 3 | 4 | trigger: none 5 | 6 | stages: 7 | - stage: simpleDeployment 8 | displayName: Deploy stage 9 | jobs: 10 | - deployment: Deploy 11 | displayName: Deploy my app 12 | pool: 13 | vmImage: 'ubuntu-latest' 14 | environment: 'dev' 15 | strategy: 16 | runOnce: 17 | deploy: 18 | steps: 19 | - checkout: self 20 | - script: echo "This is a deployment" 21 | displayName: 'This is a deployment' 22 | on: 23 | failure: 24 | steps: 25 | - script: echo "This is on failure" 26 | success: 27 | steps: 28 | - script: echo "This is on success" 29 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionDeleteSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | /****************************************************************************************** 4 | This class is used to delete a section. This section is searched using 'sectionType' 5 | and 'sectionIdentifier'. For example: 6 | Assume, that 'sectionType' has the value "stage", and 'sectionIdentifier' has the 7 | value "mystage". 8 | The stage with the identifier "mystage" is searched in the yaml pipeline. 9 | If found, the stage section is deleted from the yaml. 10 | ******************************************************************************************/ 11 | public class ActionDeleteSection extends ActionOnSection { 12 | 13 | public ActionDeleteSection(String sectionType, String sectionIdentifier) 14 | { 15 | super(ACTION.DELETE_SECTION, 16 | sectionType, 17 | sectionIdentifier, 18 | null, 19 | false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionUpdateSectionByProperty.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import java.util.Map; 4 | 5 | /****************************************************************************************** 6 | This class is used to replace a section. This section is searched using 'sectionType' 7 | and 'property'. 8 | If the section is found, it is replaced by 'newSection'. 9 | ******************************************************************************************/ 10 | public class ActionUpdateSectionByProperty extends ActionOnSectionByProperty { 11 | 12 | public ActionUpdateSectionByProperty(String sectionType, 13 | String property, 14 | String propertyValue, 15 | Map newSection) { 16 | super(ACTION.UPDATE_SECTION, 17 | sectionType, 18 | property, 19 | propertyValue, 20 | newSection, 21 | false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Henry van Merode. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE 20 | -------------------------------------------------------------------------------- /pipeline/external-resources-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Build numbering format 2 | name: $(Date:yyyyMMdd)$(Rev:.r) 3 | 4 | trigger: none 5 | 6 | # Define 2 repositories, one is an Azure DevOps repo and the other a GitHub repo. 7 | # Note, that the 'endpoint' parameter is mandatory for a GitHub repository. 8 | # Both repos must exist, otherwise test no. 11 and 12 fail. 9 | resources: 10 | repositories: 11 | - repository: external 12 | name: Templates/Templates 13 | type: git 14 | ref: refs/heads/develop 15 | - repository: external2 16 | name: hvmerode/azdo-templates 17 | type: github 18 | endpoint: GitHubEndpoint 19 | 20 | stages: 21 | - stage: externalResourcesStage 22 | displayName: stage 23 | jobs: 24 | - job: externalResourcesJob 25 | displayName: job 26 | pool: 27 | vmImage: 'ubuntu-latest' 28 | steps: 29 | - script: | 30 | echo 'External resources test' 31 | displayName: 'External resources' 32 | - template: test-template.yml@external 33 | - template: test-template.yml@external2 34 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionUpdateSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import java.util.Map; 4 | 5 | /****************************************************************************************** 6 | This class is used to replace a section. This section is searched using 'sectionType' 7 | and 'sectionIdentifier'. For example: 8 | Assume, that 'sectionType' has the value "task", and 'sectionIdentifier' has the 9 | value "AWSShellScript@1". 10 | The stage with the identifier "AWSShellScript@1" is searched in the yaml pipeline. 11 | If the task is found, it is replaced by 'newSection'. 12 | ******************************************************************************************/ 13 | public class ActionUpdateSection extends ActionOnSection { 14 | 15 | public ActionUpdateSection(String sectionType, 16 | String sectionIdentifier, 17 | Map newSection) 18 | { 19 | super(ACTION.UPDATE_SECTION, 20 | sectionType, 21 | sectionIdentifier, 22 | newSection, 23 | false); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionDeleteSectionByProperty.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | /****************************************************************************************** 4 | This class is used to delete a section. This section is searched using 'sectionType' 5 | and 'property'. For example: 6 | Assume, that 'sectionType' has the value "stage", 'property' has the value "displayName", 7 | and 'propertyValue' has the value "Execute this stage". 8 | The stage with the displaName "Execute this stage" is searched in the yaml pipeline. 9 | If found, the stage section is deleted from the yaml. 10 | ******************************************************************************************/ 11 | public class ActionDeleteSectionByProperty extends ActionOnSectionByProperty { 12 | public ActionDeleteSectionByProperty(String sectionType, 13 | String property, 14 | String propertyValue) { 15 | super(ACTION.DELETE_SECTION, 16 | sectionType, 17 | property, 18 | propertyValue, 19 | null, 20 | false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/azdo/hook/DeleteTargetFile.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.hook; 5 | 6 | import azdo.utils.Log; 7 | import azdo.utils.Utils; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | /****************************************************************************************** 12 | Hook, to delete a file from the target path. 13 | *******************************************************************************************/ 14 | public class DeleteTargetFile extends Hook { 15 | private String fullQualifiedFileName; 16 | private static final Log logger = Log.getLogger(); 17 | public DeleteTargetFile(String fullQualifiedFileName) { 18 | logger.debug("==> Class: DeleteTargetFile"); 19 | 20 | this.fullQualifiedFileName = fullQualifiedFileName; 21 | } 22 | 23 | @Override 24 | public void executeHook (){ 25 | logger.debug("==> Method: DeleteTargetFile.executeHook"); 26 | Path path = Paths.get(fullQualifiedFileName); 27 | path = path.normalize(); 28 | fullQualifiedFileName = path.toString(); 29 | Utils.deleteFile(fullQualifiedFileName); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionInsertSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import java.util.Map; 4 | 5 | /****************************************************************************************** 6 | This class is used to insert a section before or after another section. This section is 7 | searched using the 'sectionType' and 'sectionIdentifier'. For example: 8 | Assume, that 'sectionType' has the value "stage" and 'sectionIdentifier' has the value 9 | "mystage". The stage with the name (identifier) "mystage" is searched in the yaml pipeline. 10 | If found, a section - defined by 'sectionToInsert' - is inserted before or after "mystage", 11 | depending on the value of 'insertBefore'. 12 | 13 | The variable 'sectionToInsert' is of type Map, because this is the way how a section is 14 | represented in a yaml object in Snakeyaml. 15 | ******************************************************************************************/ 16 | public class ActionInsertSection extends ActionOnSection { 17 | public ActionInsertSection(String sectionType, 18 | String sectionIdentifier, 19 | Map sectionToInsert, 20 | boolean insertBefore) 21 | { 22 | super(ACTION.INSERT_SECTION, 23 | sectionType, 24 | sectionIdentifier, 25 | sectionToInsert, 26 | insertBefore); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/azdo/hook/FindReplaceInFile.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.hook; 5 | 6 | import azdo.utils.Log; 7 | import azdo.utils.Utils; 8 | 9 | /****************************************************************************************** 10 | Hook, to find and replace all literals in a given file. 11 | *******************************************************************************************/ 12 | public class FindReplaceInFile extends Hook { 13 | private String fullQualifiedFileName; 14 | private String findString; 15 | private String replaceString; 16 | private boolean replaceAll; 17 | private static final Log logger = Log.getLogger(); 18 | public FindReplaceInFile(String fullQualifiedFileName, 19 | String findString, 20 | String replaceString, 21 | boolean replaceAll) { 22 | logger.debug("==> Class: FindReplaceInFile"); 23 | 24 | this.fullQualifiedFileName = fullQualifiedFileName; 25 | this.findString = findString; 26 | this.replaceString = replaceString; 27 | this.replaceAll = replaceAll; 28 | } 29 | 30 | @Override 31 | public void executeHook (){ 32 | logger.debug("==> Method: FindReplaceInFile.executeHook"); 33 | Utils.findReplaceInFile(fullQualifiedFileName, findString, replaceString, true); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionInsertSectionByProperty.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import java.util.Map; 4 | 5 | /****************************************************************************************** 6 | This class is used to insert a section before or after another section. This section is 7 | searched using the 'sectionType' 8 | and 'property'. For example: 9 | Assume, that 'sectionType' has the value "stage", 'property' has the value "displayName", 10 | and 'propertyValue' has the value "Execute this stage". 11 | If found, a section - defined by 'sectionToInsert' - is inserted before or after "mystage", 12 | depending on the value of 'insertBefore'. 13 | 14 | The variable 'sectionToInsert' is of type Map, because this is the way how a section is 15 | represented in a yaml object in Snakeyaml. 16 | 17 | ******************************************************************************************/ 18 | public class ActionInsertSectionByProperty extends ActionOnSectionByProperty { 19 | 20 | public ActionInsertSectionByProperty(String sectionType, 21 | String property, 22 | String propertyValue, 23 | Map sectionToInsert, 24 | boolean insertBefore) { 25 | super(ACTION.INSERT_SECTION, 26 | sectionType, 27 | property, 28 | propertyValue, 29 | sectionToInsert, 30 | insertBefore); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/azdo/hook/DeleteJUnitPipelineDependency.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.hook; 5 | 6 | import azdo.junit.AzDoPipeline; 7 | import azdo.utils.Log; 8 | import azdo.utils.PomUtils; 9 | import azdo.utils.Utils; 10 | 11 | /****************************************************************************************** 12 | Hook, to remove the dependency to the 'junit-pipeline' jar, before it is deployed to the 13 | AzDo test project. Normally, this dependency is stored in a repository or in 14 | Azure DevOps artifacts. When building the Maven artifact, the location of this dependency 15 | is configured and the Maven build will not fail. 16 | The dependency is removed from the pom.xml to prevent build errors (cannot find library). 17 | ******************************************************************************************/ 18 | public class DeleteJUnitPipelineDependency extends Hook { 19 | private String pom; 20 | private String groupId; 21 | private String artifactId; 22 | private static final Log logger = Log.getLogger(); 23 | public DeleteJUnitPipelineDependency (String pom, String groupId, String artifactId) { 24 | logger.debug("==> Class: DeleteJUnitPipelineDependency"); 25 | 26 | this.pom = Utils.fixPath(pom); 27 | this.groupId = groupId; 28 | this.artifactId = artifactId; 29 | } 30 | 31 | @Override 32 | public void executeHook (){ 33 | try { 34 | logger.debug("==> Method: DeleteJUnitPipelineDependency.executeHook"); 35 | PomUtils.deleteDependency(pom, groupId, artifactId); 36 | } 37 | catch (Exception e) {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionResetTrigger.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Map; 8 | 9 | import static azdo.utils.Constants.SECTION_TRIGGER; 10 | 11 | public class ActionResetTrigger implements Action { 12 | private static final Log logger = Log.getLogger(); 13 | 14 | public ActionResetTrigger() {} 15 | 16 | /****************************************************************************************** 17 | Perform an action on the 'trigger' section (replace with new trigger section). 18 | @param actionResult Contains parts of the YAML structure. It is used to search for the 19 | section in the l3 structure. 20 | ******************************************************************************************/ 21 | public void execute (ActionResult actionResult) { 22 | logger.debug("==> Method ActionResetTrigger.execute"); 23 | logger.debug("actionResult.l1: {}", actionResult.l1); 24 | 25 | if (actionResult.l1 == null) { 26 | logger.debug("actionResult.l1 is null; return"); 27 | return; 28 | } 29 | 30 | if (actionResult.l1 instanceof Map) { 31 | logger.info("Reset trigger to \'none\'"); 32 | Map map = (Map) actionResult.l1; 33 | if (map.containsKey(SECTION_TRIGGER)) 34 | map.put(SECTION_TRIGGER, "none"); 35 | } 36 | } 37 | 38 | // This action can only be executed if the section type and section identification are found in the YAML file 39 | public boolean needsSectionIdentifier() { 40 | return false; 41 | } 42 | 43 | // This action is not a custom action 44 | public boolean isCustomAction () { return true; } 45 | } 46 | -------------------------------------------------------------------------------- /pipeline/powershell-mock.yml: -------------------------------------------------------------------------------- 1 | # Build numbering format 2 | name: $(Date:yyyyMMdd)$(Rev:.r) 3 | 4 | trigger: none 5 | 6 | stages: 7 | - stage: PowerShellMockStage 8 | jobs: 9 | - job: PowerShellMockJob 10 | pool: 11 | vmImage: 'windows-latest' 12 | steps: 13 | # Multiple Invoke-RestMethod commands to test that mockPowerShellCommandSearchStepByDisplayName is able to mock multiple 14 | # instances of the Invoke-RestMethod in one step 15 | - pwsh: | 16 | $Url = "https://server.contoso.com:8089/services/search/jobs/export" 17 | $Body = @{ 18 | search = "search index=_internal | reverse | table index,host,source,sourcetype,_raw" 19 | output_mode = "csv" 20 | earliest_time = "-2d@d" 21 | latest_time = "-1d@d" 22 | } 23 | $result = Invoke-RestMethod -Method 'Post' -Uri $url -Body $body -OutFile output.csv 24 | Write-Output "Result: $($result.element)" 25 | 26 | $result = Invoke-RestMethod -Method 'Post' -Uri $url -Body $body -OutFile output.csv 27 | Write-Output "Result: $($result.element)" 28 | displayName: 'Invoke-RestMethod step 1 of 2' 29 | 30 | # An extra step with a Invoke-RestMethod to validate that mockPowershellCommandSearchStepByDisplayName resets for each new step 31 | - pwsh: | 32 | $Url = "https://server.contoso.com:8089/services/search/people/export" 33 | $result = Invoke-RestMethod -Method 'Post' -Uri $url -OutFile output.csv 34 | Write-Output "Result: $($result.element)" 35 | displayName: 'Invoke-RestMethod step 2 of 2' 36 | 37 | # Validate that the PowerShell@2 task is supported by mockPowershellCommandSearchStepByDisplayName 38 | - task: PowerShell@2 39 | inputs: 40 | targetType: 'inline' 41 | script: | 42 | $url = "$($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI)$env:SYSTEM_TEAMPROJECTID/_apis/build/definitions/$($env:SYSTEM_DEFINITIONID)?api-version=5.0" 43 | $pipeline = Invoke-RestMethod -Uri $url -Headers @{ 44 | Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" 45 | } 46 | Write-Output "Result: $($pipeline.element)" 47 | displayName: 'PowerShell@2 task' 48 | 49 | -------------------------------------------------------------------------------- /pipeline/bash-mock.yml: -------------------------------------------------------------------------------- 1 | # Build numbering format 2 | name: $(Date:yyyyMMdd)$(Rev:.r) 3 | 4 | trigger: none 5 | 6 | stages: 7 | - stage: bashMockStage 8 | jobs: 9 | - job: bashMockJob 10 | pool: 11 | vmImage: 'ubuntu-latest' 12 | steps: 13 | # Multiple curl commands to test that mockBashCommandSearchStepByDisplayName is able to mock multiple 14 | # instances of the same command in one step 15 | - script: | 16 | var=$(curl --silent --head https://www.example.com | awk '/^HTTP/{print $2}') 17 | echo "$var" 18 | var=$(curl --silent --head https://www.example.com | awk '/^HTTP/{print $2}') 19 | echo "$var" 20 | var=$(curl --silent --head https://www.example.com | awk '/^HTTP/{print $2}') 21 | echo "$var" 22 | displayName: 'Curl step 1 of 2' 23 | 24 | # An extra step with a curl command to validate that mockBashCommandSearchStepByDisplayName resets for each new step 25 | - script: | 26 | # Return the HTTP status code. This should be 200 (if not mocked). 27 | var=$(curl --silent --head https://www.example.com | awk '/^HTTP/{print $2}') 28 | echo "$var" 29 | displayName: 'Curl step 2 of 2' 30 | 31 | # Validate that the "bash" step works for mockBashCommandSearchStepByDisplayName 32 | # Validate that the wget command is supported by mockBashCommandSearchStepByDisplayName 33 | - bash: | 34 | # The wget command downloads a .jar file 35 | wget "https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar" 36 | displayName: 'Wget step' 37 | 38 | # Validate that the ftp command is supported by mockBashCommandSearchStepByDisplayName 39 | - script: | 40 | # This ftp command fails 41 | ftp -u ftp://user:secret@ftp.example.com/my-local-file.txt my-local-file.txt 42 | displayName: 'Ftp step' 43 | 44 | # Validate that the Bash@3 task is supported by mockBashCommandSearchStepByDisplayName 45 | - task: Bash@3 46 | inputs: 47 | targetType: 'inline' 48 | script: | 49 | var=$(curl --silent --head https://www.example.com | awk '/^HTTP/{print $2}') 50 | echo "$var" 51 | displayName: 'Bash@3 task' 52 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionReturnPropertyValue.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.yaml.ActionResult; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | /****************************************************************************************** 9 | This class is used to return the value of a property in a section. 10 | This section is searched using 'sectionType' and 'property'. 11 | If the property is found, it's value is returned. 12 | ******************************************************************************************/ 13 | public class ActionReturnPropertyValue extends ActionOnSectionByProperty { 14 | 15 | private ArrayList propertyValues = new ArrayList<>(); 16 | public ActionReturnPropertyValue(String sectionType, 17 | String property) { 18 | super(ACTION.GET_PROPERTY, 19 | sectionType, 20 | property, 21 | null, 22 | null, 23 | false); 24 | } 25 | 26 | // Perform a custom execute 27 | public void execute(ActionResult actionResult) { 28 | if (actionResult.l1 == null) { 29 | logger.debug("l1 is null"); 30 | return; 31 | } 32 | 33 | if (actionResult.l1 instanceof ArrayList) { 34 | logger.debug("l1 is instance of ArrayList"); 35 | 36 | // Run through the elements of the list and update the section 37 | ArrayList list = (ArrayList) actionResult.l1; 38 | int index; 39 | int size = list.size(); 40 | for (index = 0; index < size; index++) { 41 | if (list.get(index) instanceof Map) { 42 | 43 | Map map = (Map) list.get(index); 44 | for (Map.Entry entry : map.entrySet()) { 45 | logger.debug("entry.getKey(): {}", entry.getKey()); 46 | logger.debug("entry.getValue(): {}", entry.getValue()); 47 | if (this.property.equals(entry.getKey())) { 48 | logger.debug("Return value {} of property {}", entry.getValue(), property); 49 | propertyValues.add(entry.getValue().toString()); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | public ArrayList getPropertyValues () { 58 | return propertyValues; 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/azdo/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package azdo.utils; 2 | 3 | /****************************************************************************************** 4 | Constants, used throughout the junit-pipeline code. 5 | *******************************************************************************************/ 6 | public class Constants { 7 | // *************************************** Types in YAML files *************************************** 8 | // *************************************** Sections *************************************** 9 | public static final String SECTION_TRIGGER = "trigger"; 10 | public static final String SECTION_VARIABLES = "variables"; 11 | public static final String SECTION_PARAMETERS = "parameters"; 12 | public static final String SECTION_STAGES = "stages"; 13 | public static final String SECTION_STAGE = "stage"; 14 | public static final String SECTION_JOBS = "jobs"; 15 | public static final String SECTION_JOB = "job"; 16 | public static final String SECTION_STEPS = "steps"; 17 | public static final String SECTION_STEP = "step"; 18 | public static final String SECTION_TASK = "task"; 19 | public static final String STEP_SCRIPT = "script"; 20 | public static final String SCRIPT = "script"; 21 | public static final String STEP_SCRIPT_PWSH = "pwsh"; 22 | public static final String TASK_POWERSHELL_2 = "PowerShell@2"; 23 | public static final String TASK_BASH_3 = "Bash@3"; 24 | public static final String STEP_SCRIPT_BASH = "bash"; 25 | public static final String SECTION_TEMPLATE = "template"; 26 | 27 | // *************************************** Other *************************************** 28 | public static final String IDENTIFIER_NAME = "name"; 29 | public static final String PROPERTY_DISPLAY_NAME = "displayName"; 30 | public static final String PROPERTY_VARIABLE_GROUP = "group"; 31 | public static final String PROPERTY_ENVIRONMENT = "environment"; 32 | public static final String INPUTS = "inputs"; 33 | public static final String CONDITION = "condition"; 34 | public static final String TYPE_VARIABLE = "variable"; 35 | public static final String TYPE_PARAMETER = "parameter"; 36 | public static final String TYPE_STEP = SECTION_STEP; 37 | public static final String TYPE_TEMPLATE = SECTION_TEMPLATE; 38 | 39 | // *************************************** Colors *************************************** 40 | public static final String RED = "\u001B[31m"; 41 | public static final String GREEN = "\u001B[32m"; 42 | public static final String YELLOW = "\u001B[33m"; 43 | public static final String BLUE = "\u001B[34m"; 44 | public static final String PURPLE = "\u001B[35m"; 45 | public static final String CYAN = "\u001B[36m"; 46 | public static final String WHITE = "\u001B[37m"; 47 | public static final String LIGHT_RED = "\u001B[91m"; 48 | public static final String LIGHT_GREEN = "\u001B[92m"; 49 | public static final String LIGHT_YELLOW = "\u001B[93m"; 50 | public static final String LIGHT_WHITE = "\u001B[97m"; 51 | public static final String RESET_COLOR = "\u001B[0m"; 52 | 53 | // *************************************** Files *************************************** 54 | public static final String JSON_SCHEMA = "azure-pipelines-schema.json"; 55 | 56 | // *************************************** Visualization *************************************** 57 | public static final String DEMARCATION = "=============================================================================="; 58 | public static final String HEADER_FOOTER = "--------------------------------------------------------------------------------------------------------------------------------"; 59 | public static final String ARROW_DOWN = "▼"; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/azdo/utils/Log.java: -------------------------------------------------------------------------------- 1 | package azdo.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import static azdo.utils.Constants.*; 7 | 8 | @SuppressWarnings({"UnusedDeclaration"}) 9 | /****************************************************************************************** 10 | Custom logger. 11 | *******************************************************************************************/ 12 | public class Log 13 | { 14 | private static Log instance = null; 15 | private static Logger logger = null; 16 | 17 | private Log() 18 | { 19 | logger = LoggerFactory.getLogger(Log.class); 20 | } 21 | 22 | // Singleton with double-checked locking 23 | // public static Log getLogger() { 24 | // if (instance == null) { 25 | // synchronized (Log.class) { 26 | // if (instance == null) { 27 | // instance = new Log(); 28 | // } 29 | // } 30 | // } 31 | // return instance; 32 | // } 33 | 34 | // Not the fastest Singleton because of the synchronized in each getLogger() call, but runs fine 35 | // on all platforms (compared to a double-checked locking solution) 36 | public static synchronized Log getLogger() { 37 | if (instance == null) 38 | instance = new Log(); 39 | return instance; 40 | } 41 | 42 | public void debug(Throwable t) 43 | { 44 | logger.debug(t.getMessage(), t); 45 | } 46 | 47 | public void info(Throwable t) 48 | { 49 | logger.info(t.getMessage(), t); 50 | } 51 | 52 | public void warn(Throwable t) 53 | { 54 | logger.warn(t.getMessage(), t); 55 | } 56 | 57 | public void error(Throwable t) 58 | { 59 | logger.error(LIGHT_RED + t.getMessage() + RESET_COLOR, t); 60 | } 61 | 62 | public void debug(String format, Object... args) 63 | { 64 | if (logger.isDebugEnabled()) { 65 | logger.debug(format, args); 66 | } 67 | } 68 | 69 | public void info(String format, Object... args) 70 | { 71 | if (logger.isInfoEnabled()) { 72 | logger.info(LIGHT_GREEN + format + RESET_COLOR, args); 73 | } 74 | } 75 | 76 | public void infoColor(String color, String format, Object... args) 77 | { 78 | if (logger.isInfoEnabled()) { 79 | logger.info(color + format + RESET_COLOR, args); 80 | } 81 | } 82 | 83 | public void warn(String format, Object... args) 84 | { 85 | if (logger.isWarnEnabled()) { 86 | logger.warn(LIGHT_YELLOW + format + RESET_COLOR, args); 87 | } 88 | } 89 | 90 | public void error(String format, Object... args) 91 | { 92 | if (logger.isErrorEnabled()) { 93 | logger.error(LIGHT_RED + format + RESET_COLOR, args); 94 | } 95 | } 96 | 97 | public void debug(Throwable t, String format, Object... args) 98 | { 99 | if (logger.isDebugEnabled()) { 100 | logger.debug(format, args, t); 101 | } 102 | } 103 | 104 | public void info(Throwable t, String format, Object... args) 105 | { 106 | if (logger.isInfoEnabled()) { 107 | logger.info(format, args, t); 108 | } 109 | } 110 | 111 | public void warn(Throwable t, String format, Object... args) 112 | { 113 | if (logger.isWarnEnabled()) { 114 | logger.warn(format, args, t); 115 | } 116 | } 117 | 118 | public void error(Throwable t, String format, Object... args) 119 | { 120 | if (logger.isErrorEnabled()) { 121 | logger.error(LIGHT_RED + format + RESET_COLOR, args, t); 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /pipeline/pipeline-test.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Henry van Merode. 2 | # Licensed under the MIT License. 3 | 4 | # Build numbering format 5 | name: $(Date:yyyyMMdd)$(Rev:.r) 6 | 7 | trigger: none 8 | 9 | parameters: 10 | - name: param_1 11 | type: string 12 | default: 'contains a default value' 13 | - name: environment 14 | type: string 15 | default: dev 16 | - name: sleep 17 | type: string 18 | default: 1 19 | 20 | variables: 21 | - name: myVar 22 | value: myValue 23 | - name: aws_connection 24 | value: 1234567890 25 | - name: aws_region 26 | value: us-east-1 27 | 28 | stages: 29 | - template: templates/stages/template-stages.yml 30 | parameters: 31 | aNiceParam: parameter_of_template-stages.yml 32 | template: test_parameter_with_name_template 33 | 34 | - stage: ExecuteScriptStage 35 | displayName: 'The executeScriptStage' 36 | condition: always() 37 | jobs: 38 | - job: Job_XA 39 | variables: 40 | - name: jobVar 41 | value: original_value_Job_XA 42 | displayName: ExecuteScriptStage job_xa 43 | pool: 44 | vmImage: 'ubuntu-latest' 45 | steps: 46 | - script: | 47 | echo 'This is a multiline script' 48 | echo 'Row 2 of that script' 49 | displayName: ExecuteScriptStage job_xa script 50 | - job: Job_XB 51 | displayName: ExecuteScriptStage job_xb 52 | pool: 53 | vmImage: 'ubuntu-latest' 54 | steps: 55 | - script: | 56 | echo 'Job_2.Task_1' 57 | displayName: ExecuteScriptStage job_xb script 58 | - template: templates/steps/template-steps.yml 59 | parameters: 60 | param_1: param_1_value 61 | param_2: param_2_value 62 | - job: Job_XC 63 | variables: 64 | - name: jobVar 65 | value: original_value_Job_XC 66 | displayName: ExecuteScriptStage job_xc 67 | pool: 68 | vmImage: 'ubuntu-latest' 69 | steps: 70 | - script: | 71 | echo 'Job_2.Task_3: Sleep some seconds' 72 | sleep ${{ parameters.sleep }}; 73 | displayName: ExecuteScriptStage job_xc script 74 | - template: templates/jobs/template-jobs.yml 75 | parameters: 76 | param_1: param_1_value 77 | 78 | - stage: DeployStage 79 | condition: eq(variables['Build.SourceBranchName'], 'master') 80 | displayName: 'The deployStage' 81 | jobs: 82 | - job: Job_XD 83 | displayName: DeployStage job_xd 84 | pool: 85 | vmImage: 'ubuntu-latest' 86 | steps: 87 | - script: | 88 | echo 'AWS Connection is: $(aws_connection)' 89 | echo 'AWS Region is: $(aws_region)' 90 | displayName: DeployStage job_xd script 91 | - job: Job_XE 92 | displayName: DeployStage job_xe 93 | pool: 94 | vmImage: 'ubuntu-latest' 95 | steps: 96 | - script: | 97 | echo 'Deploy to AWS' 98 | displayName: DeployStage job_xe script 99 | - task: AWSShellScript@1 100 | inputs: 101 | awsCredentials: $(aws_connection) 102 | regionName: $(aws_region) 103 | scriptType: 'inline' 104 | inlineScript: | 105 | #!/bin/bash 106 | set -ex 107 | export cdk=`find $(Pipeline.Workspace)/. -name 'cdk*.jar'` 108 | export app=`find $(Pipeline.Workspace)/. -name 'app*.jar'` 109 | 110 | echo "Deploying stack" 111 | cdk deploy --app '${JAVA_HOME_11_X64}/bin/java -cp $cdk com.org.app.Stack' \ 112 | -c env=${{ parameters.environment }} \ 113 | -c app=$app 114 | --all \ 115 | --ci \ 116 | --require-approval never 117 | displayName: DeployStage job_xe AWSShellScript 118 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | io.github.hvmerode 4 | junit-pipeline 5 | 1.3.0 6 | 7 | junit-pipeline 8 | Perform unit- and integration tests with Azure DevOps pipelines. 9 | http://github.com/hvmerode/junit-pipeline 10 | 11 | 12 | 13 | The MIT License 14 | https://opensource.org/license/mit/ 15 | 16 | 17 | 18 | 19 | 20 | Henry van Merode 21 | a@b 22 | Amazon 23 | https://www.amazon.com/Continuous-Integration-Delivery-Practical-Developing/dp/1484292278 24 | 25 | 26 | 27 | 28 | scm:git:git://github.com/hvmerode/junit-pipeline.git 29 | scm:git:ssh://github.com:hvmerode/junit-pipeline.git 30 | http://github.com/hvmerode/junit-pipeline 31 | 32 | 33 | 34 | install 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-javadoc-plugin 39 | 3.5.0 40 | 41 | public 42 | 43 | 44 | 45 | 46 | 47 | 48 | 11 49 | 11 50 | 51 | 52 | 53 | 54 | org.junit.jupiter 55 | junit-jupiter 56 | 5.9.0-M1 57 | 58 | 63 | 64 | org.eclipse.jgit 65 | org.eclipse.jgit 66 | 6.6.1.202309021850-r 67 | 68 | 69 | org.yaml 70 | snakeyaml 71 | 2.0 72 | 73 | 74 | org.apache.logging.log4j 75 | log4j-core 76 | 2.23.1 77 | 78 | 79 | org.apache.logging.log4j 80 | log4j-slf4j-impl 81 | 2.23.1 82 | 83 | 84 | commons-io 85 | commons-io 86 | 2.11.0 87 | 88 | 89 | org.apache.maven 90 | maven-model 91 | 3.5.0 92 | 93 | 94 | org.springframework 95 | spring-web 96 | 6.1.8 97 | 98 | 99 | com.networknt 100 | json-schema-validator 101 | 1.0.84 102 | 103 | 104 | org.apache.commons 105 | commons-lang3 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/main/resources/junit_pipeline.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Henry van Merode. 2 | # Licensed under the MIT License. 3 | 4 | ######################################################################################################################## 5 | # Source - File location of the application- and pipeline code (yml) 6 | ######################################################################################################################## 7 | 8 | # The local directory of the source repository on the workstation 9 | source.path=C:\\Users\\Me\\Documents\\Github\\junit-pipeline 10 | 11 | # The local directory of the external source repositories (excl. repository name) on the workstation 12 | source.base.path.external=C:\\Users\\Me\\Documents\\Github 13 | 14 | # The name of the main (source) repository 15 | source.repository.name=junit-pipeline 16 | 17 | # The source Azure DevOps project 18 | # Because the source is GitHub for junit-pipeline, there is no source project 19 | source.project= 20 | 21 | ######################################################################################################################## 22 | # Target - File location and repository 23 | ######################################################################################################################## 24 | 25 | # The target Azure DevOps project to test the pipeline 26 | target.organization=mycorp-com 27 | target.project=MyTestProject 28 | target.base.path.external=C:\\Users\\Me\\Documents\\Github\\ 29 | 30 | # The local directory of the target repository on the workstation 31 | target.path=C:\\Users\\Me\\Documents\\Github\\junit-pipeline-test 32 | 33 | # The name of the unit test repository 34 | target.repository.name=junit-pipeline-test 35 | git.commit.pattern=.,.xml,.yml,.java 36 | 37 | # Pattern of files that are not copied to the target directory. This includes: 38 | # .git directory 39 | # .idea directory 40 | # .png files 41 | # target directory 42 | target.excludelist=(?i).*(.git|.idea|.png|.class|.jar)|target$ 43 | 44 | ######################################################################################################################## 45 | # Azure DevOps credentials 46 | ######################################################################################################################## 47 | azdo.user=UserWithToken 48 | azdo.pat=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 49 | 50 | ######################################################################################################################## 51 | # Azure DevOps API 52 | ######################################################################################################################## 53 | pipelines.api=/pipelines 54 | pipelines.api.runs=/runs 55 | pipelines.api.version=api-version=7.0 56 | 57 | ######################################################################################################################## 58 | # Azure DevOps API 59 | ######################################################################################################################## 60 | git.api=/git 61 | git.api.repositories=/repositories 62 | git.api.version=api-version=7.0 63 | 64 | ######################################################################################################################## 65 | # API properties of the API 66 | ######################################################################################################################## 67 | build.api=/build/builds 68 | build.api.version=api-version=7.0 69 | 70 | ######################################################################################################################## 71 | # API properties of the API 72 | ######################################################################################################################## 73 | #project.api=/projects 74 | project.api.version=api-version=7.0 75 | 76 | ######################################################################################################################## 77 | # API properties of the API 78 | ######################################################################################################################## 79 | variable.groups.api=/distributedtask/variablegroups 80 | variable.groups.api.version=api-version=7.0 81 | variable.groups.validate=true 82 | environments.api=/distributedtask/environments 83 | environments.api.version=api-version=7.0 84 | environments.validate=true 85 | 86 | # The frequency to retrieve the build result and status using the API (in seconds) 87 | build.api.poll.frequency=10 88 | 89 | # Maximum waiting time to retrieve the build result (in seconds) 90 | build.api.poll.timeout=180 91 | 92 | ######################################################################################################################## 93 | # Miscellaneous properties 94 | ######################################################################################################################## 95 | templates.external.include=true 96 | error.continue=false 97 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionInsertLineInSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | /****************************************************************************************** 9 | This class is typically used to insert a line to the beginning of a script, although 10 | it is made generic, and it can be also used fore other section types. 11 | It searches a section using a 'property', with a 'propertyValue', for example, 12 | 'property' == "displayName", and 'propertyValue' == "Deploy step". 13 | If the section is found, a new (script) line is added to the beginning of the section. 14 | If a script look like this: 15 | {@code 16 | script: | 17 | echo "This is the first line" 18 | } 19 | and the ActionInsertLineInSection.execute() method is called, the result becomes: 20 | {@code 21 | script: | 22 | echo "And now this is the first line" 23 | echo "This is the first line" 24 | } 25 | ******************************************************************************************/ 26 | public class ActionInsertLineInSection implements Action { 27 | private static final Log logger = Log.getLogger(); 28 | private String sectionType; // Is "script", for example 29 | private String property; // The property of the section, for example "displayName" 30 | private String propertyValue; // The value of the property 31 | private String newLine; // The line to add to this section; this is the first line 32 | 33 | public ActionInsertLineInSection(String sectionType, 34 | String property, 35 | String propertyValue, 36 | String newLine) { 37 | this.sectionType = sectionType; 38 | this.property = property; 39 | this.propertyValue = propertyValue; 40 | this.newLine = newLine; 41 | } 42 | 43 | public void execute (ActionResult actionResult) { 44 | logger.debug("==> Method ActionInsertLineInSection.execute"); 45 | logger.debug("actionResult.l1: {}", actionResult.l1); 46 | logger.debug("actionResult.l2: {}", actionResult.l2); 47 | logger.debug("actionResult.L3: {}", actionResult.l3); 48 | logger.debug("sectionType: {}", sectionType); 49 | logger.debug("property: {}", property); 50 | logger.debug("propertyValue: {}", propertyValue); 51 | logger.debug("newLine: {}", newLine); 52 | 53 | if (actionResult.l3 == null) { 54 | logger.debug("actionResult.l3 is null; return"); 55 | } 56 | 57 | boolean foundType = false; 58 | if (actionResult.l3 instanceof ArrayList) { 59 | 60 | // Run through the elements of the list and insert the section 61 | ArrayList list = (ArrayList) actionResult.l3; 62 | int index; 63 | int size = list.size(); 64 | for (index = 0; index < size; index++) { 65 | if (list.get(index) instanceof Map) { 66 | 67 | Map map = (Map) list.get(index); 68 | for (Map.Entry entry : map.entrySet()) { 69 | 70 | logger.debug("entry.getKey(): {}", entry.getKey()); 71 | logger.debug("entry.getValue(): {}", entry.getValue()); 72 | if (sectionType.equals(entry.getKey())) { 73 | // Found the right type 74 | logger.debug("Found the right type: {}", sectionType); 75 | foundType = true; 76 | } 77 | if (property.equals(entry.getKey()) && propertyValue.equals(entry.getValue()) && foundType) { 78 | // Found the right property with the correct value 79 | logger.info("Add new line to step with property \'{}\': \'{}\'", property, propertyValue); 80 | String s = (String)map.get(sectionType); 81 | logger.debug("String: {}", s); 82 | s = newLine + s; 83 | logger.debug("String: {}", s); 84 | map.put(sectionType, s); 85 | logger.debug("Map: {}", map); 86 | actionResult.actionExecuted = true; 87 | return; 88 | } 89 | } 90 | } 91 | foundType = false; 92 | } 93 | } 94 | return; 95 | } 96 | 97 | // This action can be executed if only the appropriate section type is found 98 | public boolean needsSectionIdentifier() { 99 | return false; 100 | } 101 | 102 | // This action is not a custom action 103 | public boolean isCustomAction () { return false; } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionAddPropertyToSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Map; 8 | 9 | /****************************************************************************************** 10 | This class is typically used to add a new property to a section. 11 | It searches a section using a 'sectionType' and optionally a sectionIdentification, which 12 | can be "" or null. 13 | If the section is found, a property is added to the section. 14 | There is no validation whether the property is valid. 15 | ******************************************************************************************/ 16 | public class ActionAddPropertyToSection implements Action { 17 | private static final Log logger = Log.getLogger(); 18 | private String sectionType; // Is "@Bash03", for example 19 | private String sectionIdentifier; // Optional identification of the section type 20 | private String property; // The property of the section, for example "enabled" 21 | private String propertyValue; // The value of the property, for example "true" 22 | 23 | public ActionAddPropertyToSection(String sectionType, 24 | String sectionIdentifier, 25 | String property, 26 | String propertyValue) { 27 | this.sectionType = sectionType; 28 | this.sectionIdentifier = sectionIdentifier; 29 | this.property = property; 30 | this.propertyValue = propertyValue; 31 | } 32 | 33 | public void execute (ActionResult actionResult) { 34 | logger.debug("==> Method ActionAddPropertyToSection.execute"); 35 | logger.debug("actionResult.l1: {}", actionResult.l1); 36 | logger.debug("actionResult.l2: {}", actionResult.l2); 37 | logger.debug("actionResult.L3: {}", actionResult.l3); 38 | logger.debug("sectionType: {}", sectionType); 39 | logger.debug("sectionIdentifier: {}", sectionIdentifier); 40 | logger.debug("property: {}", property); 41 | logger.debug("propertyValue: {}", propertyValue); 42 | 43 | if (actionResult.l3 == null) { 44 | logger.debug("actionResult.l3 is null; return"); 45 | } 46 | 47 | boolean foundSection = false; 48 | if (actionResult.l3 instanceof ArrayList) { 49 | 50 | // Run through the elements of the list and insert the section 51 | ArrayList list = (ArrayList) actionResult.l3; 52 | int index; 53 | int size = list.size(); 54 | for (index = 0; index < size; index++) { 55 | if (list.get(index) instanceof Map) { 56 | 57 | Map map = (Map) list.get(index); 58 | for (Map.Entry entry : map.entrySet()) { 59 | 60 | logger.debug("entry.getKey(): {}", entry.getKey()); 61 | logger.debug("entry.getValue(): {}", entry.getValue()); 62 | if (entry.getKey() instanceof String && sectionType.equals(entry.getKey())) { 63 | if (sectionIdentifier == null || sectionIdentifier.isEmpty()) { 64 | logger.debug("Found the section: {}", sectionType); 65 | foundSection = true; 66 | } 67 | else if (sectionIdentifier.equals(entry.getValue())) { 68 | logger.debug("Found the section: {}", sectionType); 69 | foundSection = true; 70 | } 71 | } 72 | if (foundSection) { 73 | // Add property to the section 74 | // This means updating the old one if it already exists, or adding the new one if not present. 75 | if (property != null && propertyValue != null && !property.isEmpty() && !propertyValue.isEmpty()) { 76 | if (map.containsKey(property)) 77 | map.replace(property, propertyValue); 78 | else 79 | map.put(property, propertyValue); 80 | } 81 | actionResult.actionExecuted = true; 82 | return; 83 | } 84 | } 85 | } 86 | foundSection = false; 87 | } 88 | } 89 | return; 90 | } 91 | 92 | // This action can be executed if only the appropriate section type is found 93 | public boolean needsSectionIdentifier() { 94 | return false; 95 | } 96 | 97 | // This action is not a custom action 98 | public boolean isCustomAction () { return false; } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/azdo/utils/PomUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.utils; 5 | 6 | import org.apache.maven.model.Dependency; 7 | import org.apache.maven.model.Model; 8 | import org.apache.maven.model.io.xpp3.MavenXpp3Reader; 9 | import org.apache.maven.model.io.xpp3.MavenXpp3Writer; 10 | import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | 18 | /****************************************************************************************** 19 | Utils to manipulate the pom.xml. This class is not used in the junit-code but added 20 | for testing purposes only. 21 | *******************************************************************************************/ 22 | public class PomUtils { 23 | private static final Log logger = Log.getLogger(); 24 | 25 | public static boolean checkDependency(String pomFile, 26 | String groupID, 27 | String artifactId, 28 | String version) throws IOException, XmlPullParserException { 29 | logger.debug("==> Method: PomUtils.checkDependency"); 30 | 31 | // Create a MavenXpp3Reader to read the existing pom.xml file 32 | MavenXpp3Reader reader = new MavenXpp3Reader(); 33 | File file = new File(pomFile); 34 | InputStream inputStream = new FileInputStream(file); 35 | Model model = reader.read(inputStream); 36 | 37 | // Create a new dependency node to check for existence 38 | Dependency dependencyToCheck = new Dependency(); 39 | dependencyToCheck.setGroupId(groupID); 40 | dependencyToCheck.setArtifactId(artifactId); 41 | //dependencyToCheck.setVersion(version); 42 | 43 | // Check if the dependency already exists in the model 44 | List list = model.getDependencies(); 45 | for (Dependency dependency : list) { 46 | if (artifactId.equals(dependency.getArtifactId())) 47 | return true; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | public static void insertDependency(String pomFile, 54 | String groupID, 55 | String artifactId, 56 | String version) throws IOException, XmlPullParserException { 57 | logger.debug("==> Method: PomUtils.insertDependency"); 58 | 59 | // Create a MavenXpp3Reader to read the existing pom.xml file 60 | MavenXpp3Reader reader = new MavenXpp3Reader(); 61 | File file = new File(pomFile); 62 | InputStream inputStream = new FileInputStream(file); 63 | Model model = reader.read(inputStream); 64 | 65 | // Create a new dependency node 66 | Dependency newDependency = new Dependency(); 67 | newDependency.setGroupId(groupID); 68 | newDependency.setArtifactId(artifactId); 69 | newDependency.setVersion(version); 70 | 71 | // Add the new dependency to the model 72 | model.addDependency(newDependency); 73 | 74 | // Write the modified model to the pom.xml file 75 | MavenXpp3Writer writer = new MavenXpp3Writer(); 76 | writer.write(new java.io.FileWriter(pomFile), model); 77 | } 78 | 79 | public static void deleteDependency(String pomFile, 80 | String groupID, 81 | String artifactId) throws IOException, XmlPullParserException { 82 | logger.debug("==> Method: PomUtils.deleteDependency"); 83 | logger.debug("pomFile: {}", pomFile); 84 | logger.debug("groupID: {}", groupID); 85 | logger.debug("artifactId: {}", artifactId); 86 | 87 | // Create a MavenXpp3Reader to read the existing pom.xml file 88 | MavenXpp3Reader reader = new MavenXpp3Reader(); 89 | File file = new File(pomFile); 90 | InputStream inputStream = new FileInputStream(file); 91 | Model model = reader.read(inputStream); 92 | 93 | // Use iterator to prevent ConcurrentModificationException 94 | List list = model.getDependencies(); 95 | Iterator iterator = list.iterator(); 96 | Dependency dependency = null; 97 | while (iterator.hasNext()) { 98 | dependency = iterator.next(); 99 | if (artifactId.equals(dependency.getArtifactId()) && groupID.equals(dependency.getGroupId())) { 100 | logger.debug("Found dependency; remove"); 101 | iterator.remove(); 102 | } 103 | } 104 | 105 | // Write the modified model to the pom.xml file 106 | model.setDependencies(list); 107 | logger.debug("Found dependency; remove"); 108 | MavenXpp3Writer writer = new MavenXpp3Writer(); 109 | writer.write(new java.io.FileWriter(pomFile), model); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/azdo/junit/TimelineRecord.java: -------------------------------------------------------------------------------- 1 | package azdo.junit; 2 | 3 | import azdo.utils.Log; 4 | 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | import java.util.ArrayList; 8 | 9 | import static azdo.utils.Constants.*; 10 | 11 | /****************************************************************************************** 12 | A TimelineRecord contains information about a phase in the pipeline run. This phase can 13 | be of type "Stage", "Job", "Phase" (which is a Job), and "Task" (which represents 14 | all Steps). 15 | *******************************************************************************************/ 16 | public class TimelineRecord { 17 | private static final Log logger = Log.getLogger(); 18 | public ArrayList reorganizedTimelineRecords = new ArrayList<>(); 19 | public String id; 20 | public String parentId; 21 | public String type; 22 | public String name; 23 | public String startTime; 24 | public String finishTime; 25 | long timeInSeconds = 0; 26 | String timeInSecondsAsString = "<1"; 27 | public String state; 28 | public String result; 29 | 30 | /* 31 | Add a hierarchy to the TimelineRecords 32 | */ 33 | public void reorganize (ArrayList timelineRecords) { 34 | logger.debug("==> Method: TimelineRecord.reorganize"); 35 | 36 | int size = timelineRecords.size(); 37 | TimelineRecord timelineRecord; 38 | for (int counter = 0; counter < size; counter++) { 39 | timelineRecord = timelineRecords.get(counter); 40 | logger.debug("Type (parent) is: {}", type); 41 | if (timelineRecord != null) { 42 | if (timelineRecord.parentId.equals(id)) { 43 | // It is a child 44 | logger.debug("Child found"); 45 | reorganizedTimelineRecords.add(timelineRecord); 46 | } 47 | } 48 | } 49 | 50 | // Calculate the execution time 51 | if (!(startTime == null || startTime.isEmpty() || finishTime == null || finishTime.isEmpty())) { 52 | Instant start = Instant.parse(startTime); 53 | Instant finish = Instant.parse(finishTime); 54 | Duration res = Duration.between(start, finish); 55 | timeInSeconds = res.getSeconds(); 56 | if (timeInSeconds > 0) 57 | timeInSecondsAsString = String.valueOf(timeInSeconds); 58 | } 59 | timeInSecondsAsString = timeInSecondsAsString + " sec."; 60 | } 61 | 62 | /* 63 | Write all TimelineRecords to the log in a formatted way. 64 | */ 65 | public void dumpTimelineToLog () { 66 | logger.debug("==> Method: TimelineRecord.dumpTimelineToLog"); 67 | 68 | // Only take a subset of types into account 69 | if ("Stage".equals(type) || "Phase".equals(type) || "Job".equals(type) || "Task".equals(type)) { 70 | 71 | String displayedType = type; 72 | boolean logDetails = true; 73 | 74 | if ("Phase".equals(type) && !RunResult.Result.skipped.toString().equals(result)) { 75 | // Use the Phase instead of the Job in case the job was skipped 76 | logDetails = false; 77 | } 78 | 79 | if (logDetails) { 80 | String color = LIGHT_GREEN; 81 | String arrow = ARROW_DOWN; 82 | if (RunResult.Result.failed.toString().equals(result)) 83 | color = LIGHT_RED; 84 | if (RunResult.Result.skipped.toString().equals(result)) { 85 | color = LIGHT_WHITE; 86 | arrow = " "; 87 | } 88 | if (RunResult.Result.canceled.toString().equals(result)) 89 | color = YELLOW; 90 | if (RunResult.Result.partiallySucceeded.toString().equals(result)) 91 | color = YELLOW; 92 | if (RunResult.Result.succeededWithIssues.toString().equals(result)) 93 | color = YELLOW; 94 | 95 | if ("Stage".equals(type)) { 96 | displayedType = "Stage " + arrow + " "; 97 | } 98 | if ("Job".equals(type)) { 99 | displayedType = "Job " + arrow + " "; 100 | } 101 | if ("Phase".equals(type)) { 102 | displayedType = "Job " + arrow + " "; // The type Phase is abstract and not for display purposes 103 | } 104 | if ("Task".equals(type)) { 105 | displayedType = "Task"; 106 | } 107 | String out = String.format("%14s %80s %16s %15s", displayedType, name, timeInSecondsAsString, result); 108 | logger.infoColor(color, out); 109 | } 110 | 111 | int size = reorganizedTimelineRecords.size(); 112 | TimelineRecord timelineRecord; 113 | for (int counter = 0; counter < size; counter++) { 114 | timelineRecord = reorganizedTimelineRecords.get(counter); 115 | if (timelineRecord != null) { 116 | timelineRecord.dumpTimelineToLog(); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionOverrideElement.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | public class ActionOverrideElement implements Action { 9 | private static final Log logger = Log.getLogger(); 10 | 11 | // Defines the identifier of the elements' key (e.g., this is 'name' for both variables and parameters) 12 | private String keyIdentifier = "name"; 13 | 14 | // Defines the identifier of the elements' value (e.g., this is 'value' variables and 'default' for parameters) 15 | private String valueIdentifier = "value"; 16 | private String elementName; // Is the name of the element (e.g., a variable name) 17 | private String elementValue; // The string representation of the new value 18 | 19 | // If 'true' it updates the first occurrence of 'elementName' 20 | // If 'false', it searches for an entry with keyIdentifier and valueIdentifier; only then it updates the next 21 | // occurence of with a specific valueIdentifier 22 | private boolean overrideFirstOccurrence; 23 | 24 | public ActionOverrideElement (String elementName, 25 | String elementValue, 26 | boolean overrideFirstOccurrence) { 27 | this.elementName = elementName; 28 | this.elementValue = elementValue; 29 | this.overrideFirstOccurrence = overrideFirstOccurrence; 30 | } 31 | 32 | public ActionOverrideElement (String elementName, 33 | String elementValue, 34 | String keyIdentifier, 35 | String valueIdentifier, 36 | boolean overrideFirstOccurrence) { 37 | this.elementName = elementName; 38 | this.elementValue = elementValue; 39 | this.keyIdentifier = keyIdentifier; 40 | this.valueIdentifier = valueIdentifier; 41 | this.overrideFirstOccurrence = overrideFirstOccurrence; 42 | } 43 | public void execute (ActionResult actionResult) { 44 | logger.debug("==> Method ActionOverrideElement.execute"); 45 | logger.debug("actionResult.l1: {}", actionResult.l1); 46 | logger.debug("actionResult.l2: {}", actionResult.l2); 47 | logger.debug("actionResult.L3: {}", actionResult.l3); 48 | logger.debug("elementName: {}", elementName); 49 | logger.debug("elementValue: {}", elementValue); 50 | 51 | if (actionResult.l1 == null) { 52 | logger.debug("actionResult.l1 is null; return"); 53 | } 54 | 55 | boolean found = false; 56 | // --------------------------------- Handle the arraylist --------------------------------- 57 | if (actionResult.l1 instanceof ArrayList) { 58 | ArrayList list = (ArrayList) actionResult.l1; 59 | int size = list.size(); 60 | for (int index = 0; index < size; index++) { 61 | if (list.get(index) instanceof Map) { 62 | logger.debug("Array[{}]: ", list.get(index)); 63 | 64 | Map map = (Map) list.get(index); 65 | for (Map.Entry entry : map.entrySet()) { 66 | logger.debug("entry.getKey(): {}", entry.getKey()); 67 | logger.debug("entry.getValue(): {}", entry.getValue()); 68 | 69 | if (overrideFirstOccurrence) { 70 | // The first occurrence of elementName is the one to override 71 | if (elementName.equals(entry.getKey())) { 72 | logger.info("Override elementName \'{}\' with value \'{}\'", elementName, elementValue); 73 | entry.setValue(elementValue); 74 | actionResult.actionExecuted = true; 75 | return; 76 | } 77 | } 78 | else { 79 | if (keyIdentifier.equals(entry.getKey()) && elementName.equals(entry.getValue())) { 80 | // Take the next one to override the value 81 | found = true; 82 | } 83 | } 84 | // Only replace if it has a different value; otherwise continue searching 85 | // This allows to change multiple elements in the same file with the same name 86 | if (found && valueIdentifier.equals(entry.getKey())) { 87 | logger.info("Override elementName \'{}\' with value \'{}\'", elementName, elementValue); 88 | entry.setValue(elementValue); 89 | actionResult.actionExecuted = true; 90 | return; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | // --------------------------------- Handle the map --------------------------------- 97 | if (actionResult.l1 instanceof Map) { 98 | logger.debug("l1 is instance of map..."); 99 | Map map = (Map) actionResult.l1; 100 | String key; 101 | String stringValue; 102 | for (Map.Entry entry : map.entrySet()) { 103 | key = entry.getKey(); 104 | stringValue = entry.getValue().toString(); 105 | logger.debug("Key: {}", key); 106 | logger.debug("Value: {}", stringValue); 107 | 108 | if (overrideFirstOccurrence && elementName.equals(entry.getKey())) { 109 | logger.info("Override elementName \'{}\' with value \'{}\'", elementName, elementValue); 110 | entry.setValue(elementValue); 111 | actionResult.actionExecuted = true; 112 | return; 113 | } 114 | } 115 | } 116 | return; 117 | } 118 | 119 | // This action can be executed if only the appropriate section type is found 120 | public boolean needsSectionIdentifier() { 121 | return false; 122 | } 123 | 124 | // This action is not a custom action 125 | public boolean isCustomAction () { return false; } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/azdo/junit/RunResult.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.junit; 5 | 6 | import azdo.utils.Log; 7 | 8 | import java.util.ArrayList; 9 | 10 | import static azdo.utils.Constants.*; 11 | 12 | /****************************************************************************************** 13 | The result of the pipeline run and its details are stored in RunResult. 14 | *******************************************************************************************/ 15 | public class RunResult { 16 | private static final Log logger = Log.getLogger(); 17 | public String buildId = null; 18 | 19 | public Result result = Result.none; 20 | public Status status = Status.none; 21 | private ArrayList timelineRecords = new ArrayList<>(); 22 | 23 | public static enum Result { 24 | canceled, 25 | failed, 26 | succeeded, 27 | partiallySucceeded, 28 | succeededWithIssues, 29 | skipped, 30 | undetermined, 31 | none 32 | } 33 | 34 | public static enum Status { 35 | all, 36 | cancelling, 37 | completed, 38 | inProgress, 39 | notStarted, 40 | postponed, 41 | timeout, 42 | none 43 | } 44 | 45 | public RunResult() { 46 | } 47 | 48 | public RunResult(String result, String status, String buildId) { 49 | if (result != null) 50 | this.result = Result.valueOf(result); 51 | else 52 | this.result = Result.none; 53 | 54 | if (status != null) 55 | this.status = Status.valueOf(status); 56 | else 57 | this.status = Status.none; 58 | 59 | this.buildId = buildId; 60 | } 61 | 62 | public void addTimelineRecord(TimelineRecord timelineRecord) { 63 | timelineRecords.add(timelineRecord); 64 | } 65 | 66 | public ArrayList getTimelineRecords() { 67 | return timelineRecords; 68 | } 69 | 70 | 71 | /* 72 | The list of TimelineRecords contains information about each phase in the pipeline run. A phase can be of type 73 | "Stage", "Job", "Phase" (which is an abstract representation of a Job), and "Task" (which represents all Steps). 74 | After retrieval of the TimelineRecords, this list is unsorted. The reorganize() method takes care 75 | that the records are sorted and a hierarchy is introduced. This improves readability of the log. 76 | */ 77 | public void reorganize() { 78 | logger.debug("==> Method: RunResult.reorganize"); 79 | 80 | // Sort the list first; this properly displays the order of the phases in the pipeline (in most of the cases) 81 | sort(timelineRecords); 82 | 83 | int size = timelineRecords.size(); 84 | TimelineRecord timelineRecord; 85 | for (int counter = 0; counter < size; counter++) { 86 | timelineRecord = timelineRecords.get(counter); 87 | if (timelineRecord != null) { 88 | timelineRecord.reorganize(timelineRecords); 89 | } 90 | } 91 | } 92 | 93 | /* 94 | Sort the TimelineRecord arraylist 95 | */ 96 | private static void sort(ArrayList list) { 97 | list.sort((o1, o2) 98 | -> o1.startTime.compareTo( 99 | o2.startTime)); 100 | } 101 | 102 | /* 103 | Write all TimelineRecords to the log 104 | */ 105 | public void dumpTimelineToLog() { 106 | logger.debug("==> Method: RunResult.dumpTimelineToLog"); 107 | 108 | logger.info(""); 109 | logger.info(HEADER_FOOTER); 110 | String header = String.format("%14s %80s %16s %15s", "Type", "Name", "Execution time", "Result"); 111 | logger.info(header); 112 | logger.info(HEADER_FOOTER); 113 | int size = timelineRecords.size(); 114 | TimelineRecord timelineRecord; 115 | for (int counter = 0; counter < size; counter++) { 116 | timelineRecord = timelineRecords.get(counter); 117 | if (timelineRecord != null) { 118 | if ("Stage".equals(timelineRecord.type)) { 119 | timelineRecord.dumpTimelineToLog(); 120 | } 121 | } 122 | } 123 | logger.info(HEADER_FOOTER); 124 | } 125 | 126 | 127 | /* 128 | Return a TimelineRecord of a certain type and certain name (this is the identifier (if defined) or the displayValue) 129 | */ 130 | public Result getSectionResultSearchByName(String sectionType, 131 | String name) { 132 | logger.debug("==> Method: RunResult.getSectionResultSearchByName"); 133 | 134 | int size = timelineRecords.size(); 135 | TimelineRecord timelineRecord; 136 | for (int counter = 0; counter < size; counter++) { 137 | timelineRecord = timelineRecords.get(counter); 138 | if (timelineRecord != null) { 139 | if (timelineRecord.type.equals(sectionType)) { 140 | if (timelineRecord.name.equals(name)) { 141 | logger.debug("Found {}", name); 142 | return Result.valueOf(timelineRecord.result); 143 | } 144 | } 145 | } 146 | } 147 | 148 | return Result.none; 149 | } 150 | 151 | public Result getStageResultSearchByName(String name) { 152 | logger.debug("==> Method: RunResult.getStageResultSearchByName"); 153 | return getSectionResultSearchByName("Stage", name); 154 | } 155 | 156 | public Result getJobResultSearchByName(String name) { 157 | logger.debug("==> Method: RunResult.getJobResultSearchByName"); 158 | Result res = Result.none; 159 | res = getSectionResultSearchByName("Job", name); 160 | if (res == Result.none) { 161 | // Not Job found (for example, if the Job was skipped); try the Phase instead 162 | res = getSectionResultSearchByName("Phase", name); 163 | } 164 | 165 | return res; 166 | } 167 | 168 | public Result getStepResultSearchByName(String name) { 169 | logger.debug("==> Method: RunResult.getStepResultSearchByName"); 170 | return getSectionResultSearchByName("Task", name); 171 | } 172 | } -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionOnSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | /****************************************************************************************** 9 | This class is used to perform an action on a section. This section is searched using 10 | 'sectionType' and 'sectionIdentifier'. For example: 11 | Assume, that 'sectionType' has the value "stage", and 'sectionIdentifier' has the 12 | value "mystage". 13 | The stage with the identifier "mystage" is searched in the yaml pipeline. 14 | If found, the stage section is, for example, deleted from the yaml if the action is DELETE_SECTION. 15 | ******************************************************************************************/ 16 | public class ActionOnSection implements Action { 17 | protected static Log logger = Log.getLogger(); 18 | protected ACTION action; // The action on a section 19 | protected String sectionType; // Is "stage", "job", "script" 20 | protected String sectionIdentifier; // Identifier of the section 21 | // newSection is only needed in combination with ACTION_UPDATE and ACTION_INSERT 22 | protected Map newSection; // The new section in YAML 23 | 24 | // insertBefore is only used in combination with action == INSERT 25 | // If 'true', the 'sectionToInsert' YAML string is inserted before the given section. If 'false', 26 | // the 'sectionToInsert' YAML is inserted after the given section. 27 | protected boolean insertBefore = true; 28 | 29 | public ActionOnSection(ACTION action, 30 | String sectionType, 31 | String sectionIdentifier, 32 | Map newSection, 33 | boolean insertBefore) 34 | { 35 | this.action = action; 36 | this.sectionType = sectionType; 37 | this.sectionIdentifier = sectionIdentifier; 38 | this.newSection = newSection; 39 | this.insertBefore = insertBefore; 40 | } 41 | 42 | /****************************************************************************************** 43 | Perform an action on a section. The action properties are sey during creation of the object. 44 | @param actionResult Contains parts of the YAML structure. It is used to search for the 45 | section in the l3 structure. 46 | ******************************************************************************************/ 47 | public void execute (ActionResult actionResult) 48 | { 49 | logger.debug("==> Method ActionDeleteSection.execute"); 50 | logger.debug("actionResult.l1: {}", actionResult.l1); 51 | logger.debug("actionResult.l2: {}", actionResult.l2); 52 | logger.debug("actionResult.l3: {}", actionResult.l3); 53 | logger.debug("sectionType: {}", sectionType); 54 | logger.debug("sectionIdentifier: {}", sectionIdentifier); 55 | logger.debug("newSection: {}", newSection); 56 | logger.debug("insertBefore: {}", insertBefore); 57 | 58 | if (actionResult.l3 == null) { 59 | logger.debug("actionResult.l3 is null; return"); 60 | } 61 | 62 | if (actionResult.l3 instanceof ArrayList) { 63 | logger.debug("l3 is instance of ArrayList"); 64 | 65 | // Run through the elements of the list and remove the section 66 | ArrayList list = (ArrayList) actionResult.l3; 67 | int index; 68 | int size = list.size(); 69 | for (index = 0; index < size; index++) { 70 | if (list.get(index) instanceof Map) { 71 | logger.debug("list.get(index) is instance of ArrayList"); 72 | 73 | Map map = (Map) list.get(index); 74 | for (Map.Entry entry : map.entrySet()) { 75 | 76 | // Check whether the entry has the given key and value 77 | logger.debug("entry.getKey(): {}", entry.getKey()); 78 | logger.debug("entry.getValue(): {}", entry.getValue()); 79 | if (sectionType.equals(entry.getKey()) && sectionIdentifier.equals(entry.getValue())) { 80 | logger.debug("Found section type \'{}\' sectionIdentifier \'{}\'", sectionType, sectionIdentifier); 81 | switch (action) { 82 | case INSERT_SECTION: { 83 | logger.debug("Execute action: INSERT_SECTION"); 84 | if (insertBefore) { 85 | logger.info("Insert a new section before section \'{}\' with identifier \'{}\'", sectionType, sectionIdentifier); 86 | list.add(index, newSection); 87 | actionResult.actionExecuted = true; 88 | } 89 | else { 90 | logger.info("Insert a new section after section \'{}\' with identifier \'{}\'", sectionType, sectionIdentifier); 91 | list.add(index + 1, newSection); 92 | actionResult.actionExecuted = true; 93 | } 94 | return; 95 | } 96 | case UPDATE_SECTION: { 97 | logger.debug("Execute action: UPDATE_SECTION"); 98 | logger.info("Replace section type \'{}\' with sectionIdentifier \'{}\'", sectionType, sectionIdentifier); 99 | list.remove(index); 100 | list.add(index, newSection); 101 | actionResult.actionExecuted = true; 102 | return; 103 | } 104 | case DELETE_SECTION: { 105 | logger.debug("Execute action: DELETE_SECTION"); 106 | logger.info("Skip section type \'{}\' with sectionIdentifier \'{}\'", sectionType, sectionIdentifier); 107 | list.remove(index); 108 | actionResult.actionExecuted = true; 109 | return; 110 | } 111 | default: { 112 | logger.warn("Action {} is not supported by ActionOnSectionByProperty", action); 113 | actionResult.actionExecuted = true; 114 | return; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | return; 123 | } 124 | 125 | // This action can only be executed if the section type and section identification are found in the YAML file 126 | public boolean needsSectionIdentifier() { 127 | return true; 128 | } 129 | 130 | // This action is not a custom action 131 | public boolean isCustomAction () { return false; } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionInsertLineInInnerSection.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Map; 8 | 9 | /****************************************************************************************** 10 | This class is typically used to insert a line to the beginning of a script, although 11 | it is made generic, and it can be also used fore other section types. 12 | It searches a section using a 'property', with a 'propertyValue', for example, 13 | 'property' == "displayName", and 'propertyValue' == "Deploy step". 14 | If the section is found, a new (script) line is added to the beginning of the section. 15 | For example: 16 | {@code 17 | - task: PowerShell@2 18 | inputs: 19 | targetType: 'inline' 20 | script: | 21 | echo "This is the first line" 22 | } 23 | and the ActionInsertLineInInnerSection.execute() method is called, the result becomes: 24 | {@code 25 | - task: PowerShell@2 26 | inputs: 27 | targetType: 'inline' 28 | script: | 29 | echo "And now this is the first line" 30 | echo "This is the first line" 31 | } 32 | 33 | This action differs from ActionInsertLineInSection. The ActionInsertLineInInnerSection 34 | class searches for a certain section within another section. 35 | ******************************************************************************************/ 36 | public class ActionInsertLineInInnerSection implements Action { 37 | private static final Log logger = Log.getLogger(); 38 | private String sectionType; // Refers to the main section to be found. "Powershell@2", for example. 39 | private String property; // The property of the section, for example "displayName"; searching is based on this property. 40 | private String propertyValue; // The value of the property 41 | private String innerSectionProperty; // The property of the inner section, for example "inputs" 42 | private String innerSectionType; // The type of the inner section where to add the new line, for example "script" 43 | private String newLine; // The line to add to the most inner section of the main section; this is the first line 44 | 45 | public ActionInsertLineInInnerSection(String sectionType, 46 | String property, 47 | String propertyValue, 48 | String innerSectionProperty, 49 | String innerSectionType, 50 | String newLine) { 51 | this.sectionType = sectionType; 52 | this.property = property; 53 | this.propertyValue = propertyValue; 54 | this.innerSectionProperty = innerSectionProperty; 55 | this.innerSectionType = innerSectionType; 56 | this.newLine = newLine; 57 | } 58 | 59 | public void execute (ActionResult actionResult) { 60 | logger.debug("==> Method ActionInsertLineInInnerSection.execute"); 61 | logger.debug("actionResult.l1: {}", actionResult.l1); 62 | logger.debug("actionResult.l2: {}", actionResult.l2); 63 | logger.debug("actionResult.L3: {}", actionResult.l3); 64 | logger.debug("sectionType: {}", sectionType); 65 | logger.debug("property: {}", property); 66 | logger.debug("propertyValue: {}", propertyValue); 67 | logger.debug("innerSectionProperty: {}", innerSectionProperty); 68 | logger.debug("innerSectionType: {}", innerSectionType); 69 | logger.debug("newLine: {}", newLine); 70 | 71 | if (actionResult.l3 == null) { 72 | logger.debug("actionResult.l3 is null; return"); 73 | } 74 | 75 | boolean foundType = false; 76 | boolean foundProperty = false; 77 | boolean foundinnerSection = false; 78 | Object innerSection = null; 79 | if (actionResult.l3 instanceof ArrayList) { 80 | 81 | // Run through the elements of the list and insert the section 82 | ArrayList list = (ArrayList) actionResult.l3; 83 | int index; 84 | int size = list.size(); 85 | for (index = 0; index < size; index++) { 86 | if (list.get(index) instanceof Map) { 87 | 88 | Map map = (Map) list.get(index); 89 | for (Map.Entry entry : map.entrySet()) { 90 | 91 | logger.debug("entry.getKey(): {}", entry.getKey()); 92 | logger.debug("entry.getValue(): {}", entry.getValue()); 93 | if (sectionType.equals(entry.getKey())) { 94 | // Found the right type 95 | logger.debug("Found the right type: {}", sectionType); 96 | foundType = true; 97 | } 98 | if (property.equals(entry.getKey()) && propertyValue.equals(entry.getValue()) && foundType) { 99 | // Found the right property with the correct value 100 | logger.debug("Add new line to step with property \'{}\': \'{}\'", property, propertyValue); 101 | foundProperty = true; 102 | } 103 | if (innerSectionProperty.equals(entry.getKey())) { 104 | // The first element of the innerSectionPath equals the property of this section 105 | // For example, the "inputs" property of a "Powershel@2" task. 106 | logger.debug("Found inner section property: {}", innerSectionProperty); 107 | foundinnerSection = true; 108 | innerSection = entry.getValue(); 109 | } 110 | if (foundType && foundProperty && foundinnerSection) { 111 | innerSection(innerSectionType, innerSection); 112 | actionResult.actionExecuted = true; // Whether the line was added or not, this is the right section 113 | } 114 | } 115 | } 116 | foundType = false; 117 | } 118 | } 119 | return; 120 | } 121 | 122 | private void innerSection (String innerSectionType, Object innerSection) { 123 | if (innerSection == null) 124 | logger.debug("innerSection is null"); 125 | 126 | // Only a Map section is assumed valid 127 | if (innerSection instanceof Map) { 128 | logger.debug("innerSection is a Map: {}", innerSection); 129 | 130 | Map innerMap = (Map) innerSection; 131 | for (Map.Entry innerEntry : innerMap.entrySet()) { 132 | logger.debug("Inner section entry.getKey(): {}", innerEntry.getKey()); 133 | logger.debug("Inner section entry.getValue(): {}", innerEntry.getValue()); 134 | if (innerSectionType.equals(innerEntry.getKey())) { 135 | // Found the type; add the line 136 | logger.debug("Found the type; add the line for inner section type \'{}\'", innerSectionType); 137 | String s = (String)innerMap.get(innerSectionType); 138 | logger.debug("String: {}", s); 139 | s = newLine + s; 140 | logger.debug("String: {}", s); 141 | innerMap.put(innerSectionType, s); 142 | logger.debug("Map: {}", innerMap); 143 | } 144 | } 145 | } 146 | } 147 | 148 | // This action can be executed if only the appropriate section type is found 149 | public boolean needsSectionIdentifier() { 150 | return false; 151 | } 152 | 153 | // This action is not a custom action 154 | public boolean isCustomAction () { return false; } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/azdo/action/ActionOnSectionByProperty.java: -------------------------------------------------------------------------------- 1 | package azdo.action; 2 | 3 | import azdo.utils.Log; 4 | import azdo.yaml.ActionResult; 5 | import java.util.ArrayList; 6 | import java.util.Map; 7 | 8 | /****************************************************************************************** 9 | This class is used to perform an action on a section. This section is searched using 10 | 'sectionType' and 'property'. For example: 11 | Assume, that 'sectionType' has the value "script", 'property' has the value "displayName", 12 | and 'propertyValue' has the value "Deploy task". 13 | The script with the displayName "Deploy task" is searched in the yaml pipeline. 14 | If found, the step section is, for example, deleted from the yaml if the action is DELETE_SECTION. 15 | ******************************************************************************************/ 16 | public class ActionOnSectionByProperty implements Action { 17 | 18 | protected static Log logger = Log.getLogger(); 19 | protected ACTION action; // The action on a section 20 | protected String sectionType; // Is "job", for example 21 | protected String property; // The property of the section, for example "displayName" 22 | protected String propertyValue; // The value of the property 23 | 24 | // newSection is only needed in combination with ACTION_UPDATE and ACTION_INSERT 25 | protected Map newSection; // The new section in YAML 26 | 27 | // insertBefore is only used in combination with action == INSERT 28 | // If 'true', the 'sectionToInsert' YAML string is inserted before the given section. If 'false', 29 | // the 'sectionToInsert' YAML is inserted after the given section. 30 | protected boolean insertBefore = true; 31 | public ActionOnSectionByProperty(ACTION action, 32 | String sectionType, 33 | String property, 34 | String propertyValue, 35 | Map newSection, 36 | boolean insertBefore) { 37 | this.action = action; 38 | this.sectionType = sectionType; 39 | this.property = property; 40 | this.propertyValue = propertyValue; 41 | this.newSection = newSection; 42 | this.insertBefore = insertBefore; 43 | } 44 | 45 | /****************************************************************************************** 46 | Perform an action on a section. The action properties are sey during creation of the object. 47 | @param actionResult Contains parts of the YAML structure. It is used to search for the 48 | section in the l3 structure. 49 | ******************************************************************************************/ 50 | public void execute (ActionResult actionResult) { 51 | logger.debug("==> Method ActionOnSectionByProperty.execute"); 52 | logger.debug("actionResult.l1: {}", actionResult.l1); 53 | logger.debug("actionResult.l2: {}", actionResult.l2); 54 | logger.debug("actionResult.L3: {}", actionResult.l3); 55 | logger.debug("action: {}", action); 56 | logger.debug("sectionType: {}", sectionType); 57 | logger.debug("property: {}", property); 58 | logger.debug("propertyValue: {}", propertyValue); 59 | logger.debug("newSection: {}", newSection); 60 | logger.debug("insertBefore: {}", insertBefore); 61 | 62 | if (actionResult.l3 == null) { 63 | logger.debug("actionResult.l3 is null; return"); 64 | } 65 | 66 | boolean foundType = false; 67 | if (actionResult.l3 instanceof ArrayList) { 68 | logger.debug("l3 is instance of ArrayList"); 69 | 70 | // Run through the elements of the list and update the section 71 | ArrayList list = (ArrayList) actionResult.l3; 72 | int index; 73 | int size = list.size(); 74 | for (index = 0; index < size; index++) { 75 | if (list.get(index) instanceof Map) { 76 | 77 | Map map = (Map) list.get(index); 78 | for (Map.Entry entry : map.entrySet()) { 79 | 80 | logger.debug("entry.getKey(): {}", entry.getKey()); 81 | logger.debug("entry.getValue(): {}", entry.getValue()); 82 | if (sectionType.equals(entry.getKey())) { 83 | // Found the right type 84 | logger.debug("Found the right type: {}", sectionType); 85 | foundType = true; 86 | } 87 | 88 | // If the section has a certain property, with a certain value, determine whether there is an action associated with it 89 | if (property.equals(entry.getKey()) && propertyValue.equals(entry.getValue()) && foundType) { 90 | // Found the right property with the correct value 91 | logger.debug("Found section type \'{}\' with property \'{}\': \'{}\'", sectionType, property, propertyValue); 92 | switch (action) { 93 | case INSERT_SECTION: { 94 | logger.debug("Execute action: INSERT_SECTION"); 95 | if (insertBefore) { 96 | logger.info("Insert a new section before section \'{}\' with property \'{}\': \'{}\'", sectionType, property, propertyValue); 97 | list.add(index, newSection); 98 | actionResult.actionExecuted = true; 99 | } 100 | else { 101 | logger.info("Insert a new section after section \'{}\' with property \'{}\': \'{}\'", sectionType, property, propertyValue); 102 | list.add(index + 1, newSection); 103 | actionResult.actionExecuted = true; 104 | } 105 | return; 106 | } 107 | case UPDATE_SECTION: { 108 | logger.debug("Execute action: UPDATE_SECTION"); 109 | logger.info("Update section of type \'{}\' with property \'{}\': \'{}\'", sectionType, property, propertyValue); 110 | list.remove(index); 111 | list.add(index, newSection); 112 | actionResult.actionExecuted = true; 113 | return; 114 | } 115 | case DELETE_SECTION: { 116 | logger.debug("Execute action: DELETE_SECTION"); 117 | logger.info("Skip section of type \'{}\' with property \'{}\': \'{}\'", sectionType, property, propertyValue); 118 | list.remove(index); 119 | actionResult.actionExecuted = true; 120 | return; 121 | } 122 | default: { 123 | logger.warn("Action {} is not supported by ActionOnSectionByProperty", action); 124 | actionResult.actionExecuted = true; 125 | return; 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | foundType = false; 133 | } 134 | } 135 | return; 136 | } 137 | 138 | // This action can be executed if only the appropriate section type is found 139 | public boolean needsSectionIdentifier() { 140 | return false; 141 | } 142 | 143 | // This action is not a custom action 144 | public boolean isCustomAction () { return false; } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/azdo/yaml/YamlTemplate.java: -------------------------------------------------------------------------------- 1 | package azdo.yaml; 2 | 3 | import azdo.utils.Log; 4 | import azdo.utils.Utils; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.util.ArrayList; 8 | 9 | /****************************************************************************************** 10 | A YamlTemplate is a specific YamlDocument and represents a template YAML file. 11 | A template can be invoked by the main pipeline or invoked by other templates in the same 12 | or in other repositories. 13 | *******************************************************************************************/ 14 | public class YamlTemplate extends YamlDocument{ 15 | public enum InternalOrExternalTemplate { 16 | INTERNAL, 17 | EXTERNAL 18 | } 19 | 20 | private static final Log logger = Log.getLogger(); 21 | private InternalOrExternalTemplate internalOrExternalTemplate = InternalOrExternalTemplate.EXTERNAL; 22 | //private String templateName; // The template name as defined in the pipeline (without the @ postfix) 23 | 24 | public YamlTemplate(String templateName, 25 | String sourcePath, 26 | String targetPath, 27 | String sourceBasePathExternal, 28 | String targetBasePathExternal, 29 | String sourceRepositoryName, 30 | String targetRepositoryName, 31 | String parentAlias, 32 | ArrayList repositoryList, 33 | boolean includeExternalTemplates, 34 | boolean continueOnError){ 35 | logger.debug("==> Object: YamlTemplate"); 36 | logger.debug("templateName: {}", templateName); 37 | logger.debug("sourcePath: {}", sourcePath); 38 | logger.debug("targetPath: {}", targetPath); 39 | logger.debug("sourceBasePathExternal: {}", sourceBasePathExternal); 40 | logger.debug("targetBasePathExternal: {}", targetBasePathExternal); 41 | logger.debug("sourceRepositoryName: {}", sourceRepositoryName); 42 | logger.debug("targetRepositoryName: {}", targetRepositoryName); 43 | logger.debug("parentAlias: {}", parentAlias); 44 | logger.debug("includeExternalTemplates: {}", includeExternalTemplates); 45 | logger.debug("continueOnError: {}", continueOnError); 46 | 47 | this.templateName = templateName; 48 | this.rootInputFile = templateName; 49 | this.sourcePath = sourcePath; 50 | this.targetPath = targetPath; 51 | this.sourceRepositoryName = sourceRepositoryName; 52 | this.targetRepositoryName = targetRepositoryName; 53 | 54 | if (templateName != null && templateName.contains("@")) { 55 | // It is an EXTERNAL template (defined in another repository) 56 | logger.debug("{} is an EXTERNAL template referred with an @-character", templateName); 57 | 58 | // Only handle external templates if they are allowed to be manipulated; otherwise i 59 | if (includeExternalTemplates) { 60 | handleExternalTemplate(templateName, 61 | sourceBasePathExternal, 62 | targetBasePathExternal, 63 | parentAlias, 64 | repositoryList, 65 | true); 66 | } 67 | } 68 | else { 69 | // It can still be an EXTERNAL template, but it is referred as a local template in the external repository (not using the @) 70 | // This also means it does not reside in the main repository; check this! 71 | sourceInputFile = Utils.findFullQualifiedFileNameInDirectory(sourcePath, templateName); 72 | if (sourceInputFile == null) { 73 | // It is an EXTERNAL template 74 | logger.debug("{} is an EXTERNAL template but referred as a local file in another external template", templateName); 75 | handleExternalTemplate(templateName, 76 | sourceBasePathExternal, 77 | targetBasePathExternal, 78 | parentAlias, 79 | repositoryList, 80 | false); 81 | } 82 | else { 83 | // It is an INTERNAL template (defined in the same repository as the main pipeline file) 84 | logger.debug("{} is an INTERNAL template", templateName); 85 | handleInternalTemplate(templateName, sourcePath, continueOnError); 86 | } 87 | } 88 | sourceInputFile = Utils.fixPath(sourceInputFile); 89 | targetOutputFile = Utils.fixPath(targetOutputFile); 90 | logger.debug("sourceInputFile: {}", sourceInputFile); 91 | logger.debug("targetOutputFile: {}", targetOutputFile); 92 | } 93 | 94 | private void handleExternalTemplate (String templateName, 95 | String sourceBasePathExternal, 96 | String targetBasePathExternal, 97 | String parentAlias, 98 | ArrayList repositoryList, 99 | boolean withAt) { 100 | // It is an EXTERNAL template (defined in another repository) 101 | logger.debug("==> Method: YamlTemplate.handleExternalTemplate"); 102 | logger.debug("templateName: {}", templateName); 103 | logger.debug("sourceBasePathExternal: {}", sourceBasePathExternal); 104 | logger.debug("targetBasePathExternal: {}", targetBasePathExternal); 105 | logger.debug("parentAlias: {}", parentAlias); 106 | 107 | internalOrExternalTemplate = InternalOrExternalTemplate.EXTERNAL; 108 | if (withAt) { 109 | // The template name contains an @ 110 | repositoryAlias = templateName.substring(templateName.lastIndexOf('@') + 1); 111 | templateName = templateName.substring(0, templateName.lastIndexOf('@')); 112 | } 113 | else { 114 | // The template name does not contain an @ 115 | repositoryAlias = parentAlias; // There is no alias, so use the one of the parent 116 | } 117 | 118 | logger.debug("repositoryAlias: {}", repositoryAlias); 119 | RepositoryResource repositoryResource = findRepositoryResourceByAlias(repositoryList, repositoryAlias); 120 | if (repositoryResource != null) { 121 | logger.debug("repositoryResource.name: {}", repositoryResource.name); 122 | String temp = Utils.fixPath(templateName); 123 | sourceInputFile = sourceBasePathExternal + "/" + repositoryResource.name + RepositoryResource.LOCAL_SOURCE_POSTFIX + "/" + temp; 124 | targetOutputFile = targetBasePathExternal + "/" + repositoryResource.name + "/" + temp; 125 | logger.debug("{} is an EXTERNAL template and resides in repository: {}", templateName, repositoryResource.name); 126 | } 127 | else { 128 | logger.warn("repositoryResource is null; this may be a false-positive"); 129 | } 130 | } 131 | 132 | private void handleInternalTemplate (String templateName, 133 | String sourcePath, 134 | boolean continueOnError) { 135 | logger.debug("==> Method: YamlTemplate.handleInternalTemplate"); 136 | logger.debug("templateName: {}", templateName); 137 | logger.debug("sourcePath: {}", sourcePath); 138 | logger.debug("continueOnError: {}", continueOnError); 139 | 140 | // An internal template resides in the same repository as the main pipeline file 141 | sourceInputFile = Utils.findFullQualifiedFileNameInDirectory(sourcePath, templateName); 142 | if (sourceInputFile != null) { 143 | // Some juggling with the path is needed, because templates can be assigned by relative paths 144 | // This is particular annoying if a template with a relative path includes another templates with 145 | // a relative path 146 | Path p = Path.of(sourceInputFile); 147 | String directory = p.getParent().toString(); 148 | logger.debug("directory: {}", directory); 149 | Path f = Path.of(templateName); 150 | String fileName = f.getFileName().toString(); 151 | logger.debug("fileName: {}", fileName); 152 | 153 | // Derive the output file; it is the same as the source file, but with a different repository name 154 | String tempTargetOutputFile = directory + "/" + fileName; // This is not yet the final name 155 | Path sourceInputFilePath = Paths.get(sourceInputFile).normalize(); 156 | Path targetInputFilePath = Paths.get(tempTargetOutputFile).normalize(); 157 | if (targetInputFilePath.equals(sourceInputFilePath)) 158 | { 159 | // Replace the repository name (replace the source repository with the target repository name) 160 | // Only replace the first occurrence 161 | // TODO: Maybe better to replace the sourcePath with the targetPath? 162 | logger.debug("Replace the repository name"); 163 | logger.debug("sourceInputFilePath is: {}", sourceInputFile); 164 | logger.debug("tempTargetOutputFile is: {}", tempTargetOutputFile); 165 | targetOutputFile = tempTargetOutputFile.replace(sourceRepositoryName, targetRepositoryName); 166 | } 167 | else 168 | targetOutputFile = tempTargetOutputFile; 169 | 170 | // Validate the internal pipeline file before any other action 171 | // If it is not valid, the test may fail 172 | Utils.validatePipelineFile(sourceInputFile, continueOnError); 173 | } 174 | } 175 | 176 | private RepositoryResource findRepositoryResourceByAlias (ArrayList repositoryList, String alias) { 177 | logger.debug("==> Method: YamlTemplate.findRepositoryResourceByAlias"); 178 | logger.debug("alias: {}", alias); 179 | 180 | if (repositoryList == null) 181 | return null; 182 | logger.debug("repositoryList: {}", repositoryList.toString()); 183 | 184 | RepositoryResource repositoryResource; 185 | int size = repositoryList.size(); 186 | for (int i = 0; i < size; i++) { 187 | repositoryResource = repositoryList.get(i); 188 | if (alias.equals(repositoryResource.repository)) 189 | return repositoryResource; 190 | } 191 | return null; 192 | } 193 | 194 | public InternalOrExternalTemplate getInternalOrExternalTemplate() { 195 | return internalOrExternalTemplate; 196 | } 197 | 198 | public String getTemplateName() { 199 | return templateName; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/azdo/utils/GitUtils.java: -------------------------------------------------------------------------------- 1 | package azdo.utils; 2 | 3 | import azdo.yaml.RepositoryResource; 4 | import org.eclipse.jgit.api.AddCommand; 5 | import org.eclipse.jgit.api.Git; 6 | import org.eclipse.jgit.api.ListBranchCommand; 7 | import org.eclipse.jgit.lib.Ref; 8 | import org.eclipse.jgit.transport.CredentialsProvider; 9 | import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /****************************************************************************************** 16 | Contains methods to interact with Git, mainly using the JGit library. 17 | *******************************************************************************************/ 18 | public class GitUtils { 19 | private static final Log logger = Log.getLogger(); 20 | private static Git git = null; 21 | public static final String BRANCH_MASTER = "master"; 22 | 23 | // Clone an Azure DevOps repo to local and initialize 24 | public static Git cloneAzdoToLocal (String targetPath, 25 | String repositoryName, 26 | String azdoUser, 27 | String azdoPat, 28 | String organization, 29 | String project) { 30 | logger.debug("==> Method: GitUtils.cloneAzdoToLocal"); 31 | targetPath = Utils.fixPath(targetPath); 32 | logger.debug("targetPath: {}", targetPath); 33 | logger.debug("repositoryName: {}", repositoryName); 34 | logger.debug("organization: {}", organization); 35 | logger.debug("project: {}", project); 36 | 37 | git = null; 38 | 39 | // Delete the target path 40 | //Utils.deleteDirectory(targetPath); 41 | 42 | // Create the target path if not existing 43 | Utils.createDirectory(targetPath); 44 | 45 | // Create the credentials provider 46 | CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(azdoUser, azdoPat); 47 | 48 | // Create the uri 49 | String azdoBaseUrl = "https://dev.azure.com/" + organization; 50 | String uriSourceRepository = azdoBaseUrl + "/" + project + "/_git/" + repositoryName; 51 | uriSourceRepository = Utils.encodePath (uriSourceRepository); 52 | logger.debug("uriSourceRepository: {}", uriSourceRepository); 53 | 54 | // Clone the repo 55 | try { 56 | logger.debug("git.clone"); 57 | git = Git.cloneRepository() 58 | .setURI(uriSourceRepository) 59 | .setCloneAllBranches(true) 60 | .setCredentialsProvider(credentialsProvider) 61 | .setDirectory(new File(targetPath)) 62 | .call(); 63 | Utils.wait(1000); 64 | //git.close(); 65 | } 66 | catch (Exception e) { 67 | logger.debug("Cannot clone {}, but just proceed, {}", repositoryName, e.getMessage()); 68 | } 69 | 70 | return git; 71 | } 72 | 73 | // Clone a GitHub repo to local and initialize 74 | public static Git cloneGitHubToLocal (String targetPath, 75 | String repositoryName, 76 | String project) { 77 | logger.debug("==> Method: GitUtils.cloneGitHubToLocal"); 78 | logger.debug("targetPath: {}", targetPath); 79 | logger.debug("repositoryName: {}", repositoryName); 80 | logger.debug("project: {}", project); 81 | 82 | git = null; 83 | 84 | // Delete the target path 85 | //Utils.deleteDirectory(targetPath); 86 | 87 | // Create the target path if not existing 88 | Utils.createDirectory(targetPath); 89 | 90 | // Create the uri 91 | String baseUrl = "https://github.com"; 92 | String uriSourceRepository = baseUrl + "/" + project + "/" + repositoryName; 93 | uriSourceRepository = Utils.encodePath (uriSourceRepository); 94 | logger.debug("uriSourceRepository: {}", uriSourceRepository); 95 | 96 | // Clone the repo 97 | try { 98 | logger.debug("git.clone"); 99 | git = Git.cloneRepository() 100 | .setURI(uriSourceRepository) 101 | .setCloneAllBranches(true) 102 | .setDirectory(new File(targetPath)) 103 | .call(); 104 | Utils.wait(1000); 105 | } 106 | catch (Exception e) { 107 | logger.debug("Cannot clone {}, but just proceed, {}", repositoryName, e.getMessage()); 108 | } 109 | 110 | return git; 111 | } 112 | 113 | public static boolean containsBranch (Git git, 114 | String branchName) { 115 | logger.debug("==> Method: GitUtils.containsBranch"); 116 | logger.debug("branchName: {}", branchName); 117 | 118 | if (git == null) { 119 | logger.debug("Cannot continue; git is null"); 120 | return false; 121 | } 122 | 123 | try { 124 | ListBranchCommand command = git.branchList(); 125 | command.setListMode(ListBranchCommand.ListMode.ALL); 126 | List branches = command.call(); 127 | for (Ref ref : branches) { 128 | if (ref.getName().endsWith("/" + branchName)) { 129 | logger.debug("Branch {} exists", branchName); 130 | return true; 131 | } 132 | } 133 | } 134 | catch (Exception e) { 135 | logger.debug("Cannot check whether the branch is remote"); 136 | } 137 | 138 | logger.debug("Branch {} does not exists", branchName); 139 | return false; 140 | } 141 | 142 | public static void commitAndPush (Git git, 143 | String azdoUser, 144 | String azdoPat, 145 | ArrayList commitPatternList, 146 | RepositoryResource metadataRepository, 147 | boolean continueOnError) { 148 | logger.debug("==> Method: GitUtils.commitAndPush"); 149 | // Note, that the 'metadataRepository' is only used as meta-data for logging 150 | 151 | if (git == null) { 152 | logger.debug("Cannot continue; git is null"); 153 | return; 154 | } 155 | 156 | logger.debug("Repository {}", git.getRepository().getRemoteNames().toString()); 157 | 158 | // Push the local repo to remote 159 | try { 160 | logger.debug("git.add"); 161 | git.add() 162 | .addFilepattern(".") 163 | .call(); 164 | 165 | // Stage all changed files, including deleted files 166 | int size = commitPatternList.size(); 167 | AddCommand command = git.add(); 168 | for (int i = 0; i < size; i++) { 169 | command = command.addFilepattern(commitPatternList.get(i)); 170 | logger.debug("Pattern: {}", commitPatternList.get(i)); 171 | } 172 | command.call(); 173 | 174 | logger.debug("git.commit"); 175 | git.commit() 176 | .setAll(true) 177 | .setAuthor(azdoUser, "") 178 | .setCommitter(azdoUser, "") 179 | .setMessage("Init repo") 180 | .call(); 181 | Utils.wait(1000); 182 | 183 | // Create the credentials provider 184 | CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(azdoUser, azdoPat); 185 | 186 | logger.debug("git.push"); 187 | git.push() 188 | .setPushAll() 189 | .setCredentialsProvider(credentialsProvider) 190 | .setForce(true) 191 | .call(); 192 | Utils.wait(1000); 193 | } 194 | 195 | catch (Exception e) { 196 | if (continueOnError) { 197 | logger.debug("Exception pushing to repo: {}", e.getMessage()); 198 | } 199 | else { 200 | logger.error("Exception pushing to repo: {}", e.getMessage()); 201 | logger.error("You may need to delete the local clone of {}", metadataRepository.repository); 202 | System. exit(1); 203 | } 204 | } 205 | } 206 | 207 | public static Git checkout (Git git, 208 | String targetPath, 209 | String branchName, 210 | boolean createRemoteBranch) { 211 | logger.debug("==> Method: GitUtils.checkout"); 212 | logger.debug("targetPath: {}", targetPath); 213 | logger.debug("branchName: {}", branchName); 214 | logger.debug("createRemoteBranch: {}", createRemoteBranch); 215 | 216 | // If git object is invalid recreate it again 217 | if (git == null) { 218 | targetPath = Utils.fixPath(targetPath); 219 | git = createGit(targetPath); 220 | Utils.wait (1000); 221 | } 222 | 223 | // Perform a checkout 224 | if (git != null) { 225 | try { 226 | logger.debug("git.checkout"); 227 | checkout(git, branchName, createRemoteBranch); 228 | Utils.wait (1000); 229 | } catch (Exception e) { 230 | logger.debug("Exception occurred. Cannot checkout {}; {}", branchName, e.getMessage()); 231 | // try { 232 | // logger.debug("Trying remote"); 233 | // branchName = "origin/" + branchName; 234 | // checkout(git, branchName, createRemoteBranch); 235 | // Utils.wait(1000); 236 | // } catch (Exception eRemote) { 237 | // logger.debug("Exception occurred. Cannot checkout {}; continue: {}", branchName, eRemote.getMessage()); 238 | // } 239 | } 240 | } 241 | return git; 242 | } 243 | 244 | private static Git checkout (Git git, 245 | String branchName, 246 | boolean createRemoteBranch) throws Exception { 247 | logger.debug("==> Method: GitUtils.checkout"); 248 | logger.debug("branchName: {}", branchName); 249 | logger.debug("createRemoteBranch: {}", createRemoteBranch); 250 | 251 | if (git == null) { 252 | logger.debug("Git object is null"); 253 | return null; 254 | } 255 | 256 | logger.debug("git.checkout"); 257 | git.checkout() 258 | .setForced(true) 259 | .setCreateBranch(createRemoteBranch) 260 | .setName(branchName) 261 | .call(); 262 | Utils.wait(1000); 263 | 264 | return git; 265 | } 266 | 267 | public static Git createGit (String targetPath) { 268 | logger.debug("==> Method: GitUtils.createGit"); 269 | logger.debug("targetPath: {}", targetPath); 270 | 271 | // If git object is invalid recreate it again 272 | try { 273 | logger.debug("Recreate git object"); 274 | targetPath = Utils.fixPath(targetPath); 275 | File f = new File(targetPath); 276 | git = Git.open(f); 277 | Utils.wait(1000); 278 | } 279 | catch (IOException e) { 280 | logger.debug("Cannot create a Git object: {}", e.getMessage()); 281 | return null; 282 | } 283 | 284 | return git; 285 | } 286 | 287 | // TODO: Refs can also contain tags and remotes 288 | public static String resolveBranchNameFromRef (String ref) { 289 | logger.debug("==> Method: GitUtils.resolveBranchFromRef"); 290 | logger.debug("ref: {}", ref); 291 | String branchName = ""; 292 | if (ref.contains("heads")) 293 | branchName = ref.substring(ref.lastIndexOf('/') + 1); 294 | 295 | return branchName; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /pipeline/junit-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Henry van Merode. 2 | # Licensed under the MIT License. 3 | 4 | # This is the primary pipeline to build and deploy the junit-pipeline.jar 5 | 6 | name: $(Date:yyyyMMdd)$(Rev:.r) 7 | 8 | trigger: none 9 | 10 | variables: 11 | - group: junit-pipeline-group 12 | - name: MAVEN_CACHE_FOLDER 13 | value: $(Pipeline.Workspace)/.m2/repository 14 | 15 | stages: 16 | ################################################################################################################# 17 | # STAGE: Validate entry criteria 18 | # Check whether variables are preconfigured 19 | ################################################################################################################# 20 | - stage: Validate_entry_criteria 21 | displayName: 'Validate entry criteria' 22 | condition: always() 23 | jobs: 24 | - job: Validate_entry_criteria 25 | pool: 26 | vmImage: 'ubuntu-latest' 27 | workspace: 28 | clean: all 29 | continueOnError: "false" 30 | 31 | steps: 32 | # Validate whether pipeline variables (stored in Variable Groups) are configured 33 | # The pipeline stops is a mandatory variable is not configured 34 | - script: | 35 | echo 'Variable releaseBuild is not configured' 36 | exit 1 37 | displayName: 'Exit if the releaseBuild variable is not configured' 38 | condition: eq(variables['releaseBuild'], '') 39 | 40 | ################################################################################################################# 41 | # STAGE: Execute build + Package artifact + Publish artifact 42 | # Performs Maven snapshot- and release build 43 | ################################################################################################################# 44 | - stage: Execute_build_publish 45 | displayName: 'Execute build, and Package/Publish artifact' 46 | dependsOn: Validate_entry_criteria 47 | condition: succeeded() 48 | jobs: 49 | - job: Execute_build_publish 50 | pool: 51 | vmImage: 'ubuntu-latest' 52 | demands: maven 53 | workspace: 54 | clean: all 55 | continueOnError: "false" 56 | 57 | steps: 58 | # Add pipeline caching; force invalidating the cache 59 | - task: Cache@2 60 | displayName: 'Cache Maven local repo' 61 | inputs: 62 | key: 'maven | "$(Agent.OS)" | **/pom.xml' 63 | path: $(MAVEN_CACHE_FOLDER) 64 | 65 | # Perform Maven build 66 | - task: Maven@3 67 | displayName: 'Build artifacts' 68 | inputs: 69 | goals: 'clean package -U' 70 | javaHomeOption: 'JDKVersion' 71 | jdkVersionOption: '1.17' 72 | jdkArchitectureOption: 'x64' 73 | sonarQubeRunAnalysis: false 74 | options: '-B -DscmBranch=$(Build.SourceBranchName) -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)' 75 | testResultsFiles: '**/TEST-*.xml' 76 | publishJUnitResults: true 77 | 78 | - script: | 79 | cd ${SYSTEM_DEFAULTWORKINGDIRECTORY} 80 | 81 | # Retrieve version from pom.xml 82 | sudo apt-get install -y libxml2-utils 83 | v=`xmllint --xpath '/*[local-name()="project"]/*[local-name()="version"]/text()' ./pom.xml` 84 | echo "Version from pom.xml: $v" 85 | echo "##vso[task.setvariable variable=version;]$v" 86 | displayName: 'Derive version from pom.xml' 87 | 88 | - script: | 89 | # Create a keypair to sign the jars 90 | sudo apt-get --assume-yes install gnupg 91 | gpg --full-gen-key --batch <(echo "Key-Type: 1"; \ 92 | echo "Key-Length: 4096"; \ 93 | echo "Subkey-Type: 1"; \ 94 | echo "Subkey-Length: 4096"; \ 95 | echo "Expire-Date: 0"; \ 96 | echo "Name-Real: Root Superuser"; \ 97 | echo "Name-Email: hvmerode@io.github.com"; \ 98 | echo "%no-protection"; ) 99 | gpg --list-secret-keys 100 | 101 | cd ${SYSTEM_DEFAULTWORKINGDIRECTORY} 102 | 103 | # Export keys 104 | gpg --output public.pgp --armor --export hvmerode@io.github.com 105 | gpg --output private.pgp --armor --export-secret-key hvmerode@io.github.com 106 | keyIds=`gpg --list-packets public.pgp | awk '/keyid:/{ print $2 }'` 107 | firstKeyId=${keyIds%$'\n'*} 108 | echo "First key-id: $firstKeyId" 109 | secondKeyId=${keyIds##*$'\n'} 110 | echo "Second key-id: $secondKeyId" 111 | 112 | echo "Export keyId and sub-KeyId to keyserver" 113 | gpg --export-secret-keys -o hvmerode@io.github.com 114 | gpg --send-keys --keyserver keyserver.ubuntu.com "$firstKeyId" 115 | gpg --send-keys --keyserver keyserver.ubuntu.com "$secondKeyId" 116 | 117 | # Create jar file with sources 118 | jar cf junit-pipeline-$(version)-sources.jar ./* 119 | 120 | # Create .pom file and copy other files to root 121 | cp ./pom.xml ./junit-pipeline-$(version).pom 122 | mv ./target/junit-pipeline-$(version).jar junit-pipeline-$(version).jar 123 | 124 | # Create Java doc 125 | mvn javadoc:jar 126 | mv ./target/junit-pipeline-$(version)-javadoc.jar junit-pipeline-$(version)-javadoc.jar 127 | 128 | # Create md5 and sha1 hashes of pom file 129 | md5=($(md5sum junit-pipeline-$(version).pom)) 130 | echo $md5 > junit-pipeline-$(version).pom.md5 131 | sha1=($(sha1sum junit-pipeline-$(version).pom)) 132 | echo $sha1 > junit-pipeline-$(version).pom.sha1 133 | 134 | # Create md5 and sha1 hashes of library 135 | md5=($(md5sum junit-pipeline-$(version).jar)) 136 | echo $md5 > junit-pipeline-$(version).jar.md5 137 | sha1=($(sha1sum junit-pipeline-$(version).jar)) 138 | echo $sha1 > junit-pipeline-$(version).jar.sha1 139 | 140 | # Create md5 and sha1 hashes of sources 141 | md5=($(md5sum junit-pipeline-$(version)-sources.jar)) 142 | echo $md5 > junit-pipeline-$(version)-sources.jar.md5 143 | sha1=($(sha1sum junit-pipeline-$(version)-sources.jar)) 144 | echo $sha1 > junit-pipeline-$(version)-sources.jar.sha1 145 | 146 | # Create md5 and sha1 hashes of javadoc 147 | md5=($(md5sum junit-pipeline-$(version)-javadoc.jar)) 148 | echo $md5 > junit-pipeline-$(version)-javadoc.jar.md5 149 | sha1=($(sha1sum junit-pipeline-$(version)-javadoc.jar)) 150 | echo $sha1 > junit-pipeline-$(version)-javadoc.jar.sha1 151 | 152 | # Create GnuPG signature 153 | gpg -ab --no-tty junit-pipeline-$(version).pom 154 | gpg -ab --no-tty junit-pipeline-$(version).jar 155 | gpg -ab --no-tty junit-pipeline-$(version)-sources.jar 156 | gpg -ab --no-tty junit-pipeline-$(version)-javadoc.jar 157 | 158 | # Create a java bundle 159 | jar -cvf junit-pipeline-$(version)-bundle.jar \ 160 | junit-pipeline-$(version).pom \ 161 | junit-pipeline-$(version).pom.md5 \ 162 | junit-pipeline-$(version).pom.sha1 \ 163 | junit-pipeline-$(version).pom.asc \ 164 | junit-pipeline-$(version).jar \ 165 | junit-pipeline-$(version).jar.md5 \ 166 | junit-pipeline-$(version).jar.sha1 \ 167 | junit-pipeline-$(version).jar.asc \ 168 | junit-pipeline-$(version)-sources.jar \ 169 | junit-pipeline-$(version)-sources.jar.md5 \ 170 | junit-pipeline-$(version)-sources.jar.sha1 \ 171 | junit-pipeline-$(version)-sources.jar.asc \ 172 | junit-pipeline-$(version)-javadoc.jar \ 173 | junit-pipeline-$(version)-javadoc.jar.md5 \ 174 | junit-pipeline-$(version)-javadoc.jar.sha1 \ 175 | junit-pipeline-$(version)-javadoc.jar.asc 176 | displayName: 'Generate artifacts needed for Maven central' 177 | 178 | # Tag the pipeline if a release artifact is build 179 | - script: | 180 | echo "##vso[build.addbuildtag]$(version)" 181 | condition: and(succeeded(), eq(variables['releaseBuild'], 'true')) 182 | displayName: 'Tag the pipeline with a version' 183 | 184 | # Publish the artifacts 185 | - task: CopyFiles@2 186 | displayName: 'Copy artifacts to the staging directory' 187 | inputs: 188 | sourceFolder: $(System.DefaultWorkingDirectory) 189 | contents: '**/*.jar' 190 | targetFolder: $(Build.ArtifactStagingDirectory) 191 | flattenFolders: true 192 | - task: CopyFiles@2 193 | displayName: 'Copy artifacts to the staging directory' 194 | inputs: 195 | sourceFolder: $(System.DefaultWorkingDirectory) 196 | contents: '**/*.md5' 197 | targetFolder: $(Build.ArtifactStagingDirectory) 198 | flattenFolders: true 199 | - task: CopyFiles@2 200 | displayName: 'Copy artifacts to the staging directory' 201 | inputs: 202 | sourceFolder: $(System.DefaultWorkingDirectory) 203 | contents: '**/*.sha1' 204 | targetFolder: $(Build.ArtifactStagingDirectory) 205 | flattenFolders: true 206 | - task: CopyFiles@2 207 | displayName: 'Copy artifacts to the staging directory' 208 | inputs: 209 | sourceFolder: $(System.DefaultWorkingDirectory) 210 | contents: '**/*.asc' 211 | targetFolder: $(Build.ArtifactStagingDirectory) 212 | flattenFolders: true 213 | - task: CopyFiles@2 214 | displayName: 'Copy artifacts to the staging directory' 215 | inputs: 216 | sourceFolder: $(System.DefaultWorkingDirectory) 217 | contents: '**/*.pgp' 218 | targetFolder: $(Build.ArtifactStagingDirectory) 219 | flattenFolders: true 220 | - task: PublishBuildArtifacts@1 221 | displayName: 'Publish build artifacts' 222 | inputs: 223 | pathToPublish: $(Build.ArtifactStagingDirectory) 224 | artifactName: artifacts 225 | 226 | ################################################################################################################# 227 | # STAGE: Analyze code 228 | # This stage runs in parallel with the Execute_build_publish stage to speed up the pipeline execution time. 229 | # It makes use of SonarCloud to validate the Java code amd Whispers to check on secrets in the respo. 230 | ################################################################################################################# 231 | - stage: Analyze_code 232 | displayName: 'Analyze code' 233 | dependsOn: Validate_entry_criteria 234 | condition: succeeded() 235 | jobs: 236 | # Perform SonarCloud analysis 237 | - job: SonarCloud 238 | pool: 239 | vmImage: 'ubuntu-latest' 240 | demands: maven 241 | workspace: 242 | clean: all 243 | continueOnError: "false" 244 | 245 | steps: 246 | - task: SonarCloudPrepare@1 247 | inputs: 248 | SonarCloud: SonarCloudGithub 249 | organization: 'io-github-hvmerode' 250 | scannerMode: 'Other' 251 | extraProperties: | 252 | # Additional properties that will be passed to the scanner, 253 | # Put one key=value per line, example: 254 | # sonar.exclusions=**/*.bin 255 | sonar.projectKey=hvmerode_junit-pipeline 256 | sonar.projectName=junit-pipeline 257 | 258 | # For now, suppres 'Code Coverage' and 'Duplications' 259 | sonar.coverage.exclusions=**\*.* 260 | sonar.cpd.exclusions=**\*.* 261 | 262 | # Add pipeline caching; force invalidating the cache each month 263 | - task: Cache@2 264 | displayName: 'Cache Maven local repo' 265 | inputs: 266 | key: 'maven | "$(Agent.OS)" | **/pom.xml' 267 | path: $(MAVEN_CACHE_FOLDER) 268 | 269 | # Perform Sonar Cloud check 270 | - task: Maven@3 271 | # condition: ne(variables['Build.SourceBranchName'], 'main') 272 | displayName: 'Sonar Cloud build' 273 | inputs: 274 | goals: 'clean verify -U sonar:sonar -DskipTests' 275 | javaHomeOption: 'JDKVersion' 276 | jdkVersionOption: '1.17' 277 | jdkArchitectureOption: 'x64' 278 | sonarQubeRunAnalysis: true 279 | options: '-B -DscmBranch=$(Build.SourceBranchName) -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)' 280 | publishJUnitResults: false 281 | 282 | - task: SonarCloudPublish@1 283 | inputs: 284 | pollingTimeoutSec: '300' 285 | 286 | - task: sonarcloud-buildbreaker@2 287 | displayName: 'Sonar Cloud build breaker' 288 | inputs: 289 | SonarCloud: 'SonarCloudGithub' 290 | organization: 'io-github-hvmerode' -------------------------------------------------------------------------------- /src/main/java/azdo/utils/Utils.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | package azdo.utils; 5 | 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 9 | import com.networknt.schema.JsonSchema; 10 | import com.networknt.schema.JsonSchemaFactory; 11 | import com.networknt.schema.SpecVersion; 12 | import com.networknt.schema.ValidationMessage; 13 | import org.apache.commons.io.FileUtils; 14 | import org.apache.commons.io.IOUtils; 15 | import org.springframework.web.util.UriUtils; 16 | import java.io.*; 17 | import java.nio.charset.Charset; 18 | import java.nio.file.*; 19 | import java.util.Set; 20 | import java.util.regex.Matcher; 21 | import java.util.regex.Pattern; 22 | import static azdo.utils.Constants.*; 23 | 24 | /****************************************************************************************** 25 | Various generic utility methods. 26 | *******************************************************************************************/ 27 | public class Utils { 28 | private static final int MAX_VAL_ERR = 1; 29 | private static final Log logger = Log.getLogger(); 30 | private static final String EXCLUDEFILESLIST = "\\excludedfileslist.txt"; 31 | 32 | public static boolean isLinux(){ 33 | String os = System.getProperty("os.name"); 34 | return os.toLowerCase().indexOf("linux") >= 0; 35 | } 36 | 37 | public static boolean isWindows(){ 38 | String os = System.getProperty("os.name"); 39 | return os.toLowerCase().indexOf("windows") >= 0; 40 | } 41 | 42 | public static boolean deleteDirectory(String directoryName) { 43 | logger.debug("==> Method: Utils.deleteDirectory"); 44 | logger.debug("directoryName: {}", directoryName); 45 | 46 | String dir = fixPath(directoryName); 47 | 48 | // This method makes use of Apache FileUtils, which can be used both on Linux and Windows 49 | try { 50 | logger.debug("Executing..."); 51 | FileUtils.deleteDirectory(new File(dir)); 52 | logger.debug("Deleted directory: {}", dir); 53 | wait(1000); 54 | } 55 | catch (IOException e) 56 | { 57 | logger.debug("Cannot delete directory {}; does it exist?", dir); 58 | return false; 59 | } 60 | 61 | return true; 62 | } 63 | 64 | public static boolean deleteFile(String fileName) { 65 | logger.debug("==> Method: Utils.deleteFile"); 66 | logger.debug("fileName: {}", fileName); 67 | 68 | String dir = fixPath(fileName); 69 | 70 | // This method makes use of Apache FileUtils, which can be used both on Linux and Windows 71 | try { 72 | logger.debug("Executing..."); 73 | FileUtils.delete(new File(fileName)); 74 | logger.debug("Deleted file: {}", fileName); 75 | wait(1000); 76 | } 77 | catch (IOException e) 78 | { 79 | logger.debug("Cannot delete file {}; does it exist?", fileName); 80 | return false; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | public static void createDirectory(String directoryName) { 87 | logger.debug("==> Method: Utils.createDirectory"); 88 | logger.debug("directoryName: {}", directoryName); 89 | directoryName = fixPath(directoryName); 90 | File dir = new File (directoryName); 91 | dir.mkdirs(); 92 | wait(1000); 93 | } 94 | 95 | public static void copyAll(String sourceDirectory, String destinationDirectory, String exclusionPattern) 96 | { 97 | try { 98 | copy(new File(sourceDirectory), new File(destinationDirectory), exclusionPattern); 99 | logger.debug("Directory copied successfully!"); 100 | } catch (IOException e) { 101 | logger.debug("Failed to copy directory: {}", e.getMessage()); 102 | } 103 | } 104 | 105 | private static void copy(File source, File target, String exclusionPattern) throws IOException { 106 | logger.debug("==> Method: Utils.copyAll"); 107 | logger.debug("source: {}", source); 108 | logger.debug("target: {}", target); 109 | logger.debug("exclusionPattern: {}", exclusionPattern); 110 | 111 | if (!source.isDirectory()) { 112 | throw new IllegalArgumentException("Source must be a directory"); 113 | } 114 | 115 | if (!target.exists()) { 116 | target.mkdirs(); 117 | } 118 | 119 | File[] files = source.listFiles(); 120 | if (files == null) { 121 | return; 122 | } 123 | 124 | Pattern pattern = Pattern.compile(exclusionPattern); 125 | Matcher matcher; 126 | for (File file : files) { 127 | matcher = pattern.matcher(file.getName()); 128 | matcher.find(); 129 | if (matcher.matches()) { 130 | continue; // Exclude directories matching the pattern 131 | } 132 | 133 | Path sourcePath = file.toPath(); 134 | Path destinationPath = new File(target, file.getName()).toPath(); 135 | 136 | if (file.isDirectory()) { 137 | copy(file, new File(target, file.getName()), exclusionPattern); 138 | } else { 139 | Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING); 140 | } 141 | } 142 | wait(200); // Time needed to unlock files in Windows 143 | logger.debug("Copied source {} to target {}", source, target); 144 | } 145 | 146 | /* 147 | Makes sure that a path resembles the correct path on the file system 148 | Fortunately, Windows also accepts forward slashes 149 | */ 150 | public static String fixPath (String path){ 151 | if (path == null) 152 | return path; 153 | 154 | path = path.replace("../", ""); // Remove ../ 155 | path = path.replace("..\\", ""); // Remove ..\ 156 | path = path.replace("./", ""); // Remove ./ 157 | path = path.replace(".\\", ""); // Remove .\ 158 | path = path.replaceAll("\\\\","/"); 159 | Path normalized = Paths.get(path); 160 | normalized = normalized.normalize(); 161 | path = normalized.toString(); 162 | return path; 163 | } 164 | 165 | /* 166 | Validate whether a directory is empty 167 | */ 168 | public static boolean pathIsEmptyOrNotExisting (String path) { 169 | boolean res = false; 170 | try { 171 | // Check whether the directory is empty 172 | // If res = true the directory is empty, which is enough to check 173 | // If res = false the directory is not empty, which is also enough to check 174 | File f = new File(path); 175 | res = FileUtils.isEmptyDirectory(f); 176 | } 177 | catch (IOException e) { 178 | logger.debug("Cannot validate whether the Path is empty or does not exist; assume it does not exists"); 179 | return true; 180 | } 181 | 182 | return res; 183 | } 184 | 185 | /* 186 | TODO 187 | */ 188 | public static String findFullQualifiedFileNameInDirectory (String directory, String fileName) { 189 | //logger.debug("==> Method: Utils.findFullQualifiedFileNameInDirectory"); 190 | 191 | if (directory.isEmpty()) { 192 | logger.debug("No directory provided; just return the fileName"); 193 | return fileName; 194 | } 195 | 196 | if (fileName.isEmpty()) { 197 | logger.debug("No fileName provided; just return an empty string"); 198 | return fileName; 199 | } 200 | 201 | Path dir = Path.of(directory); 202 | Path f = Path.of(fileName); 203 | if (f != null) { 204 | f = f.normalize(); 205 | fileName = f.toString(); 206 | } 207 | fileName = Utils.fixPath(fileName); // Remove .. in front of the filename, because the full name is searched on the filesystem anyway 208 | //fileName = fileName.replace("..", ""); // Remove .. in front of the filename, because the full name is searched on the filesystem anyway 209 | String fqn = null; 210 | String compare = null; 211 | //logger.debug("directory: {}", directory); 212 | //logger.debug("fileName: {}", fileName); 213 | 214 | try(DirectoryStream stream = Files.newDirectoryStream(dir)) { 215 | for (Path path : stream) { 216 | File pathToFile = path.toFile(); 217 | if (pathToFile.isDirectory()) { 218 | // It is a directory; search recursively 219 | //logger.debug("Directory: {}", pathToFile.getAbsoluteFile()); 220 | fqn = findFullQualifiedFileNameInDirectory(pathToFile.toString(), fileName); 221 | if (fqn != null) 222 | return fqn; 223 | } 224 | else { 225 | // It is a file 226 | //logger.debug("File: {}", pathToFile.getAbsoluteFile()); 227 | compare = pathToFile.getAbsoluteFile().toString(); 228 | if (compare.contains(fileName)) { 229 | logger.debug("Match: Compared {} with {}", fileName, compare); 230 | return compare; 231 | } 232 | } 233 | } 234 | } 235 | catch(IOException e) {} 236 | 237 | return null; 238 | } 239 | 240 | public static String encodePath (String path) { 241 | path = UriUtils.encodePath(path, "UTF-8"); 242 | return path; 243 | } 244 | 245 | /* 246 | Perform a find-and-replace in a given file. 247 | */ 248 | public static void findReplaceInFile(String fileName, String findString, String replaceString, boolean replaceAll) { 249 | FileInputStream fis = null; 250 | FileOutputStream fos = null; 251 | try { 252 | File f = new File(fileName); 253 | fis = new FileInputStream(f); 254 | String content = IOUtils.toString(fis, Charset.defaultCharset()); 255 | if (replaceAll) 256 | content = content.replaceAll(findString, replaceString); 257 | else 258 | content = content.replace(findString, replaceString); 259 | fos = new FileOutputStream(f); 260 | IOUtils.write(content, fos, Charset.defaultCharset()); 261 | } catch (IOException e) { 262 | logger.debug("Cannot find {} and replace it with {} in file {}", findString, replaceString, fileName); 263 | logger.debug("Does file {} exist?", fileName); 264 | } finally { 265 | if (fis != null) { 266 | try { 267 | fis.close(); 268 | } catch (IOException e) { 269 | logger.debug("Cannot close input stream"); 270 | } 271 | } 272 | if (fos != null) { 273 | try { 274 | fos.close(); 275 | } catch (IOException e) { 276 | logger.debug("Cannot close output stream"); 277 | } 278 | } 279 | } 280 | } 281 | 282 | public static void validatePipelineFile (String fileName, boolean continueOnError) { 283 | logger.debug("==> Method: Utils.validatePipelineFile"); 284 | logger.debug("fileName: {}", fileName); 285 | 286 | logger.debug(DEMARCATION); 287 | logger.debug("Validating {}", fileName); 288 | 289 | // Read the schema from the resources folder 290 | InputStream isJsonSchema = Utils.class.getClassLoader().getResourceAsStream(JSON_SCHEMA); 291 | if (isJsonSchema == null) 292 | { 293 | logger.warn("Schema cannot not be read"); 294 | logger.debug(DEMARCATION); 295 | return; 296 | } 297 | 298 | ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); 299 | JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).objectMapper(mapper).build(); /* Using draft-07. You can choose any other draft.*/ 300 | JsonSchema schema = factory.getSchema(isJsonSchema); 301 | 302 | InputStream isYaml = null; 303 | try { 304 | isYaml = new FileInputStream(fileName); 305 | } 306 | catch (FileNotFoundException fnfe) { 307 | logger.error("{} cannot be read", fileName); 308 | logger.debug(DEMARCATION); 309 | if (continueOnError) return; else System. exit(1); 310 | } 311 | 312 | JsonNode jsonNode = null; 313 | try { 314 | jsonNode = mapper.readTree(isYaml); 315 | } 316 | catch (IOException e){ 317 | logger.error("Cannot validate {};the file cannot be found or it is not a valid YAML file", fileName); 318 | logger.debug(DEMARCATION); 319 | if (continueOnError) return; else System. exit(1); 320 | } 321 | 322 | // If jsonNode is null, the yaml was not valid 323 | if (jsonNode == null) { 324 | logger.error("File {} is not a valid YAML file", fileName); 325 | logger.debug(DEMARCATION); 326 | if (continueOnError) return; else System. exit(1); 327 | } 328 | 329 | Set validateMsg = schema.validate(jsonNode); 330 | int size = validateMsg.size(); 331 | if (size > 0) { 332 | int i = 0; 333 | int maxErr = size < MAX_VAL_ERR ? size : MAX_VAL_ERR; 334 | for (ValidationMessage msg : validateMsg) { 335 | logger.error("Validation type [{}]; Error: {}", msg.getType(), msg.getMessage()); 336 | if (i >= maxErr) 337 | break; 338 | i++; 339 | } 340 | logger.debug(DEMARCATION); 341 | if (continueOnError) return; else System. exit(1); 342 | } 343 | 344 | logger.debug("File {} looks valid", fileName); 345 | logger.debug(DEMARCATION); 346 | } 347 | 348 | public static String relativize (String base, String fileName) { 349 | logger.debug("==> Method: Utils.relativize"); 350 | logger.debug("base: {}", base); 351 | logger.debug("fileName: {}", fileName); 352 | 353 | String relative = ""; 354 | File baseFile = new File(base); 355 | File fileNameFile = new File(fileName); 356 | Path basePath = baseFile.toPath(); 357 | basePath = basePath.normalize(); 358 | Path fileNamePath = fileNameFile.toPath(); 359 | fileNamePath = fileNamePath.normalize(); 360 | 361 | Path newPath = fileNamePath; 362 | try { 363 | newPath = basePath.relativize(fileNamePath); 364 | newPath = newPath.normalize(); 365 | } 366 | catch (IllegalArgumentException e) {} 367 | 368 | String newPathToString = newPath.toString(); 369 | newPathToString = newPathToString.replaceAll("\\\\","/"); 370 | 371 | logger.debug("newPath: {}", newPathToString); 372 | return newPathToString; 373 | } 374 | 375 | public static String getFileNameWithoutPathAndExtension (String file, boolean removeAllExtensions) { 376 | if (file == null || file.isEmpty()) { 377 | return file; 378 | } 379 | 380 | File f = new File(file); 381 | file = f.getName(); // Name with extension 382 | 383 | String extPattern = "(? commitPatternList; 68 | private String targetRepositoryName; 69 | private boolean includeExternalTemplates = true; 70 | private boolean continueOnError = false; 71 | 72 | @SuppressWarnings("java:S1192") 73 | public PropertyUtils(String propertyFile) { 74 | try { 75 | logger.debug("==> Object: PropertyUtils"); 76 | logger.debug("PropertyFile: {}", propertyFile); 77 | properties = new Properties(); 78 | InputStream is = getClass().getClassLoader().getResourceAsStream(propertyFile); 79 | properties.load(is); 80 | 81 | logger.debug(""); 82 | logger.debug("#################################################################"); 83 | logger.debug("Start reading properties"); 84 | logger.debug("#################################################################"); 85 | 86 | // Source 87 | sourcePath = getStringProperty(properties, "source.path", sourcePath); 88 | sourceBasePathExternal = getStringProperty(properties, "source.base.path.external", sourceBasePathExternal); 89 | sourceRepositoryName = getStringProperty(properties, "source.repository.name", sourceRepositoryName); 90 | sourceProject = getStringProperty(properties, "source.project", sourceProject); // Only used for AzDo project 91 | 92 | // Target 93 | targetOrganization = getStringProperty(properties, "target.organization", targetOrganization); 94 | targetProject = getStringProperty(properties, "target.project", targetProject); 95 | targetPath = getStringProperty(properties, "target.path", targetPath); 96 | targetBasePathExternal = getStringProperty(properties, "target.base.path.external", targetBasePathExternal); 97 | targetRepositoryName = getStringProperty(properties, "target.repository.name", targetRepositoryName); 98 | azdoUser = getStringProperty(properties, "azdo.user", azdoUser, false); 99 | azdoPat = getStringProperty(properties, "azdo.pat", azdoPat, false); 100 | targetExludeList = getStringProperty(properties, "target.excludelist", targetExludeList); 101 | 102 | // Run trough the commit pattern and create a List 103 | commitPattern = getStringProperty(properties, "git.commit.pattern", commitPattern); 104 | commitPatternList = new ArrayList<>(); 105 | var values = commitPattern.split(","); 106 | for (int i = 0; i < values.length; i++) 107 | { 108 | commitPatternList.add(values[i]); 109 | } 110 | 111 | // Azure DevOps Pipeline API 112 | pipelinesApi = getStringProperty(properties, "pipelines.api", pipelinesApi); 113 | pipelinesApiRuns = getStringProperty(properties, "pipelines.api.runs", pipelinesApiRuns); 114 | pipelinesApiVersion = getStringProperty(properties, "pipelines.api.version", pipelinesApiVersion); 115 | 116 | // Azure DevOps Git API 117 | gitApi = getStringProperty(properties, "git.api", gitApi); 118 | gitApiRepositories = getStringProperty(properties, "git.api.repositories", gitApiRepositories); 119 | gitApiVersion = getStringProperty(properties, "git.api.version", gitApiVersion); 120 | 121 | // Azure DevOps Build API 122 | buildApi = getStringProperty(properties, "build.api", buildApi); 123 | buildApiVersion = getStringProperty(properties, "build.api.version", buildApiVersion); 124 | buildApiPollFrequency = getIntProperty(properties, "build.api.poll.frequency", buildApiPollFrequency); 125 | buildApiPollTimeout = getIntProperty(properties, "build.api.poll.timeout", buildApiPollTimeout); 126 | 127 | // Azure DevOps Project API 128 | projectApi = getStringProperty(properties, "project.api", projectApi); 129 | projectApiVersion = getStringProperty(properties, "project.api.version", projectApiVersion); 130 | 131 | // Azure DevOps Distributed task APIs 132 | variableGroupsApi = getStringProperty(properties, "variable.groups.api", variableGroupsApi); 133 | variableGroupsApiVersion = getStringProperty(properties, "variable.groups.api.version", variableGroupsApiVersion); 134 | variableGroupsValidate = getBooleanProperty(properties, "variable.groups.validate", variableGroupsValidate); 135 | environmentsApi = getStringProperty(properties, "environments.api", environmentsApi); 136 | environmentsApiVersion = getStringProperty(properties, "environments.api.version", environmentsApiVersion); 137 | environmentsValidate = getBooleanProperty(properties, "environments.validate", environmentsValidate); 138 | 139 | // Miscellaneous 140 | continueOnError = getBooleanProperty(properties, "error.continue", continueOnError); 141 | includeExternalTemplates = getBooleanProperty(properties, "templates.external.include", includeExternalTemplates); 142 | 143 | // Derived properties 144 | azdoBaseUrl="https://dev.azure.com/" + targetOrganization; 145 | logger.debug("Derived azdoBaseUrl: {}", azdoBaseUrl); 146 | uriTargetRepository = azdoBaseUrl + "/" + targetProject + "/_git/" + targetRepositoryName; 147 | uriTargetRepository = Utils.encodePath(uriTargetRepository); 148 | logger.debug("Derived uriTargetRepository: {}", uriTargetRepository); 149 | // An Azure project may contain spaces; perform URL encoding because the project name is part of the URL 150 | azdoEndpoint = azdoBaseUrl + "/" + Utils.encodePath(targetProject) + "/_apis"; 151 | logger.debug("Derived azdoEndpoint: {}", azdoEndpoint); 152 | 153 | logger.debug("#################################################################"); 154 | logger.debug("End reading properties"); 155 | logger.debug("#################################################################"); 156 | logger.debug(""); 157 | } 158 | catch (FileNotFoundException e) { 159 | logger.debug("Property file not found"); 160 | } 161 | catch (IOException e) { 162 | logger.debug("IOException"); 163 | } 164 | } 165 | 166 | private String getStringProperty (Properties properties, String propertyName, String propertyValue) { 167 | return getStringProperty (properties, propertyName, propertyValue, true); 168 | } 169 | 170 | private String getStringProperty (Properties properties, String propertyName, String propertyValue, boolean showValueInLog) { 171 | String p = properties.getProperty(propertyName); 172 | if (p != null && !p.isEmpty()) 173 | propertyValue = p; 174 | if (showValueInLog) 175 | logger.debug("{}: {}", propertyName, propertyValue); 176 | else 177 | logger.debug("{}: *****************", propertyName); 178 | 179 | return propertyValue; 180 | } 181 | 182 | private int getIntProperty (Properties properties, String propertyName, int propertyValue) { 183 | return getIntProperty (properties, propertyName, propertyValue, true); 184 | } 185 | 186 | private int getIntProperty (Properties properties, String propertyName, int propertyValue, boolean showValueInLog) { 187 | String p = properties.getProperty(propertyName); 188 | if (p != null && !p.isEmpty()) { 189 | propertyValue = Integer.parseInt(p); 190 | } 191 | if (showValueInLog) 192 | logger.debug("{}: {}", propertyName, propertyValue); 193 | else 194 | logger.debug("{}: *****************", propertyName); 195 | 196 | return propertyValue; 197 | } 198 | 199 | private boolean getBooleanProperty (Properties properties, String propertyName, boolean propertyValue) { 200 | return getBooleanProperty (properties, propertyName, propertyValue, true); 201 | } 202 | 203 | private boolean getBooleanProperty (Properties properties, String propertyName, boolean propertyValue, boolean showValueInLog) { 204 | String p = properties.getProperty(propertyName); 205 | if (p != null && !p.isEmpty()) { 206 | propertyValue = Boolean.parseBoolean(p); 207 | } 208 | if (showValueInLog) 209 | logger.debug("{}: {}", propertyName, propertyValue); 210 | else 211 | logger.debug("{}: *****************", propertyName); 212 | 213 | return propertyValue; 214 | } 215 | 216 | public void setSourcePath(String sourcePath) { 217 | this.sourcePath = sourcePath; 218 | } 219 | public String getSourcePath() { return sourcePath; } 220 | 221 | public void setSourceProject(String sourceProject) { 222 | this.sourceProject = sourceProject; 223 | } 224 | public String getSourceProject() { 225 | return sourceProject; 226 | } 227 | 228 | public void setTargetProject(String targetProject) { 229 | this.targetProject = targetProject; 230 | } 231 | public String getTargetProject() { 232 | return targetProject; 233 | } 234 | 235 | public void setTargetRepositoryName(String targetRepositoryName) { 236 | this.targetRepositoryName = targetRepositoryName; 237 | } 238 | public String getTargetRepositoryName() { return targetRepositoryName; } 239 | 240 | public void setSourceRepositoryName(String sourceRepositoryName) { 241 | this.sourceRepositoryName = sourceRepositoryName; 242 | } 243 | public String getSourceRepositoryName() { return sourceRepositoryName; } 244 | 245 | public void setTargetOrganization(String targetOrganization) { 246 | this.targetOrganization = targetOrganization; 247 | } 248 | public String getTargetOrganization() { 249 | return targetOrganization; 250 | } 251 | 252 | public void setSourceBasePathExternal(String sourceBasePathExternal) { 253 | this.sourceBasePathExternal = sourceBasePathExternal; 254 | } 255 | public String getSourceBasePathExternal() { return sourceBasePathExternal; } 256 | 257 | public void setTargetPath(String targetPath) { 258 | this.targetPath = targetPath; 259 | } 260 | public String getTargetPath() { return targetPath; } 261 | 262 | public void setTargetBasePathExternal(String targetBasePathExternal) { 263 | this.targetBasePathExternal = targetBasePathExternal; 264 | } 265 | public String getTargetBasePathExternal() { return targetBasePathExternal; } 266 | 267 | public void setUriTargetRepository (String uriTargetRepository) { 268 | this.uriTargetRepository = uriTargetRepository; 269 | } 270 | public String getUriTargetRepository() { return uriTargetRepository; } 271 | 272 | public void setAzdoEndpoint(String azdoEndpoint) { 273 | this.azdoEndpoint = azdoEndpoint; 274 | } 275 | public String getAzdoEndpoint() { return azdoEndpoint; } 276 | 277 | public void setTargetExludeList(String targetExludeList) { 278 | this.targetExludeList = targetExludeList; 279 | } 280 | public String getTargetExludeList() { return targetExludeList; } 281 | 282 | // Pipeline API 283 | 284 | public void setAzdoBaseUrl(String azdoBaseUrl) { 285 | this.azdoBaseUrl = azdoBaseUrl; 286 | } 287 | public String getAzdoBaseUrl() { return azdoBaseUrl; } 288 | 289 | public void setPipelinesApi(String pipelinesApi) { 290 | this.pipelinesApi = pipelinesApi; 291 | } 292 | public String getPipelinesApi() { return pipelinesApi; } 293 | 294 | public void setPipelinesApiRuns(String pipelinesApiRuns) { 295 | this.pipelinesApiRuns = pipelinesApiRuns; 296 | } 297 | public String getPipelinesApiRuns() { return pipelinesApiRuns; } 298 | 299 | public void setPipelinesApiVersion(String pipelinesApiVersion) { 300 | this.pipelinesApiVersion = pipelinesApiVersion; 301 | } 302 | public String getPipelinesApiVersion() { return pipelinesApiVersion; } 303 | 304 | // Git API 305 | 306 | public void setGitApi(String gitApi) { 307 | this.gitApi = gitApi; 308 | } 309 | public String getGitApi() { return gitApi; } 310 | 311 | public void setGitApiRepositories(String gitApiRepositories) { 312 | this.gitApiRepositories = gitApiRepositories; 313 | } 314 | public String getGitApiRepositories() { return gitApiRepositories; } 315 | 316 | public void setGitApiVersion(String gitApiVersion) { 317 | this.gitApiVersion = gitApiVersion; 318 | } 319 | public String getGitApiVersion() { return gitApiVersion; } 320 | 321 | public void setAzdoUser(String azdoUser) { 322 | this.azdoUser = azdoUser; 323 | } 324 | public String getAzDoUser() { return azdoUser; } 325 | 326 | public void setAzdoPat(String azdoPat) { 327 | this.azdoPat = azdoPat; 328 | } 329 | public String getAzdoPat() { return azdoPat; } 330 | 331 | // Build 332 | public void setBuildApi(String buildApi) { 333 | this.buildApi = buildApi; 334 | } 335 | public String getBuildApi() { return buildApi; } 336 | 337 | public void setBuildApiPollFrequency(int buildApiPollFrequency) { 338 | this.buildApiPollFrequency = buildApiPollFrequency; 339 | } 340 | public int getBuildApiPollFrequency() { return buildApiPollFrequency; } 341 | 342 | public void setBuildApiPollTimeout(int buildApiPollTimeout) { 343 | this.buildApiPollTimeout = buildApiPollTimeout; 344 | } 345 | public int getBuildApiPollTimeout() { return buildApiPollTimeout; } 346 | 347 | public void setBuildApiVersion(String buildApiVersion) { 348 | this.buildApiVersion = buildApiVersion; 349 | } 350 | public String getBuildApiVersion() { return buildApiVersion; } 351 | 352 | // Project 353 | public void setProjectApi(String projectApi) { 354 | this.projectApi = projectApi; 355 | } 356 | public String getProjectApi() { return projectApi;} 357 | 358 | public void setProjectApiVersion(String projectApiVersion) { 359 | this.projectApiVersion = projectApiVersion; 360 | } 361 | public String getProjectApiVersion() { 362 | return projectApiVersion; 363 | } 364 | 365 | 366 | // Distributed task 367 | public void setVariableGroupsApi(String variableGroupsApi) { 368 | this.variableGroupsApi = variableGroupsApi; 369 | } 370 | public String getVariableGroupsApi() { return variableGroupsApi;} 371 | 372 | public void setVariableGroupsApiVersion(String variableGroupsApiVersion) { 373 | this.variableGroupsApiVersion = variableGroupsApiVersion; 374 | } 375 | public String getVariableGroupsApiVersion() { 376 | return variableGroupsApiVersion; 377 | } 378 | 379 | public void setVariableGroupsValidate(boolean variableGroupsValidate) { 380 | this.variableGroupsValidate = variableGroupsValidate; 381 | } 382 | public boolean isVariableGroupsValidate() { 383 | return variableGroupsValidate; 384 | } 385 | 386 | public void setEnvironmentsApi(String environmentsApi) { 387 | this.environmentsApi = environmentsApi; 388 | } 389 | public String getEnvironmentsApi () { return environmentsApi;} 390 | 391 | public void setEnvironmentsApiVersion (String environmentsApiVersion) { 392 | this.environmentsApiVersion = environmentsApiVersion; 393 | } 394 | public String getEnvironmentsApiVersion () { 395 | return environmentsApiVersion; 396 | } 397 | 398 | public void setEnvironmentsValidate(boolean environmentsValidate) { 399 | this.environmentsValidate = environmentsValidate; 400 | } 401 | public boolean isEnvironmentsValidate() { 402 | return environmentsValidate; 403 | } 404 | 405 | 406 | // Miscellaneous 407 | public void setCommitPattern (String commitPattern) { 408 | this.commitPattern = commitPattern; 409 | } 410 | public String getCommitPattern() { return commitPattern; } 411 | 412 | 413 | public void setCommitPatternList (ArrayList commitPatternList) { 414 | this.commitPatternList = commitPatternList; 415 | } 416 | public ArrayList getCommitPatternList() { return commitPatternList; } 417 | 418 | public void setIncludeExternalTemplates (boolean includeExternalTemplates) { 419 | this.includeExternalTemplates = includeExternalTemplates; 420 | } 421 | public boolean isIncludeExternalTemplates () { 422 | return includeExternalTemplates; 423 | } 424 | 425 | public void setContinueOnError (boolean continueOnError) { 426 | this.continueOnError = continueOnError; 427 | } 428 | public boolean isContinueOnError () { return continueOnError; } 429 | } 430 | -------------------------------------------------------------------------------- /src/test/java/PipelineUnit.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Henry van Merode. 2 | // Licensed under the MIT License. 3 | 4 | import azdo.hook.Hook; 5 | import azdo.junit.AzDoPipeline; 6 | import azdo.junit.RunResult; 7 | import azdo.utils.Log; 8 | import azdo.utils.PropertyUtils; 9 | import org.junit.jupiter.api.*; 10 | import java.io.IOException; 11 | import java.util.*; 12 | import static azdo.utils.Constants.*; 13 | 14 | 15 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 16 | public class PipelineUnit { 17 | private static final Log logger = Log.getLogger(); 18 | private static AzDoPipeline pipeline; 19 | 20 | @BeforeAll 21 | public static void setUpClass() { 22 | logger.debug("setUpClass"); 23 | } 24 | 25 | @Test 26 | @Order(1) 27 | public void test1() { 28 | logger.debug(""); 29 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 30 | logger.debug("Perform unittest: test1"); 31 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 32 | 33 | // Initialize the pipeline 34 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/simple-pipeline.yml"); 35 | 36 | try { 37 | // Create a hook to perform an action just before starting the pipeline 38 | List hookList = new ArrayList<>(); 39 | class TestHook extends Hook { 40 | @Override 41 | public void executeHook() { 42 | logger.debug("Executes hook with an argument"); 43 | } 44 | } 45 | 46 | // Create a list with hooks and pass it to the startPipeline 47 | hookList.add(new TestHook()); 48 | 49 | // Manipulate the pipeline and validate the 'testVar' and the existence of file "output.csv" 50 | pipeline.resetTrigger() 51 | .overrideSectionPropertySearchByTypeAndIdentifier("pool", "", "vmImage", "windows-latest") 52 | .setVariableSearchStepByDisplayName ("Testing, testing", "testVar", "myReplacedValue") 53 | .assertFileExistsSearchStepByDisplayName("Testing, testing", "output.csv", true) 54 | .assertVariableEqualsSearchStepByDisplayName("Testing, testing", "testVar", "myReplacedValue", false) 55 | .startPipeline("master", hookList); 56 | } 57 | catch (IOException e) { 58 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 59 | } 60 | Assertions.assertEquals (RunResult.Result.failed, pipeline.getRunResult().result); 61 | RunResult.Result stageResult = pipeline.getRunResult().getStageResultSearchByName("simple_stage"); 62 | Assertions.assertEquals (RunResult.Result.failed, stageResult); 63 | logger.info("Test successful"); 64 | logger.info("Expected pipeline result: {}", RunResult.Result.failed); 65 | logger.info("Actual pipeline result: {}", pipeline.getRunResult().result); 66 | logger.info("Expected stage result: {}", RunResult.Result.failed); 67 | logger.info("Actual stage result: {}", stageResult); 68 | } 69 | 70 | @Test 71 | @Order(2) 72 | public void test2() { 73 | logger.debug(""); 74 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 75 | logger.debug("Perform unittest: test2"); 76 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 77 | 78 | // Initialize the pipeline 79 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/pipeline-test.yml"); 80 | 81 | try { 82 | String inlineScript = "echo \"This is a mock script\"\n" + 83 | "echo \"This is line 2\""; 84 | String inlineScript2 = "echo \"This is an inserted script\""; 85 | pipeline.mockStepSearchByIdentifier("AWSShellScript@1", inlineScript) 86 | .insertScriptSearchStepByDisplayName ("DeployStage job_xe script", inlineScript2, false) 87 | .setVariableSearchStepByDisplayName ("DeployStage job_xe script", "aws_connection", "42") 88 | .skipStageSearchByIdentifier("Stage_B") 89 | .skipStageSearchByIdentifier("ExecuteScriptStage") 90 | .assertVariableNotEqualsSearchStepByDisplayName ("DeployStage job_xd script", "myVar", "donotfail", true) 91 | .startPipeline(); 92 | } 93 | catch (IOException e) { 94 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 95 | } 96 | Assertions.assertEquals (RunResult.Result.succeeded, pipeline.getRunResult().result); 97 | logger.info("Test successful"); 98 | logger.info("Expected: {}", RunResult.Result.succeeded); 99 | logger.info("Actual: {}", pipeline.getRunResult().result); 100 | } 101 | 102 | @Test 103 | @Order(3) 104 | public void test3() { 105 | logger.debug(""); 106 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 107 | logger.debug("Perform unittest: test3"); 108 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 109 | 110 | // Initialize the pipeline 111 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/pipeline-test.yml"); 112 | 113 | try { 114 | String inlineScript = "echo \"This is a mock script\"\n" + 115 | "echo \"This is line 2\""; 116 | pipeline.mockStepSearchByIdentifier("AWSShellScript@1", inlineScript) 117 | .startPipeline("myFirstFeature"); 118 | } 119 | catch (IOException e) { 120 | logger.debug("Exception occurred: {}", e.getMessage()); 121 | } 122 | Assertions.assertEquals (RunResult.Result.succeeded, pipeline.getRunResult().result); 123 | logger.info("Test successful"); 124 | logger.info("Expected: {}", RunResult.Result.succeeded); 125 | logger.info("Actual: {}", pipeline.getRunResult().result); 126 | } 127 | 128 | @Test 129 | @Order(4) 130 | public void test4() { 131 | logger.debug(""); 132 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 133 | logger.debug("Perform unittest: test4"); 134 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 135 | 136 | // Initialize the pipeline 137 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/pipeline-test.yml"); 138 | 139 | try { 140 | pipeline.overrideParameterDefault("sleep", "5") 141 | .overrideTemplateParameter("aNiceParam", "replaced_parameter") 142 | .overrideVariable("jobVar", "replacedJobVar") 143 | .overrideLiteral("Job_2.Task_3: Sleep some seconds", "Sleep") 144 | .overrideVariable("aws_region", "eu-west-1") 145 | .skipJobSearchByIdentifier("Job_XD") 146 | .setVariableSearchStepByIdentifier ("AWSShellScript@1", "aws_connection", "42") 147 | .setVariableSearchTemplateByIdentifier("templates/steps/template-steps.yml", "environment", "prod") 148 | .setVariableSearchTemplateByIdentifier("templates/steps/template-steps.yml", "sleep", "2", false) 149 | .setVariableSearchStepByDisplayName ("ExecuteScriptStage job_xc script", "myVar", "myReplacedValue") 150 | .assertVariableEqualsSearchStepByDisplayName("ExecuteScriptStage job_xa script", "jobVar", "replacedJobVar") 151 | .assertVariableNotEqualsSearchStepByDisplayName("ExecuteScriptStage job_xa script", "jobVar", "replacedJobVar") 152 | .assertVariableNotEmptySearchStepByDisplayName("ExecuteScriptStage job_xa script", "jobVar") 153 | .startPipeline("myFirstFeature", null, true); 154 | } 155 | catch (IOException e) { 156 | logger.debug("Exception occurred: {}", e.getMessage()); 157 | } 158 | } 159 | 160 | @Test 161 | @Order(5) 162 | public void test5() { 163 | logger.debug(""); 164 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 165 | logger.debug("Perform unittest: test5"); 166 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 167 | 168 | // Initialize the pipeline 169 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/bash-mock.yml"); 170 | 171 | try { 172 | String[] strArr = new String[3]; 173 | strArr[0] = "HTTP/2 200"; 174 | strArr[1] = "HTTP/2 403"; 175 | strArr[2] = "HTTP/2 501"; 176 | pipeline.mockBashCommandSearchStepByDisplayName("Curl step 1 of 2", "curl", strArr) 177 | .mockBashCommandSearchStepByDisplayName("Curl step 2 of 2", "curl","HTTP/2 200") 178 | .mockBashCommandSearchStepByDisplayName("Wget step", "wget", "mock 100%[=================================================>] 15.01M 6.77MB/s in 2.2s") 179 | .mockBashCommandSearchStepByDisplayName("Ftp step", "ftp", "") 180 | .mockBashCommandSearchStepByDisplayName("Bash@3 task", "curl", "HTTP/2 403") 181 | .startPipeline(); 182 | } 183 | catch (IOException e) { 184 | logger.debug("Exception occurred: {}", e.getMessage()); 185 | } 186 | Assertions.assertEquals (RunResult.Result.succeeded, pipeline.getRunResult().result); 187 | logger.info("Test successful"); 188 | logger.info("Expected: {}", RunResult.Result.succeeded); 189 | logger.info("Actual: {}", pipeline.getRunResult().result); 190 | } 191 | 192 | @Test 193 | @Order(6) 194 | public void test6() { 195 | logger.debug(""); 196 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 197 | logger.debug("Perform unittest: test6"); 198 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 199 | 200 | // Initialize the pipeline 201 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/powershell-mock.yml"); 202 | 203 | try { 204 | String[] strArr = new String[2]; 205 | strArr[0] = "{\"element\" : \"value_1\"}"; 206 | strArr[1] = "{\"element\" : \"value_2\"}"; 207 | pipeline.mockPowerShellCommandSearchStepByDisplayName("Invoke-RestMethod step 1 of 2", 208 | "Invoke-RestMethod", 209 | strArr) 210 | .mockPowerShellCommandSearchStepByDisplayName("Invoke-RestMethod step 2 of 2", 211 | "Invoke-RestMethod", 212 | strArr[1]) 213 | .mockPowerShellCommandSearchStepByDisplayName("PowerShell@2 task", 214 | "Invoke-RestMethod", 215 | "{\"element\" : \"value_3\"}") 216 | .startPipeline("master"); 217 | } 218 | catch (IOException e) { 219 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 220 | } 221 | Assertions.assertEquals (RunResult.Result.succeeded, pipeline.getRunResult().result); 222 | logger.info("Test successful"); 223 | logger.info("Expected: {}", RunResult.Result.succeeded); 224 | logger.info("Actual: {}", pipeline.getRunResult().result); 225 | } 226 | 227 | @Test 228 | @Order(7) 229 | public void test7() { 230 | logger.debug(""); 231 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 232 | logger.debug("Perform unittest: test7"); 233 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 234 | 235 | // Initialize the pipeline 236 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/simple-deployment.yml"); 237 | 238 | try { 239 | pipeline.overrideLiteral("dev", "prod") 240 | .overrideLiteral("true:", "'on':") // This is a bug in snakeyaml; it replaces "on:" with "true:" 241 | .startPipeline(); 242 | } 243 | catch (IOException e) { 244 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 245 | } 246 | Assertions.assertEquals (RunResult.Result.succeeded, pipeline.getRunResult().result); 247 | } 248 | 249 | @Test 250 | @Order(8) 251 | public void test8() { 252 | logger.debug(""); 253 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 254 | logger.debug("Perform unittest: test8"); 255 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 256 | 257 | // Initialize the pipeline 258 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/simple-pipeline.yml"); 259 | Map stageParameters = new HashMap<>(); 260 | stageParameters.put("aNiceParam", "stage_val81"); 261 | stageParameters.put("template", "stage_val82"); 262 | Map mockParameters = new HashMap<>(); 263 | mockParameters.put("param_1", "mock_val81"); 264 | mockParameters.put("param_2", "mock_val82"); 265 | 266 | try { 267 | pipeline.insertTemplateSearchSectionByDisplayName(SECTION_STAGE, "simple_stage", "templates/stages/template-stages.yml", stageParameters, false) 268 | .insertTemplateSearchSectionByDisplayName(SECTION_JOB, "simple_job", "templates/jobs/template-jobs.yml", null) 269 | .insertTemplateSearchSectionByDisplayName(STEP_SCRIPT, "Testing, testing", "templates/steps/template-mock.yml", mockParameters) 270 | .insertTemplateSearchSectionByDisplayName(STEP_SCRIPT, "Testing, testing", "templates/steps/template-steps.yml", null, false) 271 | .startPipeline("master"); 272 | } 273 | catch (IOException e) { 274 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 275 | } 276 | 277 | RunResult pipelineRunResult = pipeline.getRunResult(); 278 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.result); 279 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStageResultSearchByName("simple_stage")); 280 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getJobResultSearchByName("simple_job")); 281 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStepResultSearchByName("Testing, testing")); 282 | } 283 | 284 | @Test 285 | @Order(9) 286 | public void test9() { 287 | logger.debug(""); 288 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 289 | logger.debug("Perform unittest: test9"); 290 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 291 | 292 | // Initialize the pipeline 293 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/simple-pipeline.yml"); 294 | Map stageParameters = new HashMap<>(); 295 | stageParameters.put("aNiceParam", "stage_val91"); 296 | stageParameters.put("template", "stage_val92"); 297 | Map jobParameters = new HashMap<>(); 298 | jobParameters.put("param_1", "job_val91"); 299 | Map stepParameters = new HashMap<>(); 300 | stepParameters.put("param_1", "step_val91"); 301 | stepParameters.put("param_2", "step_val92"); 302 | 303 | try { 304 | // Test the insertTemplateSearchSectionByIdentifier with some combinations 305 | pipeline.resetTrigger() 306 | .insertTemplateSearchSectionByIdentifier("simpleStage", "templates/stages/template-stages.yml", stageParameters, true) 307 | .insertTemplateSearchSectionByIdentifier("simpleJob", "templates/jobs/template-jobs.yml", jobParameters, true) 308 | .insertTemplateSearchSectionByIdentifier("templates/steps/template-script.yml", "templates/steps/template-mock.yml", stepParameters, false) 309 | .skipStepSearchByDisplayName("Testing, testing") 310 | .startPipeline("mySecondFeature"); 311 | } 312 | catch (IOException e) { 313 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 314 | } 315 | RunResult pipelineRunResult = pipeline.getRunResult(); 316 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.result); 317 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStageResultSearchByName("template-stages.yml stage")); 318 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStageResultSearchByName("simple_stage")); 319 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getJobResultSearchByName("template-stages.yml job")); 320 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getJobResultSearchByName("template-jobs.yml job")); 321 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getJobResultSearchByName("simple_job")); 322 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStepResultSearchByName("This is script step")); 323 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.getStepResultSearchByName("template-mock.yml script")); 324 | } 325 | 326 | @Test 327 | @Order(10) 328 | public void test10() { 329 | logger.debug(""); 330 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 331 | logger.debug("Perform unittest: test10"); 332 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 333 | 334 | // Initialize the pipeline 335 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/simple-pipeline.yml"); 336 | 337 | try { 338 | // Test the assertParameterEqualsSearchTemplateByIdentifier and assertVariableEqualsSearchTemplateByIdentifier 339 | pipeline.resetTrigger() 340 | .assertParameterEqualsSearchTemplateByIdentifier ("templates/steps/template-script.yml", "param_1", "default") 341 | .assertVariableEqualsSearchTemplateByIdentifier ("templates/steps/template-script.yml", "testVar", "test_wrong", false) 342 | .startPipeline("myFirstFeature"); 343 | } 344 | catch (IOException e) { 345 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 346 | } 347 | RunResult pipelineRunResult = pipeline.getRunResult(); 348 | Assertions.assertEquals (RunResult.Result.failed, pipelineRunResult.result); 349 | } 350 | 351 | @Test 352 | @Order(11) 353 | public void test11() { 354 | logger.debug(""); 355 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 356 | logger.debug("Perform unittest: test11"); 357 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 358 | 359 | // Initialize the pipeline 360 | // Include external resources (this makes them local resources in the pipeline and the templates in the local repos are executed) 361 | pipeline = new AzDoPipeline("junit_pipeline_my.properties", "./pipeline/external-resources-pipeline.yml"); 362 | 363 | try { 364 | // Test the external resources 365 | pipeline.startPipeline(); 366 | } 367 | catch (IOException e) { 368 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 369 | } 370 | RunResult pipelineRunResult = pipeline.getRunResult(); 371 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.result); 372 | } 373 | 374 | @Test 375 | @Order(12) 376 | public void test12() { 377 | logger.debug(""); 378 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 379 | logger.debug("Perform unittest: test12"); 380 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 381 | 382 | // Initialize the pipeline 383 | // Exclude external resources (this leaves the 'resources' section intact and the external templates are not manipulated) 384 | PropertyUtils properties = new PropertyUtils("junit_pipeline_my.properties"); 385 | properties.setIncludeExternalTemplates(false); 386 | pipeline = new AzDoPipeline(properties, "./pipeline/external-resources-pipeline.yml"); 387 | 388 | try { 389 | // Test the external resources 390 | pipeline.startPipeline(); 391 | } 392 | catch (IOException e) { 393 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 394 | } 395 | RunResult pipelineRunResult = pipeline.getRunResult(); 396 | Assertions.assertEquals (RunResult.Result.succeeded, pipelineRunResult.result); 397 | } 398 | 399 | @Test 400 | @Order(13) 401 | public void test13() { 402 | logger.debug(""); 403 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 404 | logger.debug("Perform unittest: test13"); 405 | logger.debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 406 | 407 | // Initialize the pipeline 408 | // Exclude external resources (this leaves the 'resources' section intact and the external templates are not manipulated) 409 | PropertyUtils properties = new PropertyUtils("junit_pipeline_my.properties"); 410 | properties.setIncludeExternalTemplates(false); 411 | pipeline = new AzDoPipeline(properties, "./pipeline/external-resources-pipeline.yml"); 412 | 413 | try { 414 | pipeline 415 | .addPropertyToSectionSearchByTypeAndIdentifier("repository", "external", "endpoint", "p1") // Add endpoint 416 | .addPropertyToSectionSearchByTypeAndIdentifier("repository", "external2", "endpoint", "p2") // Replace existing endpoint 417 | .addPropertyToSectionSearchByTypeAndIdentifier("job", "externalResourcesJob", "condition", "eq(1,2)") // Disable job 418 | .addPropertyToSectionSearchByTypeAndIdentifier("script", null, "enabled", "false") // Disable step 419 | .startPipeline(true); // Do not run, because it would fail (no valid endpoints) 420 | } 421 | catch (IOException e) { 422 | logger.debug("Exception occurred after the pipeline was started: {}", e.getMessage()); 423 | } 424 | } 425 | } 426 | --------------------------------------------------------------------------------