├── Dojofile ├── settings.gradle ├── json_config_repo_id.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── update-gradle-wrapper.yml │ ├── pr_workflow.yml │ └── test_and_build.yml ├── src ├── main │ ├── java │ │ └── com.tw.go.config.json │ │ │ ├── ConfigDirectoryScanner.java │ │ │ ├── cli │ │ │ ├── RootCmd.java │ │ │ ├── SyntaxCmd.java │ │ │ └── JsonPluginCli.java │ │ │ ├── AntDirectoryScanner.java │ │ │ ├── JSONUtils.java │ │ │ ├── PluginError.java │ │ │ ├── ConfigRepoMessages.java │ │ │ ├── JsonConfigHelper.java │ │ │ ├── PluginSettings.java │ │ │ ├── JsonConfigParser.java │ │ │ ├── FilenameMatcher.java │ │ │ ├── Capabilities.java │ │ │ ├── ConfigDirectoryParser.java │ │ │ ├── JsonConfigCollection.java │ │ │ ├── ParsedRequest.java │ │ │ └── JsonConfigPlugin.java │ └── resources │ │ ├── plugin-settings.template.html │ │ └── json.svg └── test │ └── java │ └── com.tw.go.config.json │ ├── AntDirectoryScannerTest.java │ ├── JsonConfigCollectionTest.java │ ├── ConfigDirectoryParserTest.java │ └── JsonConfigPluginTest.java ├── .gitignore ├── CHANGELOG.md ├── gradlew.bat ├── gradlew ├── LICENSE └── README.md /Dojofile: -------------------------------------------------------------------------------- 1 | DOJO_DOCKER_IMAGE=kudulab/openjdk-dojo:1.4.1 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'json-config-plugin' 2 | -------------------------------------------------------------------------------- /json_config_repo_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomzo/gocd-json-config-plugin/HEAD/json_config_repo_id.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomzo/gocd-json-config-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PR should contain: 2 | - tests of new/changed behavior 3 | - documentation if adding new feature 4 | - added change summary in CHANGELOG.md 5 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/ConfigDirectoryScanner.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import java.io.File; 4 | 5 | public interface ConfigDirectoryScanner { 6 | String[] getFilesMatchingPattern(File directory,String pattern); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/cli/RootCmd.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json.cli; 2 | 3 | import com.beust.jcommander.Parameter; 4 | 5 | class RootCmd { 6 | @Parameter(names = {"--help", "-h"}, help = true, description = "Print this help message") 7 | boolean help; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/cli/SyntaxCmd.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json.cli; 2 | 3 | import com.beust.jcommander.Parameter; 4 | 5 | class SyntaxCmd { 6 | @Parameter(names = {"--help", "-h"}, help = true, description = "Print this help message") 7 | boolean help; 8 | 9 | @Parameter(description = "file", required = true) 10 | String file; 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: gradle 12 | directory: / 13 | schedule: 14 | interval: monthly 15 | groups: 16 | gradle-deps: 17 | patterns: 18 | - "*" 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/AntDirectoryScanner.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import org.apache.tools.ant.DirectoryScanner; 4 | 5 | import java.io.File; 6 | 7 | public class AntDirectoryScanner implements ConfigDirectoryScanner { 8 | @Override 9 | public String[] getFilesMatchingPattern(File directory, String pattern) { 10 | DirectoryScanner scanner = new DirectoryScanner(); 11 | scanner.setBasedir(directory); 12 | scanner.setIncludes(pattern.trim().split(" *, *")); 13 | scanner.scan(); 14 | return scanner.getIncludedFiles(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/JSONUtils.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.reflect.TypeToken; 7 | 8 | import java.util.Map; 9 | 10 | class JSONUtils { 11 | 12 | private static final Gson GSON = new GsonBuilder().create(); 13 | 14 | static Map fromJSON(String json) { 15 | return GSON.fromJson(json, new TypeToken>() {}.getType()); 16 | } 17 | 18 | static String toJSON(Object object) { 19 | return GSON.toJson(object); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 1 * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Harden the runner (Audit all outbound calls) 13 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 14 | with: 15 | egress-policy: audit 16 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 17 | - name: Update Gradle Wrapper 18 | uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0 19 | with: 20 | labels: dependencies 21 | -------------------------------------------------------------------------------- /src/main/resources/plugin-settings.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{ GOINPUTNAME[pipeline_pattern].$error.server }} 5 |
6 |
7 | 8 | 9 | {{ GOINPUTNAME[environment_pattern].$error.server }} 10 |
11 | -------------------------------------------------------------------------------- /.github/workflows/pr_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Testing For PRs 2 | 3 | on: [ pull_request ] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Harden the runner (Audit all outbound calls) 13 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 14 | with: 15 | egress-policy: audit 16 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 17 | - name: Set up JDK 18 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 19 | with: 20 | java-version: 21 21 | distribution: temurin 22 | cache: gradle 23 | - name: Build with Gradle 24 | run: ./gradlew assemble check 25 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/PluginError.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | public class PluginError { 4 | private String location; 5 | private String message; 6 | 7 | public PluginError(){} 8 | public PluginError(String message){ 9 | this.message = message; 10 | } 11 | public PluginError(String message,String location) 12 | { 13 | this.location = location; 14 | this.message = message; 15 | } 16 | 17 | public String getLocation() { 18 | return location; 19 | } 20 | 21 | public void setLocation(String location) { 22 | this.location = location; 23 | } 24 | 25 | public String getMessage() { 26 | return message; 27 | } 28 | 29 | public void setMessage(String message) { 30 | this.message = message; 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/ConfigRepoMessages.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | public interface ConfigRepoMessages { 4 | String REQ_GET_PLUGIN_SETTINGS = "go.processor.plugin-settings.get"; 5 | String REQ_GET_CAPABILITIES = "get-capabilities"; 6 | String REQ_PLUGIN_SETTINGS_CHANGED = "go.plugin-settings.plugin-settings-changed"; 7 | String REQ_PLUGIN_SETTINGS_GET_CONFIGURATION = "go.plugin-settings.get-configuration"; 8 | String REQ_PLUGIN_SETTINGS_GET_VIEW = "go.plugin-settings.get-view"; 9 | String REQ_PLUGIN_SETTINGS_VALIDATE_CONFIGURATION = "go.plugin-settings.validate-configuration"; 10 | String REQ_PARSE_DIRECTORY = "parse-directory"; 11 | String REQ_PARSE_CONTENT = "parse-content"; 12 | String REQ_PIPELINE_EXPORT = "pipeline-export"; 13 | String REQ_GET_ICON = "get-icon"; 14 | String REQ_CONFIG_FILES = "config-files"; 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/test_and_build.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | inputs: 8 | prerelease: 9 | description: 'The release should be an experimental release' 10 | default: 'NO' 11 | required: true 12 | 13 | jobs: 14 | test-release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Harden the runner (Audit all outbound calls) 18 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 19 | with: 20 | egress-policy: audit 21 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 22 | with: 23 | fetch-depth: 0 24 | - name: Set up JDK 25 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 26 | with: 27 | java-version: 21 28 | distribution: temurin 29 | cache: gradle 30 | - name: Test with Gradle 31 | run: ./gradlew assemble check 32 | - name: Release with Gradle 33 | run: ./gradlew githubRelease 34 | env: 35 | GITHUB_USER: "tomzo" 36 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 37 | PRERELEASE: "${{ github.event.inputs.prerelease }}" -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/JsonConfigHelper.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 4 | import com.thoughtworks.go.plugin.api.request.GoApiRequest; 5 | 6 | import java.util.Map; 7 | 8 | class JsonConfigHelper { 9 | private JsonConfigHelper() { 10 | } 11 | 12 | static GoApiRequest request(final String api, final String responseBody, GoPluginIdentifier identifier) { 13 | return new GoApiRequest() { 14 | @Override 15 | public String api() { 16 | return api; 17 | } 18 | 19 | @Override 20 | public String apiVersion() { 21 | return "1.0"; 22 | } 23 | 24 | @Override 25 | public GoPluginIdentifier pluginIdentifier() { 26 | return identifier; 27 | } 28 | 29 | @Override 30 | public Map requestParameters() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public Map requestHeaders() { 36 | return null; 37 | } 38 | 39 | @Override 40 | public String requestBody() { 41 | return responseBody; 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/PluginSettings.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import java.util.Map; 4 | 5 | class PluginSettings { 6 | static final String DEFAULT_PIPELINE_PATTERN = "**/*.gopipeline.json"; 7 | static final String DEFAULT_ENVIRONMENT_PATTERN = "**/*.goenvironment.json"; 8 | static final String PLUGIN_SETTINGS_PIPELINE_PATTERN = "pipeline_pattern"; 9 | static final String PLUGIN_SETTINGS_ENVIRONMENT_PATTERN = "environment_pattern"; 10 | 11 | private String pipelinePattern; 12 | private String environmentPattern; 13 | 14 | PluginSettings() { 15 | this(DEFAULT_PIPELINE_PATTERN, DEFAULT_ENVIRONMENT_PATTERN); 16 | } 17 | 18 | PluginSettings(String pipelinePattern, String environmentPattern) { 19 | this.pipelinePattern = pipelinePattern; 20 | this.environmentPattern = environmentPattern; 21 | } 22 | 23 | static PluginSettings fromJson(String json) { 24 | Map raw = JSONUtils.fromJSON(json); 25 | return new PluginSettings( 26 | raw.get(PLUGIN_SETTINGS_PIPELINE_PATTERN), 27 | raw.get(PLUGIN_SETTINGS_ENVIRONMENT_PATTERN)); 28 | 29 | } 30 | 31 | String getPipelinePattern() { 32 | return pipelinePattern; 33 | } 34 | 35 | String getEnvironmentPattern() { 36 | return environmentPattern; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/JsonConfigParser.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParseException; 6 | import com.google.gson.JsonParser; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | 12 | import static java.lang.String.format; 13 | 14 | public class JsonConfigParser { 15 | public static JsonElement parseStream(JsonConfigCollection result, InputStream input, String location) { 16 | try (InputStreamReader contentReader = new InputStreamReader(input)) { 17 | if (input.available() < 1) { 18 | result.addError(new PluginError("File is empty", location)); 19 | return null; 20 | } 21 | 22 | JsonElement el = JsonParser.parseReader(contentReader); 23 | 24 | if (el == null || el.isJsonNull()) { 25 | PluginError error = new PluginError("File is empty", location); 26 | result.addError(error); 27 | } else if (el.equals(new JsonObject())) { 28 | PluginError error = new PluginError("Definition is empty", location); 29 | result.addError(error); 30 | } else { 31 | return el; 32 | } 33 | } catch (IOException | JsonParseException e) { 34 | PluginError error = new PluginError(format("Failed to parse file as JSON: %s", e.getMessage()), location); 35 | result.addError(error); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/FilenameMatcher.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import org.apache.tools.ant.types.selectors.SelectorUtils; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Convenience class that matches the filename only (not full paths) against 9 | * pipeline and environment patterns. 10 | */ 11 | class FilenameMatcher { 12 | private final String pipelineFilePattern; 13 | private final String environmentFilePattern; 14 | 15 | FilenameMatcher(String pipelineFilePattern, String environmentFilePattern) { 16 | this.pipelineFilePattern = basename(pipelineFilePattern); 17 | this.environmentFilePattern = basename(environmentFilePattern); 18 | } 19 | 20 | /** 21 | * @param pathPattern the path-matching pattern 22 | * @return a pattern that only matches the filename (i.e., basename) 23 | */ 24 | private static String basename(String pathPattern) { 25 | return new File(normalizePattern(pathPattern)).getName(); 26 | } 27 | 28 | /** 29 | * Ripped from {@link org.apache.tools.ant.DirectoryScanner} 30 | */ 31 | private static String normalizePattern(String p) { 32 | String pattern = p.replace('/', File.separatorChar) 33 | .replace('\\', File.separatorChar); 34 | if (pattern.endsWith(File.separator)) { 35 | pattern += "**"; 36 | } 37 | return pattern; 38 | } 39 | 40 | boolean isPipelineFile(String filename) { 41 | return SelectorUtils.match(pipelineFilePattern, filename); 42 | } 43 | 44 | boolean isEnvironmentFile(String filename) { 45 | return SelectorUtils.match(environmentFilePattern, filename); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/Capabilities.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class Capabilities { 7 | @Expose 8 | @SerializedName("supports_pipeline_export") 9 | private boolean supportsPipelineExport; 10 | 11 | @Expose 12 | @SerializedName("supports_parse_content") 13 | private boolean supportsParseContent; 14 | 15 | @Expose 16 | @SerializedName("supports_list_config_files") 17 | private boolean supportsListConfigFiles; 18 | 19 | @Expose 20 | @SerializedName("supports_user_defined_properties") 21 | private boolean supportsUserDefinedProperties; 22 | 23 | public Capabilities() { 24 | this.supportsPipelineExport = true; 25 | this.supportsParseContent = true; 26 | this.supportsListConfigFiles = true; 27 | this.supportsUserDefinedProperties = false; 28 | } 29 | 30 | public boolean isSupportsPipelineExport() { 31 | return supportsPipelineExport; 32 | } 33 | 34 | public void setSupportsPipelineExport(boolean supportsPipelineExport) { 35 | this.supportsPipelineExport = supportsPipelineExport; 36 | } 37 | 38 | public boolean isSupportsParseContent() { 39 | return supportsParseContent; 40 | } 41 | 42 | public void setSupportsParseContent(boolean supportsParseContent) { 43 | this.supportsParseContent = supportsParseContent; 44 | } 45 | 46 | public boolean isSupportsListConfigFiles() { 47 | return supportsListConfigFiles; 48 | } 49 | 50 | public void setSupportsListConfigFiles(boolean supportsListConfigFiles) { 51 | this.supportsListConfigFiles = supportsListConfigFiles; 52 | } 53 | 54 | public boolean isSupportsUserDefinedProperties() { 55 | return supportsUserDefinedProperties; 56 | } 57 | 58 | public void setSupportsUserDefinedProperties(boolean supportsUserDefinedProperties) { 59 | this.supportsUserDefinedProperties = supportsUserDefinedProperties; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/ConfigDirectoryParser.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileNotFoundException; 8 | 9 | class ConfigDirectoryParser { 10 | private ConfigDirectoryScanner scanner; 11 | private JsonConfigParser parser; 12 | private String pipelinePattern; 13 | private String environmentPattern; 14 | 15 | ConfigDirectoryParser(ConfigDirectoryScanner scanner, JsonConfigParser parser, String pipelinePattern, String environmentPattern) { 16 | this.scanner = scanner; 17 | this.parser = parser; 18 | this.pipelinePattern = pipelinePattern; 19 | this.environmentPattern = environmentPattern; 20 | } 21 | 22 | JsonConfigCollection parseDirectory(File baseDir) { 23 | JsonConfigCollection config = new JsonConfigCollection(); 24 | File currentFile; 25 | 26 | try { 27 | for (String environmentFile : scanner.getFilesMatchingPattern(baseDir, environmentPattern)) { 28 | currentFile = new File(baseDir, environmentFile); 29 | JsonElement environment = JsonConfigParser.parseStream(config, new FileInputStream(currentFile), currentFile.getPath()); 30 | if (null != environment) { 31 | config.addEnvironment(environment, environmentFile); 32 | } 33 | } 34 | 35 | for (String pipelineFile : scanner.getFilesMatchingPattern(baseDir, pipelinePattern)) { 36 | currentFile = new File(baseDir, pipelineFile); 37 | JsonElement pipeline = JsonConfigParser.parseStream(config, new FileInputStream(currentFile), currentFile.getPath()); 38 | if (null != pipeline) { 39 | config.addPipeline(pipeline, pipelineFile); 40 | } 41 | } 42 | } catch (FileNotFoundException e) { 43 | config.addError(new PluginError(e.getMessage())); 44 | } 45 | 46 | return config; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Gradle ### 4 | .gradle 5 | /build/ 6 | 7 | # Ignore Gradle GUI config 8 | gradle-app.setting 9 | 10 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 11 | !gradle-wrapper.jar 12 | 13 | # Cache of project 14 | .gradletasknamecache 15 | 16 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 17 | # gradle/wrapper/gradle-wrapper.properties 18 | 19 | ### Gradle Patch ### 20 | **/build/ 21 | 22 | # End of https://www.gitignore.io/api/gradle 23 | json-plugin-test-dir/ 24 | 25 | ### Java ### 26 | *.class 27 | 28 | # Mobile Tools for Java (J2ME) 29 | .mtj.tmp/ 30 | 31 | # Package Files # 32 | target/ 33 | build/ 34 | 35 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 36 | hs_err_pid* 37 | 38 | ### Intellij ### 39 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 40 | 41 | *.iml 42 | 43 | ## Directory-based project format: 44 | .idea/ 45 | # if you remove the above rule, at least ignore the following: 46 | 47 | # User-specific stuff: 48 | # .idea/workspace.xml 49 | # .idea/tasks.xml 50 | # .idea/dictionaries 51 | 52 | # Sensitive or high-churn files: 53 | # .idea/dataSources.ids 54 | # .idea/dataSources.xml 55 | # .idea/sqlDataSources.xml 56 | # .idea/dynamic.xml 57 | # .idea/uiDesigner.xml 58 | 59 | # Gradle: 60 | # .idea/gradle.xml 61 | # .idea/libraries 62 | 63 | # Mongo Explorer plugin: 64 | # .idea/mongoSettings.xml 65 | 66 | ## File-based project format: 67 | *.ipr 68 | *.iws 69 | 70 | ## Plugin-specific files: 71 | 72 | # IntelliJ 73 | /out/ 74 | 75 | # mpeltonen/sbt-idea plugin 76 | .idea_modules/ 77 | 78 | # JIRA plugin 79 | atlassian-ide-plugin.xml 80 | 81 | # Crashlytics plugin (for Android Studio and IntelliJ) 82 | com_crashlytics_export_strings.xml 83 | crashlytics.properties 84 | crashlytics-build.properties 85 | 86 | 87 | ### Maven ### 88 | target/ 89 | pom.xml.tag 90 | pom.xml.releaseBackup 91 | pom.xml.versionsBackup 92 | pom.xml.next 93 | release.properties 94 | dependency-reduced-pom.xml 95 | buildNumber.properties 96 | /docker/imagerc* 97 | /docker/libs/ 98 | /Idefile.to_be_tested 99 | /src/test/integration/test_ide_work/json-example/ 100 | src/main/resources-generated/ 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.6.1+ 2 | 3 | Please see the [releases page](https://github.com/tomzo/gocd-json-config-plugin/releases) for changelogs. 4 | 5 | ### 0.6.0 (2022-Jul-31) 6 | * Upgrade all dependencies to latest, patched versions 7 | 8 | ### 0.5.0 (2019-Oct-02) 9 | 10 | * Added support for `ignore_for_scheduling` to dependency materials. Updated README with changes in new Format Versions. 11 | * Updated library dependencies 12 | 13 | ### 0.4.3 (2019-Aug-22) 14 | 15 | * updated README documenting `allow_only_on_success` attribute in approval 16 | 17 | ### 0.4.2 (2019-Aug-21) 18 | 19 | Release deleted. 20 | 21 | ### 0.4.1 (2019-Aug-06) 22 | Fixup to endpoint that lists config files 23 | 24 | ### 0.4.0 (2019-Aug-02) 25 | 26 | * Adding endpoint that lists config files for the given directory 27 | 28 | ### 0.3.9 (2019-May-22) 29 | 30 | * updated README documenting support credentials attributes in materials 31 | 32 | ### 0.3.8 (2019-May-01) 33 | 34 | * switch build system to use open source openjdk-dojo image \#17574 35 | * remove docker image from this repo, use [new image](https://github.com/gocd-contrib/docker-gocd-cli-dojo) with gocd-cli 36 | 37 | ### 0.3.7 (2019-Feb-12) 38 | 39 | * fix repo-level configuration not being applied \#40 40 | * Generate reproducible binaries \#38 41 | 42 | # 0.3.6 (21 Jan 2019) 43 | 44 | * Changed JSON keys returned by `get-capabilities` call 45 | * Changed JSON structure returned by `parse-content` call 46 | * Implemented a new `get-icon` call that will return the icon for this plugin 47 | 48 | # 0.3.5 (15 Jan 2019) 49 | 50 | * return json from CLI command 51 | 52 | # 0.3.4 (09 Jan 2019) 53 | 54 | * Add export content metadata 55 | * Fix plugin settings request and implement handler for plugin config change notification 56 | 57 | # 0.3.3 (03 Jan 2019) 58 | 59 | * Added support for `parse-content`. 60 | 61 | # 0.3.2 (Dec 10 2018) 62 | 63 | Accept stdin input in CLI tool 64 | 65 | # 0.3.1 (Dec 5 2018) 66 | 67 | Adds CLI to validate local files. 68 | 69 | # 0.3.0 (Nov 12 2018) 70 | 71 | Adds config-repo API 2.0 and ability to export XML pipelines to JSON 72 | 73 | # 0.2.1 (Oct 24 2017) 74 | 75 | * adds recommended `format_version` to all files 76 | * (documentation change only) support for referencing templates and parameters, with GoCD >= 17.11 77 | 78 | # 0.2.0 (Jun 21 2016) 79 | 80 | Initial release 81 | -------------------------------------------------------------------------------- /src/main/resources/json.svg: -------------------------------------------------------------------------------- 1 | file_type_json2 -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/JsonConfigCollection.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | public class JsonConfigCollection { 9 | private static final int DEFAULT_VERSION = 1; 10 | private final Gson gson; 11 | 12 | private JsonObject mainObject = new JsonObject(); 13 | private JsonArray environments = new JsonArray(); 14 | private JsonArray pipelines = new JsonArray(); 15 | private JsonArray errors = new JsonArray(); 16 | 17 | public JsonConfigCollection() 18 | { 19 | gson = new Gson(); 20 | 21 | updateVersionTo(DEFAULT_VERSION); 22 | mainObject.add("environments",environments); 23 | mainObject.add("pipelines",pipelines); 24 | mainObject.add("errors",errors); 25 | } 26 | 27 | protected JsonArray getEnvironments() 28 | { 29 | return environments; 30 | } 31 | 32 | public void addEnvironment(JsonElement environment,String location) { 33 | environments.add(environment); 34 | environment.getAsJsonObject().add("location",new JsonPrimitive(location)); 35 | } 36 | 37 | public JsonObject getJsonObject() 38 | { 39 | return mainObject; 40 | } 41 | 42 | public void addPipeline(JsonElement pipeline,String location) { 43 | pipelines.add(pipeline); 44 | pipeline.getAsJsonObject().add("location",new JsonPrimitive(location)); 45 | } 46 | 47 | public JsonArray getPipelines() { 48 | return pipelines; 49 | } 50 | 51 | public JsonArray getErrors() { 52 | return errors; 53 | } 54 | 55 | public void addError(PluginError error) { 56 | errors.add(gson.toJsonTree(error)); 57 | } 58 | 59 | public void updateVersionFromPipelinesAndEnvironments() { 60 | Set uniqueVersions = new HashSet<>(); 61 | 62 | for (JsonElement pipeline : pipelines) { 63 | JsonElement versionElement = pipeline.getAsJsonObject().get("format_version"); 64 | uniqueVersions.add(versionElement == null ? DEFAULT_VERSION : versionElement.getAsInt()); 65 | } 66 | 67 | for (JsonElement environment : environments) { 68 | JsonElement versionElement = environment.getAsJsonObject().get("format_version"); 69 | uniqueVersions.add(versionElement == null ? DEFAULT_VERSION : versionElement.getAsInt()); 70 | } 71 | 72 | if (uniqueVersions.size() > 1) { 73 | throw new RuntimeException("Versions across files are not unique. Found versions: " + uniqueVersions + ". There can only be one version across the whole repository."); 74 | } 75 | updateVersionTo(uniqueVersions.iterator().hasNext() ? uniqueVersions.iterator().next() : DEFAULT_VERSION); 76 | } 77 | 78 | private void updateVersionTo(int version) { 79 | mainObject.remove("target_version"); 80 | mainObject.add("target_version", new JsonPrimitive(version)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/test/java/com.tw.go.config.json/AntDirectoryScannerTest.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.io.TempDir; 6 | 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | 10 | import static java.util.Arrays.asList; 11 | import static org.hamcrest.CoreMatchers.hasItems; 12 | import static org.hamcrest.CoreMatchers.is; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | 15 | public class AntDirectoryScannerTest { 16 | @TempDir 17 | public Path tempDir; 18 | 19 | private AntDirectoryScanner scanner; 20 | 21 | @BeforeEach 22 | public void setUp() { 23 | scanner = new AntDirectoryScanner(); 24 | } 25 | 26 | @Test 27 | public void shouldMatchSimplePattern() throws Exception { 28 | Files.createFile(tempDir.resolve("abc.xml")); 29 | Files.createFile(tempDir.resolve("def.xml")); 30 | Files.createFile(tempDir.resolve("ghi.txt")); 31 | 32 | String[] xmlFilesOnly = scanner.getFilesMatchingPattern(tempDir.toFile(), "*.xml"); 33 | 34 | assertThat(xmlFilesOnly.length, is(2)); 35 | assertThat(asList(xmlFilesOnly), hasItems("abc.xml", "def.xml")); 36 | } 37 | 38 | @Test 39 | public void shouldMatchPatternInDirectory() throws Exception { 40 | Files.createDirectories(tempDir.resolve("1").resolve("a")); 41 | Files.createDirectories(tempDir.resolve("2").resolve("d")); 42 | Files.createDirectories(tempDir.resolve("3")); 43 | Files.createDirectories(tempDir.resolve("4")); 44 | 45 | Files.createFile(tempDir.resolve("1/a/abc.xml")); 46 | Files.createFile(tempDir.resolve("2/d/def.xml")); 47 | Files.createFile(tempDir.resolve("3/ghi.xml")); 48 | Files.createFile(tempDir.resolve("4/jkl.txt")); 49 | Files.createFile(tempDir.resolve("mno.txt")); 50 | Files.createFile(tempDir.resolve("pqr.xml")); 51 | 52 | String[] xmlFilesOnly = scanner.getFilesMatchingPattern(tempDir.toFile(), "**/*.xml"); 53 | 54 | assertThat(xmlFilesOnly.length, is(4)); 55 | assertThat(asList(xmlFilesOnly), hasItems("1/a/abc.xml", "2/d/def.xml", "3/ghi.xml", "pqr.xml")); 56 | } 57 | 58 | @Test 59 | public void shouldIgnoreSpacesAroundThePattern() throws Exception { 60 | Files.createFile(tempDir.resolve("def.xml")); 61 | Files.createFile(tempDir.resolve("ghi.txt")); 62 | 63 | String[] xmlFilesOnly = scanner.getFilesMatchingPattern(tempDir.toFile(), " *.xml "); 64 | 65 | assertThat(xmlFilesOnly.length, is(1)); 66 | assertThat(asList(xmlFilesOnly), hasItems("def.xml")); 67 | } 68 | 69 | @Test 70 | public void shouldAcceptMultiplePatternsSeparatedByComma() throws Exception { 71 | Files.createDirectories(tempDir.resolve("1").resolve("a")); 72 | Files.createDirectories(tempDir.resolve("2").resolve("d")); 73 | Files.createDirectories(tempDir.resolve("3")); 74 | Files.createDirectories(tempDir.resolve("4")); 75 | 76 | 77 | Files.createFile(tempDir.resolve("1/a/abc.xml")); 78 | Files.createFile(tempDir.resolve("2/d/def.gif")); 79 | Files.createFile(tempDir.resolve("3/ghi.xml")); 80 | Files.createFile(tempDir.resolve("4/jkl.txt")); 81 | Files.createFile(tempDir.resolve("mno.jpg")); 82 | Files.createFile(tempDir.resolve("4/pqr.jpg")); 83 | Files.createFile(tempDir.resolve("stu.xml")); 84 | 85 | String[] xmlFilesOnly = scanner.getFilesMatchingPattern(tempDir.toFile(), "**/*.xml, **/*.gif, *.jpg"); 86 | 87 | assertThat(xmlFilesOnly.length, is(5)); 88 | assertThat(asList(xmlFilesOnly), hasItems("1/a/abc.xml", "2/d/def.gif", "3/ghi.xml", "mno.jpg", "stu.xml")); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/cli/JsonPluginCli.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json.cli; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import com.beust.jcommander.ParameterException; 5 | import com.google.gson.JsonObject; 6 | import com.tw.go.config.json.JsonConfigCollection; 7 | import com.tw.go.config.json.JsonConfigParser; 8 | import com.tw.go.config.json.PluginError; 9 | 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileNotFoundException; 13 | import java.io.InputStream; 14 | 15 | import static java.lang.String.format; 16 | 17 | public class JsonPluginCli { 18 | public static void main(String[] args) { 19 | RootCmd root = new RootCmd(); 20 | SyntaxCmd syntax = new SyntaxCmd(); 21 | 22 | JCommander cmd = JCommander.newBuilder(). 23 | programName("json-cli"). 24 | addObject(root). 25 | addCommand("syntax", syntax). 26 | build(); 27 | 28 | try { 29 | cmd.parse(args); 30 | 31 | if (root.help) { 32 | printUsageAndExit(0, cmd, cmd.getParsedCommand()); 33 | } 34 | 35 | if (syntax.help) { 36 | printUsageAndExit(0, cmd, cmd.getParsedCommand()); 37 | } 38 | 39 | if (null == syntax.file) { 40 | printUsageAndExit(1, cmd, cmd.getParsedCommand()); 41 | } 42 | } catch (ParameterException e) { 43 | error(e.getMessage()); 44 | printUsageAndExit(1, cmd, cmd.getParsedCommand()); 45 | } 46 | 47 | JsonConfigCollection collection = new JsonConfigCollection(); 48 | JsonObject result = collection.getJsonObject(); 49 | 50 | String location = getLocation(syntax.file); 51 | 52 | try { 53 | JsonConfigParser.parseStream(collection, getFileAsStream(syntax.file), location); 54 | } catch (Exception e) { 55 | collection.addError(new PluginError(e.getMessage(), location)); 56 | } 57 | 58 | result.remove("environments"); 59 | result.remove("pipelines"); 60 | 61 | if (collection.getErrors().size() > 0) { 62 | result.addProperty("valid", false); 63 | die(1, result.toString()); 64 | } else { 65 | die(0, "{\"valid\":true}"); 66 | } 67 | } 68 | 69 | private static String getLocation(String file) { 70 | return "-".equals(file) ? "" : file; 71 | } 72 | 73 | private static InputStream getFileAsStream(String file) { 74 | InputStream s = null; 75 | try { 76 | s = "-".equals(file) ? System.in : new FileInputStream(new File(file)); 77 | } catch (FileNotFoundException e) { 78 | die(1, e.getMessage()); 79 | } 80 | return s; 81 | } 82 | 83 | private static void echo(String message, Object... items) { 84 | System.out.println(format(message, items)); 85 | } 86 | 87 | private static void error(String message, Object... items) { 88 | System.err.println(format(message, items)); 89 | } 90 | 91 | private static void die(int exitCode, String message, Object... items) { 92 | if (exitCode != 0) { 93 | error(message, items); 94 | } else { 95 | echo(message, items); 96 | } 97 | System.exit(exitCode); 98 | } 99 | 100 | private static void printUsageAndExit(int exitCode, JCommander cmd, String command) { 101 | StringBuilder out = new StringBuilder(); 102 | if (null == command) { 103 | cmd.getUsageFormatter().usage(out); 104 | } else { 105 | cmd.getUsageFormatter().usage(command, out); 106 | } 107 | die(exitCode, out.toString()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/ParsedRequest.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | 4 | import com.google.gson.*; 5 | import com.google.gson.reflect.TypeToken; 6 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 7 | 8 | import java.util.Map; 9 | 10 | import static java.lang.String.format; 11 | 12 | class ParsedRequest { 13 | private static final Gson GSON = new Gson(); 14 | private static final String EMPTY_REQUEST_BODY_MESSAGE = "Request body cannot be null or empty"; 15 | private static final String INVALID_JSON = "Request body must be valid JSON string"; 16 | private static final String MISSING_PARAM_MESSAGE = "`%s` property is missing in `%s` request"; 17 | private static final String PARAM_NOT_A_STRING_MESSAGE = "Expected `%s` param in request type `%s` to be a string"; 18 | private static final String PARAM_FAILED_TO_PARSE_TO_TYPE = "Failed to parse parameter `%s` for request type `%s`: %s"; 19 | private static final String PARAM_CONFIGURATIONS = "configurations"; 20 | private final String requestName; 21 | private final JsonObject params; 22 | 23 | private ParsedRequest(String requestName, JsonObject params) { 24 | this.requestName = requestName; 25 | this.params = params; 26 | } 27 | 28 | static ParsedRequest parse(GoPluginApiRequest req) { 29 | String requestBody = req.requestBody(); 30 | 31 | if (null == requestBody || requestBody.trim().isEmpty()) { 32 | throw new RequestParseException(EMPTY_REQUEST_BODY_MESSAGE); 33 | } 34 | 35 | JsonElement parsed; 36 | try { 37 | parsed = JsonParser.parseString(requestBody); 38 | } catch (JsonParseException e) { 39 | throw new RequestParseException(INVALID_JSON, e); 40 | } 41 | 42 | if (parsed.equals(new JsonObject())) { 43 | throw new RequestParseException(EMPTY_REQUEST_BODY_MESSAGE); 44 | } 45 | 46 | try { 47 | return new ParsedRequest(req.requestName(), parsed.getAsJsonObject()); 48 | } catch (Exception e) { 49 | throw new ParsedRequest.RequestParseException(e); 50 | } 51 | } 52 | 53 | String getStringParam(String paramName) { 54 | params.getAsJsonPrimitive(paramName); 55 | 56 | JsonPrimitive paramValue; 57 | try { 58 | paramValue = params.getAsJsonPrimitive(paramName); 59 | } catch (Exception e) { 60 | throw new RequestParseException(e); 61 | } 62 | 63 | if (null == paramValue) { 64 | throw new RequestParseException(format(MISSING_PARAM_MESSAGE, paramName, requestName)); 65 | } 66 | 67 | try { 68 | return paramValue.getAsString(); 69 | } catch (Exception e) { 70 | throw new RequestParseException(format(PARAM_NOT_A_STRING_MESSAGE, paramName, requestName)); 71 | } 72 | } 73 | 74 | Map getParam(String paramName, Class valueClass) { 75 | try { 76 | JsonElement json = params.get(paramName); 77 | 78 | if (null == json || json.isJsonNull()) { 79 | throw new RequestParseException(format(MISSING_PARAM_MESSAGE, paramName, requestName)); 80 | } 81 | 82 | return GSON.fromJson(json, TypeToken.getParameterized(Map.class, String.class, valueClass).getType()); 83 | } catch (Exception e) { 84 | throw new RequestParseException(format(PARAM_FAILED_TO_PARSE_TO_TYPE, paramName, requestName, e.getMessage())); 85 | } 86 | } 87 | 88 | String getConfigurationKey(String keyName) { 89 | String value = null; 90 | 91 | try { 92 | JsonArray perRepoConfig = params.getAsJsonArray(PARAM_CONFIGURATIONS); 93 | 94 | if (perRepoConfig != null) { 95 | for (JsonElement config : perRepoConfig) { 96 | JsonObject configObj = config.getAsJsonObject(); 97 | String key = configObj.getAsJsonPrimitive("key").getAsString(); 98 | 99 | if (key.equals(keyName)) { 100 | value = configObj.getAsJsonPrimitive("value").getAsString(); 101 | } 102 | } 103 | } 104 | } catch (Exception e) { 105 | throw new RequestParseException(e); 106 | } 107 | 108 | return value; 109 | } 110 | 111 | static class RequestParseException extends RuntimeException { 112 | RequestParseException(String message) { 113 | super(message); 114 | } 115 | 116 | RequestParseException(Throwable cause) { 117 | super(cause); 118 | } 119 | 120 | RequestParseException(String message, Throwable cause) { 121 | super(message, cause); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com.tw.go.config.json/JsonConfigCollectionTest.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.hamcrest.CoreMatchers.containsString; 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | public class JsonConfigCollectionTest { 16 | 17 | private JsonConfigCollection jsonCollection; 18 | private JsonObject pipe1; 19 | private JsonObject pipe2; 20 | private JsonObject devEnv; 21 | private JsonObject pipeInGroup; 22 | 23 | @BeforeEach 24 | public void setUp() { 25 | jsonCollection = new JsonConfigCollection(); 26 | 27 | pipe1 = new JsonObject(); 28 | pipe1.addProperty("name", "pipe1"); 29 | 30 | pipe2 = new JsonObject(); 31 | pipe2.addProperty("name", "pipe2"); 32 | 33 | pipeInGroup = new JsonObject(); 34 | pipeInGroup.addProperty("name", "pipe3"); 35 | pipeInGroup.addProperty("group", "mygroup"); 36 | 37 | devEnv = new JsonObject(); 38 | devEnv.addProperty("name", "dev"); 39 | } 40 | 41 | @Test 42 | public void shouldReturnDefaultTargetVersionWhenThereAreNoPipelinesOrEnvironmentsDefined() { 43 | jsonCollection.updateVersionFromPipelinesAndEnvironments(); 44 | JsonObject jsonObject = jsonCollection.getJsonObject(); 45 | assertTargetVersion(jsonObject, 1); 46 | } 47 | 48 | @Test 49 | public void shouldReturnEnvironmentsArrayInJsonObjectWhenEmpty() { 50 | JsonObject jsonObject = jsonCollection.getJsonObject(); 51 | assertThat(jsonObject.get("environments") instanceof JsonArray, is(true)); 52 | assertThat(jsonObject.getAsJsonArray("environments"), is(new JsonArray())); 53 | } 54 | 55 | @Test 56 | public void shouldAppendPipelinesToPipelinesCollection() { 57 | jsonCollection.addPipeline(pipe1, "pipe1.json"); 58 | jsonCollection.addPipeline(pipe2, "pipe2.json"); 59 | JsonObject jsonObject = jsonCollection.getJsonObject(); 60 | assertThat(jsonObject.getAsJsonArray("pipelines").size(), is(2)); 61 | } 62 | 63 | 64 | @Test 65 | public void shouldReturnEnvironmentsInJsonObject() { 66 | jsonCollection.addEnvironment(devEnv, "dev.json"); 67 | JsonObject jsonObject = jsonCollection.getJsonObject(); 68 | assertThat(jsonObject.getAsJsonArray("environments").size(), is(1)); 69 | } 70 | 71 | @Test 72 | public void shouldUpdateTargetVersionWhenItIsTheSameAcrossAllPipelinesAndEnvironments() { 73 | jsonCollection.addPipeline(pipelineWithVersion(2), "pipe1.json"); 74 | jsonCollection.addPipeline(pipelineWithVersion(2), "pipe2.json"); 75 | jsonCollection.addEnvironment(envWithVersion(2), "env1.json"); 76 | jsonCollection.addEnvironment(envWithVersion(2), "env2.json"); 77 | 78 | jsonCollection.updateVersionFromPipelinesAndEnvironments(); 79 | JsonObject jsonObject = jsonCollection.getJsonObject(); 80 | 81 | assertTargetVersion(jsonObject, 2); 82 | } 83 | 84 | @Test 85 | public void shouldUpdateTargetVersionWhenItIsTheDefaultOrMissingAcrossAllPipelinesAndEnvironments() { 86 | int defaultVersionExpected = 1; 87 | 88 | jsonCollection.addPipeline(new JsonObject(), "pipe1.json"); 89 | jsonCollection.addPipeline(pipelineWithVersion(defaultVersionExpected), "pipe2.json"); 90 | jsonCollection.addEnvironment(new JsonObject(), "env1.json"); 91 | jsonCollection.addEnvironment(envWithVersion(defaultVersionExpected), "env2.json"); 92 | 93 | jsonCollection.updateVersionFromPipelinesAndEnvironments(); 94 | JsonObject jsonObject = jsonCollection.getJsonObject(); 95 | 96 | assertTargetVersion(jsonObject, defaultVersionExpected); 97 | } 98 | 99 | @Test 100 | public void shouldFailToUpdateTargetVersionWhenItIs_NOT_TheSameAcrossAllPipelinesAndEnvironments() { 101 | jsonCollection.addPipeline(pipelineWithVersion(1), "pipe1.json"); 102 | jsonCollection.addPipeline(pipelineWithVersion(2), "pipe2.json"); 103 | jsonCollection.addEnvironment(envWithVersion(1), "env1.json"); 104 | jsonCollection.addEnvironment(new JsonObject(), "env2.json"); 105 | 106 | RuntimeException e = assertThrows(RuntimeException.class, () -> jsonCollection.updateVersionFromPipelinesAndEnvironments()); 107 | assertThat(e.getMessage(), containsString("Versions across files are not unique")); 108 | } 109 | 110 | private JsonElement envWithVersion(int version) { 111 | return pipelineWithVersion(version); 112 | } 113 | 114 | private JsonElement pipelineWithVersion(int version) { 115 | JsonObject jsonObject = new JsonObject(); 116 | jsonObject.addProperty("format_version", version); 117 | return jsonObject; 118 | } 119 | 120 | private void assertTargetVersion(JsonObject jsonObject, int expectedVersion) { 121 | assertThat(jsonObject.get("target_version") instanceof JsonPrimitive, is(true)); 122 | assertThat(jsonObject.getAsJsonPrimitive("target_version").getAsInt(), is(expectedVersion)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com.tw.go.config.json/ConfigDirectoryParserTest.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.io.TempDir; 8 | 9 | import java.io.File; 10 | import java.io.FileNotFoundException; 11 | import java.io.PrintWriter; 12 | import java.io.UnsupportedEncodingException; 13 | 14 | import static org.hamcrest.CoreMatchers.is; 15 | import static org.hamcrest.CoreMatchers.startsWith; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertThrows; 19 | 20 | public class ConfigDirectoryParserTest { 21 | @TempDir 22 | private File tempDir; 23 | private ConfigDirectoryParser parser; 24 | private String pipe1String; 25 | private String devenvString; 26 | 27 | @BeforeEach 28 | public void setUp() { 29 | parser = new ConfigDirectoryParser( 30 | new AntDirectoryScanner(), new JsonConfigParser(), 31 | PluginSettings.DEFAULT_PIPELINE_PATTERN, 32 | PluginSettings.DEFAULT_ENVIRONMENT_PATTERN); 33 | 34 | Gson gson = new Gson(); 35 | 36 | JsonObject pipe1 = new JsonObject(); 37 | pipe1.addProperty("name", "pipe1"); 38 | 39 | pipe1String = gson.toJson(pipe1); 40 | 41 | JsonObject devenv = new JsonObject(); 42 | devenv.addProperty("name", "devend"); 43 | devenvString = gson.toJson(devenv); 44 | } 45 | 46 | @Test 47 | public void shouldParseEmptyDirectory() { 48 | parser.parseDirectory(tempDir); 49 | } 50 | 51 | @Test 52 | public void shouldThrowWhenDirectoryDoesNotExist() { 53 | assertThrows(RuntimeException.class, () -> parser.parseDirectory(new File(tempDir, ".doesNotExist"))); 54 | } 55 | 56 | @Test 57 | public void shouldAppendPipelineFromDirectory() throws Exception { 58 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 59 | JsonConfigCollection result = parser.parseDirectory(tempDir); 60 | assertThat(result.getPipelines().size(), is(1)); 61 | } 62 | 63 | @Test 64 | public void shouldAppendErrorsWithLocationWhenInvalidContent() throws Exception { 65 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 66 | createFileWithContent("pipeBad1.gopipeline.json", "bad pipeline"); 67 | JsonConfigCollection result = parser.parseDirectory(tempDir); 68 | assertEquals(1, result.getErrors().size()); 69 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 70 | assertEquals(pathTo("pipeBad1.gopipeline.json"), response.getAsJsonPrimitive("location").getAsString()); 71 | assertThat(response.getAsJsonPrimitive("message").getAsString(), startsWith("Failed to parse file as JSON: ")); 72 | 73 | } 74 | 75 | @Test 76 | public void shouldAppendAllErrorsWhenManyFilesHaveInvalidContent() throws Exception { 77 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 78 | createFileWithContent("pipeBad1.gopipeline.json", "bad pipeline"); 79 | createFileWithContent("pipeBad2.gopipeline.json", "bad pipeline 2"); 80 | 81 | JsonConfigCollection result = parser.parseDirectory(tempDir); 82 | assertThat(result.getErrors().size(), is(2)); 83 | } 84 | 85 | @Test 86 | public void shouldAppendErrorWhenPipelineFileIsEmpty() throws Exception { 87 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 88 | createFileWithContent("pipeBad1.gopipeline.json", ""); 89 | 90 | JsonConfigCollection result = parser.parseDirectory(tempDir); 91 | assertThat(result.getErrors().size(), is(1)); 92 | 93 | assertEquals(1, result.getErrors().size()); 94 | 95 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 96 | assertEquals(pathTo("pipeBad1.gopipeline.json"), response.getAsJsonPrimitive("location").getAsString()); 97 | assertEquals("File is empty", response.getAsJsonPrimitive("message").getAsString()); 98 | } 99 | 100 | @Test 101 | public void shouldAppendErrorWhenPipelineBlockIsEmpty() throws Exception { 102 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 103 | createFileWithContent("pipeBad1.gopipeline.json", "{}"); 104 | 105 | JsonConfigCollection result = parser.parseDirectory(tempDir); 106 | assertEquals(1, result.getErrors().size()); 107 | 108 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 109 | assertEquals(pathTo("pipeBad1.gopipeline.json"), response.getAsJsonPrimitive("location").getAsString()); 110 | assertEquals("Definition is empty", response.getAsJsonPrimitive("message").getAsString()); 111 | } 112 | 113 | @Test 114 | public void shouldAppendEnvironmentFromDirectory() throws Exception { 115 | createFileWithContent("devenv.goenvironment.json", this.devenvString); 116 | JsonConfigCollection result = parser.parseDirectory(tempDir); 117 | assertThat(result.getEnvironments().size(), is(1)); 118 | } 119 | 120 | @Test 121 | public void shouldAppendErrorsWithLocationWhenInvalidContentInEnvironment() throws Exception { 122 | createFileWithContent("devenv.goenvironment.json", this.pipe1String); 123 | createFileWithContent("badEnv.goenvironment.json", "bad environment"); 124 | JsonConfigCollection result = parser.parseDirectory(tempDir); 125 | assertEquals(1, result.getErrors().size()); 126 | 127 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 128 | assertEquals(pathTo("badEnv.goenvironment.json"), response.getAsJsonPrimitive("location").getAsString()); 129 | assertThat(response.getAsJsonPrimitive("message").getAsString(), startsWith("Failed to parse file as JSON: ")); 130 | } 131 | 132 | @Test 133 | public void shouldAppendAllErrorsWhenManyEnvironmentFilesHaveInvalidContent() throws Exception { 134 | createFileWithContent("pipe1.gopipeline.json", this.pipe1String); 135 | createFileWithContent("badEnv.goenvironment.json", "bad env"); 136 | createFileWithContent("badEnv2.goenvironment.json", "bad env 2"); 137 | JsonConfigCollection result = parser.parseDirectory(tempDir); 138 | 139 | assertEquals(2, result.getErrors().size()); 140 | } 141 | 142 | @Test 143 | public void shouldAppendErrorWhenEnvironmentFileIsEmpty() throws Exception { 144 | createFileWithContent("devenv.goenvironment.json", this.devenvString); 145 | createFileWithContent("badEnv.goenvironment.json", ""); 146 | 147 | JsonConfigCollection result = parser.parseDirectory(tempDir); 148 | assertEquals(1, result.getErrors().size()); 149 | 150 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 151 | assertEquals(pathTo("badEnv.goenvironment.json"), response.getAsJsonPrimitive("location").getAsString()); 152 | assertEquals("File is empty", response.getAsJsonPrimitive("message").getAsString()); 153 | } 154 | 155 | @Test 156 | public void shouldThrowErrorWhenEnvironmentBlockIsEmpty() throws Exception { 157 | createFileWithContent("devenv.goenvironment.json", this.devenvString); 158 | createFileWithContent("badEnv.goenvironment.json", "{}"); 159 | 160 | JsonConfigCollection result = parser.parseDirectory(tempDir); 161 | assertEquals(1, result.getErrors().size()); 162 | 163 | JsonObject response = result.getErrors().get(0).getAsJsonObject(); 164 | assertEquals(pathTo("badEnv.goenvironment.json"), response.getAsJsonPrimitive("location").getAsString()); 165 | assertEquals("Definition is empty", response.getAsJsonPrimitive("message").getAsString()); 166 | } 167 | 168 | private void createFileWithContent(String filePath, String content) throws FileNotFoundException, UnsupportedEncodingException { 169 | PrintWriter writer = new PrintWriter(pathTo(filePath), "UTF-8"); 170 | writer.println(content); 171 | writer.close(); 172 | } 173 | 174 | private String pathTo(String child) { 175 | return new File(tempDir, child).getPath(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Tomasz Sętkowski 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com.tw.go.config.json/JsonConfigPlugin.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor; 8 | import com.thoughtworks.go.plugin.api.GoPlugin; 9 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 10 | import com.thoughtworks.go.plugin.api.annotation.Extension; 11 | import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException; 12 | import com.thoughtworks.go.plugin.api.logging.Logger; 13 | import com.thoughtworks.go.plugin.api.request.GoApiRequest; 14 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; 15 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 16 | import com.thoughtworks.go.plugin.api.response.GoApiResponse; 17 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.nio.charset.StandardCharsets; 24 | import java.util.*; 25 | import java.util.function.Supplier; 26 | 27 | import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.*; 28 | import static com.tw.go.config.json.PluginSettings.*; 29 | import static java.lang.String.format; 30 | 31 | @Extension 32 | public class JsonConfigPlugin implements GoPlugin, ConfigRepoMessages { 33 | private static final String DISPLAY_NAME_ENVIRONMENT_PATTERN = "Go environment files pattern"; 34 | private static final String DISPLAY_NAME_PIPELINE_PATTERN = "Go pipeline files pattern"; 35 | private static final String PLUGIN_ID = "json.config.plugin"; 36 | private static Logger LOGGER = Logger.getLoggerFor(JsonConfigPlugin.class); 37 | 38 | private final Gson gson = new Gson(); 39 | private final Gson prettyPrint = new GsonBuilder().setPrettyPrinting().create(); 40 | private GoApplicationAccessor goApplicationAccessor; 41 | private PluginSettings settings; 42 | 43 | private static boolean isBlank(String pattern) { 44 | return pattern == null || pattern.trim().isEmpty(); 45 | } 46 | 47 | @Override 48 | public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) { 49 | this.goApplicationAccessor = goApplicationAccessor; 50 | } 51 | 52 | @Override 53 | public GoPluginApiResponse handle(GoPluginApiRequest request) throws UnhandledRequestTypeException { 54 | String requestName = request.requestName(); 55 | switch (requestName) { 56 | case REQ_PLUGIN_SETTINGS_GET_CONFIGURATION: 57 | return handleGetPluginSettingsConfiguration(); 58 | case REQ_PLUGIN_SETTINGS_GET_VIEW: 59 | try { 60 | return handleGetPluginSettingsView(); 61 | } catch (IOException e) { 62 | return error(gson.toJson(format("Failed to find template: %s", e.getMessage()))); 63 | } 64 | case REQ_PLUGIN_SETTINGS_VALIDATE_CONFIGURATION: 65 | return handleValidatePluginSettingsConfiguration(); 66 | case REQ_PARSE_CONTENT: 67 | ensureConfigured(); 68 | return handleParseContentRequest(request); 69 | case REQ_PARSE_DIRECTORY: 70 | ensureConfigured(); 71 | return handleParseDirectoryRequest(request); 72 | case REQ_CONFIG_FILES: 73 | ensureConfigured(); 74 | return handleGetConfigFiles(request); 75 | case REQ_PIPELINE_EXPORT: 76 | return handlePipelineExportRequest(request); 77 | case REQ_PLUGIN_SETTINGS_CHANGED: 78 | configurePlugin(PluginSettings.fromJson(request.requestBody())); 79 | return new DefaultGoPluginApiResponse(SUCCESS_RESPONSE_CODE, ""); 80 | case REQ_GET_CAPABILITIES: 81 | return success(gson.toJson(new Capabilities())); 82 | case REQ_GET_ICON: 83 | return handleGetIconRequest(); 84 | } 85 | throw new UnhandledRequestTypeException(request.requestName()); 86 | } 87 | 88 | private GoPluginApiResponse handleGetIconRequest() { 89 | try (InputStream is = Objects.requireNonNull(getClass().getResourceAsStream("/json.svg"))) { 90 | JsonObject jsonObject = new JsonObject(); 91 | jsonObject.addProperty("content_type", "image/svg+xml"); 92 | jsonObject.addProperty("data", Base64.getEncoder().encodeToString(is.readAllBytes())); 93 | return success(gson.toJson(jsonObject)); 94 | } catch (IOException e) { 95 | return error(e.getMessage()); 96 | } 97 | } 98 | 99 | String getPipelinePattern(ParsedRequest parseDirectoryReq) { 100 | String configRepoLevelPattern = parseDirectoryReq.getConfigurationKey(PLUGIN_SETTINGS_PIPELINE_PATTERN); 101 | if(!isBlank(configRepoLevelPattern)) 102 | return configRepoLevelPattern; 103 | if (null != settings && !isBlank(settings.getPipelinePattern())) { 104 | return settings.getPipelinePattern(); 105 | } 106 | return DEFAULT_PIPELINE_PATTERN; 107 | } 108 | 109 | String getEnvironmentPattern(ParsedRequest parseDirectoryReq) { 110 | String configRepoLevelPattern = parseDirectoryReq.getConfigurationKey(PLUGIN_SETTINGS_ENVIRONMENT_PATTERN); 111 | if(!isBlank(configRepoLevelPattern)) 112 | return configRepoLevelPattern; 113 | if (null != settings && !isBlank(settings.getEnvironmentPattern())) { 114 | return settings.getEnvironmentPattern(); 115 | } 116 | return DEFAULT_ENVIRONMENT_PATTERN; 117 | } 118 | 119 | /** 120 | * fetches plugin settings if we haven't yet 121 | */ 122 | private void ensureConfigured() { 123 | if (null == settings) { 124 | settings = fetchPluginSettings(); 125 | } 126 | } 127 | 128 | private GoPluginApiResponse handleGetPluginSettingsView() throws IOException { 129 | try (InputStream is = Objects.requireNonNull(getClass().getResourceAsStream("/plugin-settings.template.html"))) { 130 | Map response = new HashMap<>(); 131 | response.put("template", new String(is.readAllBytes(), StandardCharsets.UTF_8)); 132 | return success(gson.toJson(response)); 133 | } 134 | } 135 | private GoPluginApiResponse handleValidatePluginSettingsConfiguration() { 136 | List> response = new ArrayList<>(); 137 | return success(gson.toJson(response)); 138 | } 139 | 140 | private GoPluginApiResponse handleGetPluginSettingsConfiguration() { 141 | Map response = new HashMap<>(); 142 | response.put(PLUGIN_SETTINGS_PIPELINE_PATTERN, createField(DISPLAY_NAME_PIPELINE_PATTERN, DEFAULT_PIPELINE_PATTERN, false, false, "0")); 143 | response.put(PLUGIN_SETTINGS_ENVIRONMENT_PATTERN, createField(DISPLAY_NAME_ENVIRONMENT_PATTERN, DEFAULT_ENVIRONMENT_PATTERN, false, false, "1")); 144 | return success(gson.toJson(response)); 145 | } 146 | 147 | private Map createField(String displayName, String defaultValue, boolean isRequired, boolean isSecure, String displayOrder) { 148 | Map fieldProperties = new HashMap<>(); 149 | fieldProperties.put("display-name", displayName); 150 | fieldProperties.put("default-value", defaultValue); 151 | fieldProperties.put("required", isRequired); 152 | fieldProperties.put("secure", isSecure); 153 | fieldProperties.put("display-order", displayOrder); 154 | return fieldProperties; 155 | } 156 | 157 | private GoPluginApiResponse handleParseContentRequest(GoPluginApiRequest request) { 158 | return handlingErrors(() -> { 159 | ParsedRequest parsed = ParsedRequest.parse(request); 160 | FilenameMatcher matcher = new FilenameMatcher(getPipelinePattern(parsed), getEnvironmentPattern(parsed)); 161 | Map contents = parsed.getParam("contents", String.class); 162 | 163 | JsonConfigCollection result = new JsonConfigCollection(); 164 | contents.forEach((filename, content) -> { 165 | 166 | ByteArrayInputStream contentStream = new ByteArrayInputStream(content.getBytes()); 167 | 168 | if (matcher.isEnvironmentFile(filename)) { 169 | JsonElement env = JsonConfigParser.parseStream(result, contentStream, filename); 170 | if (null != env) { 171 | result.addEnvironment(env, filename); 172 | } 173 | } else if (matcher.isPipelineFile(filename)) { 174 | JsonElement pipe = JsonConfigParser.parseStream(result, contentStream, filename); 175 | if (null != pipe) { 176 | result.addPipeline(pipe, filename); 177 | } 178 | } else { 179 | result.addError(new PluginError("File does not match environment or pipeline pattern", filename)); 180 | } 181 | }); 182 | 183 | result.updateVersionFromPipelinesAndEnvironments(); 184 | 185 | return success(gson.toJson(result.getJsonObject())); 186 | }); 187 | } 188 | 189 | private GoPluginApiResponse handlePipelineExportRequest(GoPluginApiRequest request) { 190 | return handlingErrors(() -> { 191 | ParsedRequest parsed = ParsedRequest.parse(request); 192 | 193 | Map pipeline = parsed.getParam("pipeline", Object.class); 194 | String name = (String) pipeline.get("name"); 195 | 196 | DefaultGoPluginApiResponse response = success(gson.toJson(Collections.singletonMap("pipeline", prettyPrint.toJson(pipeline)))); 197 | 198 | response.addResponseHeader("Content-Type", "application/json; charset=utf-8"); 199 | response.addResponseHeader("X-Export-Filename", name + ".gopipeline.json"); 200 | return response; 201 | }); 202 | } 203 | 204 | private GoPluginApiResponse handleGetConfigFiles(GoPluginApiRequest request) { 205 | return handlingErrors(() -> { 206 | ParsedRequest parsed = ParsedRequest.parse(request); 207 | File baseDir = new File(parsed.getStringParam("directory")); 208 | Map> result = new HashMap<>(); 209 | 210 | ConfigDirectoryScanner scanner = new AntDirectoryScanner(); 211 | ArrayList files = new ArrayList<>(); 212 | files.addAll(Arrays.asList(scanner.getFilesMatchingPattern(baseDir, getPipelinePattern(parsed)))); 213 | files.addAll(Arrays.asList(scanner.getFilesMatchingPattern(baseDir, getEnvironmentPattern(parsed)))); 214 | result.put("files", files); 215 | return success(gson.toJson(result)); 216 | }); 217 | } 218 | 219 | private GoPluginApiResponse handleParseDirectoryRequest(GoPluginApiRequest request) { 220 | return handlingErrors(() -> { 221 | ParsedRequest parsed = ParsedRequest.parse(request); 222 | File baseDir = new File(parsed.getStringParam("directory")); 223 | 224 | JsonConfigParser parser = new JsonConfigParser(); 225 | ConfigDirectoryScanner scanner = new AntDirectoryScanner(); 226 | 227 | ConfigDirectoryParser configDirectoryParser = new ConfigDirectoryParser( 228 | scanner, parser, getPipelinePattern(parsed), getEnvironmentPattern(parsed) 229 | ); 230 | 231 | JsonConfigCollection config = configDirectoryParser.parseDirectory(baseDir); 232 | 233 | config.updateVersionFromPipelinesAndEnvironments(); 234 | JsonObject responseJsonObject = config.getJsonObject(); 235 | 236 | return success(gson.toJson(responseJsonObject)); 237 | }); 238 | } 239 | 240 | private PluginSettings fetchPluginSettings() { 241 | Map requestMap = new HashMap<>(); 242 | requestMap.put("plugin-id", PLUGIN_ID); 243 | GoApiResponse response = goApplicationAccessor.submit(createGoApiRequest(REQ_GET_PLUGIN_SETTINGS, JSONUtils.toJSON(requestMap))); 244 | if (response.responseBody() == null || response.responseBody().trim().isEmpty()) { 245 | return new PluginSettings(); 246 | } 247 | return PluginSettings.fromJson(response.responseBody()); 248 | } 249 | 250 | 251 | private GoPluginApiResponse badRequest(String message) { 252 | JsonObject responseJsonObject = new JsonObject(); 253 | responseJsonObject.addProperty("message", message); 254 | return DefaultGoPluginApiResponse.badRequest(gson.toJson(responseJsonObject)); 255 | } 256 | 257 | private GoPluginApiResponse handlingErrors(Supplier exec) { 258 | try { 259 | return exec.get(); 260 | } catch (ParsedRequest.RequestParseException e) { 261 | return badRequest(e.getMessage()); 262 | } catch (Exception e) { 263 | LOGGER.error("Unexpected error occurred in JSON configuration plugin.", e); 264 | JsonConfigCollection config = new JsonConfigCollection(); 265 | config.addError(new PluginError(e.toString(), "JSON config plugin")); 266 | return error(gson.toJson(config.getJsonObject())); 267 | } 268 | } 269 | 270 | @Override 271 | public GoPluginIdentifier pluginIdentifier() { 272 | return new GoPluginIdentifier("configrepo", Arrays.asList("1.0", "2.0", "3.0")); 273 | } 274 | 275 | private void configurePlugin(PluginSettings settings) { 276 | this.settings = settings; 277 | } 278 | 279 | private GoApiRequest createGoApiRequest(final String api, final String responseBody) { 280 | return JsonConfigHelper.request(api, responseBody, pluginIdentifier()); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/test/java/com.tw.go.config.json/JsonConfigPluginTest.java: -------------------------------------------------------------------------------- 1 | package com.tw.go.config.json; 2 | 3 | import com.google.gson.*; 4 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor; 5 | import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException; 6 | import com.thoughtworks.go.plugin.api.request.DefaultGoPluginApiRequest; 7 | import com.thoughtworks.go.plugin.api.request.GoApiRequest; 8 | import com.thoughtworks.go.plugin.api.response.DefaultGoApiResponse; 9 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 10 | import com.thoughtworks.go.plugin.api.response.GoApiResponse; 11 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.io.TempDir; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.Base64; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.Objects; 25 | 26 | import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.SUCCESS_RESPONSE_CODE; 27 | import static com.tw.go.config.json.ConfigRepoMessages.REQ_PARSE_CONTENT; 28 | import static com.tw.go.config.json.ConfigRepoMessages.REQ_PLUGIN_SETTINGS_CHANGED; 29 | import static com.tw.go.config.json.PluginSettings.*; 30 | import static java.lang.String.format; 31 | import static org.hamcrest.CoreMatchers.is; 32 | import static org.hamcrest.MatcherAssert.assertThat; 33 | import static org.junit.jupiter.api.Assertions.*; 34 | import static org.mockito.Mockito.*; 35 | 36 | public class JsonConfigPluginTest { 37 | private final Path emptyDir = Path.of("emptyDir"); 38 | @TempDir 39 | public Path tempDir; 40 | 41 | private JsonConfigPlugin plugin; 42 | private Gson gson; 43 | 44 | private GoApplicationAccessor goAccessor; 45 | 46 | @BeforeEach 47 | public void setUp() throws IOException { 48 | plugin = new JsonConfigPlugin(); 49 | goAccessor = mock(GoApplicationAccessor.class); 50 | plugin.initializeGoApplicationAccessor(goAccessor); 51 | GoApiResponse settingsResponse = DefaultGoApiResponse.success("{}"); 52 | when(goAccessor.submit(any())).thenReturn(settingsResponse); 53 | gson = new Gson(); 54 | Files.createDirectory(emptyDir); 55 | } 56 | 57 | @AfterEach 58 | public void tearDown() throws IOException { 59 | if (emptyDir.toFile().exists()) { 60 | Files.walk(emptyDir).forEach(path -> { 61 | try { 62 | Files.deleteIfExists(path); 63 | } catch (IOException e) { 64 | throw new RuntimeException(e); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | @Test 71 | public void respondsToParseContentRequest() throws Exception { 72 | final Gson gson = new Gson(); 73 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("configrepo", "2.0", REQ_PARSE_CONTENT); 74 | 75 | HashMap contents = new HashMap<>(); 76 | contents.put("foo.gopipeline.json", "{\"name\": \"a\", \"stages\":[]}"); 77 | contents.put("foo.goenvironment.json", "{\"name\": \"b\"}"); 78 | request.setRequestBody(gson.toJson(Collections.singletonMap("contents", contents))); 79 | 80 | GoPluginApiResponse response = plugin.handle(request); 81 | JsonObject jsonObjectFromResponse = getJsonObjectFromResponse(response); 82 | assertEquals(SUCCESS_RESPONSE_CODE, response.responseCode()); 83 | assertEquals(new JsonArray(), jsonObjectFromResponse.get("errors")); 84 | 85 | JsonObject expected = new JsonObject(); 86 | 87 | JsonArray pipelines = new JsonArray(); 88 | JsonArray envs = new JsonArray(); 89 | 90 | expected.add("errors", new JsonArray()); 91 | expected.add("environments", envs); 92 | expected.add("pipelines", pipelines); 93 | expected.addProperty("target_version", 1); 94 | 95 | JsonObject p1 = new JsonObject(); 96 | p1.addProperty("name", "a"); 97 | p1.add("stages", new JsonArray()); 98 | p1.addProperty("location", "foo.gopipeline.json"); 99 | pipelines.add(p1); 100 | 101 | JsonObject e1 = new JsonObject(); 102 | e1.addProperty("name", "b"); 103 | e1.addProperty("location", "foo.goenvironment.json"); 104 | envs.add(e1); 105 | 106 | assertEquals(expected, jsonObjectFromResponse); 107 | } 108 | 109 | @Test 110 | public void shouldRespondSuccessToGetConfigurationRequest() throws UnhandledRequestTypeException { 111 | DefaultGoPluginApiRequest getConfigRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "go.plugin-settings.get-configuration"); 112 | 113 | GoPluginApiResponse response = plugin.handle(getConfigRequest); 114 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 115 | } 116 | 117 | @Test 118 | public void shouldConsumePluginSettingsOnConfigChangeRequest() throws UnhandledRequestTypeException { 119 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("configrepo", "2.0", REQ_PLUGIN_SETTINGS_CHANGED); 120 | HashMap body = new HashMap<>(); 121 | body.put(PLUGIN_SETTINGS_PIPELINE_PATTERN, "*.foo.pipes.json"); 122 | body.put(PLUGIN_SETTINGS_ENVIRONMENT_PATTERN, "*.foo.envs.json"); 123 | request.setRequestBody(JSONUtils.toJSON(body)); 124 | 125 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 126 | String requestBody = "{\n" + 127 | " \"directory\":\"emptyDir\",\n" + 128 | " \"configurations\":[]\n" + 129 | "}"; 130 | parseDirectoryRequest.setRequestBody(requestBody); 131 | ParsedRequest parsed = ParsedRequest.parse(parseDirectoryRequest); 132 | 133 | assertEquals(DEFAULT_PIPELINE_PATTERN, plugin.getPipelinePattern(parsed)); 134 | assertEquals(DEFAULT_ENVIRONMENT_PATTERN, plugin.getEnvironmentPattern(parsed)); 135 | 136 | GoPluginApiResponse response = plugin.handle(request); 137 | 138 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 139 | assertEquals("*.foo.pipes.json", plugin.getPipelinePattern(parsed)); 140 | assertEquals("*.foo.envs.json", plugin.getEnvironmentPattern(parsed)); 141 | } 142 | 143 | 144 | @Test 145 | public void shouldContainEnvironmentPatternInResponseToGetConfigurationRequest() throws UnhandledRequestTypeException { 146 | DefaultGoPluginApiRequest getConfigRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "go.plugin-settings.get-configuration"); 147 | 148 | GoPluginApiResponse response = plugin.handle(getConfigRequest); 149 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 150 | JsonObject responseJsonObject = getJsonObjectFromResponse(response); 151 | JsonElement environmentPatternConfig = responseJsonObject.get("environment_pattern"); 152 | assertNotNull(environmentPatternConfig); 153 | JsonObject environmentPatternConfigAsJsonObject = environmentPatternConfig.getAsJsonObject(); 154 | assertThat(environmentPatternConfigAsJsonObject.get("display-name").getAsString(), is("Go environment files pattern")); 155 | assertThat(environmentPatternConfigAsJsonObject.get("default-value").getAsString(), is("**/*.goenvironment.json")); 156 | assertThat(environmentPatternConfigAsJsonObject.get("required").getAsBoolean(), is(false)); 157 | assertThat(environmentPatternConfigAsJsonObject.get("secure").getAsBoolean(), is(false)); 158 | assertThat(environmentPatternConfigAsJsonObject.get("display-order").getAsInt(), is(1)); 159 | } 160 | 161 | @Test 162 | public void shouldContainPipelinePatternInResponseToGetConfigurationRequest() throws UnhandledRequestTypeException { 163 | DefaultGoPluginApiRequest getConfigRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "go.plugin-settings.get-configuration"); 164 | 165 | GoPluginApiResponse response = plugin.handle(getConfigRequest); 166 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 167 | JsonObject responseJsonObject = getJsonObjectFromResponse(response); 168 | JsonElement pipelinePatternConfig = responseJsonObject.get("pipeline_pattern"); 169 | assertNotNull(pipelinePatternConfig); 170 | JsonObject pipelinePatternConfigAsJsonObject = pipelinePatternConfig.getAsJsonObject(); 171 | assertThat(pipelinePatternConfigAsJsonObject.get("display-name").getAsString(), is("Go pipeline files pattern")); 172 | assertThat(pipelinePatternConfigAsJsonObject.get("default-value").getAsString(), is("**/*.gopipeline.json")); 173 | assertThat(pipelinePatternConfigAsJsonObject.get("required").getAsBoolean(), is(false)); 174 | assertThat(pipelinePatternConfigAsJsonObject.get("secure").getAsBoolean(), is(false)); 175 | assertThat(pipelinePatternConfigAsJsonObject.get("display-order").getAsInt(), is(0)); 176 | } 177 | 178 | @Test 179 | public void getPipelinePatternShouldReturnValueAtConfigRepoLevelIfDefined() { 180 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 181 | String requestBody = "{\n" + 182 | " \"directory\":\"emptyDir\",\n" + 183 | " \"configurations\":[" + 184 | "{" + 185 | "\"key\" : \"pipeline_pattern\"," + 186 | "\"value\" : \"**/*.goprodpipeline.json\" " + 187 | "}" + 188 | "]\n" + 189 | "}"; 190 | parseDirectoryRequest.setRequestBody(requestBody); 191 | ParsedRequest parsed = ParsedRequest.parse(parseDirectoryRequest); 192 | String pattern = plugin.getPipelinePattern(parsed); 193 | assertThat(pattern, is("**/*.goprodpipeline.json")); 194 | } 195 | 196 | @Test 197 | public void getEnvironmentPatternShouldReturnValueAtConfigRepoLevelIfDefined() { 198 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 199 | String requestBody = "{\n" + 200 | " \"directory\":\"emptyDir\",\n" + 201 | " \"configurations\":[" + 202 | "{" + 203 | "\"key\" : \"environment_pattern\"," + 204 | "\"value\" : \"**/*.goprodenvironment.json\" " + 205 | "}" + 206 | "]\n" + 207 | "}"; 208 | parseDirectoryRequest.setRequestBody(requestBody); 209 | ParsedRequest parsed = ParsedRequest.parse(parseDirectoryRequest); 210 | String pattern = plugin.getEnvironmentPattern(parsed); 211 | assertThat(pattern, is("**/*.goprodenvironment.json")); 212 | } 213 | 214 | @Test 215 | public void getEnvironmentPatternShouldReturnValueAtConfigRepoLevelIfBothPatternsDefined() { 216 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 217 | String requestBody = "{\n" + 218 | " \"directory\":\"emptyDir\",\n" + 219 | " \"configurations\":[" + 220 | "{" + 221 | "\"key\" : \"environment_pattern\"," + 222 | "\"value\" : \"**/*.goprodenvironment.json\" " + 223 | "}," + 224 | "{" + 225 | "\"key\" : \"pipeline_pattern\"," + 226 | "\"value\" : \"**/*.goprodpipeline.json\" " + 227 | "}" + 228 | "]\n" + 229 | "}"; 230 | parseDirectoryRequest.setRequestBody(requestBody); 231 | ParsedRequest parsed = ParsedRequest.parse(parseDirectoryRequest); 232 | String pattern = plugin.getEnvironmentPattern(parsed); 233 | assertThat(pattern, is("**/*.goprodenvironment.json")); 234 | } 235 | 236 | private JsonObject getJsonObjectFromResponse(GoPluginApiResponse response) { 237 | String responseBody = response.responseBody(); 238 | return JsonParser.parseString(responseBody).getAsJsonObject(); 239 | } 240 | 241 | @Test 242 | public void shouldRespondSuccessToGetViewRequest() throws UnhandledRequestTypeException { 243 | DefaultGoPluginApiRequest getConfigRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "go.plugin-settings.get-view"); 244 | 245 | GoPluginApiResponse response = plugin.handle(getConfigRequest); 246 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 247 | } 248 | 249 | @Test 250 | public void shouldRespondSuccessToValidateConfigRequest() throws UnhandledRequestTypeException { 251 | DefaultGoPluginApiRequest validateRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "go.plugin-settings.validate-configuration"); 252 | 253 | GoPluginApiResponse response = plugin.handle(validateRequest); 254 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 255 | } 256 | 257 | @Test 258 | public void shouldRespondWithGetIcon() throws UnhandledRequestTypeException, IOException { 259 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("configrepo", "2.0", "get-icon"); 260 | 261 | GoPluginApiResponse response = plugin.handle(request); 262 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 263 | JsonObject jsonObject = getJsonObjectFromResponse(response); 264 | assertEquals(jsonObject.entrySet().size(), 2); 265 | assertEquals(jsonObject.get("content_type").getAsString(), "image/svg+xml"); 266 | byte[] actualData = Base64.getDecoder().decode(jsonObject.get("data").getAsString()); 267 | try (InputStream inputStream = Objects.requireNonNull(getClass().getResourceAsStream("/json.svg"))) { 268 | assertArrayEquals(inputStream.readAllBytes(), actualData); 269 | } 270 | } 271 | 272 | @Test 273 | public void shouldRespondSuccessToParseDirectoryRequestWhenEmpty() throws UnhandledRequestTypeException { 274 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 275 | String requestBody = "{\n" + 276 | " \"directory\":\"emptyDir\",\n" + 277 | " \"configurations\":[]\n" + 278 | "}"; 279 | parseDirectoryRequest.setRequestBody(requestBody); 280 | 281 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 282 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 283 | JsonObject responseJsonObject = getJsonObjectFromResponse(response); 284 | assertNull(responseJsonObject.get("pluginErrors")); 285 | } 286 | 287 | @Test 288 | public void shouldRespondBadRequestToParseDirectoryRequestWhenDirectoryIsNotSpecified() throws UnhandledRequestTypeException { 289 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 290 | String requestBody = "{\n" + 291 | " \"configurations\":[]\n" + 292 | "}"; 293 | parseDirectoryRequest.setRequestBody(requestBody); 294 | 295 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 296 | assertThat(response.responseCode(), is(DefaultGoPluginApiResponse.BAD_REQUEST)); 297 | } 298 | 299 | @Test 300 | public void shouldRespondBadRequestToParseDirectoryRequestWhenRequestBodyIsNull() throws UnhandledRequestTypeException { 301 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 302 | parseDirectoryRequest.setRequestBody(null); 303 | 304 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 305 | assertThat(response.responseCode(), is(DefaultGoPluginApiResponse.BAD_REQUEST)); 306 | } 307 | 308 | @Test 309 | public void shouldRespondBadRequestToParseDirectoryRequestWhenRequestBodyIsEmpty() throws UnhandledRequestTypeException { 310 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 311 | parseDirectoryRequest.setRequestBody("{}"); 312 | 313 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 314 | assertThat(response.responseCode(), is(DefaultGoPluginApiResponse.BAD_REQUEST)); 315 | } 316 | 317 | @Test 318 | public void shouldRespondBadRequestToParseDirectoryRequestWhenRequestBodyIsNotJson() throws UnhandledRequestTypeException { 319 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 320 | parseDirectoryRequest.setRequestBody("{bla"); 321 | 322 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 323 | assertThat(response.responseCode(), is(DefaultGoPluginApiResponse.BAD_REQUEST)); 324 | } 325 | 326 | @Test 327 | public void shouldTalkToGoApplicationAccessorToGetPluginSettings() throws UnhandledRequestTypeException { 328 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 329 | String requestBody = "{\n" + 330 | " \"directory\":\"emptyDir\",\n" + 331 | " \"configurations\":[]\n" + 332 | "}"; 333 | parseDirectoryRequest.setRequestBody(requestBody); 334 | 335 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 336 | 337 | verify(goAccessor, times(1)).submit(any(GoApiRequest.class)); 338 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 339 | } 340 | 341 | @Test 342 | public void shouldReturnListOfConfigFiles() throws UnhandledRequestTypeException, IOException { 343 | DefaultGoPluginApiRequest listConfigFilesRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "config-files"); 344 | Files.createFile(tempDir.resolve("test.gopipeline.json")); 345 | Files.createFile(tempDir.resolve("test.goenvironment.json")); 346 | String requestBody = "{\n" + 347 | " \"directory\":\"" + tempDir.toFile().getAbsolutePath() + "\"\n" + 348 | "}"; 349 | listConfigFilesRequest.setRequestBody(requestBody); 350 | 351 | GoPluginApiResponse response = plugin.handle(listConfigFilesRequest); 352 | 353 | verify(goAccessor, times(1)).submit(any(GoApiRequest.class)); 354 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 355 | assertThat(response.responseBody(), is("{\"files\":[\"test.gopipeline.json\",\"test.goenvironment.json\"]}")); 356 | } 357 | 358 | @Test 359 | public void shouldReturnEmptyListWhenNoConfigFiles() throws UnhandledRequestTypeException { 360 | DefaultGoPluginApiRequest listConfigFilesRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "config-files"); 361 | String requestBody = "{\n" + 362 | " \"directory\":\"" + tempDir.toFile().getAbsolutePath() + "\"\n" + 363 | "}"; 364 | listConfigFilesRequest.setRequestBody(requestBody); 365 | 366 | GoPluginApiResponse response = plugin.handle(listConfigFilesRequest); 367 | 368 | verify(goAccessor, times(1)).submit(any(GoApiRequest.class)); 369 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 370 | assertThat(response.responseBody(), is("{\"files\":[]}")); 371 | } 372 | 373 | @Test 374 | public void shouldGiveBackPipelineJSONForPipelineExport() throws UnhandledRequestTypeException { 375 | HashMap pipeline = new HashMap<>(); 376 | pipeline.put("name", "pipeline"); 377 | pipeline.put("group", "group"); 378 | pipeline.put("stages", Collections.emptyList()); 379 | 380 | Gson gson = new Gson(); 381 | String pipelineJson = gson.toJson(pipeline); 382 | 383 | String requestJson = format("{\"pipeline\": %s}", pipelineJson); 384 | DefaultGoPluginApiRequest pipelineExportRequest = new DefaultGoPluginApiRequest("configrepo", "2.0", "pipeline-export"); 385 | pipelineExportRequest.setRequestBody(requestJson); 386 | 387 | GoPluginApiResponse response = plugin.handle(pipelineExportRequest); 388 | 389 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 390 | Gson pretty = new GsonBuilder().setPrettyPrinting().create(); 391 | String prettyPrinted = pretty.toJson(pipeline); 392 | assertThat(response.responseBody(), is(gson.toJson(Collections.singletonMap("pipeline", prettyPrinted)))); 393 | } 394 | 395 | @Test 396 | public void shouldRespondWithCapabilities() throws UnhandledRequestTypeException { 397 | String expected = gson.toJson(new Capabilities()); 398 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("configrepo", "2.0", "get-capabilities"); 399 | 400 | GoPluginApiResponse response = plugin.handle(request); 401 | 402 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 403 | assertThat(response.responseBody(), is(expected)); 404 | } 405 | 406 | @Test 407 | public void shouldRespondSuccessToParseDirectoryRequestWhenPluginHasConfiguration() throws UnhandledRequestTypeException { 408 | GoApiResponse settingsResponse = DefaultGoApiResponse.success("{}"); 409 | when(goAccessor.submit(any(GoApiRequest.class))).thenReturn(settingsResponse); 410 | 411 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 412 | String requestBody = "{\n" + 413 | " \"directory\":\"emptyDir\",\n" + 414 | " \"configurations\":[]\n" + 415 | "}"; 416 | parseDirectoryRequest.setRequestBody(requestBody); 417 | 418 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 419 | 420 | verify(goAccessor, times(1)).submit(any(GoApiRequest.class)); 421 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 422 | } 423 | 424 | @Test 425 | public void shouldContainValidFieldsInResponseMessage() throws UnhandledRequestTypeException { 426 | GoApiResponse settingsResponse = DefaultGoApiResponse.success("{}"); 427 | when(goAccessor.submit(any(GoApiRequest.class))).thenReturn(settingsResponse); 428 | 429 | DefaultGoPluginApiRequest parseDirectoryRequest = new DefaultGoPluginApiRequest("configrepo", "1.0", "parse-directory"); 430 | String requestBody = "{\n" + 431 | " \"directory\":\"emptyDir\",\n" + 432 | " \"configurations\":[]\n" + 433 | "}"; 434 | parseDirectoryRequest.setRequestBody(requestBody); 435 | 436 | GoPluginApiResponse response = plugin.handle(parseDirectoryRequest); 437 | 438 | assertThat(response.responseCode(), is(SUCCESS_RESPONSE_CODE)); 439 | JsonElement responseObj = JsonParser.parseString(response.responseBody()); 440 | assertTrue(responseObj.isJsonObject()); 441 | JsonObject obj = responseObj.getAsJsonObject(); 442 | assertTrue(obj.has("errors")); 443 | assertTrue(obj.has("pipelines")); 444 | assertTrue(obj.has("environments")); 445 | assertTrue(obj.has("target_version")); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Google Groups](https://img.shields.io/badge/Google_Groups-user_help-purple)](https://groups.google.com/g/go-cd) 2 | [![GitHub Discussions](https://img.shields.io/badge/GitHub_discussions-user_&_dev_chat-green)](https://github.com/gocd/gocd/discussions) 3 | 4 | # GoCD JSON configuration plugin 5 | 6 | **GoCD pipeline configuration as code** 7 | 8 | This is a [GoCD](https://www.gocd.org) server plugin which allows to keep **pipelines** and **environments** configuration 9 | in version control systems supported by GoCD (git, svn, mercurial, etc.). 10 | See [this document](https://docs.gocd.org/current/advanced_usage/pipelines_as_code.html) 11 | to find out what GoCD's configuration repositories are. 12 | 13 | # Table of contents 14 | 15 | 1. [Setup](#setup) 16 | 1. [File pattern](#file-pattern) 17 | 1. [Validation](#Validation) 18 | 1. **[Format reference](#JSON-Configuration-reference)** 19 | 1. [Format version](#Format-version) 20 | 1. [Issues and questions](#Issues-and-questions) 21 | 1. [Development](#Development) 22 | 1. [License](#License) 23 | 24 | # Setup 25 | 26 | **Step 1**: GoCD versions newer than `17.8.0` already have the plugin bundled. You don't need to install anything. 27 | 28 | If you're using GoCD version *older than 17.8.0*, you need to install the plugin in the GoCD server. Download it from 29 | [the releases page](https://github.com/tomzo/gocd-json-config-plugin/releases) and place it on the GoCD server in 30 | `plugins/external` [directory](https://docs.gocd.org/current/extension_points/plugin_user_guide.html). 31 | 32 | 33 | **Step 2**: Follow [the GoCD documentation](https://docs.gocd.org/current/advanced_usage/pipelines_as_code.html#storing-pipeline-configuration-in-json) to add a new configuration repository. 34 | 35 | You can use the example repository at: `https://github.com/tomzo/gocd-json-config-example.git` as a reference. 36 | 37 | In your config repo (`tomzo/gocd-json-config-example.git` in this case), ensure that your GoCD json config is suffixed with `.gopipeline.json` for pipelines and `.goenvironment.json` for environments. Give it a minute or so for the polling to happen. Once that happens, you should see your pipeline(s) on your dashboard. 38 | 39 | ## File pattern 40 | 41 | Using this plugin you can store any number of pipeline or environment 42 | configurations in a versioned repository like git. 43 | 44 | By default **pipelines** should be stored in `*.gopipeline.json` files 45 | and **environments** should be stored in `*.goenvironment.json` files. 46 | 47 | The file name pattern can be changed on plugin configuration page. 48 | 49 | # Validation 50 | 51 | You can validate if proposed GoCD JSON changes will be accepted by the server. Currently, 2 options are available: 52 | * Use a [GoCD mergable github action](https://github.com/GaneshSPatil/gocd-mergeable) 53 | * [Validate from your local machine](#validation-using-cli) 54 | 55 | ### Validation using CLI 56 | 57 | *You may find this [introductory blog post useful](https://kudulab.io/posts/gocd-preflight-validation/).* 58 | 59 | There is an ongoing effort to allow in-depth validation of configuration **before pushing configuration to the source control**. This is provided by [GoCD's preflight API](https://api.gocd.org/current/#preflight-check-of-config-repo-configurations) and [gocd-cli](https://github.com/gocd-contrib/gocd-cli). 60 | 61 | You have several options to configure validation tools on your workstation: 62 | * If you have a local docker daemon, use the [gocd-cli-dojo](https://github.com/gocd-contrib/docker-gocd-cli-dojo) image. Follow the [setup instructions](https://github.com/gocd-contrib/docker-gocd-cli-dojo#setup) in the image readme. 63 | * If you don't want to use docker, you'll need to [setup `gocd-cli` on your host](https://github.com/gocd-contrib/gocd-cli). 64 | 65 | Either way you'll have `gocd` binary in your `PATH` or inside the docker container. 66 | 67 | ### Syntax validation 68 | 69 | This will check general validity of the yaml file, without talking to the GoCD server: 70 | ```bash 71 | gocd configrepo --json syntax ci.gopipeline.json 72 | ``` 73 | 74 | ### Preflight validation 75 | 76 | This command will parse and submit your json file to the configured GoCD server. 77 | ``` 78 | gocd configrepo preflight --json -r gocd-json-config-example *.gopipeline.json 79 | ``` 80 | Where `-r` is the configuration repository id, which you have earlier configured on GoCD server. You can check it on config repos page of your GoCD server, at `/go/admin/config_repos`. It is in the upper left corner of each config repo. 81 | ![config repo id](json_config_repo_id.png) 82 | 83 | # JSON Configuration reference 84 | 85 | The **pipeline configuration** files should be stored in format *similar* to 86 | one exposed by [GoCD API](https://api.gocd.org/current#get-pipeline-config). 87 | 88 | The format of **environment configuration** files is much simpler, 89 | you can find examples of correct environments [below](#environment). 90 | 91 | 1. [Format version](#format-version) 92 | 1. [Environment](#environment) 93 | 1. [Environment variables](#environment-variables) 94 | 1. [Pipeline](#pipeline) 95 | * [Locking](#pipeline-locking) 96 | * [Controlling the display order](#display-order-of-pipelines) 97 | * [Timer](#timer) 98 | * [Tracking tool](#tracking-tool) 99 | * [Mingle](#mingle) 100 | 1. [Stage](#stage) 101 | * [Approval](#approval) 102 | 1. [Job](#job) 103 | * [Property](#property) 104 | * [Tab](#tab) 105 | * [Many instances](#run-many-instances) 106 | 1. [Tasks](#tasks) 107 | * [rake](#rake) 108 | * [ant](#ant) 109 | * [nant](#nant) 110 | * [exec](#exec) 111 | * [fetch](#fetch) 112 | * [pluggabletask](#plugin) 113 | 1. [Materials](#materials) 114 | * [dependency](#dependency) 115 | * [package](#package) 116 | * [git](#git) 117 | * [svn](#svn) 118 | * [perforce](#perforce) 119 | * [tfs](#tfs) 120 | * [hg](#hg) 121 | * [pluggable scm](#pluggable-scm) 122 | * [configrepo](#configrepo) 123 | 124 | 125 | ### Format version 126 | 127 | Please note that it is now recommended to declare the _same_ `format_version` in each `*.gopipeline.json` or `*.goenvironment.json` file. 128 | 129 | #### GoCD server version from 19.10.0 and beyond 130 | 131 | Supports `format_version` value of `9`. In this version, support of `ignore_for_scheduling` for [dependency materials](#dependency) has been added. Setting this attribute will skip scheduling the pipeline when the dependency material has changed. 132 | 133 | Using a newer `format_version` includes all the behavior of the previous versions too. 134 | 135 | #### GoCD server version from 19.9.0 and beyond 136 | 137 | Supports `format_version` value of `7` and `8`. In version `7`, support for [properties](#property) has been removed. In version `8`, support for [mingle](#mingle) has been removed. 138 | 139 | Using a newer `format_version` includes all the behavior of the previous versions too. 140 | 141 | #### GoCD server version from 19.8.0 and beyond 142 | 143 | Supports `format_version` value of `6`. In this version, support of `allow_only_on_success` for [approval](#approval) on stage has been added. Setting this attribute will ensure that the manual trigger will be allowed only if the previous stage is successful. 144 | 145 | Using a newer `format_version` includes all the behavior of the previous versions too. 146 | 147 | #### GoCD server version from 19.4.0 to 19.7.0 148 | 149 | Supports `format_version` value of `5`. In this version, support of `username` and `encrypted_password` for [git](#git-material-update) and [hg](#hg-material-update) material has been added. In addition to that, [hg](#hg-material-update) will also support `branch` attribute. 150 | 151 | Using a newer `format_version` includes all the behavior of the previous versions too. 152 | 153 | #### GoCD server version from 19.3.0 to 19.4.0 154 | 155 | Supports `format_version` value of `4`. In this version, support has been added to control the [display order of pipelines](#display-order-of-pipelines). 156 | 157 | This server version also supports `format_version` of `3` and `2`. Using a newer `format_version` includes all the behavior of the previous versions too. 158 | 159 | #### GoCD server version from 18.7.0 to 19.2.0 160 | 161 | Supports `format_version` value of `3`. In this version [fetch artifact](#fetch) format was changed to include `artifact_origin`. 162 | 163 | This server version also supports `format_version` of `2`. Using a newer `format_version` includes all the behavior of the previous versions too. 164 | 165 | #### GoCD server version up to 18.6.0 166 | 167 | Supports `format_version` value of `2`. In this version [pipeline locking](#pipeline-locking) behavior was changed. 168 | 169 | 170 | # Environment 171 | 172 | Configures a [GoCD environment](https://docs.gocd.org/current/configuration/managing_environments.html) 173 | 174 | ```json 175 | { 176 | "name": "dev", 177 | "environment_variables": [ 178 | { 179 | "name": "key1", 180 | "value": "value1" 181 | } 182 | ], 183 | "agents": [ 184 | "123" 185 | ], 186 | "pipelines": [ 187 | "mypipeline1" 188 | ] 189 | } 190 | ``` 191 | 192 | # Environment variables 193 | 194 | Environment variables is a JSON array that can be declared in Environments, Pipelines, Stages and Jobs. 195 | 196 | Any variable must contain `name` and `value` or `encrypted_value`. 197 | 198 | ```json 199 | { 200 | "environment_variables": [ 201 | { 202 | "name": "key1", 203 | "value": "value1" 204 | }, 205 | { 206 | "name": "keyd", 207 | "encrypted_value": "v12@SDfwez" 208 | } 209 | ] 210 | } 211 | ``` 212 | 213 | # Pipeline 214 | 215 | ```json 216 | { 217 | "format_version" : 1, 218 | "group": "group1", 219 | "name": "pipe2", 220 | "label_template": "foo-1.0-${COUNT}", 221 | "enable_pipeline_locking" : false, 222 | "parameters": [ 223 | { 224 | "name": "param", 225 | "value": "parameter" 226 | } 227 | ], 228 | "tracking_tool": null, 229 | "timer": { 230 | "spec": "0 15 10 * * ? *" 231 | }, 232 | "environment_variables": [], 233 | "materials": [ 234 | ... 235 | ], 236 | "stages": [ 237 | ... 238 | ] 239 | } 240 | ``` 241 | 242 | #### Referencing an existing template in a config repo: 243 | 244 | ```json 245 | { 246 | "format_version" : 1, 247 | "group": "group1", 248 | "name": "pipe-with-template", 249 | "label_template": "foo-1.0-${COUNT}", 250 | "enable_pipeline_locking" : false, 251 | "template": "template1", 252 | "parameters": [ 253 | { 254 | "name": "param", 255 | "value": "parameter" 256 | } 257 | ], 258 | "materials": [ 259 | ... 260 | ] 261 | } 262 | ``` 263 | 264 | Please note: 265 | 266 | * Pipeline declares a group to which it belongs 267 | 268 | ### Pipeline locking 269 | 270 | Expected since GoCD v17.12, you need to use `lock_behavior` rather than `enable_pipeline_locking`. 271 | ``` 272 | "lock_behavior" : "none" 273 | ``` 274 | 275 | `lock_behavior` can be one of: 276 | * `lockOnFailure` - same as `enable_pipeline_locking: true` 277 | * `unlockWhenFinished` - 278 | * `none` - same `enable_pipeline_locking: false` 279 | 280 | 281 | 282 | ### Controlling the display order 283 | 284 | When `format_version` is `4` (see [above](#format-version)), the order of display of pipelines on the GoCD dashboard can be influenced by setting the `display_order_weight` property. 285 | 286 | - This is an integer property and the pipelines in a pipeline group will be ordered by this value. 287 | - The default value for this property is `-1`. 288 | - Pipelines defined in GoCD's config XML will also default to -1. 289 | - If multiple pipelines have the same `display_order_weight` value, their order relative to each other will be indeterminate. 290 | 291 | ```json 292 | { 293 | "name": "pipeline1", 294 | "group": "pg1", 295 | "display_order_weight": 10 296 | }, 297 | { 298 | "name": "pipeline2", 299 | "group": "pg1", 300 | "display_order_weight": -10 301 | } 302 | ``` 303 | 304 | In the above example, since both pipelines are in the same group, `pipeline2` will be shown ahead of `pipeline1`. If any pipelines are defined in the GoCD config XML, then they will appear in between these two pipelines. 305 | 306 | 307 | ### Timer 308 | 309 | ```json 310 | { 311 | "spec": "0 0 22 ? * MON-FRI", 312 | "only_on_changes": true 313 | } 314 | ``` 315 | 316 | ### Tracking tool 317 | 318 | ```json 319 | { 320 | "link": "http://your-trackingtool/yourproject/${ID}", 321 | "regex": "evo-(\\d+)" 322 | } 323 | ``` 324 | 325 | ### Mingle 326 | **DEPRECATION NOTICE: Since GoCD version 19.9 and format_version 8, this is no longer supported** 327 | 328 | ```json 329 | { 330 | "base_url": "https://mingle.example.com", 331 | "project_identifier": "foobar_widgets", 332 | "mql_grouping_conditions": "status > 'In Dev'" 333 | } 334 | ``` 335 | 336 | # Stage 337 | 338 | ```json 339 | { 340 | "name": "test", 341 | "fetch_materials": true, 342 | "never_cleanup_artifacts": false, 343 | "clean_working_directory": false, 344 | "approval" : null, 345 | "environment_variables": [ 346 | { 347 | "name": "TEST_NUM", 348 | "value": "1" 349 | } 350 | ], 351 | "jobs": [ 352 | ... 353 | ] 354 | } 355 | ``` 356 | 357 | ### Approval 358 | 359 | ```json 360 | { 361 | "type": "manual", 362 | "allow_only_on_success": true, 363 | "users": [], 364 | "roles": [ 365 | "manager" 366 | ] 367 | } 368 | ``` 369 | 370 | # Job 371 | 372 | ```json 373 | { 374 | "name": "test", 375 | "run_instance_count" : null, 376 | "environment_variables": [], 377 | "timeout": 180, 378 | "elastic_profile_id": "docker-big-image-1", 379 | "tabs": [ 380 | { 381 | "name": "test", 382 | "path": "results.xml" 383 | } 384 | ], 385 | "resources": [ 386 | "linux" 387 | ], 388 | "artifacts": [ 389 | { 390 | "source": "src", 391 | "destination": "dest", 392 | "type": "test" 393 | }, 394 | { 395 | "type": "external", 396 | "id": "docker-release-candidate", 397 | "store_id": "dockerhub", 398 | "configuration": [ 399 | { 400 | "key": "Image", 401 | "value": "gocd/gocd-demo" 402 | }, 403 | { 404 | "key": "Tag", 405 | "value": "${GO_PIPELINE_COUNTER}" 406 | }, 407 | { 408 | "key": "some_secure_property", 409 | "encrypted_value": "!@ESsdD323#sdu" 410 | } 411 | ] 412 | } 413 | ], 414 | "tasks": [ 415 | ... 416 | ] 417 | } 418 | ``` 419 | 420 | ### Artifacts 421 | 422 | There are 3 types of artifacts recognized by GoCD. `Build` and `Test` artifacts are stored on the GoCD server. 423 | The source and the destination of the artifact that should be stored on the GoCD server must be specified. 424 | 425 | #### Build 426 | 427 | ```json 428 | { 429 | "source": "src", 430 | "destination": "dest", 431 | "type": "build" 432 | } 433 | ``` 434 | 435 | #### Test 436 | 437 | ```json 438 | { 439 | "source": "src", 440 | "destination": "dest", 441 | "type": "test" 442 | } 443 | ``` 444 | 445 | #### External 446 | 447 | Artifacts of type `external` are stored in an artifact store outside of GoCD. 448 | The external artifact store's configuration must be created in the main GoCD config. Support for external artifact store config to be checked in as yaml is not available. 449 | The external artifact store is referenced by the `store_id`. The build specific artifact details that the artifact plugin needs to publish the artifact is provided as `configuration`. 450 | 451 | ```json 452 | { 453 | "type": "external", 454 | "id": "docker-release-candidate", 455 | "store_id": "dockerhub", 456 | "configuration": [ 457 | { 458 | "key": "Image", 459 | "value": "gocd/gocd-demo" 460 | }, 461 | { 462 | "key": "Tag", 463 | "value": "${GO_PIPELINE_COUNTER}" 464 | }, 465 | { 466 | "key": "some_secure_property", 467 | "encrypted_value": "!@ESsdD323#sdu" 468 | } 469 | ] 470 | } 471 | ``` 472 | 473 | ### Property 474 | **DEPRECATION NOTICE: Since GoCD version 19.9 and format_version 7, properties are no longer supported** 475 | 476 | ```json 477 | { 478 | "name": "coverage.class", 479 | "source": "target/emma/coverage.xml", 480 | "xpath": "substring-before(//report/data/all/coverage[starts-with(@type,'class')]/@value, '%')" 481 | } 482 | ``` 483 | 484 | ### Tab 485 | 486 | ```json 487 | { 488 | "name": "cobertura", 489 | "path": "target/site/cobertura/index.html" 490 | } 491 | ``` 492 | 493 | ### Run many instances 494 | 495 | Part of **job** object can be [number of job to runs](https://docs.gocd.org/current/advanced_usage/admin_spawn_multiple_jobs.html) 496 | ```json 497 | "run_instance_count" : 6 498 | ``` 499 | Or to run on all agents 500 | ```json 501 | "run_instance_count" : "all" 502 | ``` 503 | Default is `null` which runs just one job. 504 | 505 | # Materials 506 | 507 | All materials: 508 | 509 | * must have `type` - `git`, `svn`, `hg`, `p4`, `tfs`, `dependency`, `package`, `plugin`. 510 | * can have `name` and must have `name` when there is more than one material in pipeline 511 | 512 | SCM-related materials have `destination` field. 513 | 514 | ### Filter - blacklist and whitelist 515 | 516 | All scm materials can have filter object: 517 | 518 | * for **blacklisting**: 519 | ```json 520 | "filter": { 521 | "ignore": [ 522 | "externals", 523 | "tools" 524 | ] 525 | } 526 | ``` 527 | 528 | * for **whitelisting** (since Go `>=16.7.0`): 529 | ```json 530 | "filter": { 531 | "whitelist": [ 532 | "moduleA" 533 | ] 534 | } 535 | ``` 536 | 537 | ## Git 538 | 539 | ```json 540 | { 541 | "url": "http://my.git.repository.com", 542 | "branch": "feature12", 543 | "filter": { 544 | "ignore": [ 545 | "externals", 546 | "tools" 547 | ] 548 | }, 549 | "destination": "dir1", 550 | "auto_update": false, 551 | "name": "gitMaterial1", 552 | "type": "git", 553 | "shallow_clone": true, 554 | "username": "user1", 555 | "encrypted_password": "encrypted_value" 556 | } 557 | ``` 558 | 559 | 560 | For **GoCD >= 19.4.0 and `format_version: 5` and above**: 561 | 562 | You are advised to utilize `username` and `encrypted_password` for passing in material credentials as: 563 | 564 | ```json 565 | { 566 | "url": "http://my.git.repository.com", 567 | "branch": "feature12", 568 | "username": "user1", 569 | "encrypted_password": "encrypted_value" 570 | } 571 | ``` 572 | 573 | - Instead of `encrypted_password` you may specify `password` but `encrypted_password` makes more sense considering that the value is stored in SCM. 574 | - Specifying credentials both in `attributes` and `url` will result in a validation error e.g. 575 | ```log 576 | INVALID MERGED CONFIGURATION 577 | Number of errors: 1+ 578 | 1. Ambiguous credentials, must be provided either in URL or as attributes.;; 579 | - For Config Repo: https://your.config.repo.url at cbb047d78c239ab23b9565099e800c6fe4cc0anc 580 | ``` 581 | 582 | ## Svn 583 | 584 | ```json 585 | { 586 | "url": "http://svn", 587 | "username": "user1", 588 | "encrypted_password": "encrypted_value", 589 | "check_externals": true, 590 | "filter": { 591 | "ignore": [ 592 | "tools", 593 | "lib" 594 | ] 595 | }, 596 | "destination": "destDir1", 597 | "auto_update": false, 598 | "name": "svnMaterial1", 599 | "type": "svn" 600 | } 601 | ``` 602 | 603 | Instead of `encrypted_password` you may specify `password` but `encrypted_password` makes more sense considering that the value is stored in SCM. 604 | 605 | ## Hg 606 | 607 | ```json 608 | { 609 | "url": "repos/myhg", 610 | "filter": { 611 | "ignore": [ 612 | "externals", 613 | "tools" 614 | ] 615 | }, 616 | "destination": "dir1", 617 | "auto_update": false, 618 | "name": "hgMaterial1", 619 | "type": "hg", 620 | "username": "user1", 621 | "encrypted_password": "encrypted_value", 622 | "branch": "feature" 623 | } 624 | ``` 625 | 626 | 627 | For **GoCD >= 19.4.0 and `format_version: 5` and above**: 628 | 629 | You are advised to utilize `username` and `encrypted_password` for passing in material credentials as: 630 | 631 | ```json 632 | { 633 | "url": "repos/myhg", 634 | "username": "user1", 635 | "encrypted_password": "encrypted_value" 636 | } 637 | ``` 638 | 639 | - Instead of `encrypted_password` you may specify `password` but `encrypted_password` makes more sense considering that the value is stored in SCM. 640 | - Specifying credentials both in `attributes` and `url` will result in a validation error e.g. 641 | ```log 642 | INVALID MERGED CONFIGURATION 643 | Number of errors: 1+ 644 | 1. Ambiguous credentials, must be provided either in URL or as attributes.;; 645 | - For Config Repo: https://your.config.repo.url at cbb047d78c239ab23b9565099e800c6fe4cc0anc 646 | ``` 647 | 648 | In addition to that, you can also leverage `branch` attribute to specify the branch for material 649 | 650 | ```json 651 | { 652 | "branch": "feature" 653 | } 654 | ``` 655 | 656 | ## Perforce 657 | 658 | ```json 659 | { 660 | "port": "10.18.3.102:1666", 661 | "username": "user1", 662 | "encrypted_password": "encrypted_value", 663 | "use_tickets": false, 664 | "view": "//depot/dev/src... //anything/src/...", 665 | "filter": { 666 | "ignore": [ 667 | "lib", 668 | "tools" 669 | ] 670 | }, 671 | "destination": "dir1", 672 | "auto_update": false, 673 | "name": "p4materialName", 674 | "type": "p4" 675 | } 676 | ``` 677 | 678 | Instead of `encrypted_password` you may specify `password` but `encrypted_password` makes more sense considering that the value is stored in SCM. 679 | 680 | ## Tfs 681 | 682 | ```json 683 | { 684 | "url": "url3", 685 | "username": "user4", 686 | "domain": "example.com", 687 | "encrypted_password": "encrypted_value", 688 | "project": "projectDir", 689 | "filter": { 690 | "ignore": [ 691 | "tools", 692 | "externals" 693 | ] 694 | }, 695 | "destination": "dir1", 696 | "auto_update": false, 697 | "name": "tfsMaterialName", 698 | "type": "tfs" 699 | } 700 | ``` 701 | 702 | Instead of `encrypted_password` you may specify `password` but `encrypted_password` makes more sense considering that the value is stored in SCM. 703 | 704 | ## Dependency 705 | 706 | ```json 707 | { 708 | "pipeline": "pipeline2", 709 | "stage": "build", 710 | "name": "pipe2", 711 | "type": "dependency", 712 | "ignore_for_scheduling": false 713 | } 714 | ``` 715 | 716 | ## Package 717 | 718 | ```json 719 | { 720 | "package_id": "apt-repo-id", 721 | "name": "myapt", 722 | "type": "package" 723 | } 724 | ``` 725 | 726 | ## Pluggable SCM 727 | 728 | ```json 729 | { 730 | "scm_id": "someScmGitRepositoryId", 731 | "destination": "destinationDir", 732 | "filter": { 733 | "ignore": [ 734 | "dir1", 735 | "dir2" 736 | ] 737 | }, 738 | "name": "myPluggableGit", 739 | "type": "plugin" 740 | } 741 | ``` 742 | 743 | Since GoCD `>= 19.2.0` defining new pluggable materials that are not defined 744 | in the GoCD server is supported. 745 | 746 | ```json 747 | { 748 | "plugin_configuration": { 749 | "id": "plugin_id", 750 | "version": "1" 751 | }, 752 | "configuration": [ 753 | { 754 | "key": "url", 755 | "value": "git@github.com:tomzo/gocd-json-config-plugin.git" 756 | } 757 | ], 758 | "destination": "destinationDir", 759 | "filter": { 760 | "ignore": [ 761 | "dir1", 762 | "dir2" 763 | ] 764 | }, 765 | "name": "myPluggableGit", 766 | "type": "plugin" 767 | } 768 | ``` 769 | 770 | ## Configrepo 771 | 772 | This is a convenience for shorter and more consistent material declaration. 773 | When configuration repository is the same as one of pipeline materials, 774 | then you usually need to repeat definitions in XML and in JSON, for example: 775 | 776 | ```json 777 | ... 778 | "materials": [ 779 | { 780 | "url": "https://github.com/tomzo/gocd-json-config-example.git", 781 | "branch" : "ci", 782 | "type": "git", 783 | "name" : "mygit" 784 | } 785 | ], 786 | ... 787 | ``` 788 | 789 | And in server XML: 790 | ```xml 791 | 792 | 793 | 794 | 795 | 796 | ``` 797 | 798 | Notice that url and branch is repeated. This is inconvenient in case when you move repository, 799 | because it requires 2 updates, in code and in server XML. 800 | 801 | Using **`configrepo` material type**, above repetition can be avoided, 802 | last example can be refactored into: 803 | 804 | ```json 805 | ... 806 | "materials": [ 807 | { 808 | "type": "configrepo", 809 | "name" : "mygit" 810 | } 811 | ], 812 | ... 813 | ``` 814 | 815 | Server interprets `configrepo` material in this way: 816 | 817 | > Clone the material configuration of the repository we are parsing **as is in XML** and replace **name, destination and filters (whitelist/blacklist)**, 818 | then use the modified clone in place of `configrepo` material. 819 | 820 | 821 | # Tasks 822 | 823 | Every task object must have `type` field. Which can be `exec`, `ant`, `nant`, `rake`, `fetch`, `plugin` 824 | 825 | Optionally any task can have `run_if` and `on_cancel`. 826 | 827 | * `run_if` is a string. Valid values are `passed`, `failed`, `any` 828 | * `on_cancel` is a task object. Same rules apply as to tasks described on this page. 829 | 830 | ### Exec 831 | 832 | ```json 833 | { 834 | "type": "exec", 835 | "run_if": "passed", 836 | "on_cancel" : null, 837 | "command": "make", 838 | "arguments": [ 839 | "-j3", 840 | "docs", 841 | "install" 842 | ], 843 | "working_directory": null 844 | } 845 | ``` 846 | 847 | ### Ant 848 | 849 | ```json 850 | { 851 | "build_file": "mybuild.xml", 852 | "target": "compile", 853 | "type": "ant", 854 | "run_if": "any", 855 | "on_cancel" : null, 856 | } 857 | ``` 858 | 859 | ### Nant 860 | 861 | ```json 862 | { 863 | "type": "nant", 864 | "run_if": "passed", 865 | "working_directory": "script/build/123", 866 | "build_file": null, 867 | "target": null, 868 | "nant_path": null 869 | } 870 | ``` 871 | 872 | ### Rake 873 | 874 | ```json 875 | { 876 | "type": "rake", 877 | "run_if": "passed", 878 | "working_directory": "sample-project", 879 | "build_file": null, 880 | "target": null 881 | } 882 | ``` 883 | 884 | ### Fetch 885 | 886 | #### Fetch artifact from the GoCD server 887 | 888 | ```json 889 | { 890 | "type": "fetch", 891 | "artifact_origin": "gocd", 892 | "run_if": "any", 893 | "pipeline": "upstream", 894 | "stage": "upstream_stage", 895 | "job": "upstream_job", 896 | "is_source_a_file": false, 897 | "source": "result", 898 | "destination": "test" 899 | } 900 | ``` 901 | 902 | #### Fetch artifact from an external artifact store 903 | 904 | ```json 905 | { 906 | "type": "fetch", 907 | "artifact_origin": "external", 908 | "run_if": "any", 909 | "pipeline": "upstream", 910 | "stage": "upstream_stage", 911 | "job": "upstream_job", 912 | "artifact_id": "upstream_external_artifactid", 913 | "configuration": [ 914 | { 915 | "key": "DestOnAgent", 916 | "value": "foo" 917 | }, 918 | { 919 | "key": "some_secure_property", 920 | "encrypted_value": "ssd#%fFS*!Esx" 921 | } 922 | ] 923 | } 924 | ``` 925 | 926 | ### Plugin 927 | 928 | ```json 929 | { 930 | "type": "plugin", 931 | "configuration": [ 932 | { 933 | "key": "ConverterType", 934 | "value": "jsunit" 935 | }, 936 | { 937 | "key": "password", 938 | "encrypted_value": "ssd#%fFS*!Esx" 939 | } 940 | ], 941 | "run_if": "passed", 942 | "plugin_configuration": { 943 | "id": "xunit.converter.task.plugin", 944 | "version": "1" 945 | }, 946 | "on_cancel": null 947 | } 948 | ``` 949 | 950 | # Development 951 | 952 | ## Environment setup 953 | 954 | To build and test this plugin, you'll need java jdk >= 8. 955 | 956 | If you have local java environment, then you may run all tests and create a ready to use jar with: 957 | ```bash 958 | ./gradlew test jar 959 | ``` 960 | 961 | ## Building with docker and dojo 962 | 963 | You don't need to setup java on your host, if you are fine with using docker and [Dojo](https://github.com/ai-traders/dojo). 964 | This is actually how our GoCD builds the plugin: 965 | ``` 966 | dojo "gradle test jar" 967 | ``` 968 | 969 | Assuming you already have a working docker, On OSX, you can install with homebrew: 970 | ``` 971 | brew install kudulab/homebrew-dojo-osx/dojo 972 | ``` 973 | A manual install is another option: 974 | ```sh 975 | version="0.9.0" 976 | # on Linux: 977 | wget -O /tmp/dojo https://github.com/kudulab/dojo/releases/download/${version}/dojo_linux_amd64 978 | # or on Darwin: 979 | # wget -O /tmp/dojo https://github.com/kudulab/dojo/releases/download/${version}/dojo_darwin_amd64 980 | chmod +x /tmp/dojo 981 | mv /tmp/dojo /usr/bin/dojo 982 | ``` 983 | 984 | Then enter a docker container with java and gradle pre-installed, by running following command at the root of the project: 985 | ``` 986 | dojo 987 | ``` 988 | 989 | # Issues and questions 990 | 991 | * If you have **questions on usage**, please ask them on the GoCD [Google Groups forum](https://groups.google.com/g/go-cd) or 992 | [GitHub Discussions](https://github.com/gocd/gocd/discussions). 993 | 994 | * If you think there is a bug, or you have an idea for a feature and *you are not sure if it's plugin's or [GoCD](https://github.com/gocd/gocd/issues) fault/responsibity*, please ask on the chat first too. 995 | 996 | Please note this brief overview of what is done by the plugin: 997 | * parsing files into json when GoCD server asks for it. 998 | 999 | And this is done by the GoCD server: 1000 | * complex logic merging multiple config repo sources and XML 1001 | * validation of pipelines/stages/jobs/tasks domain 1002 | * any UI rendering 1003 | 1004 | ## Versioning 1005 | 1006 | We use semantic versioning. 1007 | 1008 | If you are submitting a new feature or patch, please bump the version in `build.gradle`. 1009 | 1010 | # License 1011 | 1012 | Copyright 2019 Tomasz Sętkowski 1013 | 1014 | Licensed under the Apache License, Version 2.0 (the "License"); 1015 | you may not use this file except in compliance with the License. 1016 | You may obtain a copy of the License at 1017 | 1018 | http://www.apache.org/licenses/LICENSE-2.0 1019 | 1020 | Unless required by applicable law or agreed to in writing, software 1021 | distributed under the License is distributed on an "AS IS" BASIS, 1022 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1023 | See the License for the specific language governing permissions and 1024 | limitations under the License. 1025 | --------------------------------------------------------------------------------