From 951c9499eaa84003a488db7b6f3298c23c02f73d Mon Sep 17 00:00:00 2001 From: Sandro Heinzelmann Date: Fri, 27 May 2022 15:15:34 +0200 Subject: [PATCH] Optionally set approval:manual on first stage of PR pipelines (#3) --- build.gradle | 2 +- .../cd/go/plugin/config/yaml/JSONUtils.java | 19 ++- .../go/plugin/config/yaml/PluginSettings.java | 22 ++++ .../plugin/config/yaml/YamlConfigPlugin.java | 51 +++++--- .../yaml/transforms/DefaultOverrides.java | 112 ++++++++++++++++++ .../yaml/transforms/MaterialTransform.java | 19 +-- .../yaml/transforms/PipelineTransform.java | 34 ++++-- .../yaml/transforms/StageTransform.java | 24 ++-- .../resources/plugin-settings.template.html | 10 ++ .../yaml/YamlConfigPluginIntegrationTest.java | 78 +++++++++--- .../PipelineTransformIntegrationTest.java | 109 +++++++++++++++++ .../examples.out/pullrequest.gocd.json | 38 ++++++ .../resources/examples/pullrequest.gocd.yaml | 14 +++ .../parts/explicit_approval.pipe.json | 44 +++++++ .../parts/explicit_approval.pipe.yaml | 21 ++++ .../parts/pullrequest-no-approval.pipe.json | 41 +++++++ .../resources/parts/pullrequest.pipe.json | 44 +++++++ .../resources/parts/pullrequest.pipe.yaml | 19 +++ 18 files changed, 635 insertions(+), 66 deletions(-) create mode 100644 src/test/java/cd/go/plugin/config/yaml/transforms/PipelineTransformIntegrationTest.java create mode 100644 src/test/resources/examples.out/pullrequest.gocd.json create mode 100644 src/test/resources/examples/pullrequest.gocd.yaml create mode 100644 src/test/resources/parts/explicit_approval.pipe.json create mode 100644 src/test/resources/parts/explicit_approval.pipe.yaml create mode 100644 src/test/resources/parts/pullrequest-no-approval.pipe.json create mode 100644 src/test/resources/parts/pullrequest.pipe.json create mode 100644 src/test/resources/parts/pullrequest.pipe.yaml diff --git a/build.gradle b/build.gradle index 98965ee8..cd0aecf3 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { } group 'cd.go.plugin.config.yaml' -version "0.13.2-adn.3" +version "0.13.2-adn.4" apply plugin: 'java' apply plugin: "com.github.jk1.dependency-license-report" diff --git a/src/main/java/cd/go/plugin/config/yaml/JSONUtils.java b/src/main/java/cd/go/plugin/config/yaml/JSONUtils.java index 15d65a24..66b5cfe6 100644 --- a/src/main/java/cd/go/plugin/config/yaml/JSONUtils.java +++ b/src/main/java/cd/go/plugin/config/yaml/JSONUtils.java @@ -1,10 +1,14 @@ package cd.go.plugin.config.yaml; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - import java.util.List; import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; public class JSONUtils { static T fromJSON(String json) { @@ -46,4 +50,13 @@ public static void addRequiredValue(Map dest, Map stream(JsonArray array) { + return StreamSupport.stream(array.spliterator(), false); + } + + public static String getKeyAsString(JsonElement element, String key) { + JsonElement value = element.getAsJsonObject().get(key); + return value != null ? value.getAsString() : null; + } } \ No newline at end of file diff --git a/src/main/java/cd/go/plugin/config/yaml/PluginSettings.java b/src/main/java/cd/go/plugin/config/yaml/PluginSettings.java index ab364395..ccdb7c72 100644 --- a/src/main/java/cd/go/plugin/config/yaml/PluginSettings.java +++ b/src/main/java/cd/go/plugin/config/yaml/PluginSettings.java @@ -7,11 +7,20 @@ class PluginSettings { static final String DEFAULT_FILE_PATTERN = "**/*.gocd.yaml,**/*.gocd.yml"; static final String DEFAULT_DEFAULT_AUTO_UPDATE = ""; + static final String DEFAULT_USE_APPROVAL_MANUAL_FOR_PRS = "false"; + + static final String DEFAULT_PR_MATERIAL_ID_PATTERN = null; + static final String PLUGIN_SETTINGS_DEFAULT_AUTO_UPDATE = "default_auto_update"; + static final String PLUGIN_SETTINGS_USE_APPROVAL_MANUAL_FOR_PRS = "use_approval_manual_for_prs"; + static final String PLUGIN_SETTINGS_PR_MATERIAL_ID_PATTERN = "pr_material_id_pattern"; + private String filePattern; private Boolean defaultGitAutoUpdate; + private boolean useApprovalManualForPRs; + private String prMaterialIdPattern; PluginSettings() { } @@ -23,11 +32,16 @@ class PluginSettings { static PluginSettings fromJson(String json) { Map raw = JSONUtils.fromJSON(json); PluginSettings settings = new PluginSettings(); + settings.filePattern = raw.get(PLUGIN_SETTINGS_FILE_PATTERN); + String defaultAutoUpdateVal = raw.get(PLUGIN_SETTINGS_DEFAULT_AUTO_UPDATE); settings.defaultGitAutoUpdate = defaultAutoUpdateVal == null || defaultAutoUpdateVal.isBlank() ? null : Boolean.valueOf(defaultAutoUpdateVal); + settings.useApprovalManualForPRs = Boolean.parseBoolean(raw.get(PLUGIN_SETTINGS_USE_APPROVAL_MANUAL_FOR_PRS)); + settings.prMaterialIdPattern = raw.get(PLUGIN_SETTINGS_PR_MATERIAL_ID_PATTERN); + return settings; } @@ -38,4 +52,12 @@ String getFilePattern() { Boolean getDefaultGitAutoUpdate() { return defaultGitAutoUpdate; } + + boolean useApprovalManualForPRs() { + return useApprovalManualForPRs; + } + + String getPrMaterialIdPattern() { + return prMaterialIdPattern; + } } \ No newline at end of file diff --git a/src/main/java/cd/go/plugin/config/yaml/YamlConfigPlugin.java b/src/main/java/cd/go/plugin/config/yaml/YamlConfigPlugin.java index 39e4e9be..5be05bd7 100644 --- a/src/main/java/cd/go/plugin/config/yaml/YamlConfigPlugin.java +++ b/src/main/java/cd/go/plugin/config/yaml/YamlConfigPlugin.java @@ -1,7 +1,33 @@ package cd.go.plugin.config.yaml; -import cd.go.plugin.config.yaml.transforms.RootTransform; +import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_DEFAULT_AUTO_UPDATE; +import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_FILE_PATTERN; +import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_USE_APPROVAL_MANUAL_FOR_PRS; +import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_PR_MATERIAL_ID_PATTERN; +import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_DEFAULT_AUTO_UPDATE; +import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_FILE_PATTERN; +import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_USE_APPROVAL_MANUAL_FOR_PRS; +import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_PR_MATERIAL_ID_PATTERN; +import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.SUCCESS_RESPONSE_CODE; +import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.badRequest; +import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.error; +import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.success; +import static java.lang.String.format; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + import cd.go.plugin.config.yaml.transforms.DefaultOverrides; +import cd.go.plugin.config.yaml.transforms.RootTransform; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.thoughtworks.go.plugin.api.GoApplicationAccessor; @@ -17,23 +43,14 @@ import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import org.apache.commons.io.IOUtils; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.function.Supplier; - -import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_DEFAULT_AUTO_UPDATE; -import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_FILE_PATTERN; -import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_DEFAULT_AUTO_UPDATE; -import static cd.go.plugin.config.yaml.PluginSettings.PLUGIN_SETTINGS_FILE_PATTERN; -import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.*; -import static java.lang.String.format; - @Extension public class YamlConfigPlugin implements GoPlugin, ConfigRepoMessages { private static final String DISPLAY_NAME_FILE_PATTERN = "Go YAML files pattern"; private static final String DISPLAY_NAME_DEFAULT_AUTO_UPDATE = "Default auto_update for Git materials"; + + private static final String DISPLAY_NAME_USE_APPROVAL_MANUAL_FOR_PRS = "Set approval: manual on PR pipelines"; + + private static final String DISPLAY_NAME_PR_MATERIAL_ID_PATTERN = "Regex pattern to identify PR materials by their ID"; private static final String PLUGIN_ID = "yaml.config.plugin.adn"; private static Logger LOGGER = Logger.getLoggerFor(YamlConfigPlugin.class); @@ -121,6 +138,8 @@ private void ensureConfigured() { settings = fetchPluginSettings(); } DefaultOverrides.setDefaultGitAutoUpdate(settings.getDefaultGitAutoUpdate()); + DefaultOverrides.setUseApprovalManualForPRs(settings.useApprovalManualForPRs()); + DefaultOverrides.setPrMaterialIdPattern(settings.getPrMaterialIdPattern()); } private GoPluginApiResponse handleParseContentRequest(GoPluginApiRequest request) { @@ -212,6 +231,10 @@ private GoPluginApiResponse handleGetPluginSettingsConfiguration() { response.put(PLUGIN_SETTINGS_FILE_PATTERN, createField(DISPLAY_NAME_FILE_PATTERN, DEFAULT_FILE_PATTERN, false, false, "0")); response.put(PLUGIN_SETTINGS_DEFAULT_AUTO_UPDATE, createField(DISPLAY_NAME_DEFAULT_AUTO_UPDATE, DEFAULT_DEFAULT_AUTO_UPDATE, false, false, "1")); + response.put(PLUGIN_SETTINGS_USE_APPROVAL_MANUAL_FOR_PRS, createField(DISPLAY_NAME_USE_APPROVAL_MANUAL_FOR_PRS, + DEFAULT_USE_APPROVAL_MANUAL_FOR_PRS, false, false, "2")); + response.put(PLUGIN_SETTINGS_PR_MATERIAL_ID_PATTERN, createField(DISPLAY_NAME_PR_MATERIAL_ID_PATTERN, + DEFAULT_PR_MATERIAL_ID_PATTERN, false, false, "3")); return success(gson.toJson(response)); } diff --git a/src/main/java/cd/go/plugin/config/yaml/transforms/DefaultOverrides.java b/src/main/java/cd/go/plugin/config/yaml/transforms/DefaultOverrides.java index 01e5bebd..63947d3b 100644 --- a/src/main/java/cd/go/plugin/config/yaml/transforms/DefaultOverrides.java +++ b/src/main/java/cd/go/plugin/config/yaml/transforms/DefaultOverrides.java @@ -4,21 +4,43 @@ package cd.go.plugin.config.yaml.transforms; +import static cd.go.plugin.config.yaml.JSONUtils.getKeyAsString; import static cd.go.plugin.config.yaml.transforms.MaterialTransform.JSON_MATERIAL_AUTO_UPDATE_FIELD; import static cd.go.plugin.config.yaml.transforms.MaterialTransform.JSON_MATERIAL_TYPE_FIELD; import static cd.go.plugin.config.yaml.transforms.MaterialTransform.YAML_MATERIAL_AUTO_UPDATE_FIELD; import static cd.go.plugin.config.yaml.transforms.MaterialTransform.YAML_SHORT_KEYWORD_GIT; +import static cd.go.plugin.config.yaml.transforms.MaterialTransform.YAML_SHORT_KEYWORD_SCM_ID; +import static cd.go.plugin.config.yaml.transforms.PipelineTransform.JSON_PIPELINE_MATERIALS_FIELD; +import static cd.go.plugin.config.yaml.transforms.PipelineTransform.JSON_PIPELINE_STAGES_FIELD; +import static cd.go.plugin.config.yaml.transforms.PipelineTransform.YAML_PIPELINE_MATERIALS_FIELD; +import static cd.go.plugin.config.yaml.transforms.PipelineTransform.YAML_PIPELINE_STAGES_FIELD; +import static cd.go.plugin.config.yaml.transforms.StageTransform.JSON_STAGE_APPROVAL_FIELD; +import static cd.go.plugin.config.yaml.transforms.StageTransform.JSON_STAGE_APPROVAL_TYPE_FIELD; +import static cd.go.plugin.config.yaml.transforms.StageTransform.YAML_STAGE_APPROVAL_TYPE_FIELD; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; +import cd.go.plugin.config.yaml.JSONUtils; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; public class DefaultOverrides { private static final ThreadLocal SINGLETON = new ThreadLocal<>(); + // null = not set, don't use a default + // true = set auto_update to true by default + // false = set auto_update to false by default private Boolean defaultGitAutoUpdate = null; + private boolean useApprovalManualForPRs = false; + + private String prMaterialIdPattern = null; + public static DefaultOverrides getInstance() { if (SINGLETON.get() == null) { SINGLETON.set(new DefaultOverrides()); @@ -34,6 +56,22 @@ public static Boolean getDefaultGitAutoUpdate() { return getInstance().defaultGitAutoUpdate; } + public static boolean useApprovalManualForPRs() { + return getInstance().useApprovalManualForPRs; + } + + public static void setUseApprovalManualForPRs(boolean useApprovalManualForPRs) { + getInstance().useApprovalManualForPRs = useApprovalManualForPRs; + } + + public static void setPrMaterialIdPattern(String prMaterialIdPattern) { + getInstance().prMaterialIdPattern = prMaterialIdPattern; + } + + public static String getPrMaterialIdPattern() { + return getInstance().prMaterialIdPattern; + } + public static void overrideGitMaterialAutoUpdate(JsonObject jsonOutput) { if (getDefaultGitAutoUpdate() != null && jsonOutput.has(JSON_MATERIAL_TYPE_FIELD) && @@ -51,4 +89,78 @@ public static void overrideGitMaterialAutoUpdateInverse(Map yaml } } } + + public static void addApprovalManualForPullRequests(JsonObject pipeline) { + if (!isApprovalManualForPRsFullyConfigured()) { + return; + } + + JsonArray materials = pipeline.getAsJsonArray(JSON_PIPELINE_MATERIALS_FIELD); + if (materials == null) { + return; + } + + boolean hasPRMaterials = JSONUtils.stream(materials).anyMatch(DefaultOverrides::isPRMaterial); + if (!hasPRMaterials) { + return; + } + + JsonArray stages = pipeline.getAsJsonArray(JSON_PIPELINE_STAGES_FIELD); + if (stages == null || stages.size() == 0) { + return; + } + JsonObject firstStage = stages.get(0).getAsJsonObject(); + if (firstStage.has("approval")) { + return; + } + JsonObject approvalJson = new JsonObject(); + approvalJson.addProperty(JSON_STAGE_APPROVAL_TYPE_FIELD, "manual"); + firstStage.add(JSON_STAGE_APPROVAL_FIELD, approvalJson); + } + + public static void addApprovalManualForPullRequestsInverse(Map pipeline) { + if (!isApprovalManualForPRsFullyConfigured()) { + return; + } + + Map> materials = + (Map>) pipeline.get(YAML_PIPELINE_MATERIALS_FIELD); + if (materials == null) { + return; + } + + boolean hasPRMaterials = materials.values().stream().anyMatch(DefaultOverrides::isPRMaterial); + if (!hasPRMaterials) { + return; + } + + List> stages = (List>) pipeline.get(YAML_PIPELINE_STAGES_FIELD); + if (stages == null || stages.isEmpty()) { + return; + } + Map firstStage = (Map) stages.get(0).values().iterator().next(); + + Map approval = (Map) firstStage.get("approval"); + if (approval != null && Objects.equals(approval.get(YAML_STAGE_APPROVAL_TYPE_FIELD), "manual")) { + firstStage.remove("approval"); + } + } + + private static boolean isApprovalManualForPRsFullyConfigured() { + return useApprovalManualForPRs() && getPrMaterialIdPattern() != null && !getPrMaterialIdPattern().isBlank(); + } + + private static boolean isPRMaterial(JsonElement material) { + if (!material.isJsonObject() || !Objects.equals(getKeyAsString(material, JSON_MATERIAL_TYPE_FIELD), "plugin")) { + return false; + } + + String scmId = getKeyAsString(material, "scm_id"); + return scmId != null && Pattern.matches(getPrMaterialIdPattern(), scmId); + } + + private static boolean isPRMaterial(Map material) { + String scmId = (String) material.get(YAML_SHORT_KEYWORD_SCM_ID); + return scmId != null && Pattern.matches(getPrMaterialIdPattern(), scmId); + } } diff --git a/src/main/java/cd/go/plugin/config/yaml/transforms/MaterialTransform.java b/src/main/java/cd/go/plugin/config/yaml/transforms/MaterialTransform.java index 44370a53..f2c9c640 100644 --- a/src/main/java/cd/go/plugin/config/yaml/transforms/MaterialTransform.java +++ b/src/main/java/cd/go/plugin/config/yaml/transforms/MaterialTransform.java @@ -1,17 +1,20 @@ package cd.go.plugin.config.yaml.transforms; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.internal.LinkedTreeMap; +import static cd.go.plugin.config.yaml.JSONUtils.addOptionalValue; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalBoolean; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalObject; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalString; +import static cd.go.plugin.config.yaml.YamlUtils.getOptionalString; +import static java.lang.String.format; +import static java.util.UUID.randomUUID; import java.util.HashSet; import java.util.List; import java.util.Map; -import static cd.go.plugin.config.yaml.JSONUtils.addOptionalValue; -import static cd.go.plugin.config.yaml.YamlUtils.*; -import static java.lang.String.format; -import static java.util.UUID.randomUUID; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.internal.LinkedTreeMap; public class MaterialTransform extends ConfigurationTransform { @@ -31,7 +34,7 @@ public class MaterialTransform extends ConfigurationTransform { public static final String YAML_BLACKLIST_KEYWORD = "blacklist"; private static final String YAML_SHORT_KEYWORD_DEPENDENCY = "pipeline"; - private static final String YAML_SHORT_KEYWORD_SCM_ID = "scm"; + public static final String YAML_SHORT_KEYWORD_SCM_ID = "scm"; private static final String YAML_SHORT_KEYWORD_PACKAGE_ID = "package"; private static final String YAML_SHORT_KEYWORD_SVN = "svn"; private static final String JSON_MATERIAL_CHECK_EXTERNALS_FIELD = "check_externals"; diff --git a/src/main/java/cd/go/plugin/config/yaml/transforms/PipelineTransform.java b/src/main/java/cd/go/plugin/config/yaml/transforms/PipelineTransform.java index 797baafa..7ae8f91b 100644 --- a/src/main/java/cd/go/plugin/config/yaml/transforms/PipelineTransform.java +++ b/src/main/java/cd/go/plugin/config/yaml/transforms/PipelineTransform.java @@ -1,19 +1,23 @@ package cd.go.plugin.config.yaml.transforms; -import cd.go.plugin.config.yaml.YamlConfigException; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.internal.LinkedTreeMap; +import static cd.go.plugin.config.yaml.JSONUtils.addOptionalInt; +import static cd.go.plugin.config.yaml.JSONUtils.addOptionalValue; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalBoolean; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalInteger; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalObject; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalString; +import static cd.go.plugin.config.yaml.YamlUtils.addRequiredString; +import static cd.go.plugin.config.yaml.transforms.EnvironmentVariablesTransform.JSON_ENV_VAR_FIELD; +import static cd.go.plugin.config.yaml.transforms.ParameterTransform.YAML_PIPELINE_PARAMETERS_FIELD; import java.util.ArrayList; import java.util.List; import java.util.Map; -import static cd.go.plugin.config.yaml.JSONUtils.addOptionalInt; -import static cd.go.plugin.config.yaml.JSONUtils.addOptionalValue; -import static cd.go.plugin.config.yaml.YamlUtils.*; -import static cd.go.plugin.config.yaml.transforms.EnvironmentVariablesTransform.JSON_ENV_VAR_FIELD; -import static cd.go.plugin.config.yaml.transforms.ParameterTransform.YAML_PIPELINE_PARAMETERS_FIELD; +import cd.go.plugin.config.yaml.YamlConfigException; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.internal.LinkedTreeMap; public class PipelineTransform { private static final String JSON_PIPELINE_NAME_FIELD = "name"; @@ -24,8 +28,8 @@ public class PipelineTransform { private static final String JSON_PIPELINE_LOCK_BEHAVIOR_FIELD = "lock_behavior"; private static final String JSON_PIPELINE_TRACKING_TOOL_FIELD = "tracking_tool"; private static final String JSON_PIPELINE_TIMER_FIELD = "timer"; - private static final String JSON_PIPELINE_MATERIALS_FIELD = "materials"; - private static final String JSON_PIPELINE_STAGES_FIELD = "stages"; + public static final String JSON_PIPELINE_MATERIALS_FIELD = "materials"; + public static final String JSON_PIPELINE_STAGES_FIELD = "stages"; private static final String JSON_PIPELINE_DISPLAY_ORDER_FIELD = "display_order_weight"; private static final String YAML_PIPELINE_GROUP_FIELD = "group"; @@ -35,8 +39,8 @@ public class PipelineTransform { private static final String YAML_PIPELINE_LOCK_BEHAVIOR_FIELD = "lock_behavior"; private static final String YAML_PIPELINE_TRACKING_TOOL_FIELD = "tracking_tool"; private static final String YAML_PIPELINE_TIMER_FIELD = "timer"; - private static final String YAML_PIPELINE_MATERIALS_FIELD = "materials"; - private static final String YAML_PIPELINE_STAGES_FIELD = "stages"; + public static final String YAML_PIPELINE_MATERIALS_FIELD = "materials"; + public static final String YAML_PIPELINE_STAGES_FIELD = "stages"; private static final String YAML_PIPELINE_DISPLAY_ORDER_FIELD = "display_order"; private final MaterialTransform materialTransform; @@ -89,6 +93,8 @@ public JsonObject transform(Map.Entry entry, int formatVersion) pipeline.add(YAML_PIPELINE_PARAMETERS_FIELD, params); } + DefaultOverrides.addApprovalManualForPullRequests(pipeline); + return pipeline; } @@ -125,6 +131,8 @@ public Map inverseTransform(Map pipeline) { pipelineMap.putAll(params); } + DefaultOverrides.addApprovalManualForPullRequestsInverse(pipelineMap); + result.put(name, pipelineMap); return result; diff --git a/src/main/java/cd/go/plugin/config/yaml/transforms/StageTransform.java b/src/main/java/cd/go/plugin/config/yaml/transforms/StageTransform.java index 518a14c7..f31ceffa 100644 --- a/src/main/java/cd/go/plugin/config/yaml/transforms/StageTransform.java +++ b/src/main/java/cd/go/plugin/config/yaml/transforms/StageTransform.java @@ -1,16 +1,20 @@ package cd.go.plugin.config.yaml.transforms; -import cd.go.plugin.config.yaml.YamlConfigException; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.internal.LinkedTreeMap; +import static cd.go.plugin.config.yaml.JSONUtils.addOptionalList; +import static cd.go.plugin.config.yaml.JSONUtils.addOptionalValue; +import static cd.go.plugin.config.yaml.JSONUtils.addRequiredValue; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalBoolean; +import static cd.go.plugin.config.yaml.YamlUtils.addOptionalStringList; +import static cd.go.plugin.config.yaml.YamlUtils.addRequiredString; +import static cd.go.plugin.config.yaml.transforms.EnvironmentVariablesTransform.JSON_ENV_VAR_FIELD; import java.util.List; import java.util.Map; -import static cd.go.plugin.config.yaml.JSONUtils.*; -import static cd.go.plugin.config.yaml.YamlUtils.*; -import static cd.go.plugin.config.yaml.transforms.EnvironmentVariablesTransform.JSON_ENV_VAR_FIELD; +import cd.go.plugin.config.yaml.YamlConfigException; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.internal.LinkedTreeMap; public class StageTransform { private static final String JSON_STAGE_NAME_FIELD = "name"; @@ -21,9 +25,9 @@ public class StageTransform { private static final String JSON_STAGE_CLEAN_WORK_FIELD = "clean_working_directory"; private static final String YAML_STAGE_CLEAN_WORK_FIELD = "clean_workspace"; private static final String YAML_STAGE_APPROVAL_FIELD = "approval"; - private static final String JSON_STAGE_APPROVAL_FIELD = "approval"; - private static final String JSON_STAGE_APPROVAL_TYPE_FIELD = "type"; - private static final String YAML_STAGE_APPROVAL_TYPE_FIELD = "type"; + public static final String JSON_STAGE_APPROVAL_FIELD = "approval"; + public static final String JSON_STAGE_APPROVAL_TYPE_FIELD = "type"; + public static final String YAML_STAGE_APPROVAL_TYPE_FIELD = "type"; private static final String JSON_STAGE_JOBS_FIELD = "jobs"; private static final String JSON_STAGE_APPROVAL_ALLOW_ONLY_ON_SUCCESS_FIELD = "allow_only_on_success"; private static final String YAML_STAGE_APPROVAL_ALLOW_ONLY_ON_SUCCESS_FIELD = "allow_only_on_success"; diff --git a/src/main/resources/plugin-settings.template.html b/src/main/resources/plugin-settings.template.html index 5cd8ca17..3a18f514 100644 --- a/src/main/resources/plugin-settings.template.html +++ b/src/main/resources/plugin-settings.template.html @@ -2,6 +2,7 @@ {{ GOINPUTNAME[file_pattern].$error.server }} + {{ GOINPUTNAME[default_auto_update].$error.server }} + +
+ + + {{ GOINPUTNAME[use_approval_manual_for_prs].$error.server }} + + + + {{ GOINPUTNAME[pr_material_id_pattern].$error.server }} diff --git a/src/test/java/cd/go/plugin/config/yaml/YamlConfigPluginIntegrationTest.java b/src/test/java/cd/go/plugin/config/yaml/YamlConfigPluginIntegrationTest.java index 5eb9eecf..90933aff 100644 --- a/src/test/java/cd/go/plugin/config/yaml/YamlConfigPluginIntegrationTest.java +++ b/src/test/java/cd/go/plugin/config/yaml/YamlConfigPluginIntegrationTest.java @@ -1,6 +1,36 @@ package cd.go.plugin.config.yaml; -import com.google.gson.*; +import static cd.go.plugin.config.yaml.ConfigRepoMessages.REQ_PLUGIN_SETTINGS_CHANGED; +import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_FILE_PATTERN; +import static cd.go.plugin.config.yaml.TestUtils.getResourceAsStream; +import static cd.go.plugin.config.yaml.TestUtils.readJsonObject; +import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.SUCCESS_RESPONSE_CODE; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.thoughtworks.go.plugin.api.GoApplicationAccessor; import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException; import com.thoughtworks.go.plugin.api.request.DefaultGoPluginApiRequest; @@ -17,22 +47,6 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -import java.io.File; -import java.io.IOException; -import java.io.StringWriter; -import java.util.Base64; -import java.util.Collections; - -import static cd.go.plugin.config.yaml.ConfigRepoMessages.REQ_PLUGIN_SETTINGS_CHANGED; -import static cd.go.plugin.config.yaml.PluginSettings.DEFAULT_FILE_PATTERN; -import static cd.go.plugin.config.yaml.TestUtils.getResourceAsStream; -import static cd.go.plugin.config.yaml.TestUtils.readJsonObject; -import static com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse.SUCCESS_RESPONSE_CODE; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - public class YamlConfigPluginIntegrationTest { @Rule public TemporaryFolder tempDir = new TemporaryFolder(); @@ -101,6 +115,36 @@ public void respondsToParseContentRequestWithDefaultAutoUpdate() throws Exceptio assertThat(responseJsonObject, is(new JsonObjectMatcher(expected))); } + @Test + public void respondsToParseContentRequestWithApprovalManualForPRs() throws Exception { + Map settings = new HashMap<>(); + settings.put("use_approval_manual_for_prs", "true"); + settings.put("pr_material_id_pattern", "^.*\\.pr$"); + GoApiResponse settingsResponse = DefaultGoApiResponse.success(JSONUtils.toJSON(settings)); + when(goAccessor.submit(any(GoApiRequest.class))).thenReturn(settingsResponse); + + final Gson gson = new Gson(); + DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("configrepo", "2.0", ConfigRepoMessages.REQ_PARSE_CONTENT); + + StringWriter w = new StringWriter(); + IOUtils.copy(getResourceAsStream("examples/pullrequest.gocd.yaml"), w); + request.setRequestBody(gson.toJson( + Collections.singletonMap("contents", + Collections.singletonMap("pullrequest.gocd.yaml", w.toString()) + ) + )); + + GoPluginApiResponse response = plugin.handle(request); + assertEquals(SUCCESS_RESPONSE_CODE, response.responseCode()); + JsonObject responseJsonObject = getJsonObjectFromResponse(response); + assertNoError(responseJsonObject); + + JsonArray pipelines = responseJsonObject.get("pipelines").getAsJsonArray(); + assertThat(pipelines.size(), is(1)); + JsonObject expected = (JsonObject) readJsonObject("examples.out/pullrequest.gocd.json"); + assertThat(responseJsonObject, is(new JsonObjectMatcher(expected))); + } + @Test public void respondsToGetConfigFiles() throws Exception { final Gson gson = new Gson(); diff --git a/src/test/java/cd/go/plugin/config/yaml/transforms/PipelineTransformIntegrationTest.java b/src/test/java/cd/go/plugin/config/yaml/transforms/PipelineTransformIntegrationTest.java new file mode 100644 index 00000000..de95edda --- /dev/null +++ b/src/test/java/cd/go/plugin/config/yaml/transforms/PipelineTransformIntegrationTest.java @@ -0,0 +1,109 @@ +package cd.go.plugin.config.yaml.transforms; + +import static cd.go.plugin.config.yaml.TestUtils.assertYamlEquivalent; +import static cd.go.plugin.config.yaml.TestUtils.loadString; +import static cd.go.plugin.config.yaml.TestUtils.readJsonGson; +import static cd.go.plugin.config.yaml.TestUtils.readJsonObject; +import static cd.go.plugin.config.yaml.TestUtils.readYamlObject; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.util.Map; + +import cd.go.plugin.config.yaml.JsonObjectMatcher; +import cd.go.plugin.config.yaml.YamlUtils; +import com.google.gson.JsonObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class PipelineTransformIntegrationTest { + + private PipelineTransform parser; + + @Before + public void setUp() { + EnvironmentVariablesTransform environmentTransform = new EnvironmentVariablesTransform(); + + parser = new PipelineTransform( + new MaterialTransform(), + new StageTransform(environmentTransform, new JobTransform(environmentTransform, new TaskTransform())), + environmentTransform, + new ParameterTransform()); + } + + @After + public void cleanUp() { + DefaultOverrides.setUseApprovalManualForPRs(false); + DefaultOverrides.setPrMaterialIdPattern(null); + } + + @Test + public void shouldAddApprovalManualToPRPipelines() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("^.*\\.pr$"); + + testTransform("pullrequest.pipe"); + } + + @Test + public void shouldAddApprovalManualToPRPipelines_Inverse() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("^.*\\.pr$"); + + testInverseTransform("pullrequest.pipe"); + } + + @Test + public void shouldNotAddApprovalManualIfOtherSCM() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("not-matching-pattern"); + + testTransform("pullrequest.pipe", "pullrequest-no-approval.pipe"); + } + + @Test + public void shouldNotAddApprovalManualIfOtherSCM_Inverse() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("not-matching-pattern"); + + testInverseTransform("pullrequest-no-approval.pipe", "pullrequest.pipe"); + } + + @Test + public void shouldNotAddApprovalManualIfExplicitApproval() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("^.*\\.pr$"); + + testTransform("explicit_approval.pipe"); + } + + @Test + public void shouldNotAddApprovalManualIfExplicitApproval_Inverse() throws IOException { + DefaultOverrides.setUseApprovalManualForPRs(true); + DefaultOverrides.setPrMaterialIdPattern("^.*\\.pr$"); + + testInverseTransform("explicit_approval.pipe"); + } + + private void testTransform(String caseFile) throws IOException { + testTransform(caseFile, caseFile); + } + + private void testInverseTransform(String caseFile) throws IOException { + testInverseTransform(caseFile, caseFile); + } + + private void testTransform(String caseFile, String expectedFile) throws IOException { + JsonObject expectedObject = (JsonObject) readJsonObject("parts/" + expectedFile + ".json"); + JsonObject jsonObject = parser.transform(readYamlObject("parts/" + caseFile + ".yaml"), 9); + assertThat(jsonObject, is(new JsonObjectMatcher(expectedObject))); + } + + private void testInverseTransform(String caseFile, String expectedFile) throws IOException { + String expectedObject = loadString("parts/" + expectedFile + ".yaml"); + Map actual = parser.inverseTransform(readJsonGson("parts/" + caseFile + ".json")); + assertYamlEquivalent(expectedObject, YamlUtils.dump(actual)); + } +} diff --git a/src/test/resources/examples.out/pullrequest.gocd.json b/src/test/resources/examples.out/pullrequest.gocd.json new file mode 100644 index 00000000..d02308d6 --- /dev/null +++ b/src/test/resources/examples.out/pullrequest.gocd.json @@ -0,0 +1,38 @@ +{ + "target_version": 1, + "errors": [], + "environments": [], + "pipelines": [ + { + "location": "pullrequest.gocd.yaml", + "name": "pipe1", + "group": "pullrequest", + "materials": [ + { + "name": "pipe1-pr", + "type": "plugin", + "scm_id": "foo.bar.pr" + } + ], + "stages": [ + { + "name": "build", + "approval": { + "type": "manual" + }, + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/test/resources/examples/pullrequest.gocd.yaml b/src/test/resources/examples/pullrequest.gocd.yaml new file mode 100644 index 00000000..b596f7ac --- /dev/null +++ b/src/test/resources/examples/pullrequest.gocd.yaml @@ -0,0 +1,14 @@ +# pullrequest.gocd.yaml +pipelines: + pipe1: + group: pullrequest + materials: + pipe1-pr: + scm: foo.bar.pr + stages: + - build: # name of stage + jobs: + build: # name of the job + tasks: + - exec: # indicates type of task + command: make diff --git a/src/test/resources/parts/explicit_approval.pipe.json b/src/test/resources/parts/explicit_approval.pipe.json new file mode 100644 index 00000000..2b842a8d --- /dev/null +++ b/src/test/resources/parts/explicit_approval.pipe.json @@ -0,0 +1,44 @@ +{ + "name": "pipe1", + "group": "mygroup", + "materials": [ + { + "name": "pipe1-pr", + "type": "plugin", + "scm_id": "foo.bar.pr" + } + ], + "stages": [ + { + "name": "build", + "approval": { + "type": "success" + }, + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + }, + { + "name": "test", + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/parts/explicit_approval.pipe.yaml b/src/test/resources/parts/explicit_approval.pipe.yaml new file mode 100644 index 00000000..d44068b4 --- /dev/null +++ b/src/test/resources/parts/explicit_approval.pipe.yaml @@ -0,0 +1,21 @@ +# pipeline with explicit approval -> should not be changed +pipe1: + group: mygroup + materials: + pipe1-pr: + scm: foo.bar.pr + stages: + - build: + approval: + type: success + jobs: + build: + tasks: + - exec: + command: make + - test: + jobs: + build: + tasks: + - exec: + command: make diff --git a/src/test/resources/parts/pullrequest-no-approval.pipe.json b/src/test/resources/parts/pullrequest-no-approval.pipe.json new file mode 100644 index 00000000..b9a004fd --- /dev/null +++ b/src/test/resources/parts/pullrequest-no-approval.pipe.json @@ -0,0 +1,41 @@ +{ + "name": "pipe1", + "group": "mygroup", + "materials": [ + { + "name": "pipe1-pr", + "type": "plugin", + "scm_id": "foo.bar.pr" + } + ], + "stages": [ + { + "name": "build", + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + }, + { + "name": "test", + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/parts/pullrequest.pipe.json b/src/test/resources/parts/pullrequest.pipe.json new file mode 100644 index 00000000..5bd45536 --- /dev/null +++ b/src/test/resources/parts/pullrequest.pipe.json @@ -0,0 +1,44 @@ +{ + "name": "pipe1", + "group": "mygroup", + "materials": [ + { + "name": "pipe1-pr", + "type": "plugin", + "scm_id": "foo.bar.pr" + } + ], + "stages": [ + { + "name": "build", + "approval": { + "type": "manual" + }, + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + }, + { + "name": "test", + "jobs": [ + { + "name": "build", + "tasks": [ + { + "type": "exec", + "command": "make" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/parts/pullrequest.pipe.yaml b/src/test/resources/parts/pullrequest.pipe.yaml new file mode 100644 index 00000000..debd5f21 --- /dev/null +++ b/src/test/resources/parts/pullrequest.pipe.yaml @@ -0,0 +1,19 @@ +# pipeline with a PR SCM -> should get approval: manual on first stage +pipe1: + group: mygroup + materials: + pipe1-pr: + scm: foo.bar.pr + stages: + - build: + jobs: + build: + tasks: + - exec: + command: make + - test: + jobs: + build: + tasks: + - exec: + command: make